Backend (src/server, runs on Bun): - GibbzHttpServer (health, /api/ice, /api/rooms, static, keepalive tuning) - SignalingServer with Peer/Room/RoomRegistry classes and typed messages - IceConfigProvider (env STUN/TURN + coturn HMAC ephemeral creds) - StaticFileServer (dist/ with public/ fallback, traversal guard) - Tracing (OpenTelemetry SDK), AppConfig; createApp() factory for tests Frontend (src/client, built by Vite): - VoiceApp orchestrator implementing VoiceAppContext - WebRtcManager + PeerSession (perfect negotiation, pc rebuild, quality monitor) - SignalingClient + ReconnectController, AudioManager, SpatialAudio, Microphone/ScreenShare controllers, ScreensUi/RosterUi/SpatialMap/ SpeakingMonitor, Settings/Welcome dialogs, LayoutManager, MatrixChat Tooling: bun scripts (dev/start/test/typecheck), vite.config.ts root src/client with publicDir public/static, vitest-config for CI-less runs, bun-based Dockerfile (oven/bun:1), Makefile + README updated, legacy server.js/tracing.js/test.js/public js removed. Verified locally: tsc --noEmit clean, vitest run 6/6 green, vite build ok, server smoke-tested serving dist + signaling endpoints under Node. Local bun binary segfaults (SIGILL) on this host, so 'bun test' itself was not executable here; tests are vitest-API compatible which bun test maps.
91 lines
3.2 KiB
TypeScript
91 lines
3.2 KiB
TypeScript
import fs from 'node:fs';
|
|
import { afterAll, describe, expect, test } from 'vitest';
|
|
import { WebSocket } from 'ws';
|
|
|
|
import { createApp, type GibbzApp } from '../src/server/index.ts';
|
|
import { IceConfigProvider } from '../src/server/ice.ts';
|
|
|
|
let app: GibbzApp | undefined;
|
|
let port = 0;
|
|
|
|
async function startTestApp(): Promise<number> {
|
|
if (!app) {
|
|
app = createApp();
|
|
await app.http.start(0, '127.0.0.1');
|
|
port = app.http.address()!.port;
|
|
}
|
|
return port;
|
|
}
|
|
|
|
afterAll(async () => {
|
|
await app?.stop();
|
|
});
|
|
|
|
describe('server', () => {
|
|
test('static client files exist', () => {
|
|
expect(fs.existsSync('src/client/index.html'), 'index.html exists').toBe(true);
|
|
expect(fs.existsSync('src/client/main.ts'), 'main.ts exists').toBe(true);
|
|
expect(fs.existsSync('src/client/style.css'), 'style.css exists').toBe(true);
|
|
});
|
|
|
|
test('/healthz answers ok', async () => {
|
|
const p = await startTestApp();
|
|
const res = await fetch(`http://127.0.0.1:${p}/healthz`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe('ok');
|
|
});
|
|
|
|
test('/api/ice returns cluster stun/turn', async () => {
|
|
const p = await startTestApp();
|
|
const res = await fetch(`http://127.0.0.1:${p}/api/ice`);
|
|
expect(res.status).toBe(200);
|
|
const ice = await res.json();
|
|
expect(Array.isArray(ice.iceServers)).toBe(true);
|
|
const stunUrls = ice.iceServers.flatMap((s: { urls: string | string[] }) =>
|
|
Array.isArray(s.urls) ? s.urls : [s.urls],
|
|
);
|
|
expect(stunUrls.some((u: string) => u.includes('turn.k6n.net'))).toBe(true);
|
|
});
|
|
|
|
test('/api/rooms starts empty and reflects a connected peer', async () => {
|
|
const p = await startTestApp();
|
|
const empty = await (await fetch(`http://127.0.0.1:${p}/api/rooms`)).json();
|
|
expect(empty).toEqual({});
|
|
|
|
const ws = new WebSocket(`ws://127.0.0.1:${p}/ws?room=testroom&name=tester`);
|
|
const hello = await new Promise<{ id: string; room: string; type: string }>((resolve, reject) => {
|
|
ws.on('message', (raw) => {
|
|
const msg = JSON.parse(raw.toString());
|
|
if (msg.type === 'hello') resolve(msg);
|
|
});
|
|
ws.on('error', reject);
|
|
});
|
|
expect(hello.type).toBe('hello');
|
|
expect(hello.room).toBe('testroom');
|
|
expect(hello.id).toBeTruthy();
|
|
|
|
const res = await fetch(`http://127.0.0.1:${p}/api/rooms`);
|
|
const rooms = await res.json();
|
|
expect(rooms.testroom).toBeTruthy();
|
|
expect(rooms.testroom).toHaveLength(1);
|
|
expect(rooms.testroom[0].name).toBe('tester');
|
|
ws.close();
|
|
});
|
|
|
|
test('TURN secret generates ephemeral HMAC credentials', () => {
|
|
const provider = new IceConfigProvider({ TURN_SECRET: 'secret123' });
|
|
const config = provider.get();
|
|
const turnServer = config.iceServers.find((s) =>
|
|
(Array.isArray(s.urls) ? s.urls : [s.urls]).some((u) => u.startsWith('turn')),
|
|
);
|
|
expect(turnServer).toBeTruthy();
|
|
expect(turnServer!.username!.endsWith(':gibbz')).toBe(true);
|
|
expect(turnServer!.credential!.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('ICE transport policy is passed through', () => {
|
|
const provider = new IceConfigProvider({ ICE_TRANSPORT_POLICY: 'relay' });
|
|
expect(provider.get().iceTransportPolicy).toBe('relay');
|
|
});
|
|
});
|