import { Hono } from 'hono'; import { describe, expect, it } from 'vitest'; import { type ApiTokenRecord, type AuthVariables, authMiddleware, hasScope, parseTokens, requireScope, } from '../../src/middleware/auth.js'; import { errorHandler } from '../../src/middleware/error-handler.js'; function buildApp(tokens: ApiTokenRecord[]): Hono<{ Variables: AuthVariables }> { const map = new Map(); for (const t of tokens) map.set(t.token, t); const app = new Hono<{ Variables: AuthVariables }>(); app.onError(errorHandler); app.use('/protected/*', authMiddleware(map)); app.get('/protected/read', requireScope('read:personnes'), (c) => c.json({ ok: true, scopes: Array.from(c.get('auth').scopes) }), ); app.get('/protected/admin', requireScope('admin:something'), (c) => c.json({ ok: true })); return app; } describe('parseTokens', () => { it('parse JSON valide', () => { const map = parseTokens( JSON.stringify([{ token: 'brg_x', name: 'a', scopes: ['read:personnes'] }]), ); expect(map.get('brg_x')?.name).toBe('a'); }); it('retourne map vide si raw vide', () => { expect(parseTokens(undefined).size).toBe(0); expect(parseTokens('').size).toBe(0); }); it('throw si JSON invalide', () => { expect(() => parseTokens('{nope')).toThrow(/JSON/); }); it('throw si pas un array', () => { expect(() => parseTokens('{"foo": 1}')).toThrow(/tableau/); }); it('throw si entree manque token/name/scopes', () => { expect(() => parseTokens('[{"token":"x"}]')).toThrow(); expect(() => parseTokens('[{"token":"x","name":"y"}]')).toThrow(); expect(() => parseTokens('[{"token":"x","name":"y","scopes":[1]}]')).toThrow(); }); }); describe('hasScope', () => { it('match exact', () => { expect(hasScope(new Set(['read:personnes']), 'read:personnes')).toBe(true); expect(hasScope(new Set(['read:personnes']), 'read:projets')).toBe(false); }); it('admin:* couvre tout', () => { expect(hasScope(new Set(['admin:*']), 'read:any')).toBe(true); expect(hasScope(new Set(['admin:*']), 'write:something')).toBe(true); }); }); describe('auth middleware — 5 cas', () => { const tokens: ApiTokenRecord[] = [ { token: 'brg_valid', name: 'demo', scopes: ['read:personnes'] }, ]; it('401 si pas de header', async () => { const app = buildApp(tokens); const res = await app.request('/protected/read'); expect(res.status).toBe(401); const body = (await res.json()) as { error: { code: string } }; expect(body.error.code).toBe('AUTH_REQUIRED'); }); it('401 si format wrong (pas Bearer)', async () => { const app = buildApp(tokens); const res = await app.request('/protected/read', { headers: { Authorization: 'Token brg_valid' }, }); expect(res.status).toBe(401); const body = (await res.json()) as { error: { code: string } }; expect(body.error.code).toBe('AUTH_INVALID'); }); it('401 si token inconnu', async () => { const app = buildApp(tokens); const res = await app.request('/protected/read', { headers: { Authorization: 'Bearer brg_unknown' }, }); expect(res.status).toBe(401); const body = (await res.json()) as { error: { code: string } }; expect(body.error.code).toBe('AUTH_INVALID'); }); it('403 si scope manquant', async () => { const app = buildApp(tokens); const res = await app.request('/protected/admin', { headers: { Authorization: 'Bearer brg_valid' }, }); expect(res.status).toBe(403); const body = (await res.json()) as { error: { code: string } }; expect(body.error.code).toBe('FORBIDDEN_SCOPE'); }); it('200 si token + scope OK', async () => { const app = buildApp(tokens); const res = await app.request('/protected/read', { headers: { Authorization: 'Bearer brg_valid' }, }); expect(res.status).toBe(200); const body = (await res.json()) as { ok: boolean; scopes: string[] }; expect(body.ok).toBe(true); expect(body.scopes).toContain('read:personnes'); }); });