/** * Users API Tests – List / Get / Update / Delete (password_hash stripping) */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import type { FastifyInstance } from 'fastify'; import { SqliteAdapter } from '../src/database/SqliteAdapter.js'; import { createServer } from '../src/server.js'; describe('Users API', () => { let app: FastifyInstance; let db: SqliteAdapter; let testUserId: string; beforeAll(async () => { db = new SqliteAdapter(':memory:'); await db.init(); app = await createServer({ db, port: 0 }); await app.ready(); // Create a test user directly via DB (bypassing AuthService) const user = db.createUser({ email: 'test-admin@example.com', password_hash: 'fake-hash-for-testing', name: 'Test Admin', role: 'admin', }); testUserId = user.id; }); afterAll(async () => { await app.close(); db.close(); }); // ─── List ─────────────────────────────────────────── describe('GET /api/users', () => { it('should list all users', async () => { const response = await app.inject({ method: 'GET', url: '/api/users', }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(Array.isArray(body)).toBe(true); expect(body.length).toBeGreaterThanOrEqual(2); // default user + test user }); it('should strip password_hash from list responses', async () => { const response = await app.inject({ method: 'GET', url: '/api/users', }); const body = JSON.parse(response.body); for (const user of body) { expect(user.password_hash).toBeUndefined(); } }); }); // ─── Get ──────────────────────────────────────────── describe('GET /api/users/:id', () => { it('should get a single user', async () => { const response = await app.inject({ method: 'GET', url: `/api/users/${testUserId}`, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.id).toBe(testUserId); expect(body.email).toBe('test-admin@example.com'); expect(body.name).toBe('Test Admin'); expect(body.role).toBe('admin'); }); it('should strip password_hash from single user response', async () => { const response = await app.inject({ method: 'GET', url: `/api/users/${testUserId}`, }); const body = JSON.parse(response.body); expect(body.password_hash).toBeUndefined(); }); it('should return 404 for non-existent user', async () => { const response = await app.inject({ method: 'GET', url: '/api/users/non-existent-id', }); expect(response.statusCode).toBe(404); }); }); // ─── Update ───────────────────────────────────────── describe('PATCH /api/users/:id', () => { it('should update user name', async () => { const response = await app.inject({ method: 'PATCH', url: `/api/users/${testUserId}`, payload: { name: 'Updated Name' }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.name).toBe('Updated Name'); expect(body.id).toBe(testUserId); }); it('should update user role', async () => { const response = await app.inject({ method: 'PATCH', url: `/api/users/${testUserId}`, payload: { role: 'planer' }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.role).toBe('planer'); }); it('should strip password_hash from update response', async () => { const response = await app.inject({ method: 'PATCH', url: `/api/users/${testUserId}`, payload: { name: 'Another Name' }, }); const body = JSON.parse(response.body); expect(body.password_hash).toBeUndefined(); }); it('should not update password_hash via PATCH endpoint', async () => { const response = await app.inject({ method: 'PATCH', url: `/api/users/${testUserId}`, payload: { password_hash: 'hacked-hash' }, }); expect(response.statusCode).toBe(200); // Verify the password_hash was NOT changed in DB const dbUser = db.getUser(testUserId); expect(dbUser!.password_hash).toBe('fake-hash-for-testing'); }); it('should return 404 for non-existent user update', async () => { const response = await app.inject({ method: 'PATCH', url: '/api/users/non-existent-id', payload: { name: 'New Name' }, }); expect(response.statusCode).toBe(404); }); }); // ─── Delete ───────────────────────────────────────── describe('DELETE /api/users/:id', () => { it('should delete a user', async () => { // Create a user to delete const user = db.createUser({ email: 'delete-me@example.com', password_hash: 'temp-hash', name: 'Delete Me', role: 'planer', }); const response = await app.inject({ method: 'DELETE', url: `/api/users/${user.id}`, }); expect(response.statusCode).toBe(204); // Verify it's gone const getRes = await app.inject({ method: 'GET', url: `/api/users/${user.id}`, }); expect(getRes.statusCode).toBe(404); }); it('should return 404 for non-existent user delete', async () => { const response = await app.inject({ method: 'DELETE', url: '/api/users/non-existent-id', }); expect(response.statusCode).toBe(404); }); }); });