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 | |
| 17 | const ORIGINAL_KEY = process.env.BRIVEN_ADMIN_API_KEY; |
| 18 | process.env.BRIVEN_ADMIN_API_KEY = ORIGINAL_KEY ?? 'test-admin-key-0123456789abcdef'; |
| 19 | |
| 20 | import { afterAll, describe, expect, test } from 'bun:test'; |
| 21 | |
| 22 | const { Hono } = await import('hono'); |
| 23 | const { env } = await import('../env.js'); |
| 24 | const { adminManifestRouter } = await import('./admin-manifest.js'); |
| 25 | |
| 26 | const app = new Hono(); |
| 27 | app.route('/', adminManifestRouter); |
| 28 | |
| 29 | // The effective key the gate compares against (frozen at env import). |
| 30 | const KEY = env.BRIVEN_ADMIN_API_KEY; |
| 31 | const auth = KEY ? { headers: { authorization: `Bearer ${KEY}` } } : undefined; |
| 32 | |
| 33 | afterAll(() => { |
| 34 | if (ORIGINAL_KEY === undefined) delete process.env.BRIVEN_ADMIN_API_KEY; |
| 35 | else process.env.BRIVEN_ADMIN_API_KEY = ORIGINAL_KEY; |
| 36 | }); |
| 37 | |
| 38 | describe('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 | |
| 59 | describe('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 | |
| 102 | describe('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 | }); |