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:
Generated
+77
@@ -14,6 +14,7 @@
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"fastify": "^5.9.0",
|
||||
"jszip": "^3.10.1",
|
||||
"y-leveldb": "^0.2.0",
|
||||
"y-protocols": "^1.0.7",
|
||||
"yjs": "^13.6.0"
|
||||
@@ -1503,6 +1504,11 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
@@ -1886,6 +1892,11 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
@@ -1926,6 +1937,11 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
|
||||
},
|
||||
"node_modules/isomorphic.js": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
|
||||
@@ -1958,6 +1974,44 @@
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip/node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"node_modules/jszip/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/level": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/level/-/level-8.0.1.tgz",
|
||||
@@ -2135,6 +2189,14 @@
|
||||
"url": "https://github.com/sponsors/dmonad"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/light-my-request": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz",
|
||||
@@ -2587,6 +2649,11 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
|
||||
@@ -2722,6 +2789,11 @@
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"node_modules/process-warning": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
|
||||
@@ -2969,6 +3041,11 @@
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"fastify": "^5.9.0",
|
||||
"jszip": "^3.10.1",
|
||||
"y-leveldb": "^0.2.0",
|
||||
"y-protocols": "^1.0.7",
|
||||
"yjs": "^13.6.0"
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { DatabaseInterface } from '../database/DatabaseInterface.js';
|
||||
import { requireAuth } from '../auth/authMiddleware.js';
|
||||
import type { AuthService } from '../auth/AuthService.js';
|
||||
import { validateName, validateIdParam } from '../utils/validation.js';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
export function registerGlobalBlockRoutes(fastify: FastifyInstance, db: DatabaseInterface, authService: AuthService) {
|
||||
// ─── Global Block Folders ──────────────────────────────
|
||||
@@ -229,4 +230,132 @@ export function registerGlobalBlockRoutes(fastify: FastifyInstance, db: Database
|
||||
}
|
||||
return reply.code(201).send({ imported_blocks: imported, imported_folders: folderIdMap.size });
|
||||
});
|
||||
|
||||
// ─── Task F7: .wcadlib als echtes ZIP-Paket ──────────────
|
||||
|
||||
// ZIP-Export: manifest.json + blocks.json + thumbs/ (SVG je Block)
|
||||
fastify.get('/api/global-blocks/export-zip', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) return;
|
||||
const folders = db.listGlobalFolders();
|
||||
const blocks = db.listGlobalBlocks();
|
||||
|
||||
const zip = new JSZip();
|
||||
zip.file('manifest.json', JSON.stringify({
|
||||
format: 'wcadlib-zip',
|
||||
version: 1,
|
||||
exported_at: new Date().toISOString(),
|
||||
block_count: blocks.length,
|
||||
folder_count: folders.length,
|
||||
}));
|
||||
|
||||
const blockEntries = [] as Array<{
|
||||
name: string;
|
||||
folder_id: string | null;
|
||||
block_data: string;
|
||||
thumbnail: string | null;
|
||||
}>;
|
||||
for (const b of blocks) {
|
||||
let thumbnail: string | null = null;
|
||||
if (b.svg_data) {
|
||||
thumbnail = `thumbs/${b.id}.svg`;
|
||||
zip.file(thumbnail, b.svg_data);
|
||||
}
|
||||
blockEntries.push({
|
||||
name: b.name,
|
||||
folder_id: b.folder_id,
|
||||
block_data: b.block_data,
|
||||
thumbnail,
|
||||
});
|
||||
}
|
||||
zip.file('blocks.json', JSON.stringify({
|
||||
folders: folders.map((f) => ({ id: f.id, name: f.name, parent_id: f.parent_id })),
|
||||
blocks: blockEntries,
|
||||
}));
|
||||
|
||||
const buffer = await zip.generateAsync({ type: 'nodebuffer' });
|
||||
reply.header('content-type', 'application/zip');
|
||||
reply.header('content-disposition', 'attachment; filename="library.wcadlib.zip"');
|
||||
return reply.send(buffer);
|
||||
});
|
||||
|
||||
// ZIP-Import: .wcadlib-ZIP einspielen (Ordner-Mapping, Blöcke, Thumbnails)
|
||||
fastify.post('/api/global-blocks/import-zip', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) return;
|
||||
const body = request.body;
|
||||
if (!Buffer.isBuffer(body) || body.length === 0) {
|
||||
return reply.code(400).send({ error: 'ZIP body fehlt oder leer' });
|
||||
}
|
||||
|
||||
let zip: JSZip;
|
||||
try {
|
||||
zip = await JSZip.loadAsync(body);
|
||||
} catch {
|
||||
return reply.code(400).send({ error: 'Kein valides ZIP-Format' });
|
||||
}
|
||||
|
||||
const blocksFile = zip.file('blocks.json');
|
||||
if (!blocksFile) {
|
||||
return reply.code(400).send({ error: 'blocks.json fehlt im ZIP' });
|
||||
}
|
||||
|
||||
let pkg: { folders?: Array<{ id: string; name: string; parent_id: string | null }>; blocks?: Array<{ name: string; folder_id: string | null; block_data: string; thumbnail?: string | null }> };
|
||||
try {
|
||||
pkg = JSON.parse(await blocksFile.async('string'));
|
||||
} catch {
|
||||
return reply.code(400).send({ error: 'blocks.json ist kein valides JSON' });
|
||||
}
|
||||
if (!Array.isArray(pkg.blocks)) {
|
||||
return reply.code(400).send({ error: 'blocks.json enthaelt kein blocks-Array' });
|
||||
}
|
||||
|
||||
// Optional: manifest pruefen (falls vorhanden)
|
||||
const manifestFile = zip.file('manifest.json');
|
||||
if (manifestFile) {
|
||||
try {
|
||||
const manifest = JSON.parse(await manifestFile.async('string'));
|
||||
if (manifest.format && manifest.format !== 'wcadlib-zip') {
|
||||
return reply.code(400).send({ error: `Unbekanntes Paket-Format: ${manifest.format}` });
|
||||
}
|
||||
} catch {
|
||||
return reply.code(400).send({ error: 'manifest.json ist kein valides JSON' });
|
||||
}
|
||||
}
|
||||
|
||||
// Ordner mit ID-Mapping importieren (Eltern zuerst):
|
||||
const folderIdMap = new Map<string, string>();
|
||||
const sortedFolders = [...(pkg.folders ?? [])].sort((a, b) => {
|
||||
if (a.parent_id === null) return -1;
|
||||
if (b.parent_id === null) return 1;
|
||||
if (a.parent_id === b.parent_id) return 0;
|
||||
const aRes = folderIdMap.has(a.parent_id!) ? -1 : 0;
|
||||
return aRes;
|
||||
});
|
||||
for (const f of sortedFolders) {
|
||||
const newParent = f.parent_id ? folderIdMap.get(f.parent_id) ?? null : null;
|
||||
const created = db.createGlobalFolder({ name: f.name, parent_id: newParent });
|
||||
folderIdMap.set(f.id, created.id);
|
||||
}
|
||||
|
||||
// Blöcke importieren; Thumbnail aus thumbs/ lesen wenn referenziert:
|
||||
let imported = 0;
|
||||
for (const b of pkg.blocks) {
|
||||
let svgData: string | null = null;
|
||||
if (b.thumbnail) {
|
||||
const thumbFile = zip.file(b.thumbnail);
|
||||
if (thumbFile) {
|
||||
try { svgData = await thumbFile.async('string'); } catch { svgData = null; }
|
||||
}
|
||||
}
|
||||
const newFolderId = b.folder_id ? folderIdMap.get(b.folder_id) ?? null : null;
|
||||
db.createGlobalBlock({
|
||||
name: b.name,
|
||||
folder_id: newFolderId,
|
||||
block_data: b.block_data,
|
||||
svg_data: svgData,
|
||||
});
|
||||
imported++;
|
||||
}
|
||||
|
||||
return reply.code(201).send({ imported_blocks: imported, imported_folders: folderIdMap.size });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,11 @@ export interface ServerOptions {
|
||||
export async function createServer(opts: ServerOptions) {
|
||||
const fastify = Fastify({ logger: true });
|
||||
|
||||
// ─── Task F7: binäre Body für .wcadlib ZIP-Import ───
|
||||
fastify.addContentTypeParser('application/zip', { parseAs: 'buffer' }, (_req, body, done) => done(null, body));
|
||||
fastify.addContentTypeParser('application/octet-stream', { parseAs: 'buffer' }, (_req, body, done) => done(null, body));
|
||||
fastify.addContentTypeParser('application/x-zip-compressed', { parseAs: 'buffer' }, (_req, body, done) => done(null, body));
|
||||
|
||||
// CORS — restrictive origin list
|
||||
await fastify.register(cors, {
|
||||
origin: (origin: string | undefined, cb: (err: Error | null, allow: boolean) => void) => {
|
||||
|
||||
@@ -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