admin-manifest.test.ts121 lines · main
1/**
2 * Contract tests for the admin.flndrn.com dashboard API. Exercises the
3 * auth gate + the no-DB endpoints (manifest, ping) against the real Hono
4 * router via app.request(). The DB-backed endpoints (summary, users,
5 * projects) reuse the services/admin.ts queries and are not stood up here
6 * (no postgres in unit tests).
7 *
8 * env.ts freezes process.env at first import, and that import may already
9 * have happened in a sibling test file before this one runs. So rather
10 * than fight the load order, we read whatever value env actually froze
11 * to and assert the gate's REAL behaviour for that value: a present key
12 * proves the 200 + 401 paths, an absent key proves the 503 fail-safe.
13 * The two top-level process.env lines mirror the repo convention
14 * (cli-jwt.test.ts) for the case where this file is imported first.
15 */
16
17const ORIGINAL_KEY = process.env.BRIVEN_ADMIN_API_KEY;
18process.env.BRIVEN_ADMIN_API_KEY = ORIGINAL_KEY ?? 'test-admin-key-0123456789abcdef';
19
20import { afterAll, describe, expect, test } from 'bun:test';
21
22const { Hono } = await import('hono');
23const { env } = await import('../env.js');
24const { adminManifestRouter } = await import('./admin-manifest.js');
25
26const app = new Hono();
27app.route('/', adminManifestRouter);
28
29// The effective key the gate compares against (frozen at env import).
30const KEY = env.BRIVEN_ADMIN_API_KEY;
31const auth = KEY ? { headers: { authorization: `Bearer ${KEY}` } } : undefined;
32
33afterAll(() => {
34 if (ORIGINAL_KEY === undefined) delete process.env.BRIVEN_ADMIN_API_KEY;
35 else process.env.BRIVEN_ADMIN_API_KEY = ORIGINAL_KEY;
36});
37
38describe('admin-manifest auth gate', () => {
39 test('manifest with no/blank bearer → 401 when configured, 503 when not', async () => {
40 const res = await app.request('/api/admin/v1/manifest');
41 expect(res.status).toBe(KEY ? 401 : 503);
42 });
43
44 test('wrong bearer → 401 when configured, 503 when not', async () => {
45 const res = await app.request('/api/admin/v1/manifest', {
46 headers: { authorization: 'Bearer definitely-wrong-key' },
47 });
48 expect(res.status).toBe(KEY ? 401 : 503);
49 });
50
51 test('summary runs no DB query before the auth gate', async () => {
52 // Unauthenticated → must short-circuit at the gate (401/503), never
53 // reach getDb(). A thrown DB error would surface as 500.
54 const res = await app.request('/api/admin/v1/summary');
55 expect(res.status).toBe(KEY ? 401 : 503);
56 });
57});
58
59describe('admin-manifest manifest shape', () => {
60 test('authenticated manifest → 200 with valid sections', async () => {
61 if (!auth) {
62 // Unconfigured deploy: gate returns 503, which the consumer renders
63 // as "not configured". Manifest shape is asserted in the configured
64 // path below when a key is present.
65 const res = await app.request('/api/admin/v1/manifest');
66 expect(res.status).toBe(503);
67 return;
68 }
69 const res = await app.request('/api/admin/v1/manifest', auth);
70 expect(res.status).toBe(200);
71 const body = (await res.json()) as {
72 sections: Array<{
73 key: string;
74 title: string;
75 icon: string;
76 permission: string;
77 endpoints: Array<{ kind: string; method: string; path: string }>;
78 }>;
79 };
80 expect(Array.isArray(body.sections)).toBe(true);
81 expect(body.sections.length).toBeGreaterThan(0);
82 for (const s of body.sections) {
83 expect(typeof s.key).toBe('string');
84 expect(typeof s.title).toBe('string');
85 expect(typeof s.icon).toBe('string');
86 expect(s.permission).toBe('dev.briven.read');
87 expect(s.endpoints.length).toBeGreaterThan(0);
88 for (const e of s.endpoints) {
89 expect(['list', 'detail', 'actions']).toContain(e.kind);
90 expect(e.method).toBe('GET');
91 expect(e.path.startsWith('/api/admin/v1/')).toBe(true);
92 }
93 }
94 });
95
96 test('bare /api/admin/manifest alias resolves to same handler', async () => {
97 const res = await app.request('/api/admin/manifest', auth);
98 expect(res.status).toBe(KEY ? 200 : 503);
99 });
100});
101
102describe('admin-manifest ping', () => {
103 test('authenticated ping → 200 { ok, service: briven, ts }', async () => {
104 if (!auth) {
105 const res = await app.request('/api/admin/v1/ping');
106 expect(res.status).toBe(503);
107 return;
108 }
109 const res = await app.request('/api/admin/v1/ping', auth);
110 expect(res.status).toBe(200);
111 const body = (await res.json()) as { ok: boolean; service: string; ts: string };
112 expect(body.ok).toBe(true);
113 expect(body.service).toBe('briven');
114 expect(typeof body.ts).toBe('string');
115 });
116
117 test('bare /api/admin/ping alias resolves to same handler', async () => {
118 const res = await app.request('/api/admin/ping', auth);
119 expect(res.status).toBe(KEY ? 200 : 503);
120 });
121});