import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { ADMIN, createTestContext } from '../test/helpers.ts' let ctx: Awaited> beforeAll(async () => { ctx = await createTestContext() }) afterAll(() => ctx.close()) describe('first-run setup', () => { it('reports that setup is done and refuses to run again', async () => { const client = ctx.anonymous() expect((await client.get('/api/setup')).body).toEqual({ needsSetup: false }) const again = await client.post('/api/setup', { ...ADMIN, email: 'intruder@example.com' }) expect(again.status).toBe(409) }) it('signed the new admin in', async () => { const me = await ctx.admin.get('/api/auth/me') expect(me.status).toBe(200) expect(me.body.user).toMatchObject({ email: ADMIN.email, role: 'admin' }) expect(me.body.organization.name).toBe(ADMIN.organizationName) expect(me.body.user.passwordHash).toBeUndefined() }) }) describe('sign in and out', () => { it('rejects a wrong password with a generic message', async () => { const res = await ctx.anonymous().post('/api/auth/login', { email: ADMIN.email, password: 'nope' }) expect(res.status).toBe(401) expect(res.body.error).toBe('Incorrect email or password') }) it('treats unknown emails the same as wrong passwords', async () => { const res = await ctx.anonymous().post('/api/auth/login', { email: 'nobody@example.com', password: 'whatever' }) expect(res.status).toBe(401) expect(res.body.error).toBe('Incorrect email or password') }) it('accepts email in any case and sets an http-only cookie', async () => { const client = ctx.anonymous() const res = await client.post('/api/auth/login', { email: ADMIN.email.toUpperCase(), password: ADMIN.password }) expect(res.status).toBe(200) expect(res.headers.get('set-cookie')).toMatch(/HttpOnly/) expect(res.headers.get('set-cookie')).toMatch(/SameSite=Lax/) expect((await client.get('/api/auth/me')).status).toBe(200) expect((await client.post('/api/auth/logout')).status).toBe(204) expect((await client.get('/api/auth/me')).status).toBe(401) }) it('invalidates the session server-side on logout, even if the cookie is replayed', async () => { const client = ctx.anonymous() await client.post('/api/auth/login', { email: ADMIN.email, password: ADMIN.password }) const stolen = client.cookie await client.post('/api/auth/logout') client.cookie = stolen expect((await client.get('/api/auth/me')).status).toBe(401) }) it('rate-limits repeated attempts for one email', async () => { const client = ctx.anonymous() const email = 'ratelimit@example.com' for (let i = 0; i < 10; i++) expect((await client.post('/api/auth/login', { email, password: 'x' })).status).toBe(401) expect((await client.post('/api/auth/login', { email, password: 'x' })).status).toBe(429) }) it('blocks deactivated users and ends their sessions', async () => { const worker = await ctx.createUser('reporter', 'leaver@example.com') expect((await worker.get('/api/auth/me')).status).toBe(200) await ctx.admin.patch(`/api/users/${worker.user.id}`, { active: false }) expect((await worker.get('/api/auth/me')).status).toBe(401) expect((await ctx.anonymous().post('/api/auth/login', { email: 'leaver@example.com', password: 'a sufficiently long password' })).status).toBe(401) }) }) describe('change password', () => { it('requires the current password and signs out other sessions', async () => { const user = await ctx.createUser('supervisor', 'changer@example.com') const otherDevice = ctx.anonymous() await otherDevice.post('/api/auth/login', { email: 'changer@example.com', password: 'a sufficiently long password' }) expect((await user.post('/api/auth/password', { currentPassword: 'wrong', newPassword: 'a brand new password' })).status).toBe(400) expect((await user.post('/api/auth/password', { currentPassword: 'a sufficiently long password', newPassword: 'short' })).status).toBe(400) expect((await user.post('/api/auth/password', { currentPassword: 'a sufficiently long password', newPassword: 'a brand new password' })).status).toBe(204) expect((await user.get('/api/auth/me')).status).toBe(200) expect((await otherDevice.get('/api/auth/me')).status).toBe(401) expect((await ctx.anonymous().post('/api/auth/login', { email: 'changer@example.com', password: 'a brand new password' })).status).toBe(200) }) })