News: Aufbewahrung, Datenschutz-Abschnitt und Leistungsbeschreibung (Entwurf)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-09-30 17:23:41 +00:00
1 parent 0e0776b406
commit 0dc57fe5e8
6 files changed
+220 -4

No files matched your search

+20
View File
@@ -462,3 +462,23 @@ test('Fassung 5: Widerrufsbelehrung und Formular wortgleich mit Fassung 4', () =
const muster = (belehrung) => between(belehrung, 'Widerrufsrecht Sie haben das Recht', 'Ende der Widerrufsbelehrung')
assert.equal(muster(section(cur, 'widerruf', 'formular')), muster(section(v4, 'widerruf', 'formular')))
})
test('legal_datenschutz_news: Abschnitt 6a und Leistungsbeschreibung (Entwurf)', () => {
const html = read('public/datenschutz.html')
const a = html.indexOf('id="news"')
assert.ok(a > html.indexOf('6. Kundenportal'), 'Abschnitt 6a steht nach Abschnitt 6')
const b = html.indexOf('<h2>7. ', a)
assert.ok(b > a)
const news = strip(html.slice(a, b))
for (const s of ['News-Beiträge und Fotos', 'Metadaten', 'GPS', 'Falkenstein', 'Art. 6 Abs. 1 lit. b DSGVO']) assert.ok(news.includes(s), s)
assert.ok(html.slice(Math.max(0, a - 120), a).includes('ENTWURF, Freigabe Nutzer ausstehend'))
assert.ok(html.includes('<h2>7. Abos und Zahlung über Stripe</h2>'), 'keine Umnummerierung')
const lb = read('public/vertragsunterlagen/funktionen-leistungsbeschreibung.html')
assert.ok(lb.includes('ENTWURF, Freigabe Nutzer ausstehend'))
const text = strip(lb)
for (const s of ['30,00 €', '§ 356 Abs. 4 BGB', '1–2 Wochen', 'keine laufenden Kosten']) assert.ok(text.includes(s), s)
const muster = read('docs/funktionen/website-datenschutz-absatz.md')
assert.ok(!/[–—]/.test(muster.slice(muster.indexOf('## Variante A'))), 'Kundentext ohne Gedankenstriche')
})
+42
View File
@@ -15,11 +15,14 @@ const aw = await startAppwriteMock({
subscriptions: schemaKeys('subscriptions'),
contractEvents: schemaKeys('contractEvents'),
takeoverCredits: schemaKeys('takeoverCredits'),
newsPosts: schemaKeys('newsPosts'),
portalFeatures: schemaKeys('portalFeatures'),
},
})
const st = await startFakeStripe()
const uploadDir = mkdtempSync(path.join(os.tmpdir(), 'loeschfristen-'))
process.env.TAKEOVER_UPLOAD_DIR = uploadDir
process.env.NEWS_UPLOAD_DIR = path.join(uploadDir, 'news')
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { findDueDeletions, applyDeletions, formatReport, CONSENT_METADATA_KEYS } = await import('../server/services/retention.js')
const { run } = await import('../scripts/loeschfristen.mjs')
@@ -264,3 +267,42 @@ test('genehmigter Antrag ohne decidedAt: übersprungen und gemeldet, kein Rückg
assert.ok(plan.skipped.some((t) => t.id === 'nd_tk'))
assert.match(formatReport(plan), /Übersprungen[\s\S]*nd_tk/)
})
test('retention_loescht_news: Beiträge, Fotos und Funktionen fälliger Kunden', async () => {
const { storeNewsImage, newsImagePath } = await import('../server/services/newsImages.js')
const sharp = (await import('sharp')).default
const png = () => sharp({ create: { width: 40, height: 30, channels: 3, background: '#c33' } }).png().toBuffer()
sub('rn_sub', 'rn', { status: 'canceled', endedAt: '2022-03-15T10:00:00.000Z' })
sub('rn2_sub', 'rn2', { status: 'active' }) // laufendes Abo: nichts davon wird angefasst
stripeSub('rn_sub', 'rn')
const { imageId } = await storeNewsImage({ customerId: 'rn', projectId: 'rnp', buffer: await png() })
const other = await storeNewsImage({ customerId: 'rn2', projectId: 'rntwop', buffer: await png() })
aw.seed('newsPosts', { $id: 'rn_post1', customerId: 'rn', projectId: 'rnp', type: 'photo', imageId, title: '', text: '' })
aw.seed('newsPosts', { $id: 'rn_post2', customerId: 'rn', projectId: 'rnp', type: 'note', imageId: null, title: 'Hi', text: 'Hallo' })
aw.seed('newsPosts', { $id: 'rn2_post', customerId: 'rn2', projectId: 'rntwop', type: 'photo', imageId: other.imageId, title: '', text: '' })
aw.seed('portalFeatures', { $id: 'rn_feat', customerId: 'rn', projectId: 'rnp', featureKey: 'news', state: 'active', enabled: true })
aw.seed('portalFeatures', { $id: 'rn2_feat', customerId: 'rn2', projectId: 'rntwop', featureKey: 'news', state: 'active', enabled: true })
const plan = await findDueDeletions(JAN_2026)
const entry = plan.news.find((n) => n.customerId === 'rn')
assert.deepEqual(entry.posts.map((p) => p.id).sort(), ['rn_post1', 'rn_post2'])
assert.deepEqual(entry.posts.find((p) => p.id === 'rn_post1'), { id: 'rn_post1', projectId: 'rnp', imageId })
assert.deepEqual(entry.featureIds, ['rn_feat'])
assert.ok(!plan.news.some((n) => n.customerId === 'rn2'))
assert.match(formatReport(plan), /Kunde rn: 2 News-Beitrag\/-Beiträge mit 1 Foto\(s\) zu löschen, 1 Funktion\(en\) abzuschalten/)
const result = await applyDeletions(plan)
assert.deepEqual(result.failed, [])
assert.equal(result.deleted.newsPosts, 2)
assert.equal(result.deleted.newsImages, 1)
assert.equal(result.deleted.features, 1)
assert.match(formatReport(plan, result), /News: 2 Beitrag\/Beiträge und 1 Foto\(s\) gelöscht, 1 Funktion\(en\) abgeschaltet/)
assert.equal([...aw.docs('newsPosts').values()].filter((d) => d.customerId === 'rn').length, 0)
assert.equal([...aw.docs('newsPosts').values()].filter((d) => d.customerId === 'rn2').length, 1)
assert.equal(existsSync(newsImagePath('rnp', imageId, 'l')), false)
assert.equal(existsSync(newsImagePath('rntwop', other.imageId, 'l')), true)
const feat = [...aw.docs('portalFeatures').values()].find((d) => d.$id === 'rn_feat')
assert.equal(feat.state, 'disabled')
assert.equal(feat.enabled, false)
assert.equal([...aw.docs('portalFeatures').values()].find((d) => d.$id === 'rn2_feat').state, 'active')
})