test(backend): expand helper and media coverage
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { afterEach, describe, expect, test } from 'bun:test';
|
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 };
|
const ORIGINAL_ENV = { ...process.env };
|
||||||
|
|
||||||
@@ -23,6 +23,26 @@ describe('env helpers', () => {
|
|||||||
expect(getBetterAuthBaseUrl()).toBe('http://base-url:4000');
|
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', () => {
|
test('getDeviceOnlineStaleSeconds defaults to 30', () => {
|
||||||
delete process.env.DEVICE_ONLINE_STALE_SECONDS;
|
delete process.env.DEVICE_ONLINE_STALE_SECONDS;
|
||||||
expect(getDeviceOnlineStaleSeconds()).toBe(30);
|
expect(getDeviceOnlineStaleSeconds()).toBe(30);
|
||||||
|
|||||||
26
Backend/tests/metrics.test.ts
Normal file
26
Backend/tests/metrics.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
74
Backend/tests/mock-media-provider.test.ts
Normal file
74
Backend/tests/mock-media-provider.test.ts
Normal file
@@ -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<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
68
Backend/tests/sfu-noop.test.ts
Normal file
68
Backend/tests/sfu-noop.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
49
Backend/tests/sfu-registry.test.ts
Normal file
49
Backend/tests/sfu-registry.test.ts
Normal file
@@ -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',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user