refactor: port backend and frontend to TypeScript classes (bun + vite)
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.
This commit is contained in:
1 parent
9b8c2a9599
commit
2b950e4953
48 files changed
+4589
-9200
No files matched your search
@@ -0,0 +1,90 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user