Datenschutz: Abschnitt 6c Terminplaner, Kundenansicht und Nachweise bei Funktionsbestellung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6ec8885bfd
commit
a629b2519b
5 files changed
+135
-43
No files matched your search
+114
-3
@@ -382,7 +382,7 @@ test('Rechtsrunde: Zusatzspeicher ganz oder teilweise kündbar, auch über „Ve
|
||||
|
||||
test('Rechtsrunde Datenschutz: Aufbewahrung nach dem Ende des letzten Abos, Zahl der Pakete, kurzer UptimeRobot-Eintrag', () => {
|
||||
const html = read(DS), ds = strip(html)
|
||||
assert.equal(ds.split('drei Jahre nach Ablauf des Jahres, in dem Ihr letztes Abo endet').length - 1, 2, 'Abschnitt 7 und Kündigungs-/Widerrufsseite')
|
||||
assert.equal(ds.split('drei Jahre nach Ablauf des Jahres, in dem Ihr letztes Abo endet').length - 1, 3, 'Abschnitt 7 (Bestellungen, Funktionsbestellung) und Kündigungs-/Widerrufsseite')
|
||||
for (const s of ['drei Jahre nach Vertragsende', 'nach dem Ende Ihres letzten Abos']) assert.ok(!ds.includes(s), `noch da: ${s}`)
|
||||
// Abschnitt 6 (Kundenkonto) verweist für Abos und Bestellungen auf die Fristen in Abschnitt 7
|
||||
assert.ok(between(ds, '6. Kundenportal', '7. Abos').includes('Für Ihre Abos und Bestellungen gelten die Fristen in Abschnitt 7.'))
|
||||
@@ -495,9 +495,120 @@ test('legal_datenschutz_news: Abschnitt 6a und Leistungsbeschreibung (Entwurf)',
|
||||
test('legal_datenschutz_kundenansicht: Abschnitt 6 nennt Kundenansicht, Sperren, Protokoll und 12 Monate', () => {
|
||||
const html = read(DS)
|
||||
assert.ok(!html.includes('Eine Nachricht in Ihrem Namen senden können sie dabei nicht.'))
|
||||
assert.ok(html.includes('Um Sie zu unterstützen, können unsere Mitarbeiter Ihr Portal so öffnen, wie Sie es sehen, und dabei für Sie News-Beiträge anlegen, ändern oder löschen.'))
|
||||
assert.ok(html.includes('Um Sie zu unterstützen, können unsere Mitarbeiter Ihr Portal so öffnen, wie Sie es sehen, und dabei für Sie News-Beiträge anlegen, ändern oder löschen und, wenn Sie den Terminplaner nutzen, dessen Einstellungen und Termine ansehen und ändern.'))
|
||||
assert.ok(!html.includes('News-Beiträge anlegen, ändern oder löschen. Bestellungen'), 'der alte Satz endet nicht mehr nach den News')
|
||||
assert.ok(html.includes('Bestellungen, Zahlungen, Änderungen an Ihrem Passwort sowie Nachrichten oder Anträge in Ihrem Namen sind dabei ausgeschlossen.'))
|
||||
assert.ok(html.includes('Jedes Öffnen und jede Änderung halten wir mit Mitarbeiter und Zeitpunkt fest und löschen diese Einträge nach 12 Monaten.'))
|
||||
assert.ok(html.includes('Rechtsgrundlage dafür ist Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist die Nachvollziehbarkeit von Zugriffen auf Ihr Konto.'))
|
||||
assert.ok(html.includes('Stand 01.10.2026'))
|
||||
// Stand: Tag der Änderung (TP2 F12a), nicht vor der Kundenansicht (01.10.2026) und nicht vor dem Datum aus termsVersion
|
||||
const stand = /Stand (\d{2})\.(\d{2})\.(2026)/.exec(html)
|
||||
assert.ok(stand, 'Stand JJ.MM.2026 fehlt')
|
||||
const standTag = `${stand[3]}-${stand[2]}-${stand[1]}`
|
||||
assert.ok(standTag >= '2026-10-01', `Stand ${stand[0]} liegt vor der Kundenansicht (01.10.2026)`)
|
||||
const terms = /^(\d{4})-(\d{2})-(\d{2})-v\d+$/.exec(config.abo.termsVersion)
|
||||
assert.ok(terms)
|
||||
assert.ok(standTag >= `${terms[1]}-${terms[2]}-${terms[3]}`, `Stand ${stand[0]} liegt vor termsVersion ${config.abo.termsVersion}`)
|
||||
assert.ok(html.includes('<p class="meta">Stand 05.10.2026</p>'), 'Stand = Tag der Änderung')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Abschnitt 6c und Funktionsbestellung (TP2 F12a)
|
||||
|
||||
/** Der Block von <h2 id="termine"> bis vor <h2>7. in public/datenschutz.html (roh, mit Tags). */
|
||||
const block6c = (html = read(DS)) => {
|
||||
const a = html.indexOf('<h2 id="termine">')
|
||||
assert.ok(a >= 0, '<h2 id="termine"> fehlt')
|
||||
const b = html.indexOf('<h2>7. ', a)
|
||||
assert.ok(b > a)
|
||||
return html.slice(a, b)
|
||||
}
|
||||
|
||||
test('legal_datenschutz_termine: Abschnitt 6c steht zwischen 6b und 7 und nennt Rolle, Orte, Fristen und Rückkehr', () => {
|
||||
const html = read(DS)
|
||||
const analyse = html.indexOf('id="analyse"')
|
||||
const termine = html.indexOf('id="termine"')
|
||||
const sieben = html.indexOf('<h2>7. ')
|
||||
assert.ok(analyse > 0 && termine > analyse && sieben > termine, 'Reihenfolge 6b < 6c < 7')
|
||||
assert.equal(html.split('id="termine"').length - 1, 1)
|
||||
assert.ok(html.includes('<h2 id="termine">6c. Online-Terminbuchung auf Websites unserer Kunden</h2>'))
|
||||
const roh = block6c(html)
|
||||
assert.ok(!roh.includes('<!--'), 'kein Kommentar im Block')
|
||||
const text = strip(roh)
|
||||
for (const s of ['Auftragsverarbeiter', 'Falkenstein', 'Nürnberg', '48 Stunden', '30 Tage', '90 Tage', 'sieben Tage', 'nicht dauerhaft']) assert.ok(text.includes(s), `6c: ${s}`)
|
||||
// Rolle: Auftragsverarbeiter für Gäste, Verantwortliche für Betriebsdaten, Fragebogen-Entwurf und Bestellung
|
||||
assert.ok(text.includes('ausschließlich als Auftragsverarbeiter nach den Weisungen des jeweiligen Kunden'))
|
||||
assert.ok(text.includes('Verantwortliche sind wir für die Betriebsdaten des Kunden im Kundenportal, für den Entwurf seines Fragebogens und für seine Bestellung'))
|
||||
// Entwurf, E-Mail über Hetzner (Nürnberg, Abschnitt 8), Aufruf je Modus, keine IP in der Buchung
|
||||
assert.ok(text.includes('Nicht bestellte Entwürfe löschen wir 90 Tage nach der letzten Änderung'))
|
||||
assert.ok(text.includes('(Rechenzentrum Nürnberg, Abschnitt 8)'))
|
||||
assert.ok(text.includes('mit der Buchung speichern wir die IP-Adresse nicht'))
|
||||
assert.ok(text.includes('schon beim Laden der Seite') && text.includes('Konfiguration mit den Texten und Leistungen'))
|
||||
// Ende des Abos (Wortlaut der Vorgabe)
|
||||
assert.ok(text.includes('Endet das Wartungs-Abo oder schalten wir die Funktion ab, schalten wir den Terminplaner spätestens am folgenden Tag ab und löschen alle Termindaten und Einstellungen 30 Tage danach; beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schalten wir ihn mit den vorhandenen Daten wieder ein.'))
|
||||
assert.ok(text.includes('bleiben gelöschte Daten noch bis zu sieben Tage erhalten'))
|
||||
// die Verbote der Nachbartests gelten auch hier
|
||||
for (const verboten of ['Abschnitt 6 und 7', 'spätestens nach 24 Stunden', 'Endet die Zusammenarbeit, schalten wir', 'Unterauftragsverarbeiter', 'im Rahmen des Wartungs-Abos']) assert.ok(!text.includes(verboten), `noch da: ${verboten}`)
|
||||
// Nummerierung 7 bis 15 unverändert
|
||||
const titel = [...html.matchAll(/<h2>(\d+)\. ([^<]*)<\/h2>/g)].map((m) => `${m[1]}. ${m[2]}`)
|
||||
assert.deepEqual(titel, [
|
||||
'1. Verantwortlicher', '2. Geltungsbereich', '3. Hosting, Server-Protokolle und Überwachung der Erreichbarkeit', '4. Cookies und Speicher im Browser', '5. Kontaktaufnahme', '6. Kundenportal',
|
||||
'7. Abos und Zahlung über Stripe', '8. E-Mail', '9. Buchhaltung und Rechnungen', '10. Domains unserer Kunden', '11. Interessenten', '12. Empfänger im Überblick', '13. Ihre Rechte',
|
||||
'14. Pflicht zur Bereitstellung, keine automatisierten Entscheidungen', '15. Änderungen',
|
||||
])
|
||||
})
|
||||
|
||||
test('datenschutz_6c_gleich_markdown: der Block in public/datenschutz.html ist die Markdown-Quelle, die Quelle ist ohne Entwurfs- und Prüflisten-Reste', () => {
|
||||
const md = read('docs/rechtliches/termine-datenschutz-6c.md').replace(/\r/g, '')
|
||||
const fence = /```html\n([\s\S]*?)\n```/.exec(md)
|
||||
assert.ok(fence, 'Markdown-Quelle ohne html-Block')
|
||||
const zeilen = (t) => t.split('\n').map((z) => z.trim()).filter((z) => z !== '')
|
||||
assert.deepEqual(zeilen(block6c()), zeilen(fence[1]), 'HTML-Block und Markdown-Quelle weichen ab')
|
||||
assert.equal(md.split('```').length - 1, 2, 'genau ein Codeblock')
|
||||
for (const rest of ['Einbau in TP2', 'nicht eingebaut', 'nicht veröffentlicht', 'Für die Rechtsprüfung offen', 'Entwurf für', 'anwaltlich prüfen lassen', '<!--']) assert.ok(!md.includes(rest), `noch da: ${rest}`)
|
||||
assert.ok(md.startsWith('# Datenschutzerklärung von WEBklar: Abschnitt 6c'))
|
||||
assert.ok(md.includes('Auftragsverarbeiter') && md.includes('6c. Online-Terminbuchung'))
|
||||
})
|
||||
|
||||
test('legal_datenschutz_funktionsbestellung: Abschnitt 7 nennt den Nachweis der Funktionsbestellung, die Rechtsgrundlagen, die Löschfrist und die Entwurfsdaten', () => {
|
||||
const abos = between(strip(read(DS)), '7. Abos und Zahlung über Stripe', '8. E-Mail')
|
||||
const absatz = 'Bestellen Sie eine Funktion (News-Pinnwand, Terminplaner), speichern wir als Nachweis die Texte und Fassungen, denen Sie zugestimmt haben, den Zeitpunkt Ihres Klicks auf „Zahlungspflichtig bestellen“, die Stripe-Sitzung, IP-Adresse und Browserkennung sowie Ihr Konto und die angemeldete Person. '
|
||||
+ 'Rechtsgrundlagen sind Art. 6 Abs. 1 lit. b, c und f DSGVO; unser berechtigtes Interesse ist der Nachweis des Vertragsschlusses und der Zustimmungen, insbesondere für den Widerruf. '
|
||||
+ 'Diese Angaben löschen wir drei Jahre nach Ablauf des Jahres, in dem Ihr letztes Abo endet. '
|
||||
+ 'Den Entwurf Ihres Fragebogens speichern wir bis zu 90 Tage nach Ihrer letzten Änderung und löschen ihn danach, wenn Sie nichts bestellt haben.'
|
||||
assert.ok(abos.includes(absatz), 'Absatz zur Funktionsbestellung (wörtlich) fehlt')
|
||||
assert.equal(abos.split('Bestellen Sie eine Funktion').length - 1, 1)
|
||||
// der Absatz steht im HTML als eigener Absatz von Abschnitt 7
|
||||
assert.ok(read(DS).includes(`<p>${absatz}</p>`))
|
||||
// 6c nennt die Entwurfsdaten und die Frist ebenfalls (Spec 3.7)
|
||||
assert.ok(strip(block6c()).includes('Entwurf seines Fragebogens'))
|
||||
})
|
||||
|
||||
test('wiederkehr_satz_gleich_lb: Rückkehr des Abos innerhalb von 30 Tagen steht in 6c, Leistungsbeschreibung, AVV und K22 mit „30 Tage“ und „wieder ein“', async () => {
|
||||
const { readdirSync } = await import('node:fs')
|
||||
const k22 = readdirSync(new URL('../server/mail-templates/', import.meta.url)).filter((f) => /^K22 .*\.html$/.test(f))
|
||||
assert.equal(k22.length, 1, 'genau eine Vorlage K22')
|
||||
const quellen = {
|
||||
'6c': block6c(),
|
||||
Leistungsbeschreibung: read('docs/rechtliches/leistungsbeschreibung-terminplaner.md'),
|
||||
AVV: read('docs/rechtliches/avv-terminplaner.md'),
|
||||
K22: read(`server/mail-templates/${k22[0]}`),
|
||||
}
|
||||
for (const [name, quelle] of Object.entries(quellen)) {
|
||||
// Absatz = Markdown-Absatz, <p> oder Tabellenzelle der Mail
|
||||
const absaetze = quelle.replace(/\r/g, '').split(/\n\s*\n|<\/p>|<\/td>/).map(strip)
|
||||
const treffer = absaetze.filter((a) => a.includes('wieder ein') && a.includes('Wartungs-Abo'))
|
||||
assert.equal(treffer.length, 1, `${name}: genau ein Absatz zur Rückkehr`)
|
||||
assert.ok(treffer[0].includes('30 Tage'), `${name}: Rückkehr ohne 30 Tage`)
|
||||
// die Rückkehr steht im Zusammenhang mit der Löschung nach 30 Tagen (nicht nur irgendwo im Absatz)
|
||||
const i = treffer[0].indexOf('wieder ein')
|
||||
assert.ok(treffer[0].lastIndexOf('30 Tage', i) >= 0, `${name}: die 30 Tage stehen vor der Rückkehr`)
|
||||
}
|
||||
})
|
||||
|
||||
test('datenschutz_pdf: das PDF der Datenschutzerklärung ist eingecheckt (> 20 KB, %PDF) und das Skript baut es im Modus datenschutz', () => {
|
||||
const pdf = readFileSync(file('docs/WEBklar-Datenschutzerklaerung.pdf'))
|
||||
assert.ok(pdf.length > 20 * 1024, `PDF zu klein: ${pdf.length} Bytes`)
|
||||
assert.equal(pdf.subarray(0, 5).toString('latin1'), '%PDF-')
|
||||
assert.ok(pdf.subarray(-2048).toString('latin1').includes('%%EOF'), 'PDF vollständig geschrieben')
|
||||
const skript = read('scripts/build-legal-pdfs.sh')
|
||||
assert.match(skript, /datenschutz\)\n\s*pdf public\/datenschutz\.html docs\/WEBklar-Datenschutzerklaerung\.pdf\n\s*;;/)
|
||||
})
|
||||
Reference in new issue
Block a user