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