import type { FastifyInstance } from 'fastify'; import type { Pool } from 'pg'; import { afterAll, beforeEach, describe, expect, it } from 'vitest'; import { createAdmin } from '../src/auth/admin.ts'; import { processEvents } from '../src/pipeline/process.ts'; import { makeApp } from './helpers/app.ts'; import { closeTestPool, getTestPool, resetDb } from './helpers/db.ts'; import { ALICE, ev } from './helpers/events.ts'; import { signedRequest } from './helpers/sign.ts'; let pool: Pool; let app: FastifyInstance; let token: string; beforeEach(async () => { await app?.close(); pool = await getTestPool(); await resetDb(pool); ({ app, token } = await makeApp(pool)); await createAdmin(pool, 'lucas', 'correct horse battery staple'); await processEvents(pool, [ev('player_join', { ...ALICE })], 'push', 'atm10'); }); afterAll(async () => { await app?.close(); await closeTestPool(); }); async function login(password = 'correct horse battery staple') { const res = await app.inject({ method: 'POST', url: '/api/admin/login', payload: { username: 'lucas', password } }); const cookie = res.cookies.find((c) => c.name === 'atm_admin'); return { res, cookie: cookie ? `atm_admin=${cookie.value}` : '', csrf: res.statusCode === 200 ? res.json().csrf : '' }; } describe('admin', () => { it('login with wrong password 401', async () => { const { res } = await login('wrong'); expect(res.statusCode).toBe(401); }); it('login sets an httpOnly strict cookie and the session endpoint works', async () => { const { res, cookie, csrf } = await login(); expect(res.statusCode).toBe(200); const raw = res.cookies.find((c) => c.name === 'atm_admin')!; expect(raw.httpOnly).toBe(true); expect(raw.sameSite).toBe('Strict'); const s = await app.inject({ method: 'GET', url: '/api/admin/session', headers: { cookie } }); expect(s.json()).toEqual({ username: 'lucas', csrf }); }); it('admin routes require a session', async () => { expect((await app.inject({ method: 'GET', url: '/api/admin/status' })).statusCode).toBe(401); }); it('mutating route without csrf 403', async () => { const { cookie } = await login(); const res = await app.inject({ method: 'POST', url: `/api/admin/players/${ALICE.uuid}/hidden`, headers: { cookie }, payload: { hidden: true }, }); expect(res.statusCode).toBe(403); }); it('hide player then profile 404, unhide restores it', async () => { const { cookie, csrf } = await login(); const hide = (hidden: boolean) => app.inject({ method: 'POST', url: `/api/admin/players/${ALICE.uuid}/hidden`, headers: { cookie, 'x-csrf-token': csrf }, payload: { hidden }, }); expect((await hide(true)).statusCode).toBe(200); expect((await app.inject({ method: 'GET', url: `/api/players/${ALICE.uuid}` })).statusCode).toBe(404); const list = await app.inject({ method: 'GET', url: '/api/admin/players', headers: { cookie } }); expect(list.json().players[0]).toMatchObject({ uuid: ALICE.uuid, hidden: true }); await hide(false); expect((await app.inject({ method: 'GET', url: `/api/players/${ALICE.uuid}` })).statusCode).toBe(200); }); it('rotate token invalidates old token for ingest', async () => { const { cookie, csrf } = await login(); const rot = await app.inject({ method: 'POST', url: '/api/admin/ingest-token/rotate', headers: { cookie, 'x-csrf-token': csrf } }); expect(rot.statusCode).toBe(200); const fresh = rot.json().token as string; const send = (t: string) => { const { headers, payload } = signedRequest({ protocol: 1, events: [ev('server_stop', {})] }, { token: t }); return app.inject({ method: 'POST', url: '/api/ingest', headers, payload }); }; expect((await send(token)).statusCode).toBe(401); expect((await send(fresh)).statusCode).toBe(200); }); it('status lists errors and a sync request is queued', async () => { const { cookie, csrf } = await login(); await pool.query(`INSERT INTO app_errors(source, message) VALUES ('sftp', 'connection refused')`); const status = await app.inject({ method: 'GET', url: '/api/admin/status', headers: { cookie } }); expect(status.json().lastErrors[0]).toMatchObject({ source: 'sftp', message: 'connection refused' }); const sync = await app.inject({ method: 'POST', url: '/api/admin/sync', headers: { cookie, 'x-csrf-token': csrf } }); expect(sync.statusCode).toBe(202); expect((await pool.query('SELECT count(*)::int AS n FROM sync_requests WHERE done_at IS NULL')).rows[0].n).toBe(1); }); it('logout ends the session', async () => { const { cookie, csrf } = await login(); await app.inject({ method: 'POST', url: '/api/admin/logout', headers: { cookie, 'x-csrf-token': csrf } }); expect((await app.inject({ method: 'GET', url: '/api/admin/status', headers: { cookie } })).statusCode).toBe(401); }); });