task(F7): wcadlib as real zip package - jszip export-zip/import-zip endpoints with manifest/blocks/thumbs, frontend zip buttons
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user