2026-06-26 17:48:24 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Drawings 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('Drawings API', () => {
|
|
|
|
|
|
let app: FastifyInstance;
|
|
|
|
|
|
let db: SqliteAdapter;
|
|
|
|
|
|
let projectId: string;
|
|
|
|
|
|
let createdDrawingId: string;
|
2026-07-04 16:43:55 +02:00
|
|
|
|
let authToken: string;
|
2026-06-26 17:48:24 +02:00
|
|
|
|
|
|
|
|
|
|
beforeAll(async () => {
|
|
|
|
|
|
db = new SqliteAdapter(':memory:');
|
|
|
|
|
|
await db.init();
|
|
|
|
|
|
app = await createServer({ db, port: 0 });
|
|
|
|
|
|
await app.ready();
|
|
|
|
|
|
|
2026-07-04 16:43:55 +02:00
|
|
|
|
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;
|
|
|
|
|
|
|
2026-06-26 17:48:24 +02:00
|
|
|
|
// Create a project first (drawings belong to projects)
|
|
|
|
|
|
const projectRes = await app.inject({
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
url: '/api/projects',
|
2026-07-04 16:43:55 +02:00
|
|
|
|
headers: { authorization: `Bearer ${authToken}` },
|
2026-06-26 17:48:24 +02:00
|
|
|
|
payload: { name: 'Drawings Test Project' },
|
|
|
|
|
|
});
|
|
|
|
|
|
projectId = JSON.parse(projectRes.body).id;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
|
|
await app.close();
|
|
|
|
|
|
db.close();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-04 16:43:55 +02:00
|
|
|
|
// ─── Auth Check ──────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe('Authentication', () => {
|
|
|
|
|
|
it('should return 401 without authorization header', async () => {
|
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
url: `/api/projects/${projectId}/drawings`,
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(response.statusCode).toBe(401);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-26 17:48:24 +02:00
|
|
|
|
// ─── Create ─────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe('POST /api/projects/:projectId/drawings', () => {
|
|
|
|
|
|
it('should create a drawing with 201', async () => {
|
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
url: `/api/projects/${projectId}/drawings`,
|
2026-07-04 16:43:55 +02:00
|
|
|
|
headers: { authorization: `Bearer ${authToken}` },
|
2026-06-26 17:48:24 +02:00
|
|
|
|
payload: { name: 'Ground Floor' },
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(response.statusCode).toBe(201);
|
|
|
|
|
|
const body = JSON.parse(response.body);
|
|
|
|
|
|
expect(body.id).toBeDefined();
|
|
|
|
|
|
expect(body.name).toBe('Ground Floor');
|
|
|
|
|
|
expect(body.project_id).toBe(projectId);
|
|
|
|
|
|
createdDrawingId = body.id;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should create a drawing with default name', async () => {
|
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
url: `/api/projects/${projectId}/drawings`,
|
2026-07-04 16:43:55 +02:00
|
|
|
|
headers: { authorization: `Bearer ${authToken}` },
|
2026-06-26 17:48:24 +02:00
|
|
|
|
payload: {},
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(response.statusCode).toBe(201);
|
|
|
|
|
|
const body = JSON.parse(response.body);
|
|
|
|
|
|
expect(body.name).toBe('Unbenannt');
|
|
|
|
|
|
expect(body.project_id).toBe(projectId);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── List ───────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe('GET /api/projects/:projectId/drawings', () => {
|
|
|
|
|
|
it('should list drawings for a project', async () => {
|
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
url: `/api/projects/${projectId}/drawings`,
|
2026-07-04 16:43:55 +02:00
|
|
|
|
headers: { authorization: `Bearer ${authToken}` },
|
2026-06-26 17:48:24 +02:00
|
|
|
|
});
|
|
|
|
|
|
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 project with no drawings', async () => {
|
|
|
|
|
|
// Create a new empty project
|
|
|
|
|
|
const projRes = await app.inject({
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
url: '/api/projects',
|
2026-07-04 16:43:55 +02:00
|
|
|
|
headers: { authorization: `Bearer ${authToken}` },
|
2026-06-26 17:48:24 +02:00
|
|
|
|
payload: { name: 'Empty Project' },
|
|
|
|
|
|
});
|
|
|
|
|
|
const emptyProjId = JSON.parse(projRes.body).id;
|
|
|
|
|
|
|
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
url: `/api/projects/${emptyProjId}/drawings`,
|
2026-07-04 16:43:55 +02:00
|
|
|
|
headers: { authorization: `Bearer ${authToken}` },
|
2026-06-26 17:48:24 +02:00
|
|
|
|
});
|
|
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
|
|
|
|
const body = JSON.parse(response.body);
|
|
|
|
|
|
expect(Array.isArray(body)).toBe(true);
|
|
|
|
|
|
expect(body.length).toBe(0);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Get ────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe('GET /api/drawings/:id', () => {
|
|
|
|
|
|
it('should get a single drawing', async () => {
|
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
url: `/api/drawings/${createdDrawingId}`,
|
2026-07-04 16:43:55 +02:00
|
|
|
|
headers: { authorization: `Bearer ${authToken}` },
|
2026-06-26 17:48:24 +02:00
|
|
|
|
});
|
|
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
|
|
|
|
const body = JSON.parse(response.body);
|
|
|
|
|
|
expect(body.id).toBe(createdDrawingId);
|
|
|
|
|
|
expect(body.name).toBe('Ground Floor');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should return 404 for non-existent drawing', async () => {
|
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
url: '/api/drawings/non-existent-id',
|
2026-07-04 16:43:55 +02:00
|
|
|
|
headers: { authorization: `Bearer ${authToken}` },
|
2026-06-26 17:48:24 +02:00
|
|
|
|
});
|
|
|
|
|
|
expect(response.statusCode).toBe(404);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Update ─────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe('PATCH /api/drawings/:id', () => {
|
|
|
|
|
|
it('should update drawing name', async () => {
|
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
|
url: `/api/drawings/${createdDrawingId}`,
|
2026-07-04 16:43:55 +02:00
|
|
|
|
headers: { authorization: `Bearer ${authToken}` },
|
2026-06-26 17:48:24 +02:00
|
|
|
|
payload: { name: 'First Floor' },
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
|
|
|
|
const body = JSON.parse(response.body);
|
|
|
|
|
|
expect(body.name).toBe('First Floor');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should update drawing data_json', async () => {
|
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
|
url: `/api/drawings/${createdDrawingId}`,
|
2026-07-04 16:43:55 +02:00
|
|
|
|
headers: { authorization: `Bearer ${authToken}` },
|
2026-06-26 17:48:24 +02:00
|
|
|
|
payload: { data_json: '{"layers":[]}' },
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
|
|
|
|
const body = JSON.parse(response.body);
|
|
|
|
|
|
expect(body.data_json).toBe('{"layers":[]}');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should return 404 for non-existent drawing update', async () => {
|
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
|
url: '/api/drawings/non-existent-id',
|
2026-07-04 16:43:55 +02:00
|
|
|
|
headers: { authorization: `Bearer ${authToken}` },
|
2026-06-26 17:48:24 +02:00
|
|
|
|
payload: { name: 'New Name' },
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(response.statusCode).toBe(404);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Delete ─────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe('DELETE /api/drawings/:id', () => {
|
|
|
|
|
|
it('should delete a drawing', async () => {
|
|
|
|
|
|
// Create a drawing to delete
|
|
|
|
|
|
const createRes = await app.inject({
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
url: `/api/projects/${projectId}/drawings`,
|
2026-07-04 16:43:55 +02:00
|
|
|
|
headers: { authorization: `Bearer ${authToken}` },
|
2026-06-26 17:48:24 +02:00
|
|
|
|
payload: { name: 'To Be Deleted' },
|
|
|
|
|
|
});
|
|
|
|
|
|
const drawingId = JSON.parse(createRes.body).id;
|
|
|
|
|
|
|
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
|
url: `/api/drawings/${drawingId}`,
|
2026-07-04 16:43:55 +02:00
|
|
|
|
headers: { authorization: `Bearer ${authToken}` },
|
2026-06-26 17:48:24 +02:00
|
|
|
|
});
|
|
|
|
|
|
expect(response.statusCode).toBe(204);
|
|
|
|
|
|
|
|
|
|
|
|
// Verify it's gone
|
|
|
|
|
|
const getRes = await app.inject({
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
url: `/api/drawings/${drawingId}`,
|
2026-07-04 16:43:55 +02:00
|
|
|
|
headers: { authorization: `Bearer ${authToken}` },
|
2026-06-26 17:48:24 +02:00
|
|
|
|
});
|
|
|
|
|
|
expect(getRes.statusCode).toBe(404);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should return 404 for non-existent drawing delete', async () => {
|
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
|
url: '/api/drawings/non-existent-id',
|
2026-07-04 16:43:55 +02:00
|
|
|
|
headers: { authorization: `Bearer ${authToken}` },
|
2026-06-26 17:48:24 +02:00
|
|
|
|
});
|
|
|
|
|
|
expect(response.statusCode).toBe(404);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|