Files
processor/test/helpers/directus-stub.ts
T
julian 87dec03d3c feat(live): task 1.5.6 — live broadcast integration test
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.
2026-05-02 18:38:53 +02:00

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())),
),
});
});
});
}