Files
Webklar-Kundenbereich/test/knAnsichtGutschrift.test.js
T

190 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
})