190 lines
12 KiB
JavaScript
190 lines
12 KiB
JavaScript
import { test } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { readFileSync } from 'node:fs'
|
||
import { createHash } from 'node:crypto'
|
||
import { zeigeGutschrift, sammleDateien, IO } from '../public/neu/ansicht-gutschrift.js'
|
||
import { klartext } from '../server/services/bestelltexte.js'
|
||
import { mit, me, abo, aboAntwort, anfrageGutschrift, neueWurzel, LIMITS } from './support/knDaten.js'
|
||
import { NEU, meMit, aboNeu } from './support/knBestellDaten.js'
|
||
|
||
// Gutschrift-Antrag im schlanken Muster (Spec 5.6, Fassung 6): Satz mit Links über dem Knopf, keine Klappe, kein Häkchen;
|
||
// die Bedingungen sind eine eigene Ansicht (#gutschrift?bedingungen=1), der Rechtstext bleibt unverändert
|
||
const ABO = aboNeu({ subscription: abo() })
|
||
const ANTRAEGE = { requests: [anfrageGutschrift({ id: 't1', status: 'rueckfrage', question: 'Bitte <b>Kündigungsbestätigung</b> hochladen' }), anfrageGutschrift({ id: 't2', status: 'genehmigt', amountCents: 4000, decisionNote: 'Passt.' }), anfrageGutschrift({ id: 't3', status: 'abgelehnt', decisionNote: 'Nicht erstattbar.' })], conditionsVersion: '2026-09-30', limits: LIMITS }
|
||
const SRV = { 'GET /api/abo': ABO, 'GET /api/abo/takeover-credits': ANTRAEGE }
|
||
const zeigeNeu = async (fx, m = meMit('unternehmen'), query = '') => {
|
||
const w = neueWurzel(fx)
|
||
await zeigeGutschrift(w, { route: { bereich: 'vertraege', etage: 'gutschrift', query }, me: m, nav: 'push' })
|
||
await fx.ruhe()
|
||
return w
|
||
}
|
||
async function fuelleAntrag(fx, w) {
|
||
await fx.klick(fx.finde(w, 'input[value="hosting"]'))
|
||
await fx.setze(fx.finde(w, '#tk-provider'), 'IONOS')
|
||
await fx.setze(fx.finde(w, '#tk-amount'), '119.88')
|
||
await fx.setze(fx.finde(w, '#tk-months'), '12')
|
||
await fx.setze(fx.finde(w, '#tk-paid-until'), '2027-03-31')
|
||
await fx.setze(fx.finde(w, '#tk-message'), 'Der Vertrag dort läuft noch bis Ende März.')
|
||
fx.finde(w, '#tk-files').files = [{ name: 'r.pdf', size: 10 }]
|
||
}
|
||
|
||
test('Formular: Satz vor dem Knopf, keine Klappe, kein Häkchen, Links wie im Wortlaut', mit(SRV, async (fx) => {
|
||
const w = await zeigeNeu(fx)
|
||
await fx.klick(fx.finde(w, '#tk-start'))
|
||
assert.ok(fx.finde(w, '#tk-consent') === null, 'kein Häkchen für die Zustimmung')
|
||
assert.deepEqual([fx.alle(w, 'details').length, fx.finde(w, '#tk-submit').textContent], [0, 'Antrag absenden'])
|
||
const ids = fx.alle(w, '[id]').map((e) => e.id)
|
||
assert.ok(ids.indexOf('tk-satz') > -1 && ids.indexOf('tk-satz') < ids.indexOf('tk-submit'))
|
||
assert.equal(fx.text(fx.finde(w, '#tk-satz')), klartext(ABO.bestelltexte.gutschrift.satz.text))
|
||
const [bed, ds] = fx.alle(w, '#tk-satz a')
|
||
assert.deepEqual([bed.textContent, bed.getAttribute('href'), bed.getAttribute('target'), ds.getAttribute('href'), ds.getAttribute('target')], ['Bedingungen der Übernahmegutschrift', '#gutschrift?bedingungen=1', '_blank', '/datenschutz.html#uebernahmegutschrift', '_blank'])
|
||
assert.equal(fx.alle(w, (e) => e.tagName === 'INPUT' && e.getAttribute('type') === 'checkbox' && e.getAttribute('name') !== 'services').length, 0, 'nur die Leistungen sind Häkchen')
|
||
}))
|
||
|
||
test('Absenden sendet consent und die Fassung, nie eine Kundenart', mit({ ...SRV, 'POST /api/abo/takeover-credits': { request: ANTRAEGE.requests[0] } }, async (fx) => {
|
||
IO.lies = async () => 'UERGLTEuNA=='
|
||
const w = await zeigeNeu(fx)
|
||
await fx.klick(fx.finde(w, '#tk-start'))
|
||
await fuelleAntrag(fx, w)
|
||
await fx.klick(fx.finde(w, '#tk-submit'))
|
||
await fx.ruhe()
|
||
const b = fx.fetchLog.find((a) => a.method === 'POST' && a.url === '/api/abo/takeover-credits').body
|
||
assert.deepEqual([b.consent, b.termsVersion, 'kundenart' in b, 'unternehmer' in b], [true, NEU, false, false])
|
||
assert.deepEqual({ ...b, files: undefined }, { services: ['hosting'], provider: 'IONOS', domain: '', invoiceAmount: '119.88', billingMonths: 12, paidUntil: '2027-03-31', message: 'Der Vertrag dort läuft noch bis Ende März.', email: 'anna@example.com', consent: true, termsVersion: NEU, files: undefined })
|
||
assert.deepEqual(b.files, [{ name: 'r.pdf', data: 'UERGLTEuNA==' }])
|
||
assert.match(fx.text(w), /Vielen Dank! Dein Antrag ist eingegangen\. Die Eingangsbestätigung haben wir an anna@example\.com geschickt; unsere Antwort kommt ebenfalls per E-Mail\./)
|
||
}))
|
||
|
||
test('Bedingungen-Ansicht: Titel als Ereignis, fünf Sätze aus dem Block, Rückweg, kein Formular', mit(SRV, async (fx) => {
|
||
const w = neueWurzel(fx)
|
||
const titel = []
|
||
w.addEventListener('kn:titel', (e) => titel.push(e.detail.text))
|
||
await zeigeGutschrift(w, { route: { bereich: 'vertraege', etage: 'gutschrift', query: 'bedingungen=1' }, me: meMit('unternehmen'), nav: 'push' })
|
||
await fx.ruhe()
|
||
assert.deepEqual(titel, ['Bedingungen der Übernahmegutschrift'])
|
||
assert.equal(fx.alle(w, 'h1').length, 0)
|
||
assert.equal(fx.alle(w, 'ol > li').length, 5)
|
||
assert.equal(fx.finde(w, 'ol > li').textContent, 'Die Übernahmegutschrift gibt es für Leistungen, die Ihr Abo ersetzt (Domain, Hosting bzw. Betrieb der Website, E-Mail-Postfächer) und die Sie bei Ihrem bisherigen Anbieter bereits im Voraus bezahlt haben.')
|
||
assert.equal(fx.finde(w, 'a[href="#gutschrift"]').textContent, 'Zurück zum Antrag')
|
||
assert.ok(fx.finde(w, '#tk-form') === null)
|
||
assert.equal(fx.fetchLog.length, 0, 'die Bedingungen brauchen keinen Abruf')
|
||
}))
|
||
|
||
test('Andere Abfragen zeigen den Antrag, nicht die Bedingungen', mit(SRV, async (fx) => {
|
||
for (const query of ['', 'bedingungen=0', 'x=1']) {
|
||
const w = await zeigeNeu(fx, meMit('unternehmen'), query)
|
||
assert.ok(fx.finde(w, '#tk-start'), query)
|
||
assert.equal(fx.alle(w, 'ol > li').length, 0, query)
|
||
}
|
||
}))
|
||
|
||
test('Bedingungen der Gutschrift: Block gleich dem Pin in legalTexts.test.js (Fassung 2026-09-30)', () => {
|
||
const q = readFileSync(new URL('../public/neu/ansicht-gutschrift.js', import.meta.url), 'utf8')
|
||
const i = q.indexOf('const TAKEOVER_CONDITIONS')
|
||
const block = q.slice(i, q.indexOf(']', i))
|
||
assert.equal(createHash('sha256').update(block).digest('hex'), '2b7a745cb599c0ab28f20615a745bd86da6faa8d120391ca56beac35a0473e62')
|
||
})
|
||
|
||
test('sammleDateien: Prüftexte in du', async () => {
|
||
const g = { maxFiles: 3, maxFileBytes: 8 * 1048576, maxTotalBytes: 10 * 1048576 }
|
||
const f = (name, size) => ({ name, size })
|
||
const lies = async () => 'QQ=='
|
||
await assert.rejects(sammleDateien([], g, { erforderlich: true }, lies), { message: 'Bitte lade die Rechnung deines bisherigen Anbieters hoch.' })
|
||
await assert.rejects(sammleDateien([1, 2, 3, 4].map((n) => f(`${n}.pdf`, 1)), g, { erforderlich: false }, lies), { message: 'Bitte höchstens 3 Dateien auf einmal hochladen.' })
|
||
await assert.rejects(sammleDateien([f('groß.pdf', 9 * 1048576)], g, { erforderlich: false }, lies), { message: '„groß.pdf“ ist größer als 8 MB.' })
|
||
await assert.rejects(sammleDateien([f('a.pdf', 6 * 1048576), f('b.pdf', 6 * 1048576)], g, { erforderlich: false }, lies), { message: 'Die Dateien sind zusammen größer als 10 MB.' })
|
||
assert.deepEqual(await sammleDateien([f('a.pdf', 5)], g, { erforderlich: true }, lies), [{ name: 'a.pdf', data: 'QQ==' }])
|
||
})
|
||
|
||
test('Antrag: Prüfungen in du, Meldung nach Erfolg', mit({ ...SRV, 'POST /api/abo/takeover-credits': {} }, async (fx) => {
|
||
IO.lies = async () => 'QQ=='
|
||
const w = await zeigeNeu(fx)
|
||
await fx.klick(fx.finde(w, (e) => e.textContent === 'Gutschrift beantragen'))
|
||
await fx.klick(fx.finde(w, '#tk-submit'))
|
||
assert.match(fx.text(w), /Bitte wähle, welche Leistung du bisher woanders bezahlt hast\./)
|
||
await fx.klick(fx.finde(w, 'input[value="domain"]'))
|
||
await fx.klick(fx.finde(w, '#tk-submit'))
|
||
assert.match(fx.text(w), /Bitte nenne deinen bisherigen Anbieter\./)
|
||
await fx.setze(fx.finde(w, '#tk-provider'), 'IONOS')
|
||
await fx.klick(fx.finde(w, '#tk-submit'))
|
||
assert.match(fx.text(w), /Bitte gib Rechnungsbetrag, Zeitraum und „Bezahlt bis“ an\./)
|
||
await fx.setze(fx.finde(w, '#tk-amount'), '119.88')
|
||
await fx.setze(fx.finde(w, '#tk-months'), '12')
|
||
await fx.setze(fx.finde(w, '#tk-paid-until'), '2027-03-31')
|
||
await fx.klick(fx.finde(w, '#tk-submit'))
|
||
await fx.ruhe()
|
||
assert.match(fx.text(w), /Bitte lade die Rechnung deines bisherigen Anbieters hoch\./)
|
||
assert.ok(!fx.fetchLog.some((a) => a.method === 'POST'))
|
||
fx.finde(w, '#tk-files').files = [{ name: 'r.pdf', size: 10 }]
|
||
await fx.klick(fx.finde(w, '#tk-submit'))
|
||
await fx.ruhe()
|
||
assert.match(fx.text(w), /Vielen Dank! Dein Antrag ist eingegangen\./)
|
||
}))
|
||
|
||
test('Rückfrage beantworten; Kundentext bleibt Text; Ansichtsmodus sperrt alles', mit({ ...SRV, 'POST /api/abo/takeover-credits/t1/reply': {} }, async (fx) => {
|
||
const w = await zeigeNeu(fx)
|
||
assert.ok(fx.finde(w, 'b') === null)
|
||
await fx.setze(fx.finde(w, 'form[data-reply-for="t1"] textarea'), 'Anbei.')
|
||
await fx.klick(fx.finde(w, 'form[data-reply-for="t1"] button[type="submit"]'))
|
||
await fx.ruhe()
|
||
assert.deepEqual(fx.fetchLog.find((a) => a.url.endsWith('/reply')).body, { message: 'Anbei.', files: [] })
|
||
assert.match(fx.text(w), /Danke für deine Antwort! Wir prüfen deinen Antrag weiter und melden uns per E-Mail\./)
|
||
const v = await zeigeNeu(fx, meMit('unternehmen', { viewAs: true }))
|
||
assert.match(fx.text(v), /Im Ansichtsmodus kann kein Antrag gestellt werden\./)
|
||
assert.ok(fx.alle(v, 'button').filter((b) => ['Gutschrift beantragen', 'Antwort senden'].includes(b.textContent)).every((b) => b.hasAttribute('disabled')))
|
||
}))
|
||
|
||
test('Gutschrift ohne Abo: Satz und Link zum Abo über die Brücke', mit({ 'GET /api/abo': aboAntwort(), 'GET /api/abo/takeover-credits': { requests: [], limits: LIMITS } }, async (fx) => {
|
||
const w = await zeigeNeu(fx)
|
||
assert.match(fx.text(w), /Die Übernahmegutschrift kannst du beantragen, sobald dein Abo läuft\./)
|
||
const a = fx.finde(w, 'a[href="#vertraege"]')
|
||
assert.deepEqual([a.textContent, a.getAttribute('data-view')], ['Zum Abo', 'vertraege'])
|
||
}))
|
||
|
||
test('Gutschrift: Antwort ohne Text und Nachweis wird abgewiesen, Abbrechen schließt das Formular', mit({ ...SRV, 'POST /api/abo/takeover-credits/t1/reply': {} }, async (fx) => {
|
||
const w = await zeigeNeu(fx)
|
||
await fx.klick(fx.finde(w, 'form[data-reply-for="t1"] button[type="submit"]'))
|
||
await fx.ruhe()
|
||
assert.match(fx.text(w), /Bitte schreib eine Antwort oder lade einen Nachweis hoch\./)
|
||
assert.ok(!fx.fetchLog.some((a) => a.url.endsWith('/reply')))
|
||
assert.equal(fx.finde(w, '#tk-form').hasAttribute('hidden'), true)
|
||
await fx.klick(fx.finde(w, '#tk-start'))
|
||
assert.equal(fx.finde(w, '#tk-form').hasAttribute('hidden'), false)
|
||
await fx.klick(fx.finde(w, '#tk-cancel'))
|
||
assert.equal(fx.finde(w, '#tk-form').hasAttribute('hidden'), true)
|
||
assert.match(fx.text(w), /Antrag .* · Domain/)
|
||
assert.match(fx.text(w), /Nicht genehmigt/)
|
||
assert.match(fx.text(w), /Gutschrift: 40,00\s€ – wird mit deinen nächsten Monatsgebühren verrechnet\./)
|
||
}))
|
||
|
||
test('Gutschrift: Ausfall der Anträge und des Abos', mit({ 'GET /api/abo': ABO, 'GET /api/abo/takeover-credits': () => ({ status: 500, body: { error: 'Server kaputt' } }) }, async (fx) => {
|
||
assert.match(fx.text(await zeigeNeu(fx)), /Übernahmegutschrift konnte nicht geladen werden: Server kaputt/)
|
||
fx.tabelle['GET /api/abo'] = () => ({ status: 500, body: { error: 'x' } })
|
||
assert.match(fx.text(await zeigeNeu(fx)), /Deine Verträge konnten gerade nicht geladen werden\./)
|
||
}))
|
||
|
||
test('Ohne Wortlaut der Gutschrift: nur ein Satz, kein Formular, kein Knopf; unsichere Adressen werden Text', mit({ 'GET /api/abo': ABO, 'GET /api/abo/takeover-credits': ANTRAEGE }, async (fx) => {
|
||
const ohne = aboNeu({ subscription: abo() })
|
||
delete ohne.bestelltexte.gutschrift
|
||
fx.tabelle['GET /api/abo'] = ohne
|
||
const w = await zeigeNeu(fx)
|
||
assert.ok(fx.finde(w, '#tk-start') === null)
|
||
assert.ok(fx.finde(w, '#tk-form') === null)
|
||
assert.match(fx.text(w), /Die Online-Bestellung wird gerade eingerichtet – schreib uns gern im Chat\./)
|
||
const boese = aboNeu({ subscription: abo() })
|
||
boese.bestelltexte.gutschrift.satz.urls = ['javascript:alert(1)', '//evil.example']
|
||
fx.tabelle['GET /api/abo'] = boese
|
||
const v = await zeigeNeu(fx)
|
||
await fx.klick(fx.finde(v, '#tk-start'))
|
||
assert.equal(fx.alle(v, '#tk-satz a[href]').length, 0)
|
||
}))
|
||
|
||
test('Quelltext: der Block TAKEOVER_CONDITIONS ist die einzige Sie-Stelle, keine Klappe, kein Häkchen', () => {
|
||
const q = readFileSync(new URL('../public/neu/ansicht-gutschrift.js', import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '')
|
||
const i = q.indexOf('const TAKEOVER_CONDITIONS')
|
||
const ohneBlock = q.slice(0, i) + q.slice(q.indexOf(']', i))
|
||
assert.doesNotMatch(ohneBlock, /\b(Sie|Ihr(e[nmrs]?)?|Ihnen)\b/)
|
||
assert.doesNotMatch(ohneBlock, /details|dialog|innerHTML|tk-consent|tk-conditions/)
|
||
assert.doesNotMatch(ohneBlock, /body:\s*\{[^}]*kundenart/s)
|
||
})
|