import { describe, expect, it } from 'vitest' import { createWebauthnCredential } from '@authportal/db' import { adminToken, buildTestApp } from './helpers.js' describe('webauthn / passkeys', () => { it('exposes webauthn flag on auth config', async () => { const app = await buildTestApp() const res = await app.inject({ method: 'GET', url: '/api/v1/auth/config' }) expect(res.statusCode).toBe(200) expect(res.json()).toMatchObject({ webauthn: true }) expect(app.config.webauthnRpID).toBe('auth.test.local') expect(app.config.webauthnOrigins).toContain('https://auth.test.local') expect(app.config.webauthnOrigins).toContain('http://localhost:5173') await app.close() }) it('requires JWT for register options', async () => { const app = await buildTestApp() const denied = await app.inject({ method: 'POST', url: '/api/v1/webauthn/register/options', }) expect(denied.statusCode).toBe(401) const token = await adminToken(app) const ok = await app.inject({ method: 'POST', url: '/api/v1/webauthn/register/options', headers: { authorization: `Bearer ${token}` }, }) expect(ok.statusCode).toBe(200) const body = ok.json() as { challenge_id: string options: { challenge: string; rp: { id: string } } } expect(body.challenge_id).toBeTruthy() expect(body.options.challenge).toBeTruthy() expect(body.options.rp.id).toBe('auth.test.local') await app.close() }) it('allows public login options and rejects a bogus assertion', async () => { const app = await buildTestApp() const options = await app.inject({ method: 'POST', url: '/api/v1/webauthn/login/options', }) expect(options.statusCode).toBe(200) const body = options.json() as { challenge_id: string; options: unknown } expect(body.challenge_id).toBeTruthy() const login = await app.inject({ method: 'POST', url: '/api/v1/webauthn/login', payload: { challenge_id: body.challenge_id, response: { id: 'not-a-credential', type: 'public-key' }, }, }) expect(login.statusCode).toBe(401) await app.close() }) it('lists, deletes own credentials and reports passkey_count', async () => { const app = await buildTestApp() const token = await adminToken(app) const me = await app.inject({ method: 'GET', url: '/api/v1/auth/me', headers: { authorization: `Bearer ${token}` }, }) const userId = (me.json() as { id: string }).id createWebauthnCredential(app.db, { userId, credentialId: 'dGVzdC1jcmVkLWlk', publicKey: 'dGVzdC1wdWJrZXk', counter: 0, name: 'Test key', }) const listed = await app.inject({ method: 'GET', url: '/api/v1/webauthn/credentials', headers: { authorization: `Bearer ${token}` }, }) expect(listed.statusCode).toBe(200) const creds = listed.json() as { id: string; name: string }[] expect(creds).toHaveLength(1) expect(creds[0]?.name).toBe('Test key') const users = await app.inject({ method: 'GET', url: '/api/v1/admin/users', headers: { authorization: `Bearer ${token}` }, }) const admin = ( users.json() as { email: string; passkey_count: number }[] ).find((u) => u.email === 'admin@test.local') expect(admin?.passkey_count).toBe(1) const renamed = await app.inject({ method: 'PATCH', url: `/api/v1/webauthn/credentials/${creds[0]!.id}`, headers: { authorization: `Bearer ${token}` }, payload: { name: 'Laptop' }, }) expect(renamed.statusCode).toBe(200) expect((renamed.json() as { name: string }).name).toBe('Laptop') const deleted = await app.inject({ method: 'DELETE', url: `/api/v1/webauthn/credentials/${creds[0]!.id}`, headers: { authorization: `Bearer ${token}` }, }) expect(deleted.statusCode).toBe(200) const empty = await app.inject({ method: 'GET', url: '/api/v1/webauthn/credentials', headers: { authorization: `Bearer ${token}` }, }) expect(empty.json()).toEqual([]) await app.close() }) it('lets admin reset a user passkeys', async () => { const app = await buildTestApp() const token = await adminToken(app) const me = await app.inject({ method: 'GET', url: '/api/v1/auth/me', headers: { authorization: `Bearer ${token}` }, }) const userId = (me.json() as { id: string }).id createWebauthnCredential(app.db, { userId, credentialId: 'cmVzZXQta2V5', publicKey: 'cHVia2V5', counter: 1, name: 'To reset', }) const reset = await app.inject({ method: 'DELETE', url: `/api/v1/admin/users/${userId}/passkeys`, headers: { authorization: `Bearer ${token}` }, }) expect(reset.statusCode).toBe(200) expect(reset.json()).toMatchObject({ ok: true, removed: 1 }) const listed = await app.inject({ method: 'GET', url: '/api/v1/webauthn/credentials', headers: { authorization: `Bearer ${token}` }, }) expect(listed.json()).toEqual([]) await app.close() }) })