Files
web-cad/backend/tests/globalBlocksZip.test.ts

165 lines
6.0 KiB
TypeScript
Raw Permalink 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.
/**
* Task F7 .wcadlib ZIP-Paket Export/Import (Backend).
*
* ZIP-Struktur: manifest.json (format/version/exported_at), blocks.json
* (Ordner + Blöcke), thumbs/ (SVG-Thumbnails je Block-ID). Export liefert
* application/zip; Import parst das ZIP und spielt Ordner-Hierarchie +
* Blöcke mit ID-Mapping ein. Invalides ZIP → 400.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import type { FastifyInstance } from 'fastify';
import JSZip from 'jszip';
import { SqliteAdapter } from '../src/database/SqliteAdapter.js';
import { createServer } from '../src/server.js';
describe('Task F7: .wcadlib ZIP Export/Import', () => {
let app: FastifyInstance;
let db: SqliteAdapter;
let token: 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: 'f7-zip@example.com', password: 'Password123!', name: 'F7 Zip', role: 'admin' },
});
token = JSON.parse(regRes.body).session.token;
});
afterAll(async () => {
await app.close();
db.close();
});
async function createFixture(): Promise<{ folderId: string; blockId: string }> {
const folderRes = await app.inject({
method: 'POST',
url: '/api/global-folders',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Event Set', parent_id: null },
});
const folderId = JSON.parse(folderRes.body).id;
const blockRes = await app.inject({
method: 'POST',
url: '/api/global-blocks',
headers: { authorization: `Bearer ${token}` },
payload: {
name: 'Stuhl-Reihe',
folder_id: folderId,
block_data: JSON.stringify([{ id: 'e1', type: 'chair', x: 0, y: 0, width: 45, height: 45, properties: {} }]),
svg_data: '<svg xmlns="http://www.w3.org/2000/svg"><rect/></svg>',
},
});
const blockId = JSON.parse(blockRes.body).id;
return { folderId, blockId };
}
it('GET /api/global-blocks/export-zip liefert ZIP mit manifest.json, blocks.json und thumbs/', async () => {
const { blockId } = await createFixture();
const res = await app.inject({
method: 'GET',
url: '/api/global-blocks/export-zip',
headers: { authorization: `Bearer ${token}` },
});
expect(res.statusCode).toBe(200);
expect(res.headers['content-type']).toContain('application/zip');
const zip = await JSZip.loadAsync(res.rawPayload ?? res.body);
expect(zip.file('manifest.json')).toBeDefined();
expect(zip.file('blocks.json')).toBeDefined();
const manifest = JSON.parse(await zip.file('manifest.json')!.async('string'));
expect(manifest.format).toBe('wcadlib-zip');
expect(manifest.version).toBe(1);
expect(typeof manifest.exported_at).toBe('string');
const blocks = JSON.parse(await zip.file('blocks.json')!.async('string'));
expect(blocks.folders.length).toBe(1);
expect(blocks.folders[0].name).toBe('Event Set');
expect(blocks.blocks.length).toBeGreaterThanOrEqual(1);
const chairBlock = blocks.blocks.find((b: { name: string }) => b.name === 'Stuhl-Reihe');
expect(chairBlock).toBeDefined();
expect(chairBlock.thumbnail).toBe(`thumbs/${blockId}.svg`);
const thumb = zip.file(`thumbs/${blockId}.svg`);
expect(thumb).toBeDefined();
expect(await thumb!.async('string')).toContain('<svg');
});
it('Export ohne Auth → 401', async () => {
const res = await app.inject({ method: 'GET', url: '/api/global-blocks/export-zip' });
expect(res.statusCode).toBe(401);
});
it('POST /api/global-blocks/import-zip spielt ZIP ein (Ordner-Mapping + Blöcke + Thumb-Referenz erhalten)', async () => {
const { blockId } = await createFixture();
const exportRes = await app.inject({
method: 'GET',
url: '/api/global-blocks/export-zip',
headers: { authorization: `Bearer ${token}` },
});
expect(exportRes.statusCode).toBe(200);
const zipBuffer = exportRes.rawPayload ?? exportRes.body;
const importRes = await app.inject({
method: 'POST',
url: '/api/global-blocks/import-zip',
headers: {
authorization: `Bearer ${token}`,
'content-type': 'application/zip',
},
payload: zipBuffer,
});
expect(importRes.statusCode).toBe(201);
const body = JSON.parse(importRes.body);
expect(body.imported_blocks).toBeGreaterThanOrEqual(1);
expect(body.imported_folders).toBeGreaterThanOrEqual(1);
// Verifizieren: importierter Block existiert mit Thumb-Daten
const blocksRes = await app.inject({
method: 'GET',
url: '/api/global-blocks',
headers: { authorization: `Bearer ${token}` },
});
const allBlocks = JSON.parse(blocksRes.body);
const imported = allBlocks.filter((b: { name: string }) => b.name === 'Stuhl-Reihe');
expect(imported.length).toBeGreaterThanOrEqual(2); // Original + Import
expect(imported.every((b: { svg_data: string | null }) => b.svg_data !== null)).toBe(true);
});
it('Import mit Müll-Payload → 400 (kein valides ZIP)', async () => {
const res = await app.inject({
method: 'POST',
url: '/api/global-blocks/import-zip',
headers: {
authorization: `Bearer ${token}`,
'content-type': 'application/zip',
},
payload: Buffer.from('dies ist kein zip'),
});
expect(res.statusCode).toBe(400);
expect(JSON.parse(res.body).error).toContain('ZIP');
});
it('Import-ZIP ohne blocks.json → 400', async () => {
const zip = new JSZip();
zip.file('manifest.json', JSON.stringify({ format: 'wcadlib-zip', version: 1 }));
const buf = await zip.generateAsync({ type: 'nodebuffer' });
const res = await app.inject({
method: 'POST',
url: '/api/global-blocks/import-zip',
headers: {
authorization: `Bearer ${token}`,
'content-type': 'application/zip',
},
payload: buf,
});
expect(res.statusCode).toBe(400);
});
});