255 lines
9.1 KiB
TypeScript
255 lines
9.1 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|
||
});
|
||
});
|