- routes/shop.js: gemeinsame Pruefung mitShop (Schalter, eigenes Projekt, Funktion setup|active, lesend auch disabled, shopConfig bei Bedarf anlegen); GET /status erweitert (Bereitschaft, geoeffnet, Snippet, Vorschau, Stripe ohne Konto-ID, Zahlen); Einstellungen, Artikel (mit Bild-Pruefung, Beanspruchen und Aufraeumen), Sortierung, Rechtstexte (Entwurf, Bestaetigen mit Hash), Oeffnen/Schliessen, Bild-Upload (octet-stream, 8 MB) und Vorschau; Schreib-Limit 300 je Stunde und Kunde - services/shop/bilder.js: Artikelbilder nach dem Muster von newsImages.js (1600/480 px WebP, Sidecar, 24-h-Aufraeumen, 40 Uploads/h) - Kundenansicht: sieben Verwaltungs-Pfade erlaubt und als shop_aenderung protokolliert; Anfrage, Oeffnen, Bestaetigen, Stripe bleiben gesperrt - Tests: shopRoutes, shopBilder; Erwartungen von kundenansichtGuard, customerViewLog und shopAnfrage angepasst Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
141 lines
7.4 KiB
JavaScript
141 lines
7.4 KiB
JavaScript
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 <id>-l.webp und <id>-s.webp unter <uploadDir>/<siteKey>/', 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)
|
|
})
|