/** * Blocks API Tests – CRUD (List / 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('Blocks API', () => { let app: FastifyInstance; let db: SqliteAdapter; let drawingId: string; let createdBlockId: string; 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; // Create project → drawing hierarchy const projRes = await app.inject({ method: 'POST', url: '/api/projects', headers: { authorization: `Bearer ${authToken}` }, payload: { name: 'Blocks Test Project' }, }); const projectId = JSON.parse(projRes.body).id; const drawRes = await app.inject({ method: 'POST', url: `/api/projects/${projectId}/drawings`, headers: { authorization: `Bearer ${authToken}` }, payload: { name: 'Blocks Test Drawing' }, }); drawingId = JSON.parse(drawRes.body).id; }); 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/drawings/${drawingId}/blocks`, }); expect(response.statusCode).toBe(401); }); }); // ─── Create ───────────────────────────────────────── describe('POST /api/drawings/:drawingId/blocks', () => { it('should create a block with 201', async () => { const response = await app.inject({ method: 'POST', url: `/api/drawings/${drawingId}/blocks`, headers: { authorization: `Bearer ${authToken}` }, payload: { name: 'Standard Table', category: 'Mobiliar', description: 'A standard round table' }, }); expect(response.statusCode).toBe(201); const body = JSON.parse(response.body); expect(body.id).toBeDefined(); expect(body.name).toBe('Standard Table'); expect(body.category).toBe('Mobiliar'); expect(body.description).toBe('A standard round table'); expect(body.drawing_id).toBe(drawingId); createdBlockId = body.id; }); it('should create a block with default values', async () => { const response = await app.inject({ method: 'POST', url: `/api/drawings/${drawingId}/blocks`, headers: { authorization: `Bearer ${authToken}` }, payload: { name: 'Minimal Block' }, }); expect(response.statusCode).toBe(201); const body = JSON.parse(response.body); expect(body.name).toBe('Minimal Block'); expect(body.category).toBe('Allgemein'); expect(body.description).toBeNull(); expect(body.elements_json).toBe('[]'); }); it('should reject block without name with 400', async () => { const response = await app.inject({ method: 'POST', url: `/api/drawings/${drawingId}/blocks`, headers: { authorization: `Bearer ${authToken}` }, payload: { category: 'Test' }, }); expect(response.statusCode).toBe(400); const body = JSON.parse(response.body); expect(body.error).toContain('Name is required'); }); it('should reject block with empty name with 400', async () => { const response = await app.inject({ method: 'POST', url: `/api/drawings/${drawingId}/blocks`, headers: { authorization: `Bearer ${authToken}` }, payload: { name: '' }, }); expect(response.statusCode).toBe(400); }); }); // ─── List ─────────────────────────────────────────── describe('GET /api/drawings/:drawingId/blocks', () => { it('should list blocks for a drawing', async () => { const response = await app.inject({ method: 'GET', url: `/api/drawings/${drawingId}/blocks`, 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); }); it('should return empty array for drawing with no blocks', async () => { const projRes = await app.inject({ method: 'POST', url: '/api/projects', headers: { authorization: `Bearer ${authToken}` }, payload: { name: 'Empty Blocks Project' }, }); const projId = JSON.parse(projRes.body).id; const drawRes = await app.inject({ method: 'POST', url: `/api/projects/${projId}/drawings`, headers: { authorization: `Bearer ${authToken}` }, payload: { name: 'Empty Drawing' }, }); const emptyDrawId = JSON.parse(drawRes.body).id; const response = await app.inject({ method: 'GET', url: `/api/drawings/${emptyDrawId}/blocks`, headers: { authorization: `Bearer ${authToken}` }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(Array.isArray(body)).toBe(true); expect(body.length).toBe(0); }); }); // ─── Update ───────────────────────────────────────── describe('PATCH /api/blocks/:id', () => { it('should update block name', async () => { const response = await app.inject({ method: 'PATCH', url: `/api/blocks/${createdBlockId}`, headers: { authorization: `Bearer ${authToken}` }, payload: { name: 'Updated Block Name' }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.name).toBe('Updated Block Name'); }); it('should update block category and description', async () => { const response = await app.inject({ method: 'PATCH', url: `/api/blocks/${createdBlockId}`, headers: { authorization: `Bearer ${authToken}` }, payload: { category: 'Bühne', description: 'Stage equipment' }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.category).toBe('Bühne'); expect(body.description).toBe('Stage equipment'); }); it('should update block elements_json', async () => { const response = await app.inject({ method: 'PATCH', url: `/api/blocks/${createdBlockId}`, headers: { authorization: `Bearer ${authToken}` }, payload: { elements_json: '[{"type":"rect"}]' }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.elements_json).toBe('[{"type":"rect"}]'); }); it('should return 404 for non-existent block update', async () => { const response = await app.inject({ method: 'PATCH', url: '/api/blocks/non-existent-id', headers: { authorization: `Bearer ${authToken}` }, payload: { name: 'New Name' }, }); expect(response.statusCode).toBe(404); }); }); // ─── Delete ───────────────────────────────────────── describe('DELETE /api/blocks/:id', () => { it('should delete a block', async () => { const createRes = await app.inject({ method: 'POST', url: `/api/drawings/${drawingId}/blocks`, headers: { authorization: `Bearer ${authToken}` }, payload: { name: 'To Be Deleted' }, }); const blockId = JSON.parse(createRes.body).id; const response = await app.inject({ method: 'DELETE', url: `/api/blocks/${blockId}`, headers: { authorization: `Bearer ${authToken}` }, }); expect(response.statusCode).toBe(204); // Verify it's gone via list const listRes = await app.inject({ method: 'GET', url: `/api/drawings/${drawingId}/blocks`, headers: { authorization: `Bearer ${authToken}` }, }); const blocks = JSON.parse(listRes.body); expect(blocks.find((b: any) => b.id === blockId)).toBeUndefined(); }); it('should return 404 for non-existent block delete', async () => { const response = await app.inject({ method: 'DELETE', url: '/api/blocks/non-existent-id', headers: { authorization: `Bearer ${authToken}` }, }); expect(response.statusCode).toBe(404); }); }); });