feat: Authenticated Shell, Quick Tabs and Change Password 1.01
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
/* 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user