Added auth and request storage

This commit is contained in:
2026-08-31 12:28:18 +00:00
parent 6febaf327a
commit 1ca9648c09
21 changed files with 634 additions and 117 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ const createBroadcaster = (): Broadcaster => {
};
const routeStream = (connection: TestConnection): ApplicationRouteStream => {
return new ApplicationRouteStream(connection, undefined);
return new ApplicationRouteStream(connection, undefined, '/test');
};
const expectPending = async (promise: Promise<void>): Promise<void> => {
+15 -6
View File
@@ -33,13 +33,15 @@ describe('ApplicationRouter dispatch', (): void => {
url: '/echo',
handler: async (stream): Promise<void> => {
expect(stream.connection).toBe(connection);
expect(stream.path).toBe('/echo');
expect(stream.headers).toEqual({ 'x-request-token': 'route-1' });
await stream.send(stream.body);
},
},
]),
]);
await router.dispatch({ path: '/echo', body: { value: 1 }, requestId: 'request-1' }, connection);
await router.dispatch({ path: '/echo', body: { value: 1 }, requestId: 'request-1', headers: { 'x-request-token': 'route-1' } }, connection);
expect(connection.messages).toEqual([
{
@@ -61,16 +63,23 @@ describe('ApplicationRouter dispatch', (): void => {
url: '/delayed',
handler: async (stream): Promise<void> => {
const key = (stream.body as { key: string }).key;
const token = stream.headers['x-request-token'];
await new Promise<void>((resolve) => completions.set(key, resolve));
await stream.send({ key });
await stream.send({ key, token });
},
},
]),
]);
const connection = new TestConnection(true, true);
const first = router.dispatch({ path: '/delayed', body: { key: 'A' }, requestId: 'A' }, connection);
const second = router.dispatch({ path: '/delayed', body: { key: 'B' }, requestId: 'B' }, connection);
const first = router.dispatch(
{ path: '/delayed', body: { key: 'A' }, requestId: 'A', headers: { 'x-request-token': 'token-a' } },
connection,
);
const second = router.dispatch(
{ path: '/delayed', body: { key: 'B' }, requestId: 'B', headers: { 'x-request-token': 'token-b' } },
connection,
);
completions.get('B')?.();
await second;
@@ -82,13 +91,13 @@ describe('ApplicationRouter dispatch', (): void => {
id: 'B',
type: 'response',
statusCode: 200,
body: { key: 'B' },
body: { key: 'B', token: 'token-b' },
},
{
id: 'A',
type: 'response',
statusCode: 200,
body: { key: 'A' },
body: { key: 'A', token: 'token-a' },
},
]);
});
@@ -70,6 +70,23 @@ describe('HttpTransportRouter', (): void => {
expect(await response.text()).toBe('');
});
it('passes normalized HTTP request headers to the route stream', async (): Promise<void> => {
const app = await createApp([
{
url: '/headers',
handler: async (stream): Promise<void> => stream.send({ path: stream.path, token: stream.headers['x-request-token'] }),
},
]);
const response = await app.request('/headers', {
method: 'POST',
headers: { 'X-Request-Token': 'http-token' },
});
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ path: '/headers', token: 'http-token' });
});
it('returns normalized errors for non-streaming requests', async (): Promise<void> => {
const app = await createApp([]);
@@ -149,6 +166,27 @@ describe('HttpTransportRouter', (): void => {
expect(events).toContain('data: {"ok":true}');
});
it('passes normalized HTTP request headers to an SSE route stream', async (): Promise<void> => {
const app = await createApp([
{
url: '/headers',
handler: (stream): Promise<void> => stream.send({ token: stream.headers['x-request-token'] }),
},
]);
const response = await app.request('/headers', {
method: 'POST',
headers: {
accept: 'text/event-stream',
'X-Request-Token': 'sse-token',
},
});
const events = await response.text();
expect(response.status).toBe(200);
expect(events).toContain('data: {"token":"sse-token"}');
});
it("keeps SSE open until the route's subscription promise resolves", async (): Promise<void> => {
let removeSubscription: () => Promise<void> = async () => undefined;
let markSubscribed: () => void = () => undefined;
+23 -6
View File
@@ -13,19 +13,36 @@ describe('WebSocket request decoding', (): void => {
id: 'request-1',
path: '/data/write',
body: { value: new Uint8Array([ 1, 2, 3 ]) },
headers: { 'X-Request-Token': 'ws-token' },
}))).resolves.toEqual({
requestId: 'request-1',
path: '/data/write',
body: { value: new Uint8Array([ 1, 2, 3 ]) },
headers: { 'x-request-token': 'ws-token' },
});
});
it.each([ '{}', '{"path":42}', '{"path":"/data/get","id":1}', '{"path":"/data/get","method":"POST"}' ])(
'rejects an invalid envelope: %s',
async (payload) => {
await expect(WsTransportRouter.decodeWebSocketRequest(payload)).rejects.toBeInstanceOf(z.ZodError);
},
);
it('allows the optional headers object to be omitted', async (): Promise<void> => {
await expect(WsTransportRouter.decodeWebSocketRequest('{"path":"/data/get"}')).resolves.toEqual({ path: '/data/get' });
});
it.each([
'{}',
'{"path":42}',
'{"path":"/data/get","id":1}',
'{"path":"/data/get","method":"POST"}',
'{"path":"/data/get","headers":{"x-request-token":1}}',
])('rejects an invalid envelope: %s', async (payload) => {
await expect(WsTransportRouter.decodeWebSocketRequest(payload)).rejects.toBeInstanceOf(z.ZodError);
});
it.each([
'{"path":"/data/get","headers":{"bad header":"value"}}',
'{"path":"/data/get","headers":{"x-request-token":"first","X-Request-Token":"second"}}',
'{"path":"/data/get","headers":{"x-request-token":"first\\r\\nsecond"}}',
])('rejects malformed request headers: %s', async (payload): Promise<void> => {
await expect(WsTransportRouter.decodeWebSocketRequest(payload)).rejects.toMatchObject({ statusCode: 400 });
});
it('rejects malformed JSON', async (): Promise<void> => {
await expect(WsTransportRouter.decodeWebSocketRequest('{')).rejects.toMatchObject({