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>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 09:58:31 +00:00
1 parent 2f4f360a21
commit 08d837d690
17 files changed
+288 -65

No files matched your search

+5 -3
View File
@@ -22,6 +22,7 @@ const aw = await startAppwriteMock({
shopProducts: schemaKeys('shopProducts'),
shopOrders: schemaKeys('shopOrders'),
shopWiderrufe: schemaKeys('shopWiderrufe'),
shopRechtstexteArchiv: schemaKeys('shopRechtstexteArchiv'),
},
})
const st = await startFakeStripe()
@@ -599,6 +600,7 @@ function seedFaelligMitShop(kunde, siteKey = SHOP_KEY) {
aw.seed('shopOrders', { $id: `${kunde}_ord1`, shopId, customerId: kunde, bestellnummer: '26-A', status: 'versendet', kaeuferEmail: 'k@example.com', bestellung: '{}' })
aw.seed('shopOrders', { $id: `${kunde}_ord2`, shopId, customerId: kunde, bestellnummer: '26-B', status: 'storniert', kaeuferEmail: 'k@example.com', bestellung: '{}' })
aw.seed('shopWiderrufe', { $id: `${kunde}_wid`, shopId, customerId: kunde, bestellnummer: '26-A', name: 'K', email: 'k@example.com', nachricht: 'x' })
aw.seed('shopRechtstexteArchiv', { $id: `${kunde}_rt`, shopId, customerId: kunde, hash: 'a'.repeat(64), texte: '{}', bestaetigtVon: 'K' })
const ordner = path.join(uploadDir, 'shop', siteKey)
mkdirSync(ordner, { recursive: true })
writeFileSync(path.join(ordner, 'bild-l.webp'), 'x')
@@ -640,8 +642,8 @@ test('Webshop: fällige Kunden verlieren auch Bestellungen, Artikel, Widerrufe,
assert.deepEqual(result.failed, [])
assert.deepEqual(result.held, [])
assert.equal(result.deleted.shops, 1)
assert.equal(result.deleted.shopDocs, 4)
for (const [col, ids] of [['shopOrders', ['sh1_ord1', 'sh1_ord2']], ['shopProducts', ['sh1_prod']], ['shopWiderrufe', ['sh1_wid']], ['shopConfig', ['f_sh1_shop']]]) {
assert.equal(result.deleted.shopDocs, 5)
for (const [col, ids] of [['shopOrders', ['sh1_ord1', 'sh1_ord2']], ['shopProducts', ['sh1_prod']], ['shopWiderrufe', ['sh1_wid']], ['shopRechtstexteArchiv', ['sh1_rt']], ['shopConfig', ['f_sh1_shop']]]) {
for (const docId of ids) assert.ok(!aw.docs(col).has(docId), `${col} ${docId} weg`)
}
assert.ok(!existsSync(path.join(uploadDir, 'shop', SHOP_KEY)), 'Bilder weg')
@@ -651,7 +653,7 @@ test('Webshop: fällige Kunden verlieren auch Bestellungen, Artikel, Widerrufe,
// Reihenfolge: Shopdaten vor den Vertragsereignissen und Abo-Dokumenten
assert.ok(reihenfolge.lastIndexOf('shopConfig') < reihenfolge.indexOf('contractEvents'))
assert.ok(reihenfolge.indexOf('contractEvents') < reihenfolge.indexOf('subscriptions'))
assert.match(formatReport(plan, result), /Webshop: 1 Shop\(s\) gelöscht \(4 Dokument\(e\)/)
assert.match(formatReport(plan, result), /Webshop: 1 Shop\(s\) gelöscht \(5 Dokument\(e\)/)
})
test('Webshop: scheitert das Löschen der Shopdaten, bleiben die Abo-Dokumente stehen (held) und der nächste Lauf setzt fort', async () => {
+20 -1
View File
@@ -16,6 +16,7 @@ const aw = await startAppwriteMock({
shopConfig: schemaKeys('shopConfig'),
shopProducts: schemaKeys('shopProducts'),
shopOrders: schemaKeys('shopOrders'),
shopRechtstexteArchiv: schemaKeys('shopRechtstexteArchiv'),
termineOutbox: schemaKeys('termineOutbox'),
},
datetimes: { ...SHOP_DATETIMES, ...TERMINE_DATETIMES },
@@ -29,6 +30,7 @@ const { deleteDocument, updateDocument } = await import('../server/services/appw
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const { resetOutboxForTests, warteAufVersand } = await import('../server/services/termine/outbox.js')
const K = await import('../server/services/shop/kontext.js')
const { archiviereRechtstexte } = await import('../server/services/shop/archiv.js')
const B = await import('../server/services/shop/bestellungen.js')
const J = await import('../server/services/shop/jobs.js')
const { normalisiereProdukt } = await import('../server/services/shop/produkte.js')
@@ -87,7 +89,7 @@ let sink
let log
let ctx
async function aufbau({ produkte = [artikel('a1', { bestand: 5 })], shop = {} } = {}) {
for (const c of ['portalFeatures', 'shopConfig', 'shopProducts', 'shopOrders', 'websiteProjects', 'termineOutbox', 'customers']) {
for (const c of ['portalFeatures', 'shopConfig', 'shopProducts', 'shopOrders', 'shopRechtstexteArchiv', 'websiteProjects', 'termineOutbox', 'customers']) {
for (const id of [...aw.docs(c).keys()]) await deleteDocument(config.collections[c], id)
}
aw.seed('websiteProjects', { $id: PROJEKT, customerId: KUNDE, projectName: 'Brotladen', liveDomain: 'https://www.brot.example' })
@@ -185,6 +187,23 @@ test('schliesseAb: Normalfall - bezahlt, Käuferdaten, Bestand abgezogen, G09 an
assert.equal(order(b.orderId).status, 'bezahlt')
})
test('schliesseAb I-6: G09 enthält die Rechtstexte der Bestellung (Archiv zum Hash), auch wenn der Händler sie danach geändert und neu bestätigt hat', async () => {
await aufbau()
const b = await bestelle()
await archiviereRechtstexte({ shopId: FEATURE, customerId: KUNDE, hash: HASH, at: T0.toISOString(), von: 'k@example.invalid', texte: { ...TEXTE, agb: 'AGB ZUM ZEITPUNKT DER BESTELLUNG', versandUndZahlung: 'x', gewaehrleistung: 'GEWÄHRLEISTUNG DAMALS' } })
// neue Fassung bestätigt, bevor bezahlt wird (SEPA: Tage später)
const neu = { ...TEXTE, agb: 'NEUE AGB NACH DER BESTELLUNG' }
aw.seed('shopConfig', shopDoc({ rechtstexte: JSON.stringify({ entwurf: neu, bestaetigt: { texte: neu, hash: 'e'.repeat(64), at: '2026-10-06T10:00:00.000Z', von: 'k@example.invalid' } }) }))
K.leereCache()
assert.equal(JSON.parse(order(b.orderId).bestellung).rechtstexteHash, HASH)
await B.schliesseAb({ accountId: ACCOUNT, session: zahle(b), now: nach(120) })
await warteAufVersand()
const g09 = sink.sent.find((m) => m.to[0].address === 'max@example.com')
assert.match(g09.text, /AGB ZUM ZEITPUNKT DER BESTELLUNG/)
assert.match(g09.text, /GEWÄHRLEISTUNG DAMALS/)
assert.doesNotMatch(g09.text, /NEUE AGB/)
})
test('schliesseAb: idempotent - zweiter Aufruf ändert nichts, keine zweite Mail, Bestand nur einmal', async () => {
await aufbau()
const b = await bestelle()
+66 -15
View File
@@ -5,12 +5,13 @@ import { mailSink } from './support/mailSink.js'
import { schemaKeys } from './support/schema.js'
import { useTestEnv } from './support/testEnv.js'
import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
import { SHOP_DATETIMES } from '../server/services/shop/schema.js'
// Webshop E4: Mails des Shops (services/shop/mails.js). Käufermails laufen durch die Warteschlange des Terminplaners
// (termineOutbox), Händlermails gehen direkt an das Kundenkonto. Mails landen in der Senke, nichts verlässt den Prozess.
const aw = await startAppwriteMock({
schema: { termineOutbox: schemaKeys('termineOutbox') },
datetimes: TERMINE_DATETIMES,
schema: { termineOutbox: schemaKeys('termineOutbox'), shopRechtstexteArchiv: schemaKeys('shopRechtstexteArchiv') },
datetimes: { ...TERMINE_DATETIMES, ...SHOP_DATETIMES },
defaultLimit: 25,
})
after(() => aw.close())
@@ -23,6 +24,8 @@ const { resetOutboxForTests, warteAufVersand } = await import('../server/service
const { STANDARD_EINSTELLUNGEN } = await import('../server/services/shop/einstellungen.js')
const { gewaehrleistungsHinweis } = await import('../server/services/shop/rechtstexte.js')
const M = await import('../server/services/shop/mails.js')
const { archiviereRechtstexte, holeArchivierteRechtstexte, archivId } = await import('../server/services/shop/archiv.js')
const { normalisiereRechtstexte, MAX_AGB, MAX_WIDERRUFSBELEHRUNG, MAX_WIDERRUFSFORMULAR } = await import('../server/services/shop/rechtstexte.js')
const { anKaeufer, anHaendler, kaeuferWerte, haendlerK25, haendlerK26 } = M
after(() => setMailTransportForTests(null))
@@ -89,6 +92,7 @@ let sink
let log
beforeEach(async () => {
for (const id of aw.docs('termineOutbox').keys()) await deleteDocument(config.collections.termineOutbox, id)
for (const id of aw.docs('shopRechtstexteArchiv').keys()) await deleteDocument(config.collections.shopRechtstexteArchiv, id)
for (const id of aw.docs('customers').keys()) await deleteDocument(config.collections.customers, id)
resetOutboxForTests()
config.smtp.from = 'noreply@webklar.com'
@@ -166,7 +170,6 @@ test('kaeuferWerte M-5: Telefonnummer des Anbieters steht in der Anbieterzeile,
test('G09: wird mit Shopname als Absender und Antwort an den Händler eingereiht, kein BCC, Rechtstexte stehen in der Mail', async () => {
const erg = await anKaeufer('G09', ctxMit(), bestellung(), {}, JETZT)
assert.equal(erg.ok, true)
assert.equal(erg.gekuerzt, false)
await warteAufVersand()
const [doc] = eintraege()
@@ -222,30 +225,78 @@ test('G09 Abholung: Abholort statt Lieferadresse', async () => {
assert.deepEqual(warnLog(), [])
})
test('G09: zu lang für die Warteschlange ⇒ der AGB-Text wird durch einen Link auf die AGB ersetzt, Belehrung und Formular bleiben', async () => {
const lang = 'Dies ist ein langer Satz der Geschäftsbedingungen.\n'.repeat(560) // etwa 28 000 Zeichen
const t = texte({ agb: lang, widerrufsbelehrung: 'Belehrung Satz.\n'.repeat(1000), widerrufsformular: 'Formular Zeile\n'.repeat(300) })
const erg = await anKaeufer('G09', ctxMit(einstellungen(), t), bestellung(), {}, JETZT)
/** Zeilen mit Satzzeichen und Sonderzeichen wie in echten Rechtstexten, genau n Zeichen lang. */
const textMitLaenge = (n, zeile) => {
let t = ''
while (t.length < n) t += `${zeile}\n`
return t.slice(0, n)
}
test('G09 I-7: Texte in Höchstlänge passen mit Volltext in die Warteschlange (html 100000, text 50000), nie ein Link statt der AGB', async () => {
const zeile = 'Ein Satz der Geschäftsbedingungen mit Komma, Klammern (wie hier) & einem „Zitat“ – und Zahlen 1.234,56 €.'
const t = texte({
agb: textMitLaenge(MAX_AGB, zeile),
widerrufsbelehrung: textMitLaenge(MAX_WIDERRUFSBELEHRUNG, zeile),
widerrufsformular: textMitLaenge(MAX_WIDERRUFSFORMULAR, zeile),
})
const geprueft = normalisiereRechtstexte({ ...t })
assert.equal(geprueft.fehler, undefined, 'Höchstlänge wird vom Speichern akzeptiert')
const viele = Array.from({ length: 40 }, (_, i) => ({ name: `Artikel Nummer ${i + 1} mit langem Namen`, varianteText: 'Farbe: blau, Größe: M', menge: 3, summeCent: 12345, widerruf: i % 2 ? 'gesetzlich' : 'verderblich' }))
const erg = await anKaeufer('G09', ctxMit(einstellungen(), t), bestellung({}, { positionen: viele }), {}, JETZT)
assert.equal(erg.ok, true)
assert.equal(erg.gekuerzt, true)
const doc = eintraege()[0]
assert.ok(doc.html.length <= 100000 && doc.text.length <= 50000, `${doc.html.length}/${doc.text.length}`)
assert.match(doc.text, new RegExp(`Die vollständigen AGB finden Sie unter https://project\\.webklar\\.com/shop/${SITE_KEY}\\?ansicht=agb`))
assert.doesNotMatch(doc.text, /langer Satz der Geschäftsbedingungen/)
assert.match(doc.text, /Belehrung Satz\./)
assert.match(doc.text, /Formular Zeile/)
assert.match(doc.text, /GESETZLICHE GEWÄHRLEISTUNG/)
assert.equal(doc.text.split('Ein Satz der Geschäftsbedingungen').length - 1 >= 300, true, 'alle Texte stehen vollständig in der Mail')
assert.ok(doc.text.includes(t.agb.slice(-200).trim().split('\n').pop().slice(0, 30)), 'Ende der AGB')
assert.doesNotMatch(doc.text, /ansicht=agb|vollständigen AGB finden Sie/)
})
test('G09: auch gekürzt zu lang ⇒ { ok: false } ohne Wurf und ohne Eintrag', async () => {
const t = texte({ widerrufsbelehrung: 'x'.repeat(60000) })
test('G09 I-7: zu lang für die Warteschlange ⇒ { ok: false } ohne Wurf, ohne Eintrag und ohne Link-Ersatz', async () => {
const t = texte({ agb: 'a&b<c>\n'.repeat(4000), widerrufsbelehrung: 'Belehrung Satz.\n'.repeat(500) })
const erg = await anKaeufer('G09', ctxMit(einstellungen(), t), bestellung(), {}, JETZT)
assert.deepEqual(erg, { ok: false, fehler: 'Die E-Mail an den Käufer konnte nicht eingereiht werden.' })
assert.equal(eintraege().length, 0)
assert.equal(sink.sent.length, 0)
assert.match(warnLog().join('\n'), /zu lang für die Warteschlange/)
})
test('G09 I-6: zeigt die archivierten Rechtstexte zum Hash der Bestellung, nicht die aktuell bestätigten', async () => {
const HASH = 'a'.repeat(64)
await archiviereRechtstexte({
shopId: 'f_shop01', customerId: 'c1', hash: HASH, at: JETZT.toISOString(), von: 'Erika',
texte: { agb: 'ALTE AGB', widerrufsbelehrung: 'ALTE BELEHRUNG', widerrufsformular: 'ALTES FORMULAR', impressumUrl: 'https://alt.example/impressum', datenschutzUrl: 'https://alt.example/datenschutz', versandUndZahlung: 'x', gewaehrleistung: 'ALTER GEWÄHRLEISTUNGSTEXT' },
})
const erg = await anKaeufer('G09', ctxMit(), bestellung({}, { rechtstexteHash: HASH }), {}, JETZT)
assert.equal(erg.ok, true)
const doc = eintraege()[0]
for (const teil of ['ALTE AGB', 'ALTE BELEHRUNG', 'ALTES FORMULAR', 'ALTER GEWÄHRLEISTUNGSTEXT']) assert.ok(doc.text.includes(teil), teil)
assert.doesNotMatch(doc.text, /AGB Zeile 1|Belehrung Zeile 1/)
assert.match(doc.html, /href="https:\/\/alt\.example\/impressum"/)
assert.deepEqual(warnLog(), [])
})
test('G09 I-6: fehlt die Fassung im Archiv, gilt mit Warnung die aktuell bestätigte; ohne Hash an der Bestellung ohne Warnung', async () => {
const erg = await anKaeufer('G09', ctxMit(), bestellung({}, { rechtstexteHash: 'b'.repeat(64) }), {}, JETZT)
assert.equal(erg.ok, true)
assert.match(eintraege()[0].text, /AGB Zeile 1/)
assert.match(warnLog().join('\n'), /fehlen im Archiv/)
assert.doesNotMatch(warnLog().join('\n'), /max@example\.com/)
})
test('Archiv I-6: Dokument-ID r + 35 Hex, ein zweites Anlegen derselben Fassung ist kein Fehler, fremder Shop findet sie nicht', async () => {
const HASH = 'c'.repeat(64)
assert.match(archivId('f_shop01', HASH), /^r[0-9a-f]{35}$/)
assert.notEqual(archivId('f_shop01', HASH), archivId('f_shop02', HASH))
const p = { shopId: 'f_shop01', customerId: 'c1', hash: HASH, at: JETZT.toISOString(), von: 'Erika', texte: { agb: 'A' } }
assert.deepEqual(await archiviereRechtstexte(p), { angelegt: true })
assert.deepEqual(await archiviereRechtstexte(p), { angelegt: false })
assert.deepEqual(await holeArchivierteRechtstexte('f_shop01', HASH), { agb: 'A' })
assert.equal(await holeArchivierteRechtstexte('f_shop02', HASH), null)
assert.equal(await holeArchivierteRechtstexte('f_shop01', 'd'.repeat(64)), null)
const doc = [...aw.docs('shopRechtstexteArchiv').values()][0]
assert.deepEqual([doc.shopId, doc.customerId, doc.hash, doc.bestaetigtVon], ['f_shop01', 'c1', HASH, 'Erika'])
})
test('G10: Versanddienst und Sendungsnummer (Block sendung); ohne beide kein Block', async () => {
assert.equal((await anKaeufer('G10', ctxMit(), bestellung(), { versanddienst: 'DHL', sendungsnummer: '00340434' }, JETZT)).ok, true)
assert.equal((await anKaeufer('G10', ctxMit(), bestellung({ $id: 'o_zwei' }), {}, JETZT)).ok, true)
+27 -7
View File
@@ -14,6 +14,10 @@ import {
BESTAETIGUNG_TEXT,
pruefeBestaetigbar,
MUSTER_KOPFZEILE,
MAX_AGB,
MAX_WIDERRUFSBELEHRUNG,
MAX_WIDERRUFSFORMULAR,
maskierteLaenge,
} from '../server/services/shop/rechtstexte.js'
import { STANDARD_EINSTELLUNGEN } from '../server/services/shop/einstellungen.js'
@@ -244,13 +248,29 @@ test('normalisiereRechtstexte: gueltig, Entwurf mit leeren Texten, Bereinigung',
assert.equal(vollstaendig(leer.rechtstexte), false)
})
test('normalisiereRechtstexte: Laengen', () => {
assert.ok(normalisiereRechtstexte({ agb: 'a'.repeat(30000) }).rechtstexte)
assert.ok(normalisiereRechtstexte({ agb: 'a'.repeat(30001) }).fehler.agb)
assert.ok(normalisiereRechtstexte({ widerrufsbelehrung: 'a'.repeat(20000) }).rechtstexte)
assert.ok(normalisiereRechtstexte({ widerrufsbelehrung: 'a'.repeat(20001) }).fehler.widerrufsbelehrung)
assert.ok(normalisiereRechtstexte({ widerrufsformular: 'a'.repeat(5000) }).rechtstexte)
assert.ok(normalisiereRechtstexte({ widerrufsformular: 'a'.repeat(5001) }).fehler.widerrufsformular)
test('normalisiereRechtstexte: Laengen (I-7: so kurz, dass die Bestätigungsmail nie Links statt Texte braucht)', () => {
assert.deepEqual([MAX_AGB, MAX_WIDERRUFSBELEHRUNG, MAX_WIDERRUFSFORMULAR], [20000, 12000, 3000])
assert.ok(normalisiereRechtstexte({ agb: 'a'.repeat(20000) }).rechtstexte)
assert.ok(normalisiereRechtstexte({ agb: 'a'.repeat(20001) }).fehler.agb)
assert.ok(normalisiereRechtstexte({ widerrufsbelehrung: 'a'.repeat(12000) }).rechtstexte)
assert.ok(normalisiereRechtstexte({ widerrufsbelehrung: 'a'.repeat(12001) }).fehler.widerrufsbelehrung)
assert.ok(normalisiereRechtstexte({ widerrufsformular: 'a'.repeat(3000) }).rechtstexte)
assert.ok(normalisiereRechtstexte({ widerrufsformular: 'a'.repeat(3001) }).fehler.widerrufsformular)
// die Muster passen mit grossem Abstand hinein
const e = basis()
assert.ok(musterAgb(e).length < MAX_AGB / 4 && musterWiderrufsbelehrung(e, { widerrufUrl: WIDERRUF_URL }).length < MAX_WIDERRUFSBELEHRUNG / 2 && musterWiderrufsformular(e).length < MAX_WIDERRUFSFORMULAR / 2)
})
test('normalisiereRechtstexte I-7: viele Sonderzeichen und Umbrüche, die das HTML der Mail sprengen würden, werden beim Speichern abgelehnt', () => {
assert.equal(maskierteLaenge('a&b<c>"\n\'x'), 1 + 5 + 1 + 4 + 1 + 4 + 6 + 4 + 5 + 1)
const r = normalisiereRechtstexte({ agb: '&'.repeat(10000), widerrufsbelehrung: '&'.repeat(2100) })
assert.match(r.fehler.rechtstexte, /zu lang für die Bestätigungs-E-Mail/)
// gleich viele normale Zeichen sind in Ordnung
assert.ok(normalisiereRechtstexte({ agb: 'a'.repeat(12000), widerrufsbelehrung: 'a'.repeat(6000) }).rechtstexte)
// Höchstlänge mit Umbruch alle 100 Zeichen bleibt unter dem Mail-Budget
const zeile = `${'x'.repeat(99)}\n`
const voll = normalisiereRechtstexte({ agb: zeile.repeat(200), widerrufsbelehrung: zeile.repeat(120), widerrufsformular: zeile.repeat(30) })
assert.equal(voll.fehler, undefined)
})
test('normalisiereRechtstexte: URLs nur https ohne Zugangsdaten und Leerraum', () => {
+14 -2
View File
@@ -18,6 +18,7 @@ const aw = await startAppwriteMock({
portalFeatures: schemaKeys('portalFeatures'),
shopConfig: schemaKeys('shopConfig'),
shopProducts: schemaKeys('shopProducts'),
shopRechtstexteArchiv: schemaKeys('shopRechtstexteArchiv'),
customerViewLog: schemaKeys('customerViewLog'),
},
datetimes: SHOP_DATETIMES,
@@ -32,6 +33,7 @@ const { kundenansichtGuard } = await import('../server/middleware/kundenansichtG
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')
@@ -117,7 +119,7 @@ beforeEach(async () => {
config.shop.mode = 'pilot'
config.shop.pilotEmails = [customers.c1.email]
resetShopRoutesForTests()
for (const col of ['portalFeatures', 'shopConfig', 'shopProducts', 'customerViewLog']) {
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 })
@@ -481,7 +483,7 @@ test('Rechtstexte PUT: speichert den Entwurf, normalisiert, Fehler 400; Hash aen
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) } })
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')
})
@@ -516,6 +518,16 @@ test('Rechtstexte bestaetigen: falscher oder alter Hash 409, unvollstaendig 400,
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' } })
+11 -3
View File
@@ -15,7 +15,7 @@ const { main } = await import('../scripts/shop-setup.mjs')
const { createDocument, updateDocument, getDocument } = await import('../server/services/appwriteAdmin.js')
after(() => Promise.all([aw.close(), st.close()]))
const NAMES = ['shopConfig', 'shopProducts', 'shopOrders', 'shopWiderrufe']
const NAMES = ['shopConfig', 'shopProducts', 'shopOrders', 'shopWiderrufe', 'shopRechtstexteArchiv']
const keysOf = (name) => SHOP_COLLECTIONS[name].attributes.map((a) => a.key)
/**
@@ -127,6 +127,12 @@ test('Schema: Typen und Groessen', () => {
[36, 36, 32, 200, 320, 2000, 36],
)
for (const key of ['eingangAt', 'bestaetigungAt', 'bearbeitetAt']) assert.equal(widerrufe[key].type, 'datetime', key)
// I-6: Archiv der bestaetigten Rechtstexte
assert.deepEqual(keysOf('shopRechtstexteArchiv'), ['shopId', 'customerId', 'hash', 'texte', 'bestaetigtAt', 'bestaetigtVon'])
const archiv = byKey('shopRechtstexteArchiv')
assert.deepEqual([archiv.shopId.size, archiv.customerId.size, archiv.hash.size, archiv.texte.size, archiv.bestaetigtVon.size], [36, 36, 64, 100000, 255])
assert.equal(archiv.bestaetigtAt.type, 'datetime')
})
test('Schema: jedes Attribut ist optional, hat eine gueltige Form und einen gueltigen Schluessel (kein Feld data)', () => {
@@ -160,6 +166,7 @@ test('Schema: Indizes wie in der Spezifikation und nur auf vorhandenen Attribute
['idx_erstellt', 'shopId', 'erstelltAt'],
])
assert.deepEqual(indexes('shopWiderrufe'), [['idx_shop', 'shopId', 'eingangAt']])
assert.deepEqual(indexes('shopRechtstexteArchiv'), [['idx_shop_hash', 'shopId', 'hash']])
for (const name of NAMES) {
for (const index of SHOP_COLLECTIONS[name].indexes) {
assert.equal(index.type, 'key')
@@ -174,6 +181,7 @@ test('Schema: SHOP_DATETIMES nennt je Collection genau die datetime-Attribute',
shopProducts: [],
shopOrders: ['erstelltAt', 'ablaufAt', 'bezahltAt', 'anonymisiertAt'],
shopWiderrufe: ['eingangAt', 'bestaetigungAt', 'bearbeitetAt'],
shopRechtstexteArchiv: ['bestaetigtAt'],
})
})
@@ -215,7 +223,7 @@ test('Konfiguration: Collection-IDs der Shop-Collections', () => {
assert.deepEqual(NAMES.map((name) => config.collections[name]), NAMES)
})
test('main legt auf leerer Datenbank 4 Collections, alle Attribute und Indizes an', async () => {
test('main legt auf leerer Datenbank 5 Collections, alle Attribute und Indizes an', async () => {
const client = fakeClient()
const result = await main({ client, log: quiet })
assert.deepEqual(client.calls.collections, NAMES.map((id) => ({ id, name: id })))
@@ -248,7 +256,7 @@ test('main: legt nur fehlende Attribute und Indizes einer vorhandenen Collection
assert.ok(!client.calls.collections.some((c) => c.id === 'shopWiderrufe'))
assert.deepEqual(client.calls.attrs.filter((k) => k.startsWith('shopWiderrufe.')), ['shopWiderrufe.bearbeitetAt', 'shopWiderrufe.orderId'])
assert.deepEqual(client.calls.indexes.filter((k) => k.startsWith('shopWiderrufe.')), ['shopWiderrufe.idx_shop'])
assert.equal(client.calls.collections.length, 3)
assert.equal(client.calls.collections.length, 4)
})
test('main --check legt nichts an und meldet Fehlendes', async () => {