import test from 'node:test' import assert from 'node:assert/strict' import os from 'node:os' import path from 'node:path' import { mkdtempSync, existsSync } from 'node:fs' import { readFile, writeFile } from 'node:fs/promises' import sharp from 'sharp' // Webshop C7: Artikelbilder (services/shop/bilder.js), verallgemeinert aus newsImages.js process.env.SHOP_UPLOAD_DIR = mkdtempSync(path.join(os.tmpdir(), 'shop-bilder-')) const { speichereBild, bildPfad, bildMeta, beanspruche, loescheBild, loescheAlleBilder, raeumeUnbeanspruchteAuf, bildLimitErreicht, setzeBildLimitZurueck, BILD_MAX_BYTES, } = await import('../server/services/shop/bilder.js') const KEY = 'abcdefghijklmnop' const KEY2 = 'qrstuvwxyz234567' const solid = (width, height) => sharp({ create: { width, height, channels: 3, background: '#36c' } }) const speichere = (buffer, extra = {}) => speichereBild({ customerId: 'c1', shopId: 's1', siteKey: KEY, buffer, ...extra }) test('EXIF/GPS entfernt und gedreht; Sidecar mit Besitzer, Shop und Groesse', async () => { const input = await solid(1200, 800) .withMetadata({ orientation: 6 }) .withExif({ IFD3: { GPSLatitudeRef: 'N', GPSLatitude: '48/1 30/1 0/1' } }) .jpeg().toBuffer() assert.ok((await sharp(input).metadata()).exif, 'Fixture hat EXIF') const { imageId, w, h } = await speichere(input) assert.ok(w < h, 'gedreht') for (const size of ['l', 's']) { const meta = await sharp(bildPfad(KEY, imageId, size)).metadata() assert.equal(meta.exif, undefined) assert.equal(meta.format, 'webp') } const meta = await bildMeta(KEY, imageId) assert.deepEqual([meta.customerId, meta.shopId, meta.claimed, meta.w, meta.h], ['c1', 's1', false, w, h]) assert.match(meta.createdAt, /^\d{4}-\d\d-\d\dT/) }) test('verkleinert auf 1600 und 480, Dateinamen -l.webp und -s.webp unter //', async () => { const { imageId, w, h } = await speichere(await solid(4000, 3000).png().toBuffer()) assert.deepEqual([w, h], [1600, 1200]) const l = await sharp(bildPfad(KEY, imageId, 'l')).metadata() const s = await sharp(bildPfad(KEY, imageId, 's')).metadata() assert.deepEqual([l.width, l.height, s.width, s.height], [1600, 1200, 480, 360]) assert.equal(bildPfad(KEY, imageId, 'l'), path.join(process.env.SHOP_UPLOAD_DIR, KEY, `${imageId}-l.webp`)) // kleine Bilder werden nicht vergroessert const klein = await speichere(await solid(100, 50).webp().toBuffer()) assert.deepEqual([klein.w, klein.h], [100, 50]) }) test('zu gross 413, falsches Format 415 (PDF, GIF, kaputt, leer, kein Buffer), ungueltiger Shop 400', async () => { await assert.rejects(speichere(Buffer.alloc(BILD_MAX_BYTES + 1)), { status: 413 }) assert.equal(BILD_MAX_BYTES, 8 * 1024 * 1024) await assert.rejects(speichere(Buffer.from('%PDF-1.4\n1 0 obj\n<<>>\nendobj\ntrailer\n<<>>\n%%EOF\n')), { status: 415 }) await assert.rejects(speichere(await solid(10, 10).gif().toBuffer()), { status: 415 }) await assert.rejects(speichere(Buffer.from('kein Bild')), { status: 415 }) await assert.rejects(speichere(Buffer.alloc(0)), { status: 415 }) await assert.rejects(speichere({}), { status: 415 }) await assert.rejects(speichere(await solid(10, 10).png().toBuffer(), { siteKey: '../x' }), { status: 400 }) }) test('Pfade nur fuer saubere siteKeys, IDs und Groessen', () => { assert.equal(bildPfad(KEY, '../x', 'l'), null) assert.equal(bildPfad(KEY, 'a/b', 'l'), null) assert.equal(bildPfad(KEY, 'ABC', 'l'), null) assert.equal(bildPfad('../abcdefghijkl', 'abc-1', 'l'), null) assert.equal(bildPfad('abcdefghijklmno', 'abc-1', 'l'), null, 'zu kurz') assert.equal(bildPfad('abcdefghijklmnop1', 'abc-1', 'l'), null, 'zu lang') assert.equal(bildPfad('abcdefghijklmno1', 'abc-1', 'l'), null, 'Ziffer 1 ist kein base32') assert.equal(bildPfad(KEY, 'abc-1', 'xl'), null) assert.equal(bildPfad(KEY, 'x'.repeat(65), 'l'), null) assert.ok(bildPfad(KEY, 'abc-1', 's').endsWith(path.join(KEY, 'abc-1-s.webp'))) }) test('bildMeta: unbekannte oder unsaubere IDs => null; beanspruche setzt claimed, ignoriert Unbekanntes und Wiederholung', async () => { const { imageId } = await speichere(await solid(50, 50).png().toBuffer()) assert.equal(await bildMeta(KEY, 'gibtsnicht'), null) assert.equal(await bildMeta(KEY, '../x'), null) assert.equal(await bildMeta('zu-kurz', imageId), null) await beanspruche(KEY, [imageId, 'gibtsnicht', '../x', 42]) assert.equal((await bildMeta(KEY, imageId)).claimed, true) await beanspruche(KEY, [imageId]) assert.equal((await bildMeta(KEY, imageId)).claimed, true) await beanspruche(KEY, undefined) }) test('loescheBild entfernt beide Groessen und den Sidecar; loescheAlleBilder den ganzen Shop; Fehlendes ist kein Fehler', async () => { const a = await speichere(await solid(50, 50).png().toBuffer()) const b = await speichere(await solid(50, 50).png().toBuffer()) const andere = await speichereBild({ customerId: 'c2', shopId: 's2', siteKey: KEY2, buffer: await solid(50, 50).png().toBuffer() }) await loescheBild(KEY, a.imageId) await loescheBild(KEY, a.imageId) await loescheBild(KEY, '../x') for (const size of ['l', 's']) assert.equal(existsSync(bildPfad(KEY, a.imageId, size)), false) assert.equal(await bildMeta(KEY, a.imageId), null) assert.ok(await bildMeta(KEY, b.imageId)) await loescheAlleBilder(KEY) await loescheAlleBilder('../x') await loescheAlleBilder('gibtsnicht1234567') assert.equal(await bildMeta(KEY, b.imageId), null) assert.equal(existsSync(path.join(process.env.SHOP_UPLOAD_DIR, KEY)), false) assert.ok(await bildMeta(KEY2, andere.imageId), 'anderer Shop bleibt') }) test('raeumeUnbeanspruchteAuf: nur Unbeanspruchte aelter als 24 h, in allen Shops', async () => { const png = await solid(50, 50).png().toBuffer() const alt = await speichere(png) const behalten = await speichere(png) const frisch = await speichere(png) const altAnderer = await speichereBild({ customerId: 'c2', shopId: 's2', siteKey: KEY2, buffer: png }) await beanspruche(KEY, [behalten.imageId]) const zurueckdatieren = async (key, id, stunden) => { const datei = path.join(process.env.SHOP_UPLOAD_DIR, key, `${id}.json`) const meta = JSON.parse(await readFile(datei, 'utf8')) meta.createdAt = new Date(Date.now() - stunden * 3600 * 1000).toISOString() await writeFile(datei, JSON.stringify(meta)) } await zurueckdatieren(KEY, alt.imageId, 25) await zurueckdatieren(KEY, behalten.imageId, 48) await zurueckdatieren(KEY, frisch.imageId, 23) await zurueckdatieren(KEY2, altAnderer.imageId, 30) assert.equal(await raeumeUnbeanspruchteAuf(), 2) assert.equal(await bildMeta(KEY, alt.imageId), null) assert.equal(existsSync(bildPfad(KEY, alt.imageId, 'l')), false) assert.equal(await bildMeta(KEY2, altAnderer.imageId), null) assert.ok(await bildMeta(KEY, behalten.imageId), 'beansprucht bleibt') assert.ok(await bildMeta(KEY, frisch.imageId), 'frisch bleibt') // mit spaeterem Zeitpunkt wird auch das frische geraeumt assert.equal(await raeumeUnbeanspruchteAuf(new Date(Date.now() + 3 * 3600 * 1000)), 1) assert.equal(await bildMeta(KEY, frisch.imageId), null) }) test('bildLimitErreicht: 40 je Stunde und Kunde, danach true; andere Kunden unberuehrt', () => { setzeBildLimitZurueck() for (let i = 0; i < 40; i++) assert.equal(bildLimitErreicht('c1'), false, `Aufruf ${i + 1}`) assert.equal(bildLimitErreicht('c1'), true) assert.equal(bildLimitErreicht('c2'), false) setzeBildLimitZurueck() assert.equal(bildLimitErreicht('c1'), false) })