212 lines
15 KiB
JavaScript
212 lines
15 KiB
JavaScript
import { test } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import {
|
||
TOKEN_MUSTER, KONTAKT, ANREDE_SIE, ANREDE_DU, TEXTE_GEMEINSAM, TEXTE_SIE, TEXTE_DU, texte, tokenAusAdresse, euroDe, datumDe, zeitpunktDe,
|
||
pdfPfad, agbPfad, zeigtFruehbeginn, darfBestaetigen, statusAnzeige, kartenMeta, zusammenfassung, bestaetigtZeilen, zustandAusAntwort,
|
||
fehlerMeldung, sortiereAuftraege, abNummerNormalisiert,
|
||
} from '../public/auftrag-ansicht.js'
|
||
import { TOKEN, BESTAETIGT_AM, KLEINUNTERNEHMER, ab, privat, bestaetigt, hatDuForm, hatSieForm, pruefeQuelltext } from './auftraegeKundenportalDaten.js'
|
||
|
||
const NB = '\u{a0}' // geschütztes Leerzeichen vor „€“ (wie euroDe auf dem Server)
|
||
|
||
test('Token: 43 Zeichen base64url, zuerst aus #t= (so verlinkt K31), sonst aus ?t=, nie mehrdeutig', () => {
|
||
assert.equal(TOKEN.length, 43)
|
||
assert.match(TOKEN, TOKEN_MUSTER)
|
||
assert.equal(tokenAusAdresse('', `#t=${TOKEN}`), TOKEN)
|
||
assert.equal(tokenAusAdresse(`?t=${TOKEN}`, ''), TOKEN)
|
||
assert.equal(tokenAusAdresse(`?x=1&t=${TOKEN}`, ''), TOKEN)
|
||
const anderer = 'y'.repeat(43)
|
||
assert.equal(tokenAusAdresse(`?t=${TOKEN}`, `#t=${anderer}`), anderer, 'das Fragment gewinnt')
|
||
assert.equal(tokenAusAdresse(`?t=${TOKEN}`, '#t=abc'), TOKEN, 'ungültiges Fragment: weiter mit der Suche')
|
||
for (const hash of ['', '#', '#t=', '#t=abc', `#t=${TOKEN}x`, `#t=${TOKEN.slice(0, 42)}!`, `#t=${TOKEN}&t=${TOKEN}`, `#token=${TOKEN}`]) {
|
||
assert.equal(tokenAusAdresse('', hash), '', hash)
|
||
}
|
||
assert.equal(tokenAusAdresse(`?t=${TOKEN}&t=${TOKEN}`, ''), '')
|
||
assert.equal(tokenAusAdresse(undefined, undefined), '')
|
||
})
|
||
|
||
test('Beträge: ganze Cent als deutscher Euro-Betrag wie auf dem Server', () => {
|
||
assert.equal(euroDe(80000), `800,00${NB}€`)
|
||
assert.equal(euroDe(123456), `1.234,56${NB}€`)
|
||
assert.equal(euroDe(5), `0,05${NB}€`)
|
||
assert.equal(euroDe(0), `0,00${NB}€`)
|
||
assert.equal(euroDe(-449), `−4,49${NB}€`)
|
||
for (const kaputt of [1.5, '800', null, undefined, Number.NaN]) assert.equal(euroDe(kaputt), '–')
|
||
})
|
||
|
||
test('Datum und Zeit: Berliner Zeit, unabhängig von der Zeitzone des Browsers', () => {
|
||
assert.equal(datumDe('2026-10-06'), '06.10.2026')
|
||
assert.equal(datumDe('06.10.2026'), '06.10.2026')
|
||
assert.equal(datumDe('2026-10-06T22:30:00.000Z'), '07.10.2026') // 00:30 MESZ am Folgetag
|
||
for (const kaputt of ['', null, undefined, 'morgen', '2026-10', 20261006]) assert.equal(datumDe(kaputt), '')
|
||
assert.equal(zeitpunktDe(BESTAETIGT_AM), '07.10.2026, 10:15 Uhr')
|
||
assert.equal(zeitpunktDe('2026-10-07T08:15:00.000+00:00'), '07.10.2026, 10:15 Uhr') // so liefert Appwrite
|
||
assert.equal(zeitpunktDe('2026-12-01T08:15:00Z'), '01.12.2026, 09:15 Uhr') // Winterzeit
|
||
assert.equal(zeitpunktDe('2026-10-06T22:05:00Z'), '07.10.2026, 00:05 Uhr') // Mitternacht ohne „24“
|
||
for (const kaputt of ['', null, undefined, 'bald']) assert.equal(zeitpunktDe(kaputt), '')
|
||
})
|
||
|
||
test('Links: PDF-Pfad aus dem Token, AGB nur als PDF unter /rechtliches/', () => {
|
||
assert.equal(pdfPfad(TOKEN), `/api/auftrag/${TOKEN}/pdf`)
|
||
assert.equal(agbPfad('/rechtliches/agb-2026-09-30.pdf'), '/rechtliches/agb-2026-09-30.pdf')
|
||
for (const boese of ['https://evil.example/agb.pdf', '//evil.example/agb.pdf', '/rechtliches/../x.pdf', '/rechtliches/agb.pdf?x=1', '/rechtliches/agb.html', 'javascript:alert(1)', '', null, undefined, 5]) {
|
||
assert.equal(agbPfad(boese), '', String(boese))
|
||
}
|
||
})
|
||
|
||
test('Häkchen: Frühbeginn nur mit Text vom Server; Bestätigen nur, solange nicht bestätigt', () => {
|
||
assert.equal(zeigtFruehbeginn(privat()), true)
|
||
assert.equal(zeigtFruehbeginn(ab()), false)
|
||
assert.equal(zeigtFruehbeginn(privat({ texte: { bestaetigung: 'x', fruehbeginn: ' ' } })), false)
|
||
assert.equal(zeigtFruehbeginn({}), false)
|
||
assert.equal(darfBestaetigen(ab()), true)
|
||
assert.equal(darfBestaetigen(ab({ status: 'abgerechnet' })), true) // Schlussrechnung vor der Bestätigung (Übersicht V2)
|
||
assert.equal(darfBestaetigen(bestaetigt(ab())), false)
|
||
assert.equal(darfBestaetigen(ab({ status: 'abgerechnet', bestaetigtAm: BESTAETIGT_AM })), false)
|
||
for (const status of ['storniert', 'ersetzt', 'widerrufen', 'entwurf']) assert.equal(darfBestaetigen(ab({ status })), false, status)
|
||
})
|
||
|
||
test('Status: Beschriftung und Ton; „Bitte bestätigen“ nur mit kannBestaetigen und ohne Bestätigung', () => {
|
||
assert.deepEqual(statusAnzeige({ status: 'versendet', kannBestaetigen: true, bestaetigtAm: null }), { text: 'Bitte bestätigen', ton: 'warn' })
|
||
assert.deepEqual(statusAnzeige({ status: 'abgerechnet', kannBestaetigen: true, bestaetigtAm: null }), { text: 'Bitte bestätigen', ton: 'warn' })
|
||
assert.deepEqual(statusAnzeige({ status: 'versendet' }), { text: 'Versendet', ton: 'muted' })
|
||
assert.deepEqual(statusAnzeige({ status: 'bestaetigt', bestaetigtAm: BESTAETIGT_AM, kannBestaetigen: false }), { text: 'Bestätigt', ton: 'ok' })
|
||
assert.deepEqual(statusAnzeige({ status: 'abgerechnet', bestaetigtAm: BESTAETIGT_AM }), { text: 'Abgerechnet', ton: 'ok' })
|
||
assert.deepEqual(statusAnzeige({ status: 'storniert' }), { text: 'Storniert', ton: 'muted' })
|
||
assert.deepEqual(statusAnzeige({ status: 'ersetzt' }), { text: 'Ersetzt', ton: 'muted' })
|
||
assert.deepEqual(statusAnzeige({ status: 'widerrufen' }), { text: 'Widerrufen', ton: 'rot' })
|
||
assert.deepEqual(statusAnzeige({ status: 'unbekannt' }), { text: '–', ton: 'muted' })
|
||
assert.deepEqual(statusAnzeige(undefined), { text: '–', ton: 'muted' })
|
||
})
|
||
|
||
test('Karten-Zeile und Sortierung der Portalliste', () => {
|
||
assert.equal(kartenMeta(ab()), `vom 06.10.2026 · 800,00${NB}€`)
|
||
assert.equal(kartenMeta(ab({ art: 'aufwand', positionen: [], summeCents: null })), 'vom 06.10.2026 · nach Aufwand')
|
||
assert.equal(kartenMeta(ab({ datum: '' })), `800,00${NB}€`)
|
||
assert.equal(kartenMeta({}), '')
|
||
const liste = [
|
||
{ nummer: 'AB-2026-0001', kannBestaetigen: false }, { nummer: 'AB-2026-0003', kannBestaetigen: false },
|
||
{ nummer: 'AB-2026-0002', kannBestaetigen: true }, { nummer: 'AB-2025-0009', kannBestaetigen: false },
|
||
]
|
||
assert.deepEqual(sortiereAuftraege(liste).map((a) => a.nummer), ['AB-2026-0002', 'AB-2026-0003', 'AB-2026-0001', 'AB-2025-0009'])
|
||
assert.equal(liste[0].nummer, 'AB-2026-0001', 'die Eingabe bleibt unverändert')
|
||
assert.deepEqual(sortiereAuftraege(undefined), [])
|
||
})
|
||
|
||
test('Zusammenfassung (Festpreis): Kopf, Positionen, Summe, Zahlungsplan, Termine, § 19 aus der API', () => {
|
||
assert.deepEqual(zusammenfassung(ab()), {
|
||
kopf: [{ label: 'Nummer', wert: 'AB-2026-0002' }, { label: 'Datum', wert: '06.10.2026' }, { label: 'Projekt', wert: 'Website Anelia Wolf' }],
|
||
art: 'festpreis',
|
||
positionen: [
|
||
{ beschreibung: 'Erstellung der Website anelia-wolf.com', menge: '1', einzelpreis: `600,00${NB}€`, summe: `600,00${NB}€` },
|
||
{ beschreibung: 'Texte und Bildbearbeitung', menge: '2', einzelpreis: `100,00${NB}€`, summe: `200,00${NB}€` },
|
||
],
|
||
summe: `800,00${NB}€`,
|
||
aufwand: '',
|
||
zahlung: ab().zahlungsplanText,
|
||
termine: [{ label: 'Start', wert: '12.10.2026' }, { label: 'Voraussichtliche Fertigstellung', wert: '30.11.2026' }],
|
||
hinweis: KLEINUNTERNEHMER,
|
||
})
|
||
})
|
||
|
||
test('Zusammenfassung (Aufwand): Aufwandstext statt Positionen; Termine nur, wenn gesetzt; ohne Satz der API kein § 19-Hinweis; leere Eingabe wirft nicht', () => {
|
||
const z = zusammenfassung(ab({
|
||
art: 'aufwand', positionen: [], summeCents: null, aufwandText: '100,00 € pro Stunde in Schritten von 15 Minuten.',
|
||
zahlungsplanText: 'Monatlich nach Aufwand.', startAm: '2026-10-12', fertigBis: '',
|
||
}))
|
||
assert.equal(z.art, 'aufwand')
|
||
assert.deepEqual(z.positionen, [])
|
||
assert.equal(z.summe, '')
|
||
assert.equal(z.aufwand, '100,00 € pro Stunde in Schritten von 15 Minuten.')
|
||
assert.equal(z.zahlung, 'Monatlich nach Aufwand.')
|
||
assert.deepEqual(z.termine, [{ label: 'Start', wert: '12.10.2026' }])
|
||
assert.equal(z.hinweis, KLEINUNTERNEHMER)
|
||
assert.equal(zusammenfassung(ab({ texte: { bestaetigung: 'x', fruehbeginn: null } })).hinweis, '', 'keine eigene Ersatzkonstante: der Satz kommt von der API')
|
||
assert.deepEqual(zusammenfassung({}), { kopf: [], art: 'festpreis', positionen: [], summe: '', aufwand: '', zahlung: '', termine: [], hinweis: '' })
|
||
assert.doesNotThrow(() => zusammenfassung(undefined))
|
||
})
|
||
|
||
test('Bestätigt-Zeilen: Zeitpunkt, bei Privatpersonen Frühbeginn bzw. frühester Beginn, in beiden Anreden', () => {
|
||
assert.deepEqual(bestaetigtZeilen(ab()), [])
|
||
assert.deepEqual(bestaetigtZeilen(bestaetigt(ab())), ['Bestätigt am 07.10.2026, 10:15 Uhr'])
|
||
assert.deepEqual(bestaetigtZeilen(bestaetigt(ab({ fruehesterBeginn: '2026-10-21' }))), ['Bestätigt am 07.10.2026, 10:15 Uhr'], 'Unternehmen: keine Zeile zum Beginn')
|
||
assert.deepEqual(bestaetigtZeilen(bestaetigt(privat())), ['Bestätigt am 07.10.2026, 10:15 Uhr', 'Wir beginnen frühestens am 21.10.2026.'])
|
||
const wunsch = bestaetigt(privat({ fruehbeginn: true, fruehesterBeginn: null }))
|
||
assert.deepEqual(bestaetigtZeilen(wunsch), ['Bestätigt am 07.10.2026, 10:15 Uhr', 'Wir beginnen auf Ihren Wunsch schon vor Ablauf der Widerrufsfrist.'])
|
||
assert.deepEqual(bestaetigtZeilen(wunsch, ANREDE_DU), ['Bestätigt am 07.10.2026, 10:15 Uhr', 'Wir beginnen auf deinen Wunsch schon vor Ablauf der Widerrufsfrist.'])
|
||
assert.deepEqual(bestaetigtZeilen(bestaetigt(privat({ fruehesterBeginn: null }))), ['Bestätigt am 07.10.2026, 10:15 Uhr'])
|
||
assert.deepEqual(bestaetigtZeilen({ bestaetigtAm: 'kaputt' }), ['Bestätigt'])
|
||
})
|
||
|
||
test('Zustände aus der Serverantwort: feste Texte, der 410-Grund nur aus dem Stichwort', () => {
|
||
const kontakt = 'Bitte melden Sie sich bei uns. Wir senden Ihnen gern einen neuen Link.'
|
||
assert.deepEqual(zustandAusAntwort(404, { error: 'Dieser Link ist ungültig oder abgelaufen.' }), { zustand: 'ungueltig', titel: 'Dieser Link ist ungültig oder abgelaufen.', text: kontakt })
|
||
assert.deepEqual(zustandAusAntwort(410, { error: 'Diese Auftragsbestätigung wurde ersetzt.' }), {
|
||
zustand: 'beendet', titel: 'Diese Auftragsbestätigung wurde ersetzt.', text: 'Bitte verwenden Sie den Link in der neuesten Auftragsbestätigung, die wir Ihnen geschickt haben.',
|
||
})
|
||
for (const grund of ['storniert', 'widerrufen']) {
|
||
assert.deepEqual(zustandAusAntwort(410, { error: `Diese Auftragsbestätigung wurde ${grund}.` }), {
|
||
zustand: 'beendet', titel: `Diese Auftragsbestätigung wurde ${grund}.`, text: 'Bei Fragen melden Sie sich gern bei uns.',
|
||
})
|
||
}
|
||
for (const body of [{ error: `Irgendwas mit ${TOKEN}` }, {}, undefined, null]) { // fremder Text (auch mit Token) wird nie angezeigt
|
||
const z = zustandAusAntwort(410, body)
|
||
assert.equal(z.titel, 'Diese Auftragsbestätigung ist nicht mehr gültig.')
|
||
assert.equal(z.text, 'Bei Fragen melden Sie sich gern bei uns.')
|
||
}
|
||
assert.deepEqual(zustandAusAntwort(429, { error: 'Zu viele Anfragen. Bitte später erneut versuchen.' }), { zustand: 'limit', titel: 'Zu viele Anfragen', text: 'Bitte versuchen Sie es in einigen Minuten noch einmal.' })
|
||
for (const status of [0, 400, 401, 500, 502, 503]) {
|
||
assert.deepEqual(zustandAusAntwort(status, { error: 'Interner Fehler.' }), {
|
||
zustand: 'fehler', titel: 'Das hat leider nicht geklappt', text: 'Ihre Auftragsbestätigung konnte gerade nicht geladen werden. Bitte versuchen Sie es in einigen Minuten noch einmal.',
|
||
})
|
||
}
|
||
})
|
||
|
||
test('Fehlermeldungen beim Bestätigen: feste eigene Texte nach Status, Sie auf der Seite und Du im Portal', () => {
|
||
assert.equal(fehlerMeldung(0), 'Keine Verbindung. Bitte prüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.')
|
||
assert.equal(fehlerMeldung(400), 'Bitte setzen Sie das Häkchen, um den Auftrag zu bestätigen.')
|
||
assert.equal(fehlerMeldung(401), 'Ihre Anmeldung ist abgelaufen. Bitte laden Sie die Seite neu und melden Sie sich erneut an.')
|
||
for (const status of [403, 409]) assert.equal(fehlerMeldung(status), 'Das Bestätigen ist gerade nicht möglich. Bitte laden Sie die Seite neu.')
|
||
assert.equal(fehlerMeldung(429), 'Zu viele Anfragen. Bitte versuchen Sie es in einigen Minuten noch einmal.')
|
||
for (const status of [500, 502, 503, undefined]) assert.equal(fehlerMeldung(status), 'Die Bestätigung konnte nicht gespeichert werden. Bitte versuchen Sie es später noch einmal.')
|
||
assert.equal(fehlerMeldung(0, ANREDE_DU), 'Keine Verbindung. Bitte prüfe deine Internetverbindung und versuche es erneut.')
|
||
assert.equal(fehlerMeldung(400, ANREDE_DU), 'Bitte setze das Häkchen, um den Auftrag zu bestätigen.')
|
||
assert.equal(fehlerMeldung(401, ANREDE_DU), 'Deine Anmeldung ist abgelaufen. Bitte lade die Seite neu und melde dich erneut an.')
|
||
for (const status of [403, 409]) assert.equal(fehlerMeldung(status, ANREDE_DU), 'Das Bestätigen ist gerade nicht möglich. Bitte lade die Seite neu.')
|
||
assert.equal(fehlerMeldung(429, ANREDE_DU), 'Zu viele Anfragen. Bitte versuche es in einigen Minuten noch einmal.')
|
||
for (const status of [500, 502, 503, undefined]) assert.equal(fehlerMeldung(status, ANREDE_DU), 'Die Bestätigung konnte nicht gespeichert werden. Bitte versuche es später noch einmal.')
|
||
})
|
||
|
||
const werte = (objekt) => Object.values(objekt).flatMap((wert) => (wert && typeof wert === 'object' ? werte(wert) : [wert])).filter((wert) => typeof wert === 'string')
|
||
|
||
test('Anrede: gemeinsame Texte neutral, öffentliche Seite Sie, Portal Du; jede Du-Variante hat ihr Sie-Gegenstück', () => {
|
||
assert.equal(ANREDE_SIE, 'sie')
|
||
assert.equal(ANREDE_DU, 'du')
|
||
for (const text of werte(TEXTE_GEMEINSAM)) assert.ok(!hatDuForm(text) && !hatSieForm(text), text)
|
||
for (const text of werte(TEXTE_SIE)) assert.ok(!hatDuForm(text), text)
|
||
for (const text of werte(TEXTE_DU)) assert.ok(!hatSieForm(text), text)
|
||
assert.deepEqual(Object.keys(TEXTE_DU).sort(), ['fehler', 'formular', 'fruehbeginnWunsch', 'speicherHinweis'])
|
||
for (const schluessel of Object.keys(TEXTE_DU)) assert.ok(schluessel in TEXTE_SIE, schluessel)
|
||
assert.deepEqual(Object.keys(TEXTE_DU.fehler), Object.keys(TEXTE_SIE.fehler))
|
||
assert.equal(texte().formular, 'Ihre Bestätigung', 'ohne Angabe gilt Sie')
|
||
assert.equal(texte(ANREDE_SIE).speicherHinweis, 'Ihre Bestätigung wird mit Datum und Uhrzeit gespeichert.')
|
||
assert.equal(texte(ANREDE_DU).formular, 'Deine Bestätigung')
|
||
assert.equal(texte(ANREDE_DU).speicherHinweis, 'Deine Bestätigung wird mit Datum und Uhrzeit gespeichert.')
|
||
assert.equal(texte(ANREDE_DU).knopf, 'Auftrag bestätigen', 'gemeinsame Texte gibt es in beiden Anreden')
|
||
assert.equal(texte(ANREDE_DU).ansichtsmodus, 'Im Ansichtsmodus kann der Auftrag nicht bestätigt werden. Die Bestätigung gibt der Kunde selbst ab.')
|
||
})
|
||
|
||
test('AB-Nummer: Schreibweisen der Praxis werden zu AB-JJJJ-NNNN, alles andere zu leer', () => {
|
||
assert.equal(abNummerNormalisiert('AB-2026-0001'), 'AB-2026-0001')
|
||
assert.equal(abNummerNormalisiert(' ab-2026-0001 '), 'AB-2026-0001')
|
||
assert.equal(abNummerNormalisiert('ab 2026 0001'), 'AB-2026-0001')
|
||
assert.equal(abNummerNormalisiert('AB – 2026 – 0001'), 'AB-2026-0001') // Halbgeviertstrich mit Leerzeichen
|
||
assert.equal(abNummerNormalisiert('AB−2026−0001'), 'AB-2026-0001') // Minuszeichen
|
||
for (const kaputt of ['', ' ', 'AB-2026-01', 'AB-26-0001', 'RE-2026-0001', 'AB-2026-0001-1', 'AB20260001', undefined, null]) assert.equal(abNummerNormalisiert(kaputt), '', String(kaputt))
|
||
})
|
||
|
||
test('Quelltext und Kontakt: reines Modul ohne Importe, fetch und DOM; Kontaktdaten wie in den Rechtstexten', () => {
|
||
pruefeQuelltext('auftrag-ansicht.js', { mitTexten: true })
|
||
assert.deepEqual(KONTAKT, { email: 'support@webklar.com', telefon: '0176 23726355' })
|
||
})
|