Files
web-cad/backend/tests/globalBlocks.test.ts
T

427 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Global Blocks API Tests CRUD for folders and blocks
*/
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('Global Blocks API', () => {
let app: FastifyInstance;
let db: SqliteAdapter;
let authToken: string;
let createdFolderId: string;
let subFolderId: string;
let createdBlockId: 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: 'global-test@example.com', password: 'Password123!', name: 'Global 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 for folders', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/global-folders',
});
expect(response.statusCode).toBe(401);
});
it('should return 401 without authorization header for blocks', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/global-blocks',
});
expect(response.statusCode).toBe(401);
});
});
// ─── Folder CRUD ─────────────────────────────────────
describe('POST /api/global-folders', () => {
it('should create a root folder with 201', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/global-folders',
headers: { authorization: `Bearer ${authToken}` },
payload: { name: 'Möbel' },
});
expect(response.statusCode).toBe(201);
const body = JSON.parse(response.body);
expect(body.id).toBeDefined();
expect(body.name).toBe('Möbel');
expect(body.parent_id).toBeNull();
createdFolderId = body.id;
});
it('should create a sub-folder with parent_id', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/global-folders',
headers: { authorization: `Bearer ${authToken}` },
payload: { name: 'Stühle', parent_id: createdFolderId },
});
expect(response.statusCode).toBe(201);
const body = JSON.parse(response.body);
expect(body.name).toBe('Stühle');
expect(body.parent_id).toBe(createdFolderId);
subFolderId = body.id;
});
it('should reject folder without name with 400', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/global-folders',
headers: { authorization: `Bearer ${authToken}` },
payload: {},
});
expect(response.statusCode).toBe(400);
const body = JSON.parse(response.body);
expect(body.error).toContain('Name is required');
});
it('should reject folder with empty name with 400', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/global-folders',
headers: { authorization: `Bearer ${authToken}` },
payload: { name: '' },
});
expect(response.statusCode).toBe(400);
});
});
describe('GET /api/global-folders', () => {
it('should list all folders', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/global-folders',
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 list root folders (parentId=null)', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/global-folders?parentId=null',
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
expect(Array.isArray(body)).toBe(true);
expect(body.every((f: { parent_id: string | null }) => f.parent_id === null)).toBe(true);
});
it('should list sub-folders for a parent', async () => {
const response = await app.inject({
method: 'GET',
url: `/api/global-folders?parentId=${createdFolderId}`,
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
expect(body.length).toBeGreaterThanOrEqual(1);
expect(body[0].parent_id).toBe(createdFolderId);
});
});
describe('PATCH /api/global-folders/:id', () => {
it('should rename a folder', async () => {
const response = await app.inject({
method: 'PATCH',
url: `/api/global-folders/${createdFolderId}`,
headers: { authorization: `Bearer ${authToken}` },
payload: { name: 'Möbel & Ausstattung' },
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
expect(body.name).toBe('Möbel & Ausstattung');
});
it('should reject setting parent to self with 400', async () => {
const response = await app.inject({
method: 'PATCH',
url: `/api/global-folders/${createdFolderId}`,
headers: { authorization: `Bearer ${authToken}` },
payload: { parent_id: createdFolderId },
});
expect(response.statusCode).toBe(400);
});
it('should return 404 for non-existent folder', async () => {
const response = await app.inject({
method: 'PATCH',
url: '/api/global-folders/non-existent',
headers: { authorization: `Bearer ${authToken}` },
payload: { name: 'New Name' },
});
expect(response.statusCode).toBe(404);
});
});
// ─── Block CRUD ──────────────────────────────────────
describe('POST /api/global-blocks', () => {
it('should create a global block in a folder with 201', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/global-blocks',
headers: { authorization: `Bearer ${authToken}` },
payload: {
name: 'Konferenzstuhl',
folder_id: subFolderId,
block_data: JSON.stringify({ type: 'chair', width: 50, height: 50 }),
svg_data: '<svg><rect width="50" height="50"/></svg>',
},
});
expect(response.statusCode).toBe(201);
const body = JSON.parse(response.body);
expect(body.id).toBeDefined();
expect(body.name).toBe('Konferenzstuhl');
expect(body.folder_id).toBe(subFolderId);
expect(body.block_data).toContain('chair');
expect(body.svg_data).toContain('<svg>');
createdBlockId = body.id;
});
it('should create a global block without folder (root level)', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/global-blocks',
headers: { authorization: `Bearer ${authToken}` },
payload: { name: 'Root Block' },
});
expect(response.statusCode).toBe(201);
const body = JSON.parse(response.body);
expect(body.folder_id).toBeNull();
expect(body.block_data).toBe('{}');
});
it('should reject block without name with 400', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/global-blocks',
headers: { authorization: `Bearer ${authToken}` },
payload: { folder_id: createdFolderId },
});
expect(response.statusCode).toBe(400);
const body = JSON.parse(response.body);
expect(body.error).toContain('Name is required');
});
it('should reject non-string block_data with 400', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/global-blocks',
headers: { authorization: `Bearer ${authToken}` },
payload: { name: 'Bad Block', block_data: { not: 'a string' } },
});
expect(response.statusCode).toBe(400);
expect(JSON.parse(response.body).error).toContain('block_data must be a JSON string');
});
});
describe('GET /api/global-blocks', () => {
it('should list all global blocks', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/global-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 list blocks for a specific folder', async () => {
const response = await app.inject({
method: 'GET',
url: `/api/global-blocks?folderId=${subFolderId}`,
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
expect(body.length).toBeGreaterThanOrEqual(1);
expect(body.every((b: { folder_id: string | null }) => b.folder_id === subFolderId)).toBe(true);
});
it('should list root-level blocks (folderId=null)', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/global-blocks?folderId=null',
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
expect(body.every((b: { folder_id: string | null }) => b.folder_id === null)).toBe(true);
});
});
describe('GET /api/global-blocks/:id', () => {
it('should get a single global block', async () => {
const response = await app.inject({
method: 'GET',
url: `/api/global-blocks/${createdBlockId}`,
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
expect(body.id).toBe(createdBlockId);
expect(body.name).toBe('Konferenzstuhl');
});
it('should return 404 for non-existent block', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/global-blocks/non-existent',
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(404);
});
});
describe('PATCH /api/global-blocks/:id', () => {
it('should rename a block', async () => {
const response = await app.inject({
method: 'PATCH',
url: `/api/global-blocks/${createdBlockId}`,
headers: { authorization: `Bearer ${authToken}` },
payload: { name: 'Updated Chair' },
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
expect(body.name).toBe('Updated Chair');
});
it('should move a block to a different folder', async () => {
const response = await app.inject({
method: 'PATCH',
url: `/api/global-blocks/${createdBlockId}`,
headers: { authorization: `Bearer ${authToken}` },
payload: { folder_id: createdFolderId },
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
expect(body.folder_id).toBe(createdFolderId);
});
it('should update block_data', async () => {
const response = await app.inject({
method: 'PATCH',
url: `/api/global-blocks/${createdBlockId}`,
headers: { authorization: `Bearer ${authToken}` },
payload: { block_data: JSON.stringify({ type: 'updated', width: 60 }) },
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
expect(body.block_data).toContain('updated');
});
it('should return 404 for non-existent block update', async () => {
const response = await app.inject({
method: 'PATCH',
url: '/api/global-blocks/non-existent',
headers: { authorization: `Bearer ${authToken}` },
payload: { name: 'New Name' },
});
expect(response.statusCode).toBe(404);
});
});
// ─── Delete ───────────────────────────────────────────
describe('DELETE /api/global-blocks/:id', () => {
it('should delete a global block', async () => {
const createRes = await app.inject({
method: 'POST',
url: '/api/global-blocks',
headers: { authorization: `Bearer ${authToken}` },
payload: { name: 'To Delete' },
});
const blockId = JSON.parse(createRes.body).id;
const response = await app.inject({
method: 'DELETE',
url: `/api/global-blocks/${blockId}`,
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(204);
});
it('should return 404 for non-existent block delete', async () => {
const response = await app.inject({
method: 'DELETE',
url: '/api/global-blocks/non-existent',
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(404);
});
});
describe('DELETE /api/global-folders/:id', () => {
it('should delete a folder and cascade to sub-folders', async () => {
const createRes = await app.inject({
method: 'POST',
url: '/api/global-folders',
headers: { authorization: `Bearer ${authToken}` },
payload: { name: 'Temp Folder' },
});
const folderId = JSON.parse(createRes.body).id;
const subRes = await app.inject({
method: 'POST',
url: '/api/global-folders',
headers: { authorization: `Bearer ${authToken}` },
payload: { name: 'Temp Sub', parent_id: folderId },
});
const subId = JSON.parse(subRes.body).id;
const response = await app.inject({
method: 'DELETE',
url: `/api/global-folders/${folderId}`,
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(204);
// Sub-folder should be gone (cascade)
const checkSub = await app.inject({
method: 'GET',
url: `/api/global-folders/${subId}`,
headers: { authorization: `Bearer ${authToken}` },
});
expect(checkSub.statusCode).toBe(404);
});
it('should return 404 for non-existent folder delete', async () => {
const response = await app.inject({
method: 'DELETE',
url: '/api/global-folders/non-existent',
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(404);
});
});
});