Files
open-safetylog-ehs/server/routes/auth.test.ts
T
jayjennings c69eccb5c1
CI / Lint, typecheck, test, build (push) Canceled after 0s
CI / End-to-end tests (push) Canceled after 0s
feat: implement incident reporting and management system
- Add incident reporting routes and logic to handle creation, updating, and retrieval of incidents.
- Implement visibility rules for users based on roles (reporters, supervisors).
- Create tests for incident reporting functionality to ensure correctness and reliability.
- Introduce site and area management routes, including validation for area hierarchy.
- Establish user management routes with role-based access control for creating and updating users.
- Add setup routes for initializing the application with an organization and admin user.
- Implement shared enums and schemas for consistent validation and data handling across the application.
- Create a test helper for setting up an in-memory database and API client for testing.
- Add TypeScript configuration for server-side code with strict type checking and module resolution.
2026-10-06 10:51:18 -07:00

92 lines
4.4 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { ADMIN, createTestContext } from '../test/helpers.ts'
let ctx: Awaited<ReturnType<typeof createTestContext>>
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)
})
})