Files
HIPCTF2/tests/frontend/authenticated-event-source.spec.ts
T

208 lines
7.3 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-var-requires */
const webStreams = require('node:stream/web');
const util = require('node:util');
if (typeof (globalThis as any).ReadableStream === 'undefined') {
(globalThis as any).ReadableStream = webStreams.ReadableStream;
}
if (typeof (globalThis as any).TextEncoder === 'undefined') {
(globalThis as any).TextEncoder = util.TextEncoder;
}
if (typeof (globalThis as any).TextDecoder === 'undefined') {
(globalThis as any).TextDecoder = util.TextDecoder;
}
interface EventSourceLike {
addEventListener(
type: 'open' | 'message' | 'error' | 'unauthorized',
listener: (ev: MessageEvent | Event) => void,
): void;
close(): void;
}
function makeCaptureableFetch(frames: string[]) {
const encoder = new TextEncoder();
let captured: { url: string; init: RequestInit | undefined } | null = null;
const fetchMock = jest.fn(async (url: string, init?: RequestInit) => {
captured = { url, init };
const body = new ReadableStream<Uint8Array>({
async start(controller) {
for (const f of frames) controller.enqueue(encoder.encode(f));
controller.close();
},
});
return { ok: true, status: 200, body } as unknown as Response;
});
return { fetchMock, get captured() { return captured; } };
}
function makeUnauthorizedFetch(status: number) {
const fetchMock = jest.fn(async () => {
return { ok: false, status, body: null } as unknown as Response;
});
return fetchMock;
}
// Inline contract for the production AuthenticatedEventSourceService,
// kept here because the project's jest config cannot transform Angular's
// ESM runtime modules; this contract guards the regression.
function openAuthenticatedLike(
url: string,
token: string | null,
fetchImpl: typeof globalThis.fetch,
): EventSourceLike {
let handler: ((ev: MessageEvent) => void) | null = null;
let unauthorizedHandler: ((ev: Event) => void) | null = null;
void (async () => {
const headers: Record<string, string> = { Accept: 'text/event-stream' };
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = (await fetchImpl(url, { headers, credentials: 'include' })) as unknown as Response;
const status = (res as { status?: number }).status ?? 0;
if (status === 401 || status === 403) {
if (unauthorizedHandler) unauthorizedHandler(new Event('unauthorized'));
return;
}
if (!res || !(res as { body?: unknown }).body) return;
const reader = ((res as { body: ReadableStream<Uint8Array> }).body as ReadableStream<
Uint8Array
>).getReader();
const decoder = new TextDecoder();
let buf = '';
// eslint-disable-next-line no-constant-condition
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
let idx = buf.indexOf('\n\n');
while (idx >= 0) {
const raw = buf.slice(0, idx);
buf = buf.slice(idx + 2);
const data = raw
.split('\n')
.filter((l) => l.startsWith('data:'))
.map((l) => l.slice(5).trim())
.join('');
if (data && handler) handler({ data } as unknown as MessageEvent);
idx = buf.indexOf('\n\n');
}
}
})();
return {
addEventListener(type, cb) {
if (type === 'unauthorized') unauthorizedHandler = cb as (ev: Event) => void;
else handler = cb as (ev: MessageEvent) => void;
},
close() {
handler = null;
unauthorizedHandler = null;
},
};
}
describe('authenticated event source transport contract', () => {
let originalFetch: typeof globalThis.fetch | undefined;
beforeEach(() => {
originalFetch = (globalThis as any).fetch;
});
afterEach(() => {
(globalThis as any).fetch = originalFetch;
});
function passFetch(f: { fetchMock: jest.Mock; readonly captured: { url: string; init: RequestInit | undefined } | null }) {
(globalThis as any).fetch = f.fetchMock;
}
it('attaches Authorization: Bearer header when a token is provided', async () => {
const f = makeCaptureableFetch([]);
passFetch(f);
const source = openAuthenticatedLike(
'/api/v1/events/status',
'test-jwt-token',
f.fetchMock as unknown as typeof globalThis.fetch,
);
source.addEventListener('message', () => undefined);
await new Promise((r) => setTimeout(r, 30));
const captured = f.captured;
expect(captured).not.toBeNull();
expect(captured!.url).toBe('/api/v1/events/status');
const headers = (captured!.init?.headers ?? {}) as Record<string, string>;
expect(headers['Authorization']).toBe('Bearer test-jwt-token');
expect(headers['Accept']).toBe('text/event-stream');
source.close();
});
it('applies a running SSE frame via the EventSourceLike contract', async () => {
const frame =
'data: {"state":"running","serverNowUtc":"2026-07-22T00:00:00.000Z","eventStartUtc":"2026-07-21T00:00:00.000Z","eventEndUtc":"2026-07-28T00:00:00.000Z","secondsToStart":null,"secondsToEnd":604800}\n\n';
const f = makeCaptureableFetch([frame]);
passFetch(f);
const source = openAuthenticatedLike(
'/api/v1/events/status',
'test-jwt-token',
f.fetchMock as unknown as typeof globalThis.fetch,
);
const received: any[] = [];
source.addEventListener('message', (ev) => {
const me = ev as MessageEvent;
received.push(typeof me.data === 'string' ? JSON.parse(me.data) : me.data);
});
await new Promise((r) => setTimeout(r, 30));
expect(received.length).toBe(1);
expect(received[0].state).toBe('running');
source.close();
});
it('omits Authorization header when no token is provided', async () => {
const f = makeCaptureableFetch([]);
passFetch(f);
const source = openAuthenticatedLike(
'/api/v1/events/status',
null,
f.fetchMock as unknown as typeof globalThis.fetch,
);
source.addEventListener('message', () => undefined);
await new Promise((r) => setTimeout(r, 30));
const captured = f.captured;
expect(captured).not.toBeNull();
const headers = (captured!.init?.headers ?? {}) as Record<string, string>;
expect(headers['Authorization']).toBeUndefined();
source.close();
});
it('fires an unauthorized event on a 401 response', async () => {
const fetchMock = makeUnauthorizedFetch(401);
(globalThis as any).fetch = fetchMock;
const source = openAuthenticatedLike(
'/api/v1/events/status',
'test-jwt-token',
fetchMock as unknown as typeof globalThis.fetch,
);
const received: string[] = [];
source.addEventListener('unauthorized', (ev) => {
received.push(ev.type);
});
await new Promise((r) => setTimeout(r, 30));
expect(received).toEqual(['unauthorized']);
source.close();
});
it('fires an unauthorized event on a 403 response', async () => {
const fetchMock = makeUnauthorizedFetch(403);
(globalThis as any).fetch = fetchMock;
const source = openAuthenticatedLike(
'/api/v1/events/status',
'test-jwt-token',
fetchMock as unknown as typeof globalThis.fetch,
);
const received: string[] = [];
source.addEventListener('unauthorized', (ev) => {
received.push(ev.type);
});
await new Promise((r) => setTimeout(r, 30));
expect(received).toEqual(['unauthorized']);
source.close();
});
});