import { Hono } from 'hono' import * as Organizations from '../db/tables/organizations.js' import * as TestAdmin from '../../test/Admin.js' // Route behavior (mint/list/revoke, the scope catalog) is covered in // `./apps/api-keys.test.ts` and `./apps/scopes.test.ts`; this file covers the // app-level wiring: the auth gate, cache policy, and the SPA shell. describe('create', () => { describe('authentication', () => { test('denied identity → 401 on config + the data API', async () => { const { app } = TestAdmin.setup({ identify: () => null }) const routes = [ new Request('http://admin/config.json'), new Request('http://admin/scopes'), new Request('http://admin/api-keys'), new Request('http://admin/api-keys', { body: JSON.stringify({ orgId: 'org', scopes: ['data:read'] }), headers: { 'content-type': 'application/json' }, method: 'POST', }), new Request('http://admin/api-keys/key_x', { method: 'DELETE' }), new Request('http://admin/earn/verified-vaults', { body: JSON.stringify({}), headers: { 'content-type': 'application/json' }, method: 'PUT', }), new Request('http://admin/organizations'), new Request('http://admin/organizations/org_x'), new Request('http://admin/organizations/org_x/billing-sources/stripe', { method: 'PUT' }), new Request('http://admin/organizations/org_x/billing-sources/stripe', { method: 'DELETE', }), new Request('http://admin/organizations/org_x/members'), new Request('http://admin/organizations/org_x/projects'), new Request('http://admin/verified-tokens?chainId=42431', { body: JSON.stringify({ tokens: [] }), headers: { 'content-type': 'application/json' }, method: 'PUT', }), ] for (const request of routes) { const response = await app.request(request) expect(response.status).toBe(401) expect(response.headers.get('cache-control')).toBe('no-store') } }) test('denied identity → shell + assets stay public (so the SPA can show login)', async () => { const { app } = TestAdmin.setup({ identify: () => null, ui: { loginUrl: '/login' } }) const shell = await app.request('http://admin/') expect(shell.status).toBe(200) // The shell inlines `identity: null` so the SPA renders login on first paint. const html = await shell.text() const json = html.match(/window\.__TEMPO_API_CONFIG__=({.*?})<\/script>/)?.[1] expect(JSON.parse(json!)).toEqual({ auth: { identity: null, loginUrl: '/login' } }) const assetPath = html.match(/\.\/(assets\/[^"]+\.js)/)?.[1] expect((await app.request(`http://admin/${assetPath}`)).status).toBe(200) }) test('public auth routes mounted ahead of the admin app bypass the gate', async () => { // The consumer owns auth endpoints: mount them on a parent app before the // admin app, so they never reach the gate while the admin surface stays gated. const { app: admin } = TestAdmin.setup({ identify: () => null }) const app = new Hono().get('/api/auth/ping', (c) => c.text('ok')).route('/', admin) expect((await app.request('http://admin/api/auth/ping')).status).toBe(200) expect((await app.request('http://admin/api-keys')).status).toBe(401) }) test('verified identity is recorded as createdBy on minted keys', async () => { const { app, db } = TestAdmin.setup({ identify: () => ({ email: 'ops@tempo.xyz' }) }) await Organizations.create(db, { id: 'org', name: 'Operations' }) const response = await app.request('http://admin/api-keys', { body: JSON.stringify({ name: 'Operations', orgId: 'org', scopes: ['data:read'] }), headers: { 'content-type': 'application/json' }, method: 'POST', }) const body = (await response.json()) as { createdBy?: string } expect(body.createdBy).toBe('ops@tempo.xyz') }) }) describe('cache policy', () => { test('every response is no-store (admin data is per-principal and mutating)', async () => { const { app } = TestAdmin.setup() for (const path of ['/scopes', '/api-keys', '/organizations', '/config.json']) { const response = await app.request(`http://admin${path}`) expect(response.headers.get('cache-control')).toBe('no-store') } }) }) describe('audit logging', () => { test('records authenticated data access with actor, request id, route, and status', async () => { const { app, db } = TestAdmin.setup({ identify: () => ({ email: 'ops@tempo.xyz' }), }) const response = await app.request('http://admin/organizations?limit=5') const records = await db.kysely .selectFrom('admin_audit_logs') .selectAll() .orderBy('createdAt', 'desc') .execute() expect(response.status).toBe(200) expect(records[0]).toMatchObject({ actor: 'ops@tempo.xyz', method: 'GET', path: '/organizations', query: 'limit=5', status: 200, }) expect(records[0]?.requestId).toBeTruthy() }) }) describe('GET /config.json', () => { test('reports the signed-in identity and sign-in/out URLs', async () => { const { app } = TestAdmin.setup({ identify: () => ({ email: 'ops@tempo.xyz' }), ui: { loginUrl: '/login', logoutUrl: '/logout' }, }) const response = await app.request('http://admin/config.json') expect(response.status).toBe(200) expect(response.headers.get('cache-control')).toBe('no-store') expect(await response.json()).toMatchInlineSnapshot(` { "auth": { "identity": { "email": "ops@tempo.xyz", }, "loginUrl": "/login", "logoutUrl": "/logout", }, } `) }) test('omits sign-in/out URLs when no ui config is provided', async () => { const { app } = TestAdmin.setup({ identify: () => ({ email: 'ops@tempo.xyz' }) }) const response = await app.request('http://admin/config.json') expect(response.status).toBe(200) expect(await response.json()).toMatchInlineSnapshot(` { "auth": { "identity": { "email": "ops@tempo.xyz", }, }, } `) }) }) describe('GET / (SPA shell)', () => { test('serves the prerendered SPA shell by default', async () => { const { app } = TestAdmin.setup() const response = await app.request('http://admin/') expect(response.status).toBe(200) expect(response.headers.get('content-type')).toContain('text/html') const html = await response.text() expect(html).toContain('
') expect(html).not.toContain('data-theme="dark"') expect(html).toContain("localStorage.getItem('tempo-api-admin.theme')") // Standalone mount → `` so relative asset/API URLs resolve. expect(html).toContain('') }) test('inlines the gated auth config into the shell (no bootstrap round trip)', async () => { const { app } = TestAdmin.setup({ ui: { loginUrl: 'https://sso/login', logoutUrl: 'https://sso/logout' }, }) const html = await (await app.request('http://admin/')).text() const json = html.match(/window\.__TEMPO_API_CONFIG__=({.*?})<\/script>/)?.[1] expect(json).toBeDefined() expect(JSON.parse(json!)).toEqual({ auth: { identity: { email: 'admin@tempo.xyz' }, loginUrl: 'https://sso/login', logoutUrl: 'https://sso/logout', }, }) }) test('HTML navigation to a data-route path (e.g. /api-keys) serves the shell', async () => { const { app } = TestAdmin.setup() // `/api-keys` is both an SPA route and a data endpoint; a browser hard // reload (Accept: text/html) must get the shell, not the JSON list. const response = await app.request('http://admin/api-keys', { headers: { Accept: 'text/html,application/xhtml+xml' }, }) expect(response.status).toBe(200) expect(response.headers.get('content-type')).toContain('text/html') expect(await response.text()).toContain('
') }) test('fetch/RPC to a data-route path still returns JSON (not the shell)', async () => { const { app } = TestAdmin.setup() const response = await app.request('http://admin/api-keys') expect(response.headers.get('content-type')).toContain('application/json') expect((await response.json()) as { data: unknown[] }).toEqual({ data: [] }) }) test('any non-API GET falls back to the shell (client-side routing)', async () => { const { app } = TestAdmin.setup() const response = await app.request('http://admin/verified-tokens-ui') expect(response.status).toBe(200) expect(await response.text()).toContain('
') }) test('path-mounted: shell gets a `` of the mount root', async () => { const { app } = TestAdmin.setup() const api = new Hono().route('/admin', app) const response = await api.request('http://host/admin/some/deep/link') expect(response.status).toBe(200) // The base href is the mount root, not the deep link, so assets/API // resolve under `/admin/` from any in-app route. expect(await response.text()).toContain('') }) test('ui: false → no shell; `/` returns the JSON 404', async () => { const { app } = TestAdmin.setup({ ui: false }) const response = await app.request('http://admin/') expect(response.status).toBe(404) expect(((await response.json()) as { error: { code: string } }).error.code).toBe('not_found') }) }) describe('GET /assets/* (SPA assets)', () => { test('serves a hashed asset with an immutable cache header', async () => { const { app } = TestAdmin.setup() // Drive the asset name from the generated shell so the test survives UI // churn (hashes change on every build). const shell = await (await app.request('http://admin/')).text() const assetPath = shell.match(/\.\/(assets\/[^"]+\.js)/)?.[1] expect(assetPath).toBeDefined() const response = await app.request(`http://admin/${assetPath}`) expect(response.status).toBe(200) expect(response.headers.get('content-type')).toContain('text/javascript') expect(response.headers.get('cache-control')).toContain('immutable') }) test('unknown asset → JSON 404', async () => { const { app } = TestAdmin.setup() const response = await app.request('http://admin/assets/does-not-exist.js') expect(response.status).toBe(404) }) }) describe('not found', () => { test('ui: false → unknown routes get the JSON 404 envelope', async () => { const { app } = TestAdmin.setup({ ui: false }) const response = await app.request('http://admin/nope') const body = (await response.json()) as { error: { code: string } } expect(response.status).toBe(404) expect(body.error.code).toBe('not_found') }) test('non-GET unknown routes get the JSON 404 envelope (shell only serves GET)', async () => { const { app } = TestAdmin.setup() const response = await app.request('http://admin/nope', { method: 'POST' }) const body = (await response.json()) as { error: { code: string } } expect(response.status).toBe(404) expect(body.error.code).toBe('not_found') }) }) })