Files
Webklar-Kundenbereich/test/shopRoutes.test.js
T
ClaudeandClaude Opus 5.5 08d837d690 Shop Review B I-6, I-7: Archiv der bestaetigten Rechtstexte, G09 ohne Link-Ersatz
I-6: Neue Collection shopRechtstexteArchiv (ID r + 35 Hex aus sha256(shopId + hash)); das
Bestaetigen legt die Fassung samt Versand-/Zahlungstext und Gewaehrleistungstext ab, G09 zeigt
die Fassung zum rechtstexteHash der Bestellung (Rueckfall mit Warnung auf die aktuell
bestaetigte), loescheShopDaten loescht das Archiv mit.

I-7: Grenzen AGB 20000, Belehrung 12000, Formular 3000 plus Pruefung der maskierten Laenge beim
Speichern; G09 ersetzt die AGB nie mehr durch einen Link, sondern scheitert (Marker bleibt
offen, wird wiederholt). Test mit Texten in Hoechstlaenge gegen die Grenzen der Warteschlange.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:58:31 +00:00

696 lines
42 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, after, beforeEach } 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 { readdir, rm } from 'node:fs/promises'
import sharp from 'sharp'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
import { schemaKeys } from './support/schema.js'
import { SHOP_DATETIMES } from '../server/services/shop/schema.js'
// Webshop C7: Verwaltungs-Routen /api/shop (Status, Einstellungen, Artikel, Rechtstexte, Oeffnen, Bilder), Kundenansicht
process.env.SHOP_UPLOAD_DIR = mkdtempSync(path.join(os.tmpdir(), 'shop-routes-'))
const aw = await startAppwriteMock({
schema: {
portalFeatures: schemaKeys('portalFeatures'),
shopConfig: schemaKeys('shopConfig'),
shopProducts: schemaKeys('shopProducts'),
shopRechtstexteArchiv: schemaKeys('shopRechtstexteArchiv'),
customerViewLog: schemaKeys('customerViewLog'),
},
datetimes: SHOP_DATETIMES,
})
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const { startTestApp, send, post } = await import('./support/testApp.js')
const { customerCookie, kundenansichtCookie, viewAsCookie, seedFixtures, customers } = await import('./support/fixtures.js')
const { default: shopRoutes, resetShopRoutesForTests } = await import('../server/routes/shop.js')
const { kundenansichtGuard } = await import('../server/middleware/kundenansichtGuard.js')
const sf = await import('../server/services/siteFeatures.js')
const { deleteDocument, updateDocument } = await import('../server/services/appwriteAdmin.js')
const { shopPerSiteKey } = await import('../server/services/shop/kontext.js')
const { archivId } = await import('../server/services/shop/archiv.js')
const { rechtstexteHash } = await import('../server/services/shop/rechtstexte.js')
const { STANDARD_EINSTELLUNGEN } = await import('../server/services/shop/einstellungen.js')
const app = await startTestApp({ '/': kundenansichtGuard, '/api/shop': shopRoutes })
after(() => Promise.all([app.close(), aw.close(), st.close()]))
seedFixtures(aw)
aw.seed('websiteProjects', { $id: 'pt1', customerId: 'c1', projectName: 'Töpferei', archived: false })
aw.seed('websiteProjects', { $id: 'pt2', customerId: 'c1', projectName: 'Zweiter Shop' })
aw.seed('websiteProjects', { $id: 'pt3', customerId: 'c2', projectName: 'Fremdes Projekt' })
aw.seed('websiteProjects', { $id: 'pt4', customerId: 'c1', projectName: 'Altprojekt', archived: true })
const cookie = customerCookie('c1')
const KV = { cookie: kundenansichtCookie({ customerId: 'c1' }), headers: { 'X-Kundenansicht': 'a'.repeat(32) } }
const EINSTELLUNGEN = {
...JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN)),
kleinunternehmer: true,
lieferzeit: '2–4 Werktage',
shopname: 'Töpferei Keramik',
anbieter: { name: 'Töpferei Keramik', strasse: 'Hauptstraße 5', plz: '67655', ort: 'Kaiserslautern', land: 'DE', email: 'shop@toepferei.example', telefon: '', ustId: '' },
}
const TEXTE = {
agb: 'Unsere AGB.',
widerrufsbelehrung: 'Unsere Widerrufsbelehrung.',
widerrufsformular: 'Unser Widerrufsformular.',
impressumUrl: 'https://toepferei.example/impressum',
datenschutzUrl: 'https://toepferei.example/datenschutz',
}
const ARTIKEL = { name: 'Tasse', preisCent: 1800 }
/** Bestaetigungsfaehige Texte: die Belehrung nennt die Adresse der Widerrufsfunktion dieses Shops (I-5). */
const WIDERRUF = (feature) => `${config.publicBaseUrl}/shop/${feature.siteKey}/widerruf`
const texteFuer = (feature) => ({ ...TEXTE, widerrufsbelehrung: `Unsere Widerrufsbelehrung. Widerruf online: ${WIDERRUF(feature)}` })
const q = (projectId = 'pt1') => `projectId=${projectId}`
const lesen = (pfad, projectId = 'pt1', opts = { cookie }) => send('GET', `/api/shop/${pfad}${pfad.includes('?') ? '&' : '?'}${q(projectId)}`, opts)
const schreiben = (method, pfad, body = {}, opts = { cookie }) => send(method, `/api/shop/${pfad}`, { ...opts, body: { projectId: 'pt1', ...body } })
async function aktiviere(projectId = 'pt1', state = 'active') {
const feature = await sf.ensureFeatureDoc('c1', projectId, 'shop')
await updateDocument(config.collections.portalFeatures, feature.$id, sf.stateFields(state))
return sf.getFeatureDoc('c1', projectId, 'shop')
}
const shopConfigDoc = (feature) => aw.docs('shopConfig').get(feature.$id)
const dateien = async (siteKey) => (await readdir(path.join(process.env.SHOP_UPLOAD_DIR, siteKey)).catch(() => [])).sort()
async function png(w = 64, h = 48) {
return sharp({ create: { width: w, height: h, channels: 3, background: '#c63' } }).png().toBuffer()
}
async function lade(buffer, { projectId = 'pt1', opts = { cookie }, type = 'application/octet-stream' } = {}) {
const res = await fetch(`${app.url}/api/shop/bild?projectId=${projectId}`, {
method: 'POST',
headers: { 'content-type': type, ...(opts.cookie ? { cookie: opts.cookie } : {}), ...(opts.headers || {}) },
body: buffer,
})
const text = await res.text()
let body = text
try {
body = JSON.parse(text)
} catch {
/* Text */
}
return { status: res.status, body }
}
/** Shop bis "bereit zum Oeffnen": active, Einstellungen, Rechtstexte bestaetigt, Stripe bereit, ein Artikel. */
async function machBereit({ mitArtikel = true } = {}) {
const feature = await aktiviere()
assert.equal((await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })).status, 200)
if (mitArtikel) assert.equal((await schreiben('POST', 'produkte', { produkt: ARTIKEL })).status, 201)
assert.equal((await schreiben('PUT', 'rechtstexte', { rechtstexte: texteFuer(feature) })).status, 200)
const hash = (await lesen('rechtstexte')).body.hash
assert.equal((await schreiben('POST', 'rechtstexte/bestaetigen', { hash })).status, 200)
await updateDocument(config.collections.shopConfig, feature.$id, {
stripeAccountId: 'acct_test1',
stripeStatus: JSON.stringify({ charges_enabled: true, payouts_enabled: true, details_submitted: true, currently_due: [], past_due: [], disabled_reason: null, geheim: 'nie', geprueftAt: '2026-10-05T10:00:00.000Z' }),
})
return feature
}
let origin
beforeEach(async () => {
origin ??= { mode: config.shop.mode, pilotEmails: config.shop.pilotEmails }
config.shop.mode = 'pilot'
config.shop.pilotEmails = [customers.c1.email]
resetShopRoutesForTests()
for (const col of ['portalFeatures', 'shopConfig', 'shopProducts', 'shopRechtstexteArchiv', 'customerViewLog']) {
for (const id of [...aw.docs(col).keys()]) await deleteDocument(config.collections[col], id)
}
await rm(process.env.SHOP_UPLOAD_DIR, { recursive: true, force: true })
const { leereCache } = await import('../server/services/shop/kontext.js')
leereCache()
})
// ---------------------------------------------------------------- Zugang
test('Zugang: ohne Session 401, ohne Projekt 400, fremdes oder unbekanntes Projekt 403, Schalter aus 409, Funktion nicht eingerichtet 409', async () => {
await aktiviere()
assert.equal((await send('GET', '/api/shop/einstellungen?projectId=pt1')).status, 401)
assert.equal((await send('GET', '/api/shop/einstellungen', { cookie })).status, 400)
assert.equal((await lesen('einstellungen', 'pt3')).status, 403)
assert.equal((await lesen('einstellungen', 'gibtsnicht')).status, 403)
for (const pfad of ['einstellungen', 'produkte', 'rechtstexte']) assert.equal((await lesen(pfad, 'pt2')).status, 409, `${pfad}: pt2 hat keine Funktion`)
const keine = await lesen('einstellungen', 'pt2')
assert.deepEqual([keine.body.code, keine.body.error], ['nicht_eingerichtet', 'Der Webshop ist für dieses Projekt noch nicht eingerichtet.'])
config.shop.mode = 'off'
const aus = await lesen('einstellungen')
assert.deepEqual([aus.status, aus.body.code], [409, 'nicht_verfuegbar'])
config.shop.mode = 'pilot'
config.shop.pilotEmails = ['jemand@example.invalid']
assert.equal((await lesen('produkte')).status, 409)
})
test('Zustaende: ordered und off => 409; disabled nur lesend; setup legt shopConfig an (ID = portalFeatures-ID, siteKey kopiert)', async () => {
const feature = await aktiviere('pt1', 'ordered')
assert.equal((await lesen('einstellungen')).status, 409)
await updateDocument(config.collections.portalFeatures, feature.$id, sf.stateFields('setup'))
assert.equal(aw.docs('shopConfig').size, 0)
const setup = await lesen('einstellungen')
assert.equal(setup.status, 200)
assert.equal(setup.body.einstellungen.lieferzeit, '')
assert.equal(setup.body.einstellungen.kleinunternehmer, null)
assert.equal(setup.body.vollstaendig, false)
const angelegt = shopConfigDoc(feature)
assert.deepEqual([angelegt.customerId, angelegt.projectId, angelegt.siteKey], ['c1', 'pt1', feature.siteKey])
assert.equal((await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })).status, 200, 'setup darf schreiben')
await updateDocument(config.collections.portalFeatures, feature.$id, sf.stateFields('disabled'))
for (const pfad of ['einstellungen', 'produkte', 'rechtstexte']) assert.equal((await lesen(pfad)).status, 200, `${pfad} lesbar`)
for (const [m, pfad, body] of [
['PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN }], ['POST', 'produkte', { produkt: ARTIKEL }], ['PUT', 'rechtstexte', { rechtstexte: TEXTE }],
['POST', 'oeffnen', { offen: false }], ['POST', 'produkte/sortierung', { ids: [] }],
]) {
const res = await schreiben(m, pfad, body)
assert.deepEqual([res.status, res.body.code], [409, 'nicht_eingerichtet'], `${m} ${pfad}`)
}
assert.equal((await lesen('einstellungen', 'pt4')).status, 409)
})
test('archiviertes Projekt: Lesen ja (falls Funktion da), Schreiben 409 wie bei Bestellungen', async () => {
const feature = await sf.ensureFeatureDoc('c1', 'pt4', 'shop')
await updateDocument(config.collections.portalFeatures, feature.$id, sf.stateFields('active'))
assert.equal((await lesen('einstellungen', 'pt4')).status, 200)
const res = await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN, projectId: 'pt4' })
assert.equal(res.status, 409)
})
// ---------------------------------------------------------------- Status
test('GET /status ohne Funktion: gleiche Form mit leeren Werten, legt nichts an', async () => {
const res = await lesen('status')
assert.equal(res.status, 200)
assert.deepEqual(res.body, {
verfuegbar: true, state: 'off', ticketWoid: null, anfrage: null, bereitschaft: null, geoeffnet: false, schalter: false, siteKey: null,
snippet: null, vorschauUrl: null, stripe: { verbunden: false, status: null }, zahlen: null,
})
assert.equal(aw.docs('portalFeatures').size + aw.docs('shopConfig').size, 0)
assert.equal((await lesen('status', 'pt3')).status, 403)
})
test('GET /status mit Shop: Bereitschaft, Snippet, Vorschau, Stripe ohne Konto-ID und fremde Felder, Zahlen; geoeffnet erst nach Oeffnen', async () => {
const feature = await machBereit()
const res = await lesen('status')
assert.equal(res.status, 200)
const b = res.body
assert.deepEqual([b.verfuegbar, b.state, b.siteKey, b.schalter, b.geoeffnet], [true, 'active', feature.siteKey, false, false])
assert.equal(b.bereitschaft.bereit, true)
assert.deepEqual(b.bereitschaft.punkte.map((p) => [p.id, p.ok]), [['einbau', true], ['stripe', true], ['einstellungen', true], ['rechtstexte', true], ['artikel', true]])
assert.equal(b.snippet, `<div data-webklar-shop="${feature.siteKey}"></div>\n<script src="${config.publicBaseUrl}/embed/shop.js" defer></script>`)
assert.equal(b.vorschauUrl, `${config.publicBaseUrl}/shop/${feature.siteKey}`)
assert.deepEqual(b.stripe, {
verbunden: true,
status: { charges_enabled: true, payouts_enabled: true, details_submitted: true, currently_due: [], past_due: [], disabled_reason: null, geprueftAt: '2026-10-05T10:00:00.000Z' },
})
assert.ok(!JSON.stringify(b).includes('acct_test1') && !JSON.stringify(b).includes('geheim'))
assert.deepEqual(b.zahlen, { artikel: 1, aktiveArtikel: 1, offeneBestellungen: 0, bestellungen30: 0, umsatz30Cent: 0 })
assert.equal(b.ticketWoid, null)
assert.equal((await schreiben('POST', 'oeffnen', { offen: true })).status, 200)
assert.equal((await lesen('status')).body.geoeffnet, true)
assert.equal((await lesen('status')).body.schalter, true)
})
// ---------------------------------------------------------------- Einstellungen
test('Einstellungen: speichern, Version zaehlt, veraltete Version 409, Fehler je Feld 400', async () => {
await aktiviere()
const leer = await lesen('einstellungen')
assert.equal(leer.body.version, 0)
const gut = await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN, version: 0 })
assert.equal(gut.status, 200)
assert.deepEqual([gut.body.version, gut.body.vollstaendig, gut.body.einstellungen.shopname], [1, true, 'Töpferei Keramik'])
assert.equal((await lesen('einstellungen')).body.einstellungen.anbieter.ort, 'Kaiserslautern')
const veraltet = await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN, version: 0 })
assert.deepEqual([veraltet.status, veraltet.body.code], [409, 'EINSTELLUNG_VERALTET'])
assert.equal((await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })).status, 200, 'ohne Version keine Pruefung')
const schlecht = await schreiben('PUT', 'einstellungen', { einstellungen: { ...EINSTELLUNGEN, akzent: 'rot' } })
assert.equal(schlecht.status, 400)
assert.ok(schlecht.body.fehler && Object.keys(schlecht.body.fehler).length > 0)
assert.equal(schlecht.body.error, Object.values(schlecht.body.fehler)[0])
assert.equal((await schreiben('PUT', 'einstellungen', { einstellungen: 'text' })).status, 400)
})
test('Einstellungen speichern leert den 30-s-Speicher von shopPerSiteKey', async () => {
const feature = await aktiviere()
await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })
const vorher = await shopPerSiteKey(feature.siteKey)
assert.equal(vorher.shop.einstellungen.shopname, 'Töpferei Keramik')
await schreiben('PUT', 'einstellungen', { einstellungen: { ...EINSTELLUNGEN, shopname: 'Neu' } })
assert.equal((await shopPerSiteKey(feature.siteKey)).shop.einstellungen.shopname, 'Neu')
})
// ---------------------------------------------------------------- Artikel
test('Artikel I3: Speichern ohne Bestandsaenderung behaelt den inzwischen verkauften Bestand, geaenderter Bestand wird geschrieben', async () => {
await aktiviere()
await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })
const neu = await schreiben('POST', 'produkte', { produkt: { ...ARTIKEL, bestand: 5, bestandGeladen: 5 } })
assert.equal(neu.status, 201)
const id = neu.body.produkt.id
assert.equal(neu.body.produkt.bestand, 5)
assert.equal(neu.body.produkt.bestandGeladen, undefined)
// Zwischenzeitlich wurden 3 Stueck verkauft
const doc = aw.docs('shopProducts').get(id)
await updateDocument(config.collections.shopProducts, id, { produkt: JSON.stringify({ ...JSON.parse(doc.produkt), bestand: 2 }) })
const nurText = await schreiben('PUT', `produkte/${id}`, { produkt: { ...ARTIKEL, name: 'Tasse 2', bestand: 5, bestandGeladen: 5 } })
assert.equal(nurText.status, 200)
assert.deepEqual([nurText.body.produkt.name, nurText.body.produkt.bestand], ['Tasse 2', 2])
assert.ok(!aw.docs('shopProducts').get(id).produkt.includes('bestandGeladen'))
const aufgestockt = await schreiben('PUT', `produkte/${id}`, { produkt: { ...ARTIKEL, bestand: 12, bestandGeladen: 5 } })
assert.equal(aufgestockt.body.produkt.bestand, 12)
const ohneMarker = await schreiben('PUT', `produkte/${id}`, { produkt: { ...ARTIKEL, bestand: 7 } })
assert.equal(ohneMarker.body.produkt.bestand, 7, 'alter Browser ohne bestandGeladen: wie bisher')
})
test('Artikel: anlegen 201, lesen, aendern, Fehler 400 je Feld, unbekannte ID 404, Kleinunternehmer-Steuer', async () => {
await aktiviere()
await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN }) // Kleinunternehmer => 0 %
const neu = await schreiben('POST', 'produkte', { produkt: { ...ARTIKEL, kurz: 'Handgemacht', extra: 'ignoriert' } })
assert.equal(neu.status, 201)
const id = neu.body.produkt.id
assert.deepEqual([neu.body.produkt.name, neu.body.produkt.preisCent, neu.body.produkt.steuersatz, neu.body.produkt.aktiv, neu.body.produkt.sort], ['Tasse', 1800, 0, true, 0])
assert.equal(neu.body.produkt.extra, undefined)
const liste = await lesen('produkte')
assert.equal(liste.status, 200)
assert.deepEqual(liste.body.produkte.map((p) => p.id), [id])
assert.deepEqual([liste.body.maxArtikel, liste.body.maxBilder], [config.shop.maxArtikel, config.shop.maxBilder])
const geaendert = await schreiben('PUT', `produkte/${id}`, { produkt: { ...ARTIKEL, name: 'Große Tasse', aktiv: false } })
assert.equal(geaendert.status, 200)
assert.deepEqual([geaendert.body.produkt.id, geaendert.body.produkt.name, geaendert.body.produkt.aktiv], [id, 'Große Tasse', false])
assert.equal((await lesen('produkte')).body.produkte[0].aktiv, false)
assert.equal(aw.docs('shopProducts').size, 1)
const schlecht = await schreiben('POST', 'produkte', { produkt: { name: '', preisCent: -5 } })
assert.equal(schlecht.status, 400)
assert.ok(schlecht.body.fehler.name && schlecht.body.fehler.preisCent)
assert.equal((await schreiben('POST', 'produkte', { produkt: 'text' })).status, 400)
assert.equal((await schreiben('POST', 'produkte', {})).status, 400)
assert.equal((await schreiben('PUT', 'produkte/gibtsnicht', { produkt: ARTIKEL })).status, 404)
assert.equal((await schreiben('DELETE', 'produkte/gibtsnicht')).status, 404)
assert.equal(aw.docs('shopProducts').size, 1)
})
test('Artikel mit Regelbesteuerung: Steuersatz kommt aus den gespeicherten Einstellungen', async () => {
await aktiviere()
await schreiben('PUT', 'einstellungen', { einstellungen: { ...EINSTELLUNGEN, kleinunternehmer: false } })
const res = await schreiben('POST', 'produkte', { produkt: { ...ARTIKEL, steuersatz: 7 } })
assert.equal(res.body.produkt.steuersatz, 7)
})
test('Artikel gehoeren zu ihrem Shop: fremder Shop kann sie weder sehen noch aendern', async () => {
await aktiviere()
const neu = await schreiben('POST', 'produkte', { produkt: ARTIKEL })
await aktiviere('pt2')
assert.deepEqual((await lesen('produkte', 'pt2')).body.produkte, [])
assert.equal((await schreiben('PUT', `produkte/${neu.body.produkt.id}`, { produkt: ARTIKEL, projectId: 'pt2' })).status, 404)
assert.equal((await schreiben('DELETE', `produkte/${neu.body.produkt.id}`, {}, { cookie }) ).status, 200)
})
test('Sortierung: neue Reihenfolge, falsche Liste 400', async () => {
await aktiviere()
const ids = []
for (const name of ['A', 'B', 'C']) ids.push((await schreiben('POST', 'produkte', { produkt: { name, preisCent: 100 } })).body.produkt.id)
const res = await schreiben('POST', 'produkte/sortierung', { ids: [ids[2], ids[0], ids[1]] })
assert.equal(res.status, 200)
assert.deepEqual(res.body.produkte.map((p) => p.name), ['C', 'A', 'B'])
assert.deepEqual((await lesen('produkte')).body.produkte.map((p) => p.name), ['C', 'A', 'B'])
assert.equal((await schreiben('POST', 'produkte/sortierung', { ids: [ids[0]] })).status, 400)
assert.equal((await schreiben('POST', 'produkte/sortierung', {})).status, 400)
})
// ---------------------------------------------------------------- Bilder
test('Bild hochladen: 201, Sidecar mit Shop und Kunde, Vorschau lesbar; Typfehler 415, zu gross 413, ohne Session 401, fremdes Projekt 403', async () => {
const feature = await aktiviere()
const res = await lade(await png(2000, 1000))
assert.equal(res.status, 201)
assert.deepEqual([res.body.w, res.body.h], [1600, 800])
assert.match(res.body.imageId, /^[a-z0-9-]{36}$/)
const dateiListe = await dateien(feature.siteKey)
assert.deepEqual(dateiListe, [`${res.body.imageId}-l.webp`, `${res.body.imageId}-s.webp`, `${res.body.imageId}.json`].sort())
const meta = JSON.parse(await (await import('node:fs/promises')).readFile(path.join(process.env.SHOP_UPLOAD_DIR, feature.siteKey, `${res.body.imageId}.json`), 'utf8'))
assert.deepEqual([meta.customerId, meta.shopId, meta.claimed], ['c1', feature.$id, false])
for (const datei of [`${res.body.imageId}-s.webp`, `${res.body.imageId}-l.webp`]) {
const bild = await fetch(`${app.url}/api/shop/bild/pt1/${datei}`, { headers: { cookie } })
assert.equal(bild.status, 200)
assert.equal(bild.headers.get('content-type'), 'image/webp')
assert.equal(bild.headers.get('x-content-type-options'), 'nosniff')
assert.equal(bild.headers.get('cache-control'), 'private, max-age=3600')
assert.equal((await sharp(Buffer.from(await bild.arrayBuffer())).metadata()).format, 'webp')
}
assert.equal((await lade(Buffer.from('%PDF-1.4 kein Bild'))).status, 415)
assert.equal((await lade(await png(), { type: 'text/plain' })).status, 415, 'kein octet-stream')
const zuGross = await lade(Buffer.alloc(8 * 1024 * 1024 + 1))
assert.deepEqual([zuGross.status, zuGross.body.error], [413, 'Das Bild ist größer als 8 MB.'])
assert.equal((await lade(await png(), { opts: {} })).status, 401)
assert.equal((await lade(await png(), { projectId: 'pt3' })).status, 403)
assert.equal((await lade(await png(), { projectId: 'pt2' })).status, 409, 'pt2 hat keine Funktion')
assert.equal((await dateien(feature.siteKey)).length, 3, 'nichts Weiteres abgelegt')
})
test('Bild-Vorschau: nur eigene Projekte, nur gueltige Dateinamen, nur vorhandene Bilder', async () => {
const feature = await aktiviere()
const { body } = await lade(await png())
const url = (projekt, datei) => `${app.url}/api/shop/bild/${projekt}/${datei}`
const hole = (projekt, datei, opts = { cookie }) => fetch(url(projekt, datei), { headers: opts.cookie ? { cookie: opts.cookie } : {} })
assert.equal((await hole('pt1', `${body.imageId}-s.webp`, {})).status, 401)
assert.equal((await hole('pt3', `${body.imageId}-s.webp`)).status, 403)
assert.equal((await hole('pt1', `${body.imageId}-xl.webp`)).status, 404)
assert.equal((await hole('pt1', `${body.imageId}.json`)).status, 404, 'Sidecar wird nicht ausgeliefert')
assert.equal((await hole('pt1', '..%2F..%2Fetc%2Fpasswd')).status, 404)
assert.equal((await hole('pt1', 'gibtsnicht-s.webp')).status, 404)
assert.equal((await hole('pt1', `${body.imageId}-s.webp`, { cookie: customerCookie('c2') })).status, 403)
// Datei ohne Sidecar gilt als nicht vorhanden
await rm(path.join(process.env.SHOP_UPLOAD_DIR, feature.siteKey, `${body.imageId}.json`))
assert.equal((await hole('pt1', `${body.imageId}-s.webp`)).status, 404)
})
test('Bilder: 40 Uploads je Stunde, der 41. wird mit 429 abgelehnt', async () => {
await aktiviere()
const klein = await png(8, 8)
for (let i = 0; i < 40; i++) assert.equal((await lade(klein)).status, 201, `Upload ${i + 1}`)
const zuViele = await lade(klein)
assert.deepEqual([zuViele.status, zuViele.body.error], [429, 'Zu viele Bilder in kurzer Zeit. Bitte versuch es später wieder.'])
assert.equal((await lade(klein, { opts: { cookie: customerCookie('c1') } })).status, 429)
})
test('Artikel mit Bildern: unbekannte oder fremde ID 400, Beanspruchen, Entfernen loescht, geteiltes Bild bleibt bis zuletzt, Loeschen des Artikels raeumt auf', async () => {
const feature = await aktiviere()
const a = (await lade(await png())).body.imageId
const b = (await lade(await png())).body.imageId
const c = (await lade(await png())).body.imageId
const meta = async (id) => JSON.parse(await (await import('node:fs/promises')).readFile(path.join(process.env.SHOP_UPLOAD_DIR, feature.siteKey, `${id}.json`), 'utf8'))
const da = (id) => existsSync(path.join(process.env.SHOP_UPLOAD_DIR, feature.siteKey, `${id}-l.webp`))
const unbekannt = await schreiben('POST', 'produkte', { produkt: { ...ARTIKEL, bilder: [a, 'gibtsnicht'] } })
assert.equal(unbekannt.status, 400)
assert.ok(unbekannt.body.fehler.bilder)
assert.equal(aw.docs('shopProducts').size, 0)
assert.equal((await meta(a)).claimed, false, 'nichts beansprucht, wenn der Artikel nicht gespeichert wurde')
// Bild eines anderen Shops
const f2 = await aktiviere('pt2')
const fremd = (await lade(await png(), { projectId: 'pt2' })).body.imageId
const mitFremd = await schreiben('POST', 'produkte', { produkt: { ...ARTIKEL, bilder: [fremd] } })
assert.equal(mitFremd.status, 400)
assert.ok(existsSync(path.join(process.env.SHOP_UPLOAD_DIR, f2.siteKey, `${fremd}-l.webp`)))
const mitPfad = await schreiben('POST', 'produkte', { produkt: { ...ARTIKEL, bilder: ['../x'] } })
assert.equal(mitPfad.status, 400)
const p1 = await schreiben('POST', 'produkte', { produkt: { ...ARTIKEL, bilder: [a, b] } })
assert.equal(p1.status, 201)
assert.deepEqual(p1.body.produkt.bilder, [a, b])
assert.deepEqual(p1.body.produkt.bildUrls[0], { id: a, klein: `/api/shop/bild/pt1/${a}-s.webp`, gross: `/api/shop/bild/pt1/${a}-l.webp` })
assert.equal((await meta(a)).claimed, true)
assert.equal((await meta(b)).claimed, true)
assert.equal((await meta(c)).claimed, false)
const p2 = await schreiben('POST', 'produkte', { produkt: { name: 'Zweiter', preisCent: 500, bilder: [b] } })
assert.equal(p2.status, 201)
// a entfernen => weg; b entfernen aus p1, aber p2 nutzt es noch => bleibt
const ohne = await schreiben('PUT', `produkte/${p1.body.produkt.id}`, { produkt: { ...ARTIKEL, bilder: [] } })
assert.equal(ohne.status, 200)
assert.equal(da(a), false)
assert.equal(existsSync(path.join(process.env.SHOP_UPLOAD_DIR, feature.siteKey, `${a}.json`)), false)
assert.equal(da(b), true, 'p2 nutzt b noch')
assert.equal(da(c), true, 'unbenutzt hochgeladene Bilder raeumt nur der Nachtjob auf')
// p2 loeschen => b weg
assert.equal((await schreiben('DELETE', `produkte/${p2.body.produkt.id}`)).status, 200)
assert.equal(da(b), false)
assert.equal(aw.docs('shopProducts').size, 1)
// Reihenfolge und Anzahl der Bilder bleiben, mehr als maxBilder abgelehnt
const viele = []
for (let i = 0; i < config.shop.maxBilder + 1; i++) viele.push((await lade(await png(8, 8))).body.imageId)
const zuViele = await schreiben('PUT', `produkte/${p1.body.produkt.id}`, { produkt: { ...ARTIKEL, bilder: viele } })
assert.equal(zuViele.status, 400)
assert.ok(zuViele.body.fehler.bilder)
const ok = await schreiben('PUT', `produkte/${p1.body.produkt.id}`, { produkt: { ...ARTIKEL, bilder: viele.slice(0, 2).reverse() } })
assert.deepEqual(ok.body.produkt.bilder, viele.slice(0, 2).reverse())
})
// ---------------------------------------------------------------- Rechtstexte
test('Rechtstexte GET: Entwurf leer, nichts bestaetigt, Muster, erzeugte Texte, Gewaehrleistung, Hash, Bestaetigungstext', async () => {
const feature = await aktiviere()
await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })
const res = await lesen('rechtstexte')
assert.equal(res.status, 200)
const b = res.body
assert.deepEqual(Object.keys(b).sort(), ['bestaetigt', 'bestaetigungText', 'entwurf', 'gewaehrleistung', 'hash', 'muster', 'probleme', 'versandUndZahlung', 'vollstaendig'])
assert.deepEqual(b.entwurf, { agb: '', widerrufsbelehrung: '', widerrufsformular: '', impressumUrl: '', datenschutzUrl: '' })
assert.equal(b.bestaetigt, null)
assert.equal(b.vollstaendig, false)
assert.deepEqual(b.probleme, [], 'leerer Entwurf: nur vollstaendig meldet etwas')
assert.match(b.hash, /^[0-9a-f]{64}$/)
assert.equal(b.hash, rechtstexteHash(b.entwurf, (await lesen('einstellungen')).body.einstellungen))
for (const k of ['agb', 'widerrufsbelehrung', 'widerrufsformular']) assert.ok(b.muster[k].length > 100, k)
assert.ok(b.muster.agb.includes('Töpferei Keramik'))
assert.ok(b.muster.widerrufsbelehrung.includes(`${config.publicBaseUrl}/shop/${feature.siteKey}/widerruf`))
assert.ok(b.versandUndZahlung.includes('Versand'))
assert.deepEqual([b.gewaehrleistung.bild, b.gewaehrleistung.text.startsWith('GESETZLICHE GEWÄHRLEISTUNG')], ['/img/gewaehrleistung-eu.webp', true])
assert.match(b.bestaetigungText, /^Ich habe die Texte geprüft/)
})
test('Rechtstexte PUT: speichert den Entwurf, normalisiert, Fehler 400; Hash aendert sich mit dem Text', async () => {
const feature = await aktiviere()
const leer = (await lesen('rechtstexte')).body.hash
const res = await schreiben('PUT', 'rechtstexte', { rechtstexte: { ...TEXTE, agb: ' Neue AGB \r\n\r\n\r\n\r\nZeile', extra: 'x' } })
assert.equal(res.status, 200)
assert.equal(res.body.entwurf.agb, 'Neue AGB\n\nZeile')
assert.equal(res.body.vollstaendig, true)
assert.notEqual(res.body.hash, leer)
assert.equal((await lesen('rechtstexte')).body.entwurf.widerrufsformular, 'Unser Widerrufsformular.')
assert.equal(JSON.parse(shopConfigDoc(feature).rechtstexte).bestaetigt, null)
const schlecht = await schreiben('PUT', 'rechtstexte', { rechtstexte: { ...TEXTE, impressumUrl: 'http://unsicher.example', datenschutzUrl: 'javascript:alert(1)' } })
assert.equal(schlecht.status, 400)
assert.ok(schlecht.body.fehler.impressumUrl && schlecht.body.fehler.datenschutzUrl)
assert.equal((await schreiben('PUT', 'rechtstexte', { rechtstexte: 'text' })).status, 400)
assert.equal((await schreiben('PUT', 'rechtstexte', {})).status, 400)
const zuLang = await schreiben('PUT', 'rechtstexte', { rechtstexte: { ...TEXTE, agb: 'x'.repeat(20001) } })
assert.equal(zuLang.status, 400)
assert.equal((await lesen('rechtstexte')).body.entwurf.agb, 'Neue AGB\n\nZeile', 'Fehler aendert nichts')
})
test('Rechtstexte bestaetigen: falscher oder alter Hash 409, unvollstaendig 400, sonst Kopie mit Zeit, Person und Hash', async () => {
const feature = await aktiviere()
await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })
const unvollstaendig = await schreiben('PUT', 'rechtstexte', { rechtstexte: { ...texteFuer(feature), datenschutzUrl: '' } })
const hashUnvollstaendig = unvollstaendig.body.hash
const leer = await schreiben('POST', 'rechtstexte/bestaetigen', { hash: hashUnvollstaendig })
assert.equal(leer.status, 400)
assert.equal(shopConfigDoc(feature).rechtstexteHash || '', '')
const voll = await schreiben('PUT', 'rechtstexte', { rechtstexte: texteFuer(feature) })
const falsch = await schreiben('POST', 'rechtstexte/bestaetigen', { hash: 'abc' })
assert.deepEqual([falsch.status, falsch.body.code], [409, 'texte_geaendert'])
assert.match(falsch.body.error, /^Die Texte wurden inzwischen geändert/)
assert.equal((await schreiben('POST', 'rechtstexte/bestaetigen', {})).status, 409)
assert.equal((await schreiben('POST', 'rechtstexte/bestaetigen', { hash: hashUnvollstaendig })).status, 409, 'Hash des frueheren Entwurfs')
const ok = await schreiben('POST', 'rechtstexte/bestaetigen', { hash: voll.body.hash })
assert.equal(ok.status, 200)
assert.deepEqual(ok.body.bestaetigt.texte, texteFuer(feature))
assert.equal(ok.body.bestaetigt.von, `${customers.c1.name} <${customers.c1.email}>`)
assert.equal(ok.body.bestaetigt.aktuell, true)
assert.ok(Math.abs(Date.now() - Date.parse(ok.body.bestaetigt.at)) < 10_000)
const doc = shopConfigDoc(feature)
assert.equal(doc.rechtstexteHash, voll.body.hash)
assert.equal(doc.rechtstexteBestaetigtVon, `${customers.c1.name} <${customers.c1.email}>`)
assert.equal(new Date(doc.rechtstexteBestaetigtAt).toISOString(), new Date(ok.body.bestaetigt.at).toISOString())
const gespeichert = JSON.parse(doc.rechtstexte)
assert.deepEqual(gespeichert.entwurf, texteFuer(feature))
assert.deepEqual([gespeichert.bestaetigt.texte, gespeichert.bestaetigt.hash], [texteFuer(feature), voll.body.hash])
assert.equal((await shopPerSiteKey(feature.siteKey)).shop.rechtstexte.bestaetigt.hash, voll.body.hash, 'Speicher wurde geleert')
// I-6: die bestaetigte Fassung liegt im Archiv (eigene ID, Texte samt Versand/Zahlung und Gewaehrleistung, Person und Zeit)
const archiv = [...aw.docs('shopRechtstexteArchiv').values()]
assert.equal(archiv.length, 1)
assert.equal(archiv[0].$id, archivId(feature.$id, voll.body.hash))
assert.deepEqual([archiv[0].shopId, archiv[0].customerId, archiv[0].hash, archiv[0].bestaetigtVon], [feature.$id, 'c1', voll.body.hash, `${customers.c1.name} <${customers.c1.email}>`])
assert.equal(new Date(archiv[0].bestaetigtAt).toISOString(), new Date(ok.body.bestaetigt.at).toISOString())
const archTexte = JSON.parse(archiv[0].texte)
assert.deepEqual({ ...archTexte, versandUndZahlung: undefined, gewaehrleistung: undefined }, { ...texteFuer(feature), versandUndZahlung: undefined, gewaehrleistung: undefined })
assert.equal(archTexte.versandUndZahlung, voll.body.versandUndZahlung)
assert.equal(archTexte.gewaehrleistung, voll.body.gewaehrleistung.text)
// Entwurf aendern: die bestaetigte Fassung bleibt, ist aber nicht mehr aktuell; Bereitschaft verlangt neue Bestaetigung
const geaendert = await schreiben('PUT', 'rechtstexte', { rechtstexte: { ...texteFuer(feature), agb: 'Geaenderte AGB' } })
assert.deepEqual(geaendert.body.bestaetigt.texte, texteFuer(feature))
assert.equal(geaendert.body.bestaetigt.aktuell, false)
assert.equal(geaendert.body.entwurf.agb, 'Geaenderte AGB')
const stand = (await lesen('status')).body.bereitschaft.punkte.find((p) => p.id === 'rechtstexte')
assert.equal(stand.ok, false)
assert.equal((await schreiben('POST', 'rechtstexte/bestaetigen', { hash: voll.body.hash })).status, 409, 'alter Hash')
})
test('Rechtstexte: Hash haengt auch an den Einstellungen (Versand und Zahlung), eine Aenderung dort verlangt neue Bestaetigung', async () => {
const feature = await aktiviere()
await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })
await schreiben('PUT', 'rechtstexte', { rechtstexte: texteFuer(feature) })
const alt = (await lesen('rechtstexte')).body.hash
await schreiben('PUT', 'einstellungen', { einstellungen: { ...EINSTELLUNGEN, versand: { ...EINSTELLUNGEN.versand, kostenCent: { DE: 690 } } } })
const neu = (await lesen('rechtstexte')).body.hash
assert.notEqual(alt, neu)
assert.equal((await schreiben('POST', 'rechtstexte/bestaetigen', { hash: alt })).status, 409)
assert.equal((await schreiben('POST', 'rechtstexte/bestaetigen', { hash: neu })).status, 200)
})
test('Rechtstexte bestaetigen I-5: Platzhalter, Musterkopf und fehlende Adresse der Widerrufsfunktion ⇒ 400 nicht_bestaetigbar mit Liste; GET zeigt dieselben Probleme', async () => {
const feature = await aktiviere()
await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })
const schlecht = { ...texteFuer(feature), agb: 'Muster – bitte prüfen und anpassen\n\nAGB von [Name], [Anschrift]', widerrufsbelehrung: 'Unsere Widerrufsbelehrung ohne Adresse.' }
const put = await schreiben('PUT', 'rechtstexte', { rechtstexte: schlecht })
assert.equal(put.status, 200)
assert.equal(put.body.probleme.length, 3)
assert.deepEqual((await lesen('rechtstexte')).body.probleme, put.body.probleme)
const res = await schreiben('POST', 'rechtstexte/bestaetigen', { hash: put.body.hash })
assert.equal(res.status, 400)
assert.equal(res.body.code, 'nicht_bestaetigbar')
assert.deepEqual(res.body.probleme, put.body.probleme)
assert.match(res.body.probleme.join('\n'), /„\[Name\]“, „\[Anschrift\]“/)
assert.match(res.body.probleme.join('\n'), /Muster – bitte prüfen und anpassen/)
assert.ok(res.body.probleme.join('\n').includes(WIDERRUF(feature)))
assert.equal(shopConfigDoc(feature).rechtstexteHash || '', '', 'nichts bestaetigt')
// behoben ⇒ ohne Probleme und bestaetigbar
const gut = await schreiben('PUT', 'rechtstexte', { rechtstexte: texteFuer(feature) })
assert.deepEqual(gut.body.probleme, [])
assert.equal((await schreiben('POST', 'rechtstexte/bestaetigen', { hash: gut.body.hash })).status, 200)
})
// ---------------------------------------------------------------- Oeffnen
test('Oeffnen: nur wenn bereit (sonst 409 mit Punkten), Schliessen immer, Speicher geleert, Eingabe geprueft', async () => {
const feature = await machBereit({ mitArtikel: false })
const nicht = await schreiben('POST', 'oeffnen', { offen: true })
assert.deepEqual([nicht.status, nicht.body.code], [409, 'nicht_bereit'])
assert.deepEqual(nicht.body.punkte.filter((p) => !p.ok).map((p) => p.id), ['artikel'])
assert.ok(nicht.body.punkte.every((p) => typeof p.text === 'string'))
assert.equal(shopConfigDoc(feature).geoeffnet !== true, true)
assert.equal((await schreiben('POST', 'oeffnen', { offen: 'ja' })).status, 400)
assert.equal((await schreiben('POST', 'oeffnen', {})).status, 400)
await schreiben('POST', 'produkte', { produkt: ARTIKEL })
const vorher = await shopPerSiteKey(feature.siteKey)
assert.equal(vorher.shop.geoeffnet, false)
const auf = await schreiben('POST', 'oeffnen', { offen: true })
assert.equal(auf.status, 200)
assert.deepEqual([auf.body.schalter, auf.body.geoeffnet, auf.body.bereitschaft.bereit], [true, true, true])
assert.equal(shopConfigDoc(feature).geoeffnet, true)
assert.match(shopConfigDoc(feature).geoeffnetAt, /^\d{4}-\d\d-\d\d/)
assert.equal((await shopPerSiteKey(feature.siteKey)).shop.geoeffnet, true, 'Speicher geleert')
const zu = await schreiben('POST', 'oeffnen', { offen: false })
assert.deepEqual([zu.status, zu.body.schalter, zu.body.geoeffnet], [200, false, false])
assert.equal((await shopPerSiteKey(feature.siteKey)).shop.geoeffnet, false)
// Stripe faellt aus: Oeffnen scheitert, Schliessen geht trotzdem
await updateDocument(config.collections.shopConfig, feature.$id, { stripeStatus: JSON.stringify({ charges_enabled: false }) })
const stripeAus = await schreiben('POST', 'oeffnen', { offen: true })
assert.deepEqual([stripeAus.status, stripeAus.body.punkte.filter((p) => !p.ok).map((p) => p.id)], [409, ['stripe']])
assert.equal((await schreiben('POST', 'oeffnen', { offen: false })).status, 200)
})
// ---------------------------------------------------------------- Kundenansicht
test('Kundenansicht: Einstellungen, Artikel, Rechtstexte-Entwurf und Bilder erlaubt und protokolliert; Lesen ebenfalls', async () => {
const feature = await aktiviere()
assert.equal((await lesen('einstellungen', 'pt1', KV)).status, 200)
assert.equal((await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN }, KV)).status, 200)
const neu = await schreiben('POST', 'produkte', { produkt: ARTIKEL }, KV)
assert.equal(neu.status, 201)
const id = neu.body.produkt.id
assert.equal((await schreiben('PUT', `produkte/${id}`, { produkt: { ...ARTIKEL, name: 'Kanne' } }, KV)).status, 200)
assert.equal((await schreiben('POST', 'produkte/sortierung', { ids: [id] }, KV)).status, 200)
assert.equal((await schreiben('PUT', 'rechtstexte', { rechtstexte: TEXTE }, KV)).status, 200)
const bild = await lade(await png(), { opts: KV })
assert.equal(bild.status, 201)
assert.equal((await schreiben('DELETE', `produkte/${id}`, {}, KV)).status, 200)
assert.equal((await dateien(feature.siteKey)).length, 3, 'das hochgeladene Bild gehoert keinem Artikel und wartet auf den Nachtjob')
const eintraege = [...aw.docs('customerViewLog').values()]
assert.ok(eintraege.every((e) => e.action === 'shop_aenderung' && e.staffName === 'Andrej' && e.customerId === 'c1'))
assert.deepEqual(eintraege.map((e) => e.detail).sort(), [
'Artikel angelegt: Tasse', 'Artikel geändert: Kanne', 'Artikel gelöscht: Kanne', 'Artikel sortiert', 'Bild hochgeladen',
'Einstellungen gespeichert', 'Rechtstexte-Entwurf gespeichert',
].sort())
})
test('Kundenansicht: Anfrage, Oeffnen und Bestaetigen sind gesperrt (Waechter), Stripe-Pfade auch', async () => {
await machBereit()
const hash = (await lesen('rechtstexte')).body.hash
for (const [pfad, body] of [['oeffnen', { offen: true }], ['rechtstexte/bestaetigen', { hash }], ['anfrage', {}], ['stripe/verbinden', {}]]) {
const res = await schreiben('POST', pfad, body, KV)
assert.deepEqual([res.status, res.body.code], [403, 'view_blocked'], pfad)
}
assert.equal(shopConfigDoc(await sf.getFeatureDoc('c1', 'pt1', 'shop')).geoeffnet !== true, true)
})
test('Kundenansicht: der Router sperrt Oeffnen und Bestaetigen selbst, auch ohne Waechter (alte Ansicht und Kundenansicht)', async () => {
await machBereit()
const hash = (await lesen('rechtstexte')).body.hash
const express = (await import('express')).default
const { sessionMiddleware } = await import('../server/middleware/session.js')
const roh = express()
roh.use(sessionMiddleware())
roh.use(express.json())
roh.use('/api/shop', shopRoutes)
const server = await new Promise((resolve) => {
const s = roh.listen(0, '127.0.0.1', () => resolve(s))
})
try {
const url = `http://127.0.0.1:${server.address().port}`
for (const [pfad, body] of [['/api/shop/oeffnen', { projectId: 'pt1', offen: true }], ['/api/shop/rechtstexte/bestaetigen', { projectId: 'pt1', hash }]]) {
for (const c of [kundenansichtCookie({ customerId: 'c1' }), viewAsCookie('c1')]) {
const res = await fetch(url + pfad, { method: 'POST', headers: { 'content-type': 'application/json', cookie: c }, body: JSON.stringify(body) })
assert.equal(res.status, 403, pfad)
assert.equal((await res.json()).error, 'In der Kundenansicht kann das nicht geändert werden.')
}
}
} finally {
await new Promise((resolve) => server.close(resolve))
}
assert.equal(JSON.parse(shopConfigDoc(await sf.getFeatureDoc('c1', 'pt1', 'shop')).rechtstexte).bestaetigt.hash, hash, 'unveraendert')
assert.equal(shopConfigDoc(await sf.getFeatureDoc('c1', 'pt1', 'shop')).geoeffnet !== true, true)
})
// ---------------------------------------------------------------- Rate-Limit und Fehler
test('Schreib-Limit: 300 je Stunde und Kunde, danach 429; Lesen zaehlt nicht; anderer Kunde unberuehrt', async () => {
await aktiviere()
for (let i = 0; i < 300; i++) {
const res = await send('POST', '/api/shop/oeffnen', { cookie, body: { projectId: 'pt1' } })
assert.equal(res.status, 400, `Aufruf ${i + 1}`)
}
const zuViele = await send('POST', '/api/shop/oeffnen', { cookie, body: { projectId: 'pt1', offen: false } })
assert.deepEqual([zuViele.status, zuViele.body.error], [429, 'Zu viele Änderungen in kurzer Zeit. Bitte versuch es in einer Stunde wieder.'])
assert.equal((await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })).status, 429)
assert.equal((await lesen('einstellungen')).status, 200)
assert.equal((await lesen('status')).status, 200)
assert.equal((await schreiben('POST', 'oeffnen', { offen: false }, { cookie: customerCookie('c2') })).status !== 429, true)
})
test('Fehlertexte von Appwrite verlassen den Server nie (500 ohne Rohtext)', async () => {
await aktiviere()
aw.failNext({ collection: 'shopProducts', method: 'GET', status: 500, message: 'GEHEIM interner Appwrite-Text' })
const res = await lesen('produkte')
assert.equal(res.status, 500)
assert.deepEqual(res.body, { error: 'Interner Serverfehler' })
assert.equal((await lesen('produkte')).status, 200)
})