Shop Review B I-5: Bestaetigen nur ohne Platzhalter, Musterkopf und mit Adresse der Widerrufsfunktion
pruefeBestaetigbar listet die Probleme in Du-Form; POST /api/shop/rechtstexte/bestaetigen antwortet 400 nicht_bestaetigbar mit der Liste, GET und PUT liefern sie als probleme mit. Der Reiter Rechtliches zeigt die Punkte im Bestaetigungsblock und sperrt den Knopf. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1f56b86791
commit
2f4f360a21
6 files changed
+169
-15
No files matched your search
@@ -12,6 +12,8 @@ import {
|
||||
vollstaendig,
|
||||
rechtstexteHash,
|
||||
BESTAETIGUNG_TEXT,
|
||||
pruefeBestaetigbar,
|
||||
MUSTER_KOPFZEILE,
|
||||
} from '../server/services/shop/rechtstexte.js'
|
||||
import { STANDARD_EINSTELLUNGEN } from '../server/services/shop/einstellungen.js'
|
||||
|
||||
@@ -340,3 +342,35 @@ test('Markdown-Muster und Code teilen die Kernsaetze', () => {
|
||||
assert.ok(md.includes('Wir tragen die Kosten der Rücksendung der Waren.'))
|
||||
assert.doesNotMatch(md, /ec\.europa\.eu\/consumers\/odr/)
|
||||
})
|
||||
|
||||
test('pruefeBestaetigbar I-5: Muster mit Platzhaltern, Kopfzeile oder ohne Adresse der Widerrufsfunktion lässt sich nicht bestätigen', () => {
|
||||
const leer = JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN))
|
||||
const roh = { agb: musterAgb(leer), widerrufsbelehrung: musterWiderrufsbelehrung(leer, {}), widerrufsformular: musterWiderrufsformular(leer) }
|
||||
const p = pruefeBestaetigbar(roh, { widerrufUrl: WIDERRUF_URL })
|
||||
const alle = p.join('\n')
|
||||
for (const s of ['[Name]', '[Anschrift]', '[E-Mail-Adresse]', '[Internetadresse der Widerrufsfunktion]', '[Lieferzeit]', MUSTER_KOPFZEILE, WIDERRUF_URL]) assert.ok(alle.includes(s), s)
|
||||
assert.match(alle, /^In den AGB stehen noch Platzhalter/m)
|
||||
assert.match(alle, /dem Muster-Widerrufsformular/)
|
||||
|
||||
// ausgefuellt, ohne Kopfzeile, mit Adresse: keine Probleme
|
||||
const e = basis()
|
||||
const gut = {
|
||||
agb: musterAgb(e).replace(`${MUSTER_KOPFZEILE}\n\n`, ''),
|
||||
widerrufsbelehrung: musterWiderrufsbelehrung(e, { widerrufUrl: WIDERRUF_URL }),
|
||||
widerrufsformular: musterWiderrufsformular(e),
|
||||
}
|
||||
assert.deepEqual(pruefeBestaetigbar(gut, { widerrufUrl: WIDERRUF_URL }), [])
|
||||
// nur die Kopfzeile
|
||||
assert.equal(pruefeBestaetigbar({ ...gut, agb: musterAgb(e) }, { widerrufUrl: WIDERRUF_URL }).length, 1)
|
||||
// Belehrung ohne die Adresse
|
||||
const ohneUrl = pruefeBestaetigbar({ ...gut, widerrufsbelehrung: gut.widerrufsbelehrung.replace(WIDERRUF_URL, 'https://anders.example/') }, { widerrufUrl: WIDERRUF_URL })
|
||||
assert.equal(ohneUrl.length, 1)
|
||||
assert.match(ohneUrl[0], /Adresse deiner Widerrufsfunktion enthalten: https:\/\/brot\.example\/shop#widerruf/)
|
||||
// ohne bekannte Adresse wird sie nicht verlangt; beliebige Angabe in eckigen Klammern zaehlt, einzelne Zeichen nicht
|
||||
assert.deepEqual(pruefeBestaetigbar(gut, {}), [])
|
||||
assert.equal(pruefeBestaetigbar({ ...gut, agb: `${gut.agb}\n[Zahlungsarten]` }, { widerrufUrl: WIDERRUF_URL }).length, 1)
|
||||
assert.deepEqual(pruefeBestaetigbar({ ...gut, agb: `${gut.agb}\nSiehe Anlage [1]` }, { widerrufUrl: WIDERRUF_URL }), [])
|
||||
// fehlende Texte stoeren hier nicht (das prueft vollstaendig)
|
||||
assert.doesNotThrow(() => pruefeBestaetigbar(null))
|
||||
assert.deepEqual(pruefeBestaetigbar({}, { widerrufUrl: WIDERRUF_URL }), [])
|
||||
})
|
||||
+37
-11
@@ -61,6 +61,9 @@ const TEXTE = {
|
||||
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)
|
||||
@@ -98,7 +101,7 @@ async function machBereit({ mitArtikel = true } = {}) {
|
||||
const feature = await aktiviere()
|
||||
assert.equal((await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })).status, 200)
|
||||
if (mitArtikel) assert.equal((await schreiben('POST', 'produkte', { produkt: ARTIKEL })).status, 201)
|
||||
assert.equal((await schreiben('PUT', 'rechtstexte', { rechtstexte: TEXTE })).status, 200)
|
||||
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, {
|
||||
@@ -447,10 +450,11 @@ test('Rechtstexte GET: Entwurf leer, nichts bestaetigt, Muster, erzeugte Texte,
|
||||
const res = await lesen('rechtstexte')
|
||||
assert.equal(res.status, 200)
|
||||
const b = res.body
|
||||
assert.deepEqual(Object.keys(b).sort(), ['bestaetigt', 'bestaetigungText', 'entwurf', 'gewaehrleistung', 'hash', 'muster', 'versandUndZahlung', 'vollstaendig'])
|
||||
assert.deepEqual(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)
|
||||
@@ -485,13 +489,13 @@ test('Rechtstexte PUT: speichert den Entwurf, normalisiert, Fehler 400; Hash aen
|
||||
test('Rechtstexte bestaetigen: falscher oder alter Hash 409, unvollstaendig 400, sonst Kopie mit Zeit, Person und Hash', async () => {
|
||||
const feature = await aktiviere()
|
||||
await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })
|
||||
const unvollstaendig = await schreiben('PUT', 'rechtstexte', { rechtstexte: { ...TEXTE, datenschutzUrl: '' } })
|
||||
const 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: TEXTE })
|
||||
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/)
|
||||
@@ -500,7 +504,7 @@ test('Rechtstexte bestaetigen: falscher oder alter Hash 409, unvollstaendig 400,
|
||||
|
||||
const ok = await schreiben('POST', 'rechtstexte/bestaetigen', { hash: voll.body.hash })
|
||||
assert.equal(ok.status, 200)
|
||||
assert.deepEqual(ok.body.bestaetigt.texte, TEXTE)
|
||||
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)
|
||||
@@ -509,13 +513,13 @@ test('Rechtstexte bestaetigen: falscher oder alter Hash 409, unvollstaendig 400,
|
||||
assert.equal(doc.rechtstexteBestaetigtVon, `${customers.c1.name} <${customers.c1.email}>`)
|
||||
assert.equal(new Date(doc.rechtstexteBestaetigtAt).toISOString(), new Date(ok.body.bestaetigt.at).toISOString())
|
||||
const gespeichert = JSON.parse(doc.rechtstexte)
|
||||
assert.deepEqual(gespeichert.entwurf, TEXTE)
|
||||
assert.deepEqual([gespeichert.bestaetigt.texte, gespeichert.bestaetigt.hash], [TEXTE, voll.body.hash])
|
||||
assert.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')
|
||||
|
||||
// Entwurf aendern: die bestaetigte Fassung bleibt, ist aber nicht mehr aktuell; Bereitschaft verlangt neue Bestaetigung
|
||||
const geaendert = await schreiben('PUT', 'rechtstexte', { rechtstexte: { ...TEXTE, agb: 'Geaenderte AGB' } })
|
||||
assert.deepEqual(geaendert.body.bestaetigt.texte, TEXTE)
|
||||
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')
|
||||
@@ -524,9 +528,9 @@ test('Rechtstexte bestaetigen: falscher oder alter Hash 409, unvollstaendig 400,
|
||||
})
|
||||
|
||||
test('Rechtstexte: Hash haengt auch an den Einstellungen (Versand und Zahlung), eine Aenderung dort verlangt neue Bestaetigung', async () => {
|
||||
await aktiviere()
|
||||
const feature = await aktiviere()
|
||||
await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })
|
||||
await schreiben('PUT', 'rechtstexte', { rechtstexte: TEXTE })
|
||||
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
|
||||
@@ -535,6 +539,28 @@ test('Rechtstexte: Hash haengt auch an den Einstellungen (Versand und Zahlung),
|
||||
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 () => {
|
||||
|
||||
@@ -257,6 +257,42 @@ test('ungespeicherte Änderungen sperren das Bestätigen: „Erst speichern“,
|
||||
}
|
||||
})
|
||||
|
||||
test('I-5: Probleme des Servers (Platzhalter, Musterkopf, Adresse der Widerrufsfunktion) stehen im Bestätigungsblock und sperren den Knopf', async () => {
|
||||
const probleme = ['In den AGB stehen noch Platzhalter in eckigen Klammern: „[Name]“.', 'Die Widerrufsbelehrung muss die Adresse deiner Widerrufsfunktion enthalten: https://x.example/w.']
|
||||
const fx = startFx({ get: () => antwort(VOLL, { hash: 'h1', probleme }), post: () => ({ body: antwort(VOLL) }) })
|
||||
try {
|
||||
const { root } = await oeffne(fx)
|
||||
const box = root.querySelector('[data-probleme]')
|
||||
assert.ok(box, 'Liste sichtbar')
|
||||
assert.deepEqual([...box.querySelectorAll('li')].map((li) => li.textContent), probleme)
|
||||
await fx.klick(haken(root))
|
||||
assert.equal(knopf(root, 'bestaetigen').disabled, true, 'mit Problemen gesperrt')
|
||||
await fx.klick(knopf(root, 'bestaetigen'))
|
||||
assert.equal(sendet(fx, 'POST').length, 0)
|
||||
} finally {
|
||||
fx.restore()
|
||||
}
|
||||
})
|
||||
|
||||
test('I-5: lehnt der Server mit nicht_bestaetigbar ab, zeigt der Reiter dessen Liste', async () => {
|
||||
const probleme = ['In den AGB steht noch die Zeile „Muster – bitte prüfen und anpassen“. Prüfe den Text, passe ihn an und lösche die Zeile.']
|
||||
const fx = startFx({
|
||||
get: () => antwort(VOLL, { hash: 'h1' }),
|
||||
post: () => ({ status: 400, body: { error: 'Die Texte können noch nicht bestätigt werden. Bitte behebe zuerst die genannten Punkte.', code: 'nicht_bestaetigbar', probleme } }),
|
||||
})
|
||||
try {
|
||||
const { root } = await oeffne(fx)
|
||||
assert.equal(root.querySelector('[data-probleme]'), null)
|
||||
await fx.klick(haken(root))
|
||||
await fx.klick(knopf(root, 'bestaetigen'))
|
||||
assert.equal(sendet(fx, 'POST').length, 1)
|
||||
assert.deepEqual([...root.querySelector('[data-probleme]').querySelectorAll('li')].map((li) => li.textContent), probleme)
|
||||
assert.equal(knopf(root, 'bestaetigen').disabled, true)
|
||||
} finally {
|
||||
fx.restore()
|
||||
}
|
||||
})
|
||||
|
||||
test('unvollständige Texte lassen sich nicht bestätigen und sagen warum', mitDom({ get: () => antwort({ ...VOLL, datenschutzUrl: '' }) }, async (fx) => {
|
||||
const { root } = await oeffne(fx)
|
||||
await fx.klick(haken(root))
|
||||
|
||||
Reference in new issue
Block a user