154 lines
5.5 KiB
TypeScript
154 lines
5.5 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', 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; } };
|
|
}
|
|
|
|
// 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;
|
|
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' });
|
|
if (!res || !(res as any).body) return;
|
|
const reader = ((res as any).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) {
|
|
handler = cb as (ev: MessageEvent) => void;
|
|
},
|
|
close() {
|
|
handler = 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();
|
|
});
|
|
});
|