/** * Projects API Tests – CRUD (List / Get / Create / Update / Delete) */ 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('Projects API', () => { let app: FastifyInstance; let db: SqliteAdapter; let createdProjectId: string | null = null; let authToken: string; beforeAll(async () => { db = new SqliteAdapter(':memory:'); await db.init(); app = await createServer({ db, port: 0 }); await app.ready(); const regRes = await app.inject({ method: 'POST', url: '/api/auth/register', payload: { email: 'admin-test@example.com', password: 'Password123!', name: 'Admin Test', role: 'admin' }, }); authToken = JSON.parse(regRes.body).session.token; }); afterAll(async () => { await app.close(); db.close(); }); // ─── Auth Check ────────────────────────────────────── describe('Authentication', () => { it('should return 401 without authorization header', async () => { const response = await app.inject({ method: 'GET', url: '/api/projects', }); expect(response.statusCode).toBe(401); }); }); // ─── Create ───────────────────────────────────────── describe('POST /api/projects', () => { it('should create a project with 201', async () => { const response = await app.inject({ method: 'POST', url: '/api/projects', headers: { authorization: `Bearer ${authToken}` }, payload: { name: 'Test Project', description: 'A test project', }, }); expect(response.statusCode).toBe(201); const body = JSON.parse(response.body); expect(body.id).toBeDefined(); expect(body.name).toBe('Test Project'); expect(body.description).toBe('A test project'); createdProjectId = body.id; }); it('should reject project without name with 400', async () => { const response = await app.inject({ method: 'POST', url: '/api/projects', headers: { authorization: `Bearer ${authToken}` }, payload: { description: 'No name' }, }); expect(response.statusCode).toBe(400); }); it('should create project with default values', async () => { const response = await app.inject({ method: 'POST', url: '/api/projects', headers: { authorization: `Bearer ${authToken}` }, payload: { name: 'Minimal Project' }, }); expect(response.statusCode).toBe(201); const body = JSON.parse(response.body); expect(body.name).toBe('Minimal Project'); expect(body.description).toBeNull(); }); }); // ─── List ─────────────────────────────────────────── describe('GET /api/projects', () => { it('should list all projects', async () => { const response = await app.inject({ method: 'GET', url: '/api/projects', headers: { authorization: `Bearer ${authToken}` }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(Array.isArray(body)).toBe(true); expect(body.length).toBeGreaterThanOrEqual(2); }); }); // ─── Get ──────────────────────────────────────────── describe('GET /api/projects/:id', () => { it('should get a single project', async () => { const response = await app.inject({ method: 'GET', url: `/api/projects/${createdProjectId}`, headers: { authorization: `Bearer ${authToken}` }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.id).toBe(createdProjectId); expect(body.name).toBe('Test Project'); }); it('should return 404 for non-existent project', async () => { const response = await app.inject({ method: 'GET', url: '/api/projects/non-existent-id', headers: { authorization: `Bearer ${authToken}` }, }); expect(response.statusCode).toBe(404); }); }); // ─── Update ───────────────────────────────────────── describe('PATCH /api/projects/:id', () => { it('should update project name', async () => { const response = await app.inject({ method: 'PATCH', url: `/api/projects/${createdProjectId}`, headers: { authorization: `Bearer ${authToken}` }, payload: { name: 'Updated Project Name' }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.name).toBe('Updated Project Name'); expect(body.id).toBe(createdProjectId); }); it('should update project description', async () => { const response = await app.inject({ method: 'PATCH', url: `/api/projects/${createdProjectId}`, headers: { authorization: `Bearer ${authToken}` }, payload: { description: 'Updated description' }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.description).toBe('Updated description'); }); it('should return 404 for non-existent project update', async () => { const response = await app.inject({ method: 'PATCH', url: '/api/projects/non-existent-id', headers: { authorization: `Bearer ${authToken}` }, payload: { name: 'New Name' }, }); expect(response.statusCode).toBe(404); }); }); // ─── Delete ───────────────────────────────────────── describe('DELETE /api/projects/:id', () => { it('should delete a project', async () => { // First create a project to delete const createRes = await app.inject({ method: 'POST', url: '/api/projects', headers: { authorization: `Bearer ${authToken}` }, payload: { name: 'To Be Deleted' }, }); const projectId = JSON.parse(createRes.body).id; const response = await app.inject({ method: 'DELETE', url: `/api/projects/${projectId}`, headers: { authorization: `Bearer ${authToken}` }, }); expect(response.statusCode).toBe(204); // Verify it's gone const getRes = await app.inject({ method: 'GET', url: `/api/projects/${projectId}`, headers: { authorization: `Bearer ${authToken}` }, }); expect(getRes.statusCode).toBe(404); }); it('should return 404 for non-existent project delete', async () => { const response = await app.inject({ method: 'DELETE', url: '/api/projects/non-existent-id', headers: { authorization: `Bearer ${authToken}` }, }); expect(response.statusCode).toBe(404); }); }); });