/** * Elements 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('Elements API', () => { let app: FastifyInstance; let db: SqliteAdapter; let drawingId: string; let layerId: string; let createdElementId: string; beforeAll(async () => { db = new SqliteAdapter(':memory:'); await db.init(); app = await createServer({ db, port: 0 }); await app.ready(); // Create project → drawing → layer hierarchy const projRes = await app.inject({ method: 'POST', url: '/api/projects', payload: { name: 'Elements Test Project' }, }); const projectId = JSON.parse(projRes.body).id; const drawRes = await app.inject({ method: 'POST', url: `/api/projects/${projectId}/drawings`, payload: { name: 'Elements Test Drawing' }, }); drawingId = JSON.parse(drawRes.body).id; const layerRes = await app.inject({ method: 'POST', url: `/api/drawings/${drawingId}/layers`, payload: { name: 'Elements Layer' }, }); layerId = JSON.parse(layerRes.body).id; }); afterAll(async () => { await app.close(); db.close(); }); // ─── Create ───────────────────────────────────────── describe('POST /api/drawings/:drawingId/elements', () => { it('should create an element with 201', async () => { const response = await app.inject({ method: 'POST', url: `/api/drawings/${drawingId}/elements`, payload: { layer_id: layerId, type: 'rect', x: 10, y: 20, width: 100, height: 50 }, }); expect(response.statusCode).toBe(201); const body = JSON.parse(response.body); expect(body.id).toBeDefined(); expect(body.type).toBe('rect'); expect(body.x).toBe(10); expect(body.y).toBe(20); expect(body.width).toBe(100); expect(body.height).toBe(50); expect(body.drawing_id).toBe(drawingId); expect(body.layer_id).toBe(layerId); createdElementId = body.id; }); it('should create an element with default values', async () => { const response = await app.inject({ method: 'POST', url: `/api/drawings/${drawingId}/elements`, payload: { layer_id: layerId, type: 'circle' }, }); expect(response.statusCode).toBe(201); const body = JSON.parse(response.body); expect(body.type).toBe('circle'); expect(body.x).toBe(0); expect(body.y).toBe(0); expect(body.width).toBe(0); expect(body.height).toBe(0); expect(body.properties_json).toBe('{}'); }); }); // ─── List ─────────────────────────────────────────── describe('GET /api/drawings/:drawingId/elements', () => { it('should list elements for a drawing', async () => { const response = await app.inject({ method: 'GET', url: `/api/drawings/${drawingId}/elements`, }); 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 elements', async () => { // Create a new drawing with no elements const projRes = await app.inject({ method: 'POST', url: '/api/projects', payload: { name: 'Empty Elements Project' }, }); const projId = JSON.parse(projRes.body).id; const drawRes = await app.inject({ method: 'POST', url: `/api/projects/${projId}/drawings`, payload: { name: 'Empty Drawing' }, }); const emptyDrawId = JSON.parse(drawRes.body).id; const response = await app.inject({ method: 'GET', url: `/api/drawings/${emptyDrawId}/elements`, }); 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/elements/:id', () => { it('should update element position', async () => { const response = await app.inject({ method: 'PATCH', url: `/api/elements/${createdElementId}`, payload: { x: 100, y: 200 }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.x).toBe(100); expect(body.y).toBe(200); }); it('should update element dimensions', async () => { const response = await app.inject({ method: 'PATCH', url: `/api/elements/${createdElementId}`, payload: { width: 300, height: 150 }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.width).toBe(300); expect(body.height).toBe(150); }); it('should update element properties_json', async () => { const response = await app.inject({ method: 'PATCH', url: `/api/elements/${createdElementId}`, payload: { properties_json: '{"color":"red"}' }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.properties_json).toBe('{"color":"red"}'); }); it('should return 404 for non-existent element update', async () => { const response = await app.inject({ method: 'PATCH', url: '/api/elements/non-existent-id', payload: { x: 0 }, }); expect(response.statusCode).toBe(404); }); }); // ─── Delete ───────────────────────────────────────── describe('DELETE /api/elements/:id', () => { it('should delete an element', async () => { // Create an element to delete const createRes = await app.inject({ method: 'POST', url: `/api/drawings/${drawingId}/elements`, payload: { layer_id: layerId, type: 'line' }, }); const elemId = JSON.parse(createRes.body).id; const response = await app.inject({ method: 'DELETE', url: `/api/elements/${elemId}`, }); expect(response.statusCode).toBe(204); // Verify it's gone via list const listRes = await app.inject({ method: 'GET', url: `/api/drawings/${drawingId}/elements`, }); const elements = JSON.parse(listRes.body); expect(elements.find((e: any) => e.id === elemId)).toBeUndefined(); }); it('should return 404 for non-existent element delete', async () => { const response = await app.inject({ method: 'DELETE', url: '/api/elements/non-existent-id', }); expect(response.statusCode).toBe(404); }); }); });