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:
1 parent
2f4f360a21
commit
08d837d690
17 files changed
+288
-65
No files matched your search
@@ -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 () => {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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 () => {
|
||||
|
||||
Reference in new issue
Block a user