87dec03d3c
Adds end-to-end integration test for the WebSocket live broadcast pipeline: Redis + TimescaleDB containers + Directus stub → full pipeline boot → real WS client assertions. Mirrors pipeline.integration.test.ts pattern with the skip-on-no-Docker guard. Key additions: - test/live.integration.test.ts: 6 test scenarios — happy path (subscribe → snapshot → live position), auth rejection (401), forbidden subscription (error/forbidden), multi-client fan-out (both receive position), orphan position (no WS frame), faulty snapshot exclusion (next-best non-faulty) - test/helpers/directus-stub.ts: bare http.createServer stub for /users/me and /items/events/:id endpoints with cookie-based user lookup - test/fixtures/test-schema.sql: minimal schema subset (events, entries, entry_devices with IMEI-as-device_id for Phase 1 join semantics) The integration test runs via `pnpm test:integration`, not `pnpm test`. Docker required; the suite skips cleanly when Docker is unavailable.
108 lines
3.2 KiB
TypeScript
108 lines
3.2 KiB
TypeScript
/**
|
|
* Minimal HTTP server stub impersonating the two Directus endpoints the
|
|
* Processor calls:
|
|
*
|
|
* GET /users/me — returns a fake user if the cookie matches
|
|
* GET /items/events/:id — returns 200 if (cookie, eventId) is allowed
|
|
*
|
|
* Instantiate with `createDirectusStub(opts)` and tear down with
|
|
* `stub.close()`. The stub binds to a random OS port and exposes `stub.url`
|
|
* for config injection.
|
|
*
|
|
* Design: bare `node:http` — no Express dependency.
|
|
*/
|
|
|
|
import * as http from 'node:http';
|
|
import type { AddressInfo } from 'node:net';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export type FakeUser = {
|
|
readonly id: string;
|
|
readonly email: string;
|
|
readonly role: string | null;
|
|
readonly first_name: string;
|
|
readonly last_name: string;
|
|
};
|
|
|
|
export type StubOptions = {
|
|
/**
|
|
* Map from the raw cookie header value (e.g. `"session=abc"`) to the fake
|
|
* user that cookie represents. Any cookie not in this map → 401.
|
|
*/
|
|
readonly allowedCookieToUser: Map<string, FakeUser>;
|
|
/**
|
|
* Map from Directus user ID → set of event IDs that user may access.
|
|
* A request from a valid user for an event not in their set → 403.
|
|
*/
|
|
readonly allowedEvents: Map<string, Set<string>>;
|
|
};
|
|
|
|
export type DirectusStub = {
|
|
readonly url: string;
|
|
readonly close: () => Promise<void>;
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Factory
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Creates and starts a Directus stub server on a random port.
|
|
* Returns a promise that resolves once the server is listening.
|
|
*/
|
|
export function createDirectusStub(opts: StubOptions): Promise<DirectusStub> {
|
|
const server = http.createServer((req, res) => {
|
|
const cookie = req.headers['cookie'] ?? '';
|
|
const user = opts.allowedCookieToUser.get(cookie);
|
|
|
|
// GET /users/me
|
|
if (req.url === '/users/me') {
|
|
if (!user) {
|
|
res.writeHead(401).end();
|
|
return;
|
|
}
|
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
res.end(JSON.stringify({ data: user }));
|
|
return;
|
|
}
|
|
|
|
// GET /items/events/:id
|
|
const eventMatch = /^\/items\/events\/([0-9a-f-]+)/i.exec(req.url ?? '');
|
|
if (eventMatch) {
|
|
if (!user) {
|
|
res.writeHead(401).end();
|
|
return;
|
|
}
|
|
const eventId = eventMatch[1]!;
|
|
const allowed = opts.allowedEvents.get(user.id)?.has(eventId) ?? false;
|
|
if (!allowed) {
|
|
res.writeHead(403).end();
|
|
return;
|
|
}
|
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
res.end(JSON.stringify({ data: { id: eventId } }));
|
|
return;
|
|
}
|
|
|
|
res.writeHead(404).end();
|
|
});
|
|
|
|
return new Promise((resolve, reject) => {
|
|
server.on('error', reject);
|
|
server.listen(0, '127.0.0.1', () => {
|
|
server.off('error', reject);
|
|
const addr = server.address() as AddressInfo;
|
|
resolve({
|
|
url: `http://127.0.0.1:${addr.port}`,
|
|
close: () =>
|
|
new Promise((res, rej) =>
|
|
server.close((err) => (err ? rej(err) : res())),
|
|
),
|
|
});
|
|
});
|
|
});
|
|
}
|