Der Editor sendet neben bestand den beim Laden gesehenen Wert (bestandGeladen, je Variantenwert mit id). speichereProdukt behaelt unter der Artikelsperre den aktuell gespeicherten Stand, wenn gesendet gleich geladen ist, und schreibt nur vom Haendler geaenderte Bestaende. bestandGeladen wird nie gespeichert; ohne den Wert (alter Browser) gilt der gesendete Bestand wie bisher. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
655 lines
39 KiB
JavaScript
655 lines
39 KiB
JavaScript
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'),
|
||
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 { 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)),
|
||
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 }
|
||
|
||
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: TEXTE })).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', '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, '2–4 Werktage')
|
||
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', 'versandUndZahlung', 'vollstaendig'])
|
||
assert.deepEqual(b.entwurf, { agb: '', widerrufsbelehrung: '', widerrufsformular: '', impressumUrl: '', datenschutzUrl: '' })
|
||
assert.equal(b.bestaetigt, null)
|
||
assert.equal(b.vollstaendig, false)
|
||
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(30001) } })
|
||
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: { ...TEXTE, 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: TEXTE })
|
||
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, TEXTE)
|
||
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, TEXTE)
|
||
assert.deepEqual([gespeichert.bestaetigt.texte, gespeichert.bestaetigt.hash], [TEXTE, voll.body.hash])
|
||
assert.equal((await shopPerSiteKey(feature.siteKey)).shop.rechtstexte.bestaetigt.hash, voll.body.hash, 'Speicher wurde geleert')
|
||
|
||
// Entwurf aendern: die bestaetigte Fassung bleibt, ist aber nicht mehr aktuell; Bereitschaft verlangt neue Bestaetigung
|
||
const geaendert = await schreiben('PUT', 'rechtstexte', { rechtstexte: { ...TEXTE, agb: 'Geaenderte AGB' } })
|
||
assert.deepEqual(geaendert.body.bestaetigt.texte, TEXTE)
|
||
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 () => {
|
||
await aktiviere()
|
||
await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })
|
||
await schreiben('PUT', 'rechtstexte', { rechtstexte: TEXTE })
|
||
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)
|
||
})
|
||
|
||
// ---------------------------------------------------------------- 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)
|
||
})
|