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

+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)