-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathhttp-dispatcher.requireauth.test.ts
More file actions
91 lines (77 loc) · 3.96 KB
/
Copy pathhttp-dispatcher.requireauth.test.ts
File metadata and controls
91 lines (77 loc) · 3.96 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
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
//
// Regression coverage for the `requireAuth` gate on the dispatcher's service
// route families (AI + metadata catch-all). These routes declare `auth: true`
// but nothing enforced it before — an anonymous caller reached e.g.
// `GET /api/v1/ai/status` (and the metadata reader) on a tenant-less host and
// got adapter/model/schema data back. The gate mirrors the REST `enforceAuth`
// seam: on a `requireAuth` deployment, anonymous callers get 401 while an
// authenticated (or internal system) context passes.
import { describe, it, expect } from 'vitest';
import { HttpDispatcher } from './http-dispatcher.js';
const aiRoute = {
method: 'GET',
path: '/api/v1/ai/status',
auth: true,
handler: async () => ({ status: 200, body: { adapter: 'test' } }),
};
const makeKernel = (extra: Record<string, unknown> = {}) =>
({
context: {
getService: (name: string) => (name === 'ai' ? { adapterName: 'test' } : null),
},
__aiRoutes: [aiRoute],
...extra,
}) as any;
const anon = { request: {}, executionContext: undefined } as any;
const authed = { request: {}, executionContext: { userId: 'u1' } } as any;
const system = { request: {}, executionContext: { isSystem: true } } as any;
describe('HttpDispatcher anonymous-deny — AI routes (handleAI) (#3963)', () => {
it('401s an anonymous caller (unconditional)', async () => {
const d = new HttpDispatcher(makeKernel());
const r = await d.handleAI('/ai/status', 'GET', undefined, undefined, anon);
expect(r.response?.status).toBe(401);
expect(r.response?.body?.error?.details?.code ?? r.response?.body?.error?.code).toBeDefined();
});
it('lets an authenticated caller through', async () => {
const d = new HttpDispatcher(makeKernel());
const r = await d.handleAI('/ai/status', 'GET', undefined, undefined, authed);
expect(r.response?.status).toBe(200);
expect(r.response?.body?.adapter).toBe('test');
});
it('lets an internal system context through', async () => {
const d = new HttpDispatcher(makeKernel());
const r = await d.handleAI('/ai/status', 'GET', undefined, undefined, system);
expect(r.response?.status).toBe(200);
});
it('an auth:false route is the only way an anonymous caller gets through (no global opt-out)', async () => {
// There is no `requireAuth: false` any more; only a route declaring
// auth:false opens itself — asserted by the opt-out test below.
const d = new HttpDispatcher(makeKernel());
const r = await d.handleAI('/ai/status', 'GET', undefined, undefined, anon);
expect(r.response?.status).toBe(401);
});
it('does not gate a route that opts out with auth:false', async () => {
const openRoute = { ...aiRoute, path: '/api/v1/ai/public', auth: false };
const d = new HttpDispatcher(makeKernel({ __aiRoutes: [openRoute] }));
const r = await d.handleAI('/ai/public', 'GET', undefined, undefined, anon);
expect(r.response?.status).toBe(200);
});
});
describe('HttpDispatcher anonymous-deny — metadata catch-all (handleMetadata) (#3963)', () => {
it('401s an anonymous caller (unconditional)', async () => {
const d = new HttpDispatcher(makeKernel());
const r = await d.handleMetadata('/object', anon, 'GET');
expect(r.response?.status).toBe(401);
});
it('does not 401 an authenticated caller (proceeds past the gate)', async () => {
const d = new HttpDispatcher(makeKernel());
const r = await d.handleMetadata('/object', authed, 'GET');
expect(r.response?.status).not.toBe(401);
});
it('401s an anonymous caller unconditionally — the opt-out is retired (#3963)', async () => {
const d = new HttpDispatcher(makeKernel());
const r = await d.handleMetadata('/object', anon, 'GET');
expect(r.response?.status).toBe(401);
});
});