import { describe, it, expect } from 'vitest' import { buildTestApp, setupAdmin, getCookie, auth, loginAs } from './helpers.js' describe('auth routes', () => { it('setup creates admin when no users exist, then is closed', async () => { const app = await buildTestApp() const status = await app.inject({ method: 'GET', url: '/api/setup' }) expect(status.json()).toEqual({ needed: true }) const cookie = await setupAdmin(app) const me = await app.inject({ method: 'GET', url: '/api/me', ...auth(cookie) }) expect(me.statusCode).toBe(200) expect(me.json()).toEqual({ user: { id: 1, username: 'admin', isAdmin: true } }) const again = await app.inject({ method: 'POST', url: '/api/setup', payload: { username: 'x', password: 'password123' }, }) expect(again.statusCode).toBe(403) await app.close() }) it('setup validates input', async () => { const app = await buildTestApp() const bad = await app.inject({ method: 'POST', url: '/api/setup', payload: { username: 'ab', password: 'short' }, }) expect(bad.statusCode).toBe(400) await app.close() }) it('login and logout', async () => { const app = await buildTestApp() await setupAdmin(app) const login = await app.inject({ method: 'POST', url: '/api/login', payload: { username: 'admin', password: 'adminpass123' }, }) expect(login.statusCode).toBe(200) const cookie = getCookie(login) const me = await app.inject({ method: 'GET', url: '/api/me', ...auth(cookie) }) expect(me.statusCode).toBe(200) await app.inject({ method: 'POST', url: '/api/logout', ...auth(cookie) }) const after = await app.inject({ method: 'GET', url: '/api/me', ...auth(cookie) }) expect(after.statusCode).toBe(401) await app.close() }) it('login rejects wrong password', async () => { const app = await buildTestApp() await setupAdmin(app) const res = await app.inject({ method: 'POST', url: '/api/login', payload: { username: 'admin', password: 'wrongpass123' }, }) expect(res.statusCode).toBe(401) await app.close() }) it('protected route requires auth', async () => { const app = await buildTestApp() const res = await app.inject({ method: 'GET', url: '/api/me' }) expect(res.statusCode).toBe(401) await app.close() }) it('re-login invalidates the previous session', async () => { const app = await buildTestApp() await setupAdmin(app) const first = await app.inject({ method: 'POST', url: '/api/login', payload: { username: 'admin', password: 'adminpass123' }, }) const firstCookie = getCookie(first) const second = await app.inject({ method: 'POST', url: '/api/login', ...auth(firstCookie), payload: { username: 'admin', password: 'adminpass123' }, }) expect(second.statusCode).toBe(200) const oldStale = await app.inject({ method: 'GET', url: '/api/me', ...auth(firstCookie) }) expect(oldStale.statusCode).toBe(401) const newCookie = getCookie(second) const fresh = await app.inject({ method: 'GET', url: '/api/me', ...auth(newCookie) }) expect(fresh.statusCode).toBe(200) await app.close() }) }) describe('user admin', () => { async function adminApp() { const app = await buildTestApp() const cookie = await setupAdmin(app) return { app, cookie } } it('admin creates a user and lists users', async () => { const { app, cookie } = await adminApp() const created = await app.inject({ method: 'POST', url: '/api/users', ...auth(cookie), payload: { username: 'bob', password: 'bobpass123' }, }) expect(created.statusCode).toBe(200) expect(created.json()).toEqual({ id: 2, username: 'bob', isAdmin: false }) const list = await app.inject({ method: 'GET', url: '/api/users', ...auth(cookie) }) expect(list.json().users.map((u: { username: string }) => u.username)).toEqual(['admin', 'bob']) await app.close() }) it('non-admin cannot list or create users', async () => { const { app, cookie } = await adminApp() const bobCookie = await loginAs(app, cookie, 'bob', 'bobpass123') const list = await app.inject({ method: 'GET', url: '/api/users', ...auth(bobCookie) }) expect(list.statusCode).toBe(403) const create = await app.inject({ method: 'POST', url: '/api/users', ...auth(bobCookie), payload: { username: 'eve', password: 'evepass123' }, }) expect(create.statusCode).toBe(403) await app.close() }) it('cannot delete self; deleting another user works and cascades settings', async () => { const { app, cookie } = await adminApp() await loginAs(app, cookie, 'bob', 'bobpass123') const settingsCount = () => (app.db.prepare('SELECT COUNT(*) AS n FROM settings WHERE user_id = 2').get() as { n: number }).n const sessionsCount = () => (app.db.prepare('SELECT COUNT(*) AS n FROM sessions WHERE user_id = 2').get() as { n: number }).n expect(settingsCount()).toBe(1) expect(sessionsCount()).toBe(1) const selfDelete = await app.inject({ method: 'DELETE', url: '/api/users/1', ...auth(cookie) }) expect(selfDelete.statusCode).toBe(400) const del = await app.inject({ method: 'DELETE', url: '/api/users/2', ...auth(cookie) }) expect(del.statusCode).toBe(200) expect(settingsCount()).toBe(0) expect(sessionsCount()).toBe(0) const list = await app.inject({ method: 'GET', url: '/api/users', ...auth(cookie) }) expect(list.json().users).toHaveLength(1) await app.close() }) it('rejects duplicate username', async () => { const { app, cookie } = await adminApp() await app.inject({ method: 'POST', url: '/api/users', ...auth(cookie), payload: { username: 'bob', password: 'bobpass123' }, }) const again = await app.inject({ method: 'POST', url: '/api/users', ...auth(cookie), payload: { username: 'bob', password: 'otherpass123' }, }) expect(again.statusCode).toBe(409) await app.close() }) })