From 5f3daf7922344c53025999bdc40c0b803e39749c Mon Sep 17 00:00:00 2001 From: Matiss Jurevics Date: Tue, 14 Apr 2026 15:30:00 +0100 Subject: [PATCH] test(backend): expand helper and media coverage --- Backend/tests/env.test.ts | 22 ++++++- Backend/tests/metrics.test.ts | 26 ++++++++ Backend/tests/mock-media-provider.test.ts | 74 +++++++++++++++++++++++ Backend/tests/sfu-noop.test.ts | 68 +++++++++++++++++++++ Backend/tests/sfu-registry.test.ts | 49 +++++++++++++++ 5 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 Backend/tests/metrics.test.ts create mode 100644 Backend/tests/mock-media-provider.test.ts create mode 100644 Backend/tests/sfu-noop.test.ts create mode 100644 Backend/tests/sfu-registry.test.ts diff --git a/Backend/tests/env.test.ts b/Backend/tests/env.test.ts index 607d877..e530c48 100644 --- a/Backend/tests/env.test.ts +++ b/Backend/tests/env.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, test } from 'bun:test'; -import { getBetterAuthBaseUrl, getDeviceOnlineStaleSeconds, getFirstDefinedEnv } from '../utils/env'; +import { getBetterAuthBaseUrl, getDeviceOnlineStaleSeconds, getFirstDefinedEnv, getRequiredEnv } from '../utils/env'; const ORIGINAL_ENV = { ...process.env }; @@ -23,6 +23,26 @@ describe('env helpers', () => { expect(getBetterAuthBaseUrl()).toBe('http://base-url:4000'); }); + test('getBetterAuthBaseUrl falls back to localhost with current port', () => { + delete process.env.BETTER_AUTH_BASE_URL; + delete process.env.BETTER_AUTH_URL; + process.env.PORT = '8088'; + + expect(getBetterAuthBaseUrl()).toBe('http://localhost:8088'); + }); + + test('getRequiredEnv returns a trimmed value', () => { + process.env.EXAMPLE_SECRET = ' super-secret '; + + expect(getRequiredEnv('EXAMPLE_SECRET')).toBe('super-secret'); + }); + + test('getRequiredEnv throws for missing values', () => { + delete process.env.MISSING_SECRET; + + expect(() => getRequiredEnv('MISSING_SECRET')).toThrow('MISSING_SECRET is required'); + }); + test('getDeviceOnlineStaleSeconds defaults to 30', () => { delete process.env.DEVICE_ONLINE_STALE_SECONDS; expect(getDeviceOnlineStaleSeconds()).toBe(30); diff --git a/Backend/tests/metrics.test.ts b/Backend/tests/metrics.test.ts new file mode 100644 index 0000000..dfac94f --- /dev/null +++ b/Backend/tests/metrics.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from 'bun:test'; + +import { getAllMetrics, incrementMetric } from '../observability/metrics'; + +describe('observability metrics', () => { + test('initializes and increments named counters', () => { + const metricName = `requests_${Date.now()}`; + + incrementMetric(metricName); + incrementMetric(metricName, 2); + + expect(getAllMetrics()[metricName]).toBe(3); + }); + + test('tracks multiple counters independently', () => { + const metricA = `camera_${Date.now()}_a`; + const metricB = `camera_${Date.now()}_b`; + + incrementMetric(metricA, 5); + incrementMetric(metricB, 2); + + const metrics = getAllMetrics(); + expect(metrics[metricA]).toBe(5); + expect(metrics[metricB]).toBe(2); + }); +}); diff --git a/Backend/tests/mock-media-provider.test.ts b/Backend/tests/mock-media-provider.test.ts new file mode 100644 index 0000000..f76510a --- /dev/null +++ b/Backend/tests/mock-media-provider.test.ts @@ -0,0 +1,74 @@ +import { afterAll, beforeAll, describe, expect, test } from 'bun:test'; + +const ORIGINAL_ENV = { ...process.env }; + +const decodeTokenPayload = (token: string) => { + const [encodedPayload] = token.split('.'); + return JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8')) as Record; +}; + +let MockMediaProvider: typeof import('../media/providers/mock').MockMediaProvider; + +beforeAll(async () => { + process.env.BETTER_AUTH_SECRET = 'mock-provider-secret'; + process.env.MEDIA_MOCK_BASE_URL = 'http://media.example.test'; + ({ MockMediaProvider } = await import('../media/providers/mock')); +}); + +afterAll(() => { + process.env = { ...ORIGINAL_ENV }; +}); + +describe('mock media provider', () => { + test('creates deterministic publish and subscribe endpoints for a session', async () => { + const provider = new MockMediaProvider(); + + const session = await provider.createSession({ + streamSessionId: 'stream-1', + ownerUserId: 'user-1', + cameraDeviceId: 'camera-1', + requesterDeviceId: 'client-1', + }); + + expect(session.provider).toBe('mock'); + expect(session.mediaSessionId).toBe('mock_stream-1'); + expect(session.publishUrl).toBe('http://media.example.test/media/mock/publish/mock_stream-1'); + expect(session.subscribeUrl).toBe('http://media.example.test/media/mock/subscribe/mock_stream-1'); + }); + + test('issues publish credentials with a signed token payload', async () => { + const provider = new MockMediaProvider(); + + const credentials = await provider.issuePublishCredentials({ + mediaSessionId: 'mock_stream-1', + cameraDeviceId: 'camera-1', + ownerUserId: 'user-1', + }); + + const payload = decodeTokenPayload(credentials.publishToken); + expect(credentials.provider).toBe('mock'); + expect(credentials.publishUrl).toBe('http://media.example.test/media/mock/publish/mock_stream-1'); + expect(credentials.expiresInSeconds).toBe(600); + expect(payload.typ).toBe('publish'); + expect(payload.mediaSessionId).toBe('mock_stream-1'); + expect(payload.cameraDeviceId).toBe('camera-1'); + }); + + test('issues subscribe credentials with a signed token payload', async () => { + const provider = new MockMediaProvider(); + + const credentials = await provider.issueSubscribeCredentials({ + mediaSessionId: 'mock_stream-1', + viewerDeviceId: 'client-1', + ownerUserId: 'user-1', + }); + + const payload = decodeTokenPayload(credentials.subscribeToken); + expect(credentials.provider).toBe('mock'); + expect(credentials.subscribeUrl).toBe('http://media.example.test/media/mock/subscribe/mock_stream-1'); + expect(credentials.expiresInSeconds).toBe(600); + expect(payload.typ).toBe('subscribe'); + expect(payload.mediaSessionId).toBe('mock_stream-1'); + expect(payload.viewerDeviceId).toBe('client-1'); + }); +}); diff --git a/Backend/tests/sfu-noop.test.ts b/Backend/tests/sfu-noop.test.ts new file mode 100644 index 0000000..83c3fe8 --- /dev/null +++ b/Backend/tests/sfu-noop.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, test } from 'bun:test'; + +import { NoopSfuService } from '../media/sfu/noop'; + +const buildInput = () => ({ + streamSessionId: 'stream-1', + ownerUserId: 'user-1', + cameraDeviceId: 'camera-1', + requesterDeviceId: 'client-1', +}); + +describe('noop SFU service', () => { + test('starts a session in starting state', async () => { + const service = new NoopSfuService(); + + const session = await service.startSession(buildInput()); + + expect(session.streamSessionId).toBe('stream-1'); + expect(session.state).toBe('starting'); + }); + + test('returns the existing session when startSession is called twice', async () => { + const service = new NoopSfuService(); + + const first = await service.startSession(buildInput()); + const second = await service.startSession(buildInput()); + + expect(second).toEqual(first); + }); + + test('updates session state through the lifecycle and can read it back', async () => { + const service = new NoopSfuService(); + await service.startSession(buildInput()); + await service.setSessionState('stream-1', 'live'); + + expect(await service.getSession('stream-1')).toEqual({ + ...buildInput(), + state: 'live', + createdAt: expect.any(String), + }); + }); + + test('marks sessions as ended when endSession is called', async () => { + const service = new NoopSfuService(); + await service.startSession(buildInput()); + await service.endSession('stream-1'); + + expect((await service.getSession('stream-1'))?.state).toBe('ended'); + }); + + test('creates publish and subscribe transports with generated ids', async () => { + const service = new NoopSfuService(); + + const publish = await service.createPublishTransport({ + streamSessionId: 'stream-1', + cameraDeviceId: 'camera-1', + }); + const subscribe = await service.createSubscribeTransport({ + streamSessionId: 'stream-1', + viewerDeviceId: 'client-1', + }); + + expect(publish.transportId.startsWith('pub_')).toBe(true); + expect(subscribe.transportId.startsWith('sub_')).toBe(true); + expect(Array.isArray(publish.iceServers)).toBe(true); + expect(Array.isArray(subscribe.iceServers)).toBe(true); + }); +}); diff --git a/Backend/tests/sfu-registry.test.ts b/Backend/tests/sfu-registry.test.ts new file mode 100644 index 0000000..3f56e52 --- /dev/null +++ b/Backend/tests/sfu-registry.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, test } from 'bun:test'; + +import { SfuSessionRegistry } from '../media/sfu/registry'; + +const buildSession = (streamSessionId: string) => ({ + streamSessionId, + ownerUserId: 'user-1', + cameraDeviceId: 'camera-1', + requesterDeviceId: 'client-1', + state: 'starting' as const, + createdAt: '2026-04-16T12:00:00.000Z', +}); + +describe('SFU session registry', () => { + test('stores and retrieves a session descriptor without internal metadata', () => { + const registry = new SfuSessionRegistry(); + registry.set(buildSession('stream-1')); + + expect(registry.get('stream-1')).toEqual(buildSession('stream-1')); + }); + + test('returns null when updating a missing session state', () => { + const registry = new SfuSessionRegistry(); + + expect(registry.updateState('missing-stream', 'live')).toBeNull(); + }); + + test('updates state and lists all registered sessions', () => { + const registry = new SfuSessionRegistry(); + registry.set(buildSession('stream-1')); + registry.set({ ...buildSession('stream-2'), cameraDeviceId: 'camera-2' }); + + expect(registry.updateState('stream-1', 'live')).toEqual({ + ...buildSession('stream-1'), + state: 'live', + }); + + expect(registry.list()).toEqual([ + { + ...buildSession('stream-1'), + state: 'live', + }, + { + ...buildSession('stream-2'), + cameraDeviceId: 'camera-2', + }, + ]); + }); +});