-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathdispatcher-plugin.routes.test.ts
More file actions
99 lines (86 loc) · 3.6 KB
/
Copy pathdispatcher-plugin.routes.test.ts
File metadata and controls
99 lines (86 loc) · 3.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { describe, it, expect } from 'vitest';
import { createDispatcherPlugin } from './dispatcher-plugin.js';
/**
* Regression: the dispatcher mounts routes EXPLICITLY on the HTTP server (there
* is no catch-all). A dispatch() branch with no matching `server.<verb>()`
* registration is unreachable over HTTP and 404s before reaching the handler —
* which is exactly how /mcp and /keys shipped broken (unit tests called the
* handlers directly, hiding it). This test asserts the routes are registered.
*/
function makeFakeServer() {
const routes: string[] = [];
const handlers: Record<string, (req: any, res: any) => any> = {};
const rec = (verb: string) => (path: string, handler: any) => {
routes.push(`${verb} ${path}`);
handlers[`${verb} ${path}`] = handler;
};
return {
routes,
handlers,
server: {
get: rec('GET'),
post: rec('POST'),
put: rec('PUT'),
delete: rec('DELETE'),
patch: rec('PATCH'),
},
};
}
function makeCtx(fakeServer: any) {
const kernel = {
getService: () => undefined,
getServiceAsync: async () => undefined,
};
return {
getKernel: () => kernel,
getService: (name: string) => (name === 'http.server' ? fakeServer : undefined),
environmentId: undefined,
logger: { info() {}, warn() {}, error() {}, debug() {} },
hook: () => {},
on: () => {},
} as any;
}
describe('createDispatcherPlugin — HTTP route registration', () => {
it('mounts /mcp (GET/POST/DELETE) and /keys (POST) so they reach dispatch()', async () => {
const { server, routes } = makeFakeServer();
const plugin = createDispatcherPlugin({ prefix: '/api/v1', securityHeaders: false });
await plugin.start?.(makeCtx(server));
expect(routes).toContain('POST /api/v1/mcp');
expect(routes).toContain('GET /api/v1/mcp');
expect(routes).toContain('DELETE /api/v1/mcp');
expect(routes).toContain('POST /api/v1/keys');
});
it('also mounts a known existing route (sanity that start() ran)', async () => {
const { server, routes } = makeFakeServer();
const plugin = createDispatcherPlugin({ prefix: '/api/v1', securityHeaders: false });
await plugin.start?.(makeCtx(server));
expect(routes).toContain('POST /api/v1/analytics/query');
});
it('honours a custom prefix', async () => {
const { server, routes } = makeFakeServer();
const plugin = createDispatcherPlugin({ prefix: '/v2', securityHeaders: false });
await plugin.start?.(makeCtx(server));
expect(routes).toContain('POST /v2/mcp');
expect(routes).toContain('POST /v2/keys');
});
// cloud#152: discovery reflects mutable runtime config (e.g. routes.mcp toggles
// with OS_MCP_SERVER_ENABLED). It must be served Cache-Control: no-store so an
// edge/CDN never serves a stale payload after the config changes.
it('serves both discovery routes with Cache-Control: no-store', async () => {
const { server, handlers } = makeFakeServer();
const plugin = createDispatcherPlugin({ prefix: '/api/v1', securityHeaders: false });
await plugin.start?.(makeCtx(server));
for (const route of ['GET /.well-known/objectstack', 'GET /api/v1/discovery']) {
const handler = handlers[route];
expect(handler, `${route} should be registered`).toBeTypeOf('function');
const headers: Record<string, string> = {};
const res: any = {
header: (k: string, v: string) => { headers[k] = v; },
json: () => {},
};
await handler({}, res);
expect(headers['Cache-Control'], `${route} Cache-Control`).toBe('no-store');
}
});
});