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>
696 lines
42 KiB
JavaScript
696 lines
42 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'),
|
||
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)
|
||
})
|