test(backend): expand helper and media coverage

This commit is contained in:
2026-04-14 15:30:00 +01:00
parent 928d49250e
commit 5f3daf7922
5 changed files with 238 additions and 1 deletions

View File

@@ -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);

View 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);
});
});

View 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');
});
});

View 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);
});
});

View 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',
},
]);
});
});