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('