Analyse: Einwilligungs-Historie, Nachweis beim Einschalten, Sperre ohne Verantwortlichen-Angaben, Textform für Mitarbeiter
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c6824f83a6
commit
3d05472b25
14 files changed
+585
-115
No files matched your search
+166
-21
@@ -7,11 +7,13 @@ import { schemaKeys } from './support/schema.js'
|
||||
import { mailSink } from './support/mailSink.js'
|
||||
|
||||
// Das Setup-Skript laedt config.js: erst nach useTestEnv() importieren, daher die Attributnamen hier fest
|
||||
const ANALYSE_KEYS = ['accentColor', 'avvVersion', 'avvAcceptedAt', 'avvAcceptedBy', 'activatedAt', 'offSince', 'privacyTicketId', 'activatedMailAt']
|
||||
const ANALYSE_KEYS = ['accentColor', 'avvVersion', 'avvAcceptedAt', 'avvAcceptedBy', 'activatedAt', 'offSince', 'privacyTicketId', 'activatedMailAt', 'avvConsentDate', 'avvConsentChannel', 'removalTicketId', 'publishTicketId']
|
||||
const CONSENT_KEYS = ['customerId', 'projectId', 'siteKey', 'event', 'avvVersion', 'via', 'acceptedAt', 'acceptedBy', 'textformDate', 'textformChannel']
|
||||
const aw = await startAppwriteMock({
|
||||
schema: {
|
||||
portalFeatures: [...schemaKeys('portalFeatures'), ...ANALYSE_KEYS],
|
||||
subscriptions: schemaKeys('subscriptions'),
|
||||
analyseConsents: CONSENT_KEYS,
|
||||
},
|
||||
})
|
||||
const st = await startFakeStripe()
|
||||
@@ -20,6 +22,8 @@ const { config } = await import('../server/config.js')
|
||||
const { wartungDoc, hosting2Doc } = await import('./support/fixtures.js')
|
||||
const an = await import('../server/services/analyse.js')
|
||||
const sf = await import('../server/services/siteFeatures.js')
|
||||
const cl = await import('../server/services/analyseConsent.js')
|
||||
const priv = await import('../server/services/analysePrivacy.js')
|
||||
const { updateDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
|
||||
@@ -36,18 +40,28 @@ an.analyseDeps.client = { putSite: async (siteKey, body) => puts.push({ siteKey,
|
||||
an.analyseDeps.rebuild = async (project) => rebuilds.push(project.$id)
|
||||
|
||||
const consent = () => ({ accepted: true, avvVersion: config.analyse.avvVersion })
|
||||
const actor = { name: 'Kunde Eins', email: 'eins@example.invalid' }
|
||||
const cust = (extra = {}) => ({ consent: consent(), actor, ...extra })
|
||||
const textform = { date: '2026-10-02', channel: 'email' }
|
||||
const NOW = new Date('2026-10-05T10:00:00.000Z')
|
||||
const consents = (p) => cl.listConsents(p)
|
||||
const analyseTickets = () => [...aw.docs('workorders').values()].filter((t) => String(t.topic).includes('Hinweis auf Analyse'))
|
||||
const doc = (c, p) => sf.getFeatureDoc(c, p, 'analyse')
|
||||
|
||||
aw.seed('customers', { $id: 'c1', name: 'Kunde Eins', email: 'eins@example.invalid', location: 'Berlin' })
|
||||
aw.seed('customers', { $id: 'c2', name: 'Kunde Zwei', email: 'zwei@example.invalid' })
|
||||
aw.seed('customers', { $id: 'c4', name: '', email: 'vier@example.invalid' })
|
||||
aw.seed('customers', { $id: 'c5', name: 'Kunde Fuenf', email: 'x' })
|
||||
aw.seed('customers', { $id: 'c3', name: 'Kunde Drei', email: 'drei@example.invalid' })
|
||||
aw.seed('subscriptions', wartungDoc)
|
||||
aw.seed('subscriptions', hosting2Doc)
|
||||
for (const c of ['c4', 'c5']) aw.seed('subscriptions', { ...wartungDoc, $id: `sub_w_${c}`, stripeSubscriptionId: `sub_w_${c}`, customerId: c })
|
||||
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Bäckerei', subdomain: 'baeckerei', liveDomain: 'https://Baeckerei.DE/start', isPublic: true, repoFullName: 'WEBklar/baeckerei' })
|
||||
aw.seed('websiteProjects', { $id: 'p2', customerId: 'c1', projectName: 'Privat', subdomain: 'privat', isPublic: false })
|
||||
aw.seed('websiteProjects', { $id: 'p3', customerId: 'c1', projectName: 'Ohne Sub', isPublic: true })
|
||||
aw.seed('websiteProjects', { $id: 'p4', customerId: 'c2', projectName: 'Hosting-Kunde', subdomain: 'hosting', isPublic: true })
|
||||
aw.seed('websiteProjects', { $id: 'p11', customerId: 'c4', projectName: 'Ohne Name', subdomain: 'ohnename', isPublic: true, repoFullName: 'WEBklar/ohnename' })
|
||||
aw.seed('websiteProjects', { $id: 'p12', customerId: 'c5', projectName: 'Schlechte Mail', subdomain: 'schlechtemail', isPublic: true })
|
||||
aw.seed('websiteProjects', { $id: 'p5', customerId: 'c3', projectName: 'Ohne Abo', subdomain: 'ohneabo', isPublic: true })
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -67,19 +81,19 @@ test('Berechtigung: Wartungs-Abo, öffentlich, Subdomain, Eigentum', async () =>
|
||||
})
|
||||
|
||||
test('Einschalten abgelehnt: Hosting-Abo, kein Abo, nicht öffentlich, AVV fehlt, Farbe', async () => {
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c2', projectId: 'p4', consent: consent() }), { status: 403 })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c3', projectId: 'p5', consent: consent() }), { status: 403 })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p2', consent: consent() }), { status: 409 })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c2', projectId: 'p4', ...cust() }), { status: 403 })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c3', projectId: 'p5', ...cust() }), { status: 403 })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p2', ...cust() }), { status: 409 })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p1' }), { status: 400, message: 'Bitte den Auftragsverarbeitungsvertrag bestätigen' })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p1', consent: { accepted: true, avvVersion: 'alt' } }), { status: 400 })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p1', consent: { accepted: false, avvVersion: config.analyse.avvVersion } }), { status: 400 })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p1', accentColor: 'rot', consent: consent() }), { status: 400 })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p1', accentColor: 'rot', ...cust() }), { status: 400 })
|
||||
assert.equal(puts.length, 0)
|
||||
assert.equal(analyseTickets().length, 0)
|
||||
})
|
||||
|
||||
test('Einschalten: Dokument, Sammeldienst, Neubau, Ticket, Mail K18', async () => {
|
||||
const { doc: d, changed } = await an.activateAnalyse({ customerId: 'c1', projectId: 'p1', accentColor: '#1a73e8', consent: consent() })
|
||||
const { doc: d, changed } = await an.activateAnalyse({ customerId: 'c1', projectId: 'p1', accentColor: '#1a73e8', ...cust() })
|
||||
assert.equal(changed, true)
|
||||
assert.equal(d.featureKey, 'analyse')
|
||||
assert.equal(d.state, 'active')
|
||||
@@ -87,7 +101,7 @@ test('Einschalten: Dokument, Sammeldienst, Neubau, Ticket, Mail K18', async () =
|
||||
assert.match(d.siteKey, /^[a-z0-9]{16,40}$/)
|
||||
assert.equal(d.accentColor, '#1a73e8')
|
||||
assert.equal(d.avvVersion, config.analyse.avvVersion)
|
||||
assert.equal(d.avvAcceptedBy, 'customer')
|
||||
assert.equal(d.avvAcceptedBy, 'Kunde Eins')
|
||||
assert.ok(d.avvAcceptedAt && d.activatedAt)
|
||||
assert.deepEqual(puts, [{ siteKey: d.siteKey, projectId: 'p1', domains: ['baeckerei.project.webklar.com', 'baeckerei.de', 'www.baeckerei.de'], active: true }])
|
||||
assert.deepEqual(rebuilds, ['p1'])
|
||||
@@ -106,16 +120,17 @@ test('Einschalten: Dokument, Sammeldienst, Neubau, Ticket, Mail K18', async () =
|
||||
|
||||
test('Doppelt einschalten: kein zweites Ticket, keine zweite Mail, nur Farbe', async () => {
|
||||
const before = await doc('c1', 'p1')
|
||||
const same = await an.activateAnalyse({ customerId: 'c1', projectId: 'p1', accentColor: '#1a73e8', consent: consent() })
|
||||
const same = await an.activateAnalyse({ customerId: 'c1', projectId: 'p1', accentColor: '#1a73e8', ...cust() })
|
||||
assert.equal(same.changed, false)
|
||||
assert.equal(rebuilds.length, 0)
|
||||
const recolored = await an.activateAnalyse({ customerId: 'c1', projectId: 'p1', accentColor: '#CC0000', consent: consent() })
|
||||
const recolored = await an.activateAnalyse({ customerId: 'c1', projectId: 'p1', accentColor: '#CC0000', ...cust() })
|
||||
assert.equal(recolored.doc.accentColor, '#cc0000')
|
||||
assert.equal(recolored.doc.siteKey, before.siteKey)
|
||||
assert.deepEqual(rebuilds, ['p1'])
|
||||
assert.equal(analyseTickets().length, 1)
|
||||
assert.equal(mails.sent.length, 0)
|
||||
assert.equal(puts.length, 0)
|
||||
assert.equal((await consents('p1')).length, 1, 'Farbwechsel/zweites Aktivieren schreibt keine Historie')
|
||||
})
|
||||
|
||||
test('Ausschalten: off, offSince, Sammeldienst inaktiv, Neubau; erneut = nichts', async () => {
|
||||
@@ -132,15 +147,17 @@ test('Ausschalten: off, offSince, Sammeldienst inaktiv, Neubau; erneut = nichts'
|
||||
await assert.rejects(an.deactivateAnalyse({ customerId: 'c2', projectId: 'p1' }), { status: 403 })
|
||||
})
|
||||
|
||||
test('Wieder einschalten: gleicher siteKey, offSince leer, kein neues Ticket', async () => {
|
||||
test('Wieder einschalten: gleicher siteKey, offSince leer, neues Ticket und Historie', async () => {
|
||||
const before = await doc('c1', 'p1')
|
||||
const { doc: d } = await an.activateAnalyse({ customerId: 'c1', projectId: 'p1', consent: consent(), by: 'Kenso' })
|
||||
const { doc: d } = await an.activateAnalyse({ customerId: 'c1', projectId: 'p1', consent: consent(), by: 'Kenso', textform, now: NOW })
|
||||
assert.equal(d.siteKey, before.siteKey)
|
||||
assert.equal(d.state, 'active')
|
||||
assert.equal(d.offSince, '')
|
||||
assert.equal(d.accentColor, '#111111')
|
||||
assert.equal(d.avvAcceptedBy, 'Kenso')
|
||||
assert.equal(analyseTickets().length, 1)
|
||||
assert.equal(d.avvConsentDate, '2026-10-02')
|
||||
assert.equal(d.avvConsentChannel, 'email')
|
||||
assert.equal(analyseTickets().length, 2, 'neues Ticket nach Aus/An')
|
||||
})
|
||||
|
||||
test('Abo-Ende-Job: aktive Analyse ohne Wartungs-Abo wird beendet, K19', async () => {
|
||||
@@ -185,18 +202,18 @@ test('selbstheilend: Ticket scheitert einmal, Wiederholung holt Ticket, Neubau u
|
||||
return original(...args)
|
||||
}
|
||||
const before = analyseTickets().length
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p6', consent: consent() }), /Ticketsystem down/)
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p6', ...cust() }), /Ticketsystem down/)
|
||||
assert.equal((await doc('c1', 'p6')).state, 'active')
|
||||
assert.equal(rebuilds.length, 0)
|
||||
assert.equal(mails.sent.length, 0)
|
||||
const r = await an.activateAnalyse({ customerId: 'c1', projectId: 'p6', consent: consent() })
|
||||
const r = await an.activateAnalyse({ customerId: 'c1', projectId: 'p6', ...cust() })
|
||||
assert.equal(r.doc.state, 'active')
|
||||
assert.ok(r.doc.privacyTicketId && r.doc.activatedMailAt)
|
||||
assert.equal(analyseTickets().length, before + 1)
|
||||
assert.deepEqual(rebuilds, ['p6'])
|
||||
assert.equal(mails.sent.length, 1)
|
||||
// dritter Aufruf: nichts mehr offen
|
||||
await an.activateAnalyse({ customerId: 'c1', projectId: 'p6', consent: consent() })
|
||||
await an.activateAnalyse({ customerId: 'c1', projectId: 'p6', ...cust() })
|
||||
assert.equal(analyseTickets().length, before + 1)
|
||||
assert.deepEqual(rebuilds, ['p6'])
|
||||
assert.equal(mails.sent.length, 1)
|
||||
@@ -207,7 +224,7 @@ test('putSite scheitert: Dokument bleibt unverändert', async () => {
|
||||
aw.seed('websiteProjects', { $id: 'p7', customerId: 'c1', projectName: 'Down', subdomain: 'down', isPublic: true })
|
||||
const ok = an.analyseDeps.client
|
||||
an.analyseDeps.client = { putSite: async () => { throw Object.assign(new Error('Auswertung nicht erreichbar'), { status: 502 }) } }
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p7', consent: consent() }), { status: 502 })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p7', ...cust() }), { status: 502 })
|
||||
assert.equal(await doc('c1', 'p7') === null || (await doc('c1', 'p7')).state === 'off', true)
|
||||
assert.equal(analyseTickets().filter((t) => t.details.includes('Down')).length, 0)
|
||||
an.analyseDeps.client = ok
|
||||
@@ -217,7 +234,7 @@ test('Neubau scheitert: Analyse bleibt aktiv, kein Fehler', async () => {
|
||||
aw.seed('websiteProjects', { $id: 'p8', customerId: 'c1', projectName: 'Bau', subdomain: 'bau', isPublic: true, repoFullName: 'WEBklar/bau' })
|
||||
const ok = an.analyseDeps.rebuild
|
||||
an.analyseDeps.rebuild = async () => { throw new Error('Runner weg') }
|
||||
const r = await an.activateAnalyse({ customerId: 'c1', projectId: 'p8', consent: consent() })
|
||||
const r = await an.activateAnalyse({ customerId: 'c1', projectId: 'p8', ...cust() })
|
||||
assert.equal(r.doc.state, 'active')
|
||||
assert.equal(mails.sent.length, 1)
|
||||
an.analyseDeps.rebuild = ok
|
||||
@@ -228,8 +245,8 @@ test('parallel einschalten: ein Ticket, eine Mail', async () => {
|
||||
const before = analyseTickets().length
|
||||
mails.sent.length = 0
|
||||
await Promise.all([
|
||||
an.activateAnalyse({ customerId: 'c1', projectId: 'p9', consent: consent() }),
|
||||
an.activateAnalyse({ customerId: 'c1', projectId: 'p9', consent: consent() }),
|
||||
an.activateAnalyse({ customerId: 'c1', projectId: 'p9', ...cust() }),
|
||||
an.activateAnalyse({ customerId: 'c1', projectId: 'p9', ...cust() }),
|
||||
])
|
||||
assert.equal(analyseTickets().length, before + 1)
|
||||
assert.equal(mails.sent.length, 1)
|
||||
@@ -239,8 +256,8 @@ test('gesperrte Analyse: Kunde 403, Mitarbeiter darf', async () => {
|
||||
aw.seed('websiteProjects', { $id: 'p10', customerId: 'c1', projectName: 'Gesperrt', subdomain: 'gesperrt', isPublic: true })
|
||||
const g = await sf.ensureFeatureDoc('c1', 'p10', 'analyse')
|
||||
await sf.disable(g.$id, {})
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p10', consent: consent() }), { status: 403, message: 'Analyse wurde gesperrt – bitte melde dich bei uns' })
|
||||
const r = await an.activateAnalyse({ customerId: 'c1', projectId: 'p10', consent: consent(), by: 'Kenso' })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p10', ...cust() }), { status: 403, message: 'Analyse wurde gesperrt – bitte melde dich bei uns' })
|
||||
const r = await an.activateAnalyse({ customerId: 'c1', projectId: 'p10', consent: consent(), by: 'Kenso', textform, now: NOW })
|
||||
assert.equal(r.doc.state, 'active')
|
||||
})
|
||||
|
||||
@@ -254,3 +271,131 @@ test('analyseDomains: www-/Apex-Gegenstück der Live-Domain, ohne Doppelte, max.
|
||||
// gespeicherter Host bleibt die Live-Domain (Heatmap-Link, Ticket)
|
||||
assert.deepEqual(an.analyseHosts({ subdomain: 'x', liveDomain: 'example.de' }), ['x.project.webklar.com', 'example.de'])
|
||||
})
|
||||
|
||||
// ---- Rechtsrunde Task 5: Verantwortlichen-Angaben, Historie, Textform ----
|
||||
|
||||
test('analysePrivacyData: Firma/Name, E-Mail, Bereinigung, Adresse', () => {
|
||||
const d = priv.analysePrivacyData
|
||||
assert.deepEqual(d({ companyName: '', name: '', email: 'a@b.de' }).missing, ['firma'])
|
||||
const nameOnly = d({ name: 'Max Muster', email: 'a@b.de' })
|
||||
assert.equal(nameOnly.company, 'Max Muster')
|
||||
assert.equal(nameOnly.ok, true)
|
||||
assert.deepEqual(nameOnly.missing, [])
|
||||
assert.deepEqual(d({ name: 'Max', email: 'abc' }).missing, ['email'])
|
||||
assert.deepEqual(d({ name: '', email: '' }).missing, ['firma', 'email'])
|
||||
assert.deepEqual(d(null).missing, ['firma', 'email'])
|
||||
assert.equal(d({ companyName: 'Muster\nGmbH\t& Co', email: 'a@b.de' }).company, 'Muster GmbH & Co')
|
||||
assert.equal(d({ name: 'M', email: 'a@b.de', street: 'Weg 1' }).address, 'Weg 1')
|
||||
assert.equal(d({ name: 'M', email: 'a@b.de', street: 'Weg 1', postalCode: '12345', city: 'Berlin' }).address, 'Weg 1, 12345 Berlin')
|
||||
assert.equal(d({ name: 'x'.repeat(300), email: 'a@b.de' }).company.length, 200)
|
||||
assert.deepEqual(d({ name: 'M', email: `${'a'.repeat(200)}@b.de` }).missing, ['email'])
|
||||
assert.deepEqual(d({ name: 'M', email: 'a b@c.de' }).missing, ['email'])
|
||||
})
|
||||
|
||||
test('validateTextform: Datum, Zukunft, Fassung, Weg', () => {
|
||||
const ctx = { today: '2026-10-05', minDate: '2026-10-02' }
|
||||
const bad = (t) => assert.throws(() => an.validateTextform(t, ctx), { status: 400 })
|
||||
bad({ date: '2026-10-01', channel: 'chat' })
|
||||
bad({ date: '2026-10-06', channel: 'chat' })
|
||||
bad({ date: '2026-02-30', channel: 'chat' })
|
||||
bad({ date: '02.10.2026', channel: 'chat' })
|
||||
bad({ date: '2026-10-02', channel: 'telefon' })
|
||||
bad({ date: '2026-10-02' })
|
||||
bad(undefined)
|
||||
assert.deepEqual(an.validateTextform({ date: '2026-10-02', channel: 'email' }, ctx), { date: '2026-10-02', channel: 'email' })
|
||||
assert.deepEqual(an.validateTextform({ date: '2026-10-05', channel: 'chat' }, ctx), { date: '2026-10-05', channel: 'chat' })
|
||||
})
|
||||
|
||||
test('Einschalten ohne Firma/Name oder mit ungültiger E-Mail: 409, nichts passiert', async () => {
|
||||
const before = analyseTickets().length
|
||||
for (const [c, p] of [['c4', 'p11'], ['c5', 'p12']]) {
|
||||
await assert.rejects(an.activateAnalyse({ customerId: c, projectId: p, ...cust() }), {
|
||||
status: 409,
|
||||
message: 'Für die Zusatzseite fehlen in Deinen Kundendaten Firma oder Name und eine gültige E-Mail-Adresse. Schreib uns im Chat, wir ergänzen sie.',
|
||||
})
|
||||
assert.equal((await doc(c, p)).state, 'off')
|
||||
}
|
||||
assert.equal(puts.length, 0)
|
||||
assert.equal(analyseTickets().length, before)
|
||||
assert.equal(mails.sent.length, 0)
|
||||
assert.equal((await consents('p11')).length, 0)
|
||||
})
|
||||
|
||||
test('Kunde ohne actor-Namen oder Mitarbeiter ohne Textform: 400', async () => {
|
||||
aw.seed('websiteProjects', { $id: 'p13', customerId: 'c1', projectName: 'Nachweis', subdomain: 'nachweis', isPublic: true })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p13', consent: consent() }), { status: 400 })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p13', consent: consent(), actor: {} }), { status: 400 })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p13', consent: consent(), by: 'Kenso', now: NOW }), { status: 400 })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p13', by: 'Kenso', textform: { date: '2026-10-01', channel: 'chat' }, now: NOW }), { status: 400 })
|
||||
assert.equal(puts.length, 0)
|
||||
assert.equal((await consents('p13')).length, 0)
|
||||
})
|
||||
|
||||
test('Historie: Kunde, Aus, An durch Mitarbeiter in zeitlicher Folge; Portal-Dokument gleich', async () => {
|
||||
aw.seed('websiteProjects', { $id: 'p14', customerId: 'c1', projectName: 'Historie', subdomain: 'historie', isPublic: true })
|
||||
const { doc: d1 } = await an.activateAnalyse({ customerId: 'c1', projectId: 'p14', ...cust() })
|
||||
let h = await consents('p14')
|
||||
assert.equal(h.length, 1)
|
||||
assert.equal(h[0].event, 'on')
|
||||
assert.equal(h[0].via, 'portal')
|
||||
assert.equal(h[0].avvVersion, '2026-10-02')
|
||||
assert.equal(h[0].acceptedBy, 'Kunde Eins')
|
||||
assert.equal(h[0].customerId, 'c1')
|
||||
assert.equal(h[0].siteKey, d1.siteKey)
|
||||
assert.match(h[0].acceptedAt, /^\d{4}-\d\d-\d\dT[\d:.]+Z$/)
|
||||
assert.equal(h[0].acceptedAt, d1.avvAcceptedAt)
|
||||
assert.equal(d1.avvAcceptedBy, 'Kunde Eins')
|
||||
assert.equal(d1.avvConsentDate, '')
|
||||
assert.equal(d1.avvConsentChannel, '')
|
||||
// Farbwechsel und erneutes Aktivieren: keine neue Zeile
|
||||
await an.activateAnalyse({ customerId: 'c1', projectId: 'p14', accentColor: '#222222', ...cust() })
|
||||
await an.activateAnalyse({ customerId: 'c1', projectId: 'p14', ...cust() })
|
||||
assert.equal((await consents('p14')).length, 1)
|
||||
// Marker zurücksetzen beim Wiedereinschalten
|
||||
await updateDocument(config.collections.portalFeatures, d1.$id, { removalTicketId: 'r1', publishTicketId: 'u1' })
|
||||
await an.deactivateAnalyse({ customerId: 'c1', projectId: 'p14', now: NOW, actor })
|
||||
const { doc: d3 } = await an.activateAnalyse({ customerId: 'c1', projectId: 'p14', consent: consent(), by: 'Kenso', textform, now: NOW })
|
||||
assert.equal(d3.privacyTicketId && d3.privacyTicketId !== d1.privacyTicketId, true, 'neues Datenschutz-Ticket')
|
||||
assert.equal(d3.removalTicketId, '')
|
||||
assert.equal(d3.publishTicketId, '')
|
||||
h = await consents('p14')
|
||||
assert.deepEqual(h.map((x) => x.event), ['on', 'off', 'on'])
|
||||
assert.deepEqual(h.map((x) => x.via), ['portal', 'portal', 'woms'])
|
||||
assert.equal(h[1].acceptedBy, 'Kunde Eins')
|
||||
assert.equal(h[2].acceptedBy, 'Kenso')
|
||||
assert.equal(h[2].textformDate, '2026-10-02')
|
||||
assert.equal(h[2].textformChannel, 'email')
|
||||
assert.equal(d3.avvConsentDate, '2026-10-02')
|
||||
assert.equal(d3.avvAcceptedBy, 'Kenso')
|
||||
})
|
||||
|
||||
test('analyseConsent exportiert nur recordConsent und listConsents', () => {
|
||||
assert.deepEqual(Object.keys(cl).sort(), ['listConsents', 'recordConsent'])
|
||||
})
|
||||
|
||||
test('recordConsent scheitert: putSite wird zurückgenommen, Dokument bleibt aus', async () => {
|
||||
aw.seed('websiteProjects', { $id: 'p15', customerId: 'c1', projectName: 'Rollback', subdomain: 'rollback', isPublic: true })
|
||||
const prev = an.analyseDeps.recordConsent
|
||||
an.analyseDeps.recordConsent = async () => { throw new Error('Historie weg') }
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p15', ...cust() }), /Historie weg/)
|
||||
an.analyseDeps.recordConsent = prev
|
||||
assert.equal(puts.length, 2)
|
||||
assert.equal(puts[0].active, true)
|
||||
assert.equal(puts[1].active, false)
|
||||
assert.equal((await doc('c1', 'p15')).state, 'off')
|
||||
assert.equal(analyseTickets().filter((t) => t.details.includes('Rollback')).length, 0)
|
||||
})
|
||||
|
||||
test('Ausschalten schreibt off-Historie; Fehler dort verhindert das Ausschalten nicht', async () => {
|
||||
aw.seed('websiteProjects', { $id: 'p16', customerId: 'c1', projectName: 'Aus', subdomain: 'aus', isPublic: true })
|
||||
await an.activateAnalyse({ customerId: 'c1', projectId: 'p16', ...cust() })
|
||||
const prev = an.analyseDeps.recordConsent
|
||||
an.analyseDeps.recordConsent = async () => { throw new Error('Historie weg') }
|
||||
const r = await an.deactivateAnalyse({ customerId: 'c1', projectId: 'p16', now: NOW, actor })
|
||||
an.analyseDeps.recordConsent = prev
|
||||
assert.equal(r.doc.state, 'off')
|
||||
assert.deepEqual((await consents('p16')).map((x) => x.event), ['on'])
|
||||
await an.activateAnalyse({ customerId: 'c1', projectId: 'p16', ...cust() })
|
||||
await an.deactivateAnalyse({ customerId: 'c1', projectId: 'p16', now: NOW, actor })
|
||||
assert.deepEqual((await consents('p16')).map((x) => x.event), ['on', 'on', 'off'])
|
||||
})
|
||||
+60
-14
@@ -6,14 +6,15 @@ import { useTestEnv } from './support/testEnv.js'
|
||||
import { schemaKeys } from './support/schema.js'
|
||||
import { mailSink } from './support/mailSink.js'
|
||||
|
||||
const ANALYSE_KEYS = ['accentColor', 'avvVersion', 'avvAcceptedAt', 'avvAcceptedBy', 'activatedAt', 'offSince', 'privacyTicketId', 'activatedMailAt']
|
||||
const ANALYSE_KEYS = ['accentColor', 'avvVersion', 'avvAcceptedAt', 'avvAcceptedBy', 'activatedAt', 'offSince', 'privacyTicketId', 'activatedMailAt', 'avvConsentDate', 'avvConsentChannel', 'removalTicketId', 'publishTicketId']
|
||||
const CONSENT_KEYS = ['customerId', 'projectId', 'siteKey', 'event', 'avvVersion', 'via', 'acceptedAt', 'acceptedBy', 'textformDate', 'textformChannel']
|
||||
const aw = await startAppwriteMock({
|
||||
schema: { portalFeatures: [...schemaKeys('portalFeatures'), ...ANALYSE_KEYS], subscriptions: schemaKeys('subscriptions') },
|
||||
schema: { portalFeatures: [...schemaKeys('portalFeatures'), ...ANALYSE_KEYS], subscriptions: schemaKeys('subscriptions'), analyseConsents: CONSENT_KEYS },
|
||||
})
|
||||
const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const { config } = await import('../server/config.js')
|
||||
const { startTestApp, get, rawGet, post } = await import('./support/testApp.js')
|
||||
const { startTestApp, get, rawGet, post, sessionCookie } = await import('./support/testApp.js')
|
||||
const { customerCookie, wartungDoc, seedFixtures } = await import('./support/fixtures.js')
|
||||
const { default: funktionenRoutes } = await import('../server/routes/funktionen.js')
|
||||
const { default: adminAnalyseRoutes } = await import('../server/routes/admin/analyse.js')
|
||||
@@ -46,6 +47,8 @@ an.analyseDeps.client = {
|
||||
an.analyseDeps.rebuild = async () => {}
|
||||
config.analyse.enabled = true
|
||||
config.analyse.heatSecret = 'h'.repeat(32)
|
||||
// Die Fassung 2026-10-02 liegt (je nach Uhr) noch in der Zukunft: Textform-Mindestdatum fuer die Tests auf frueh setzen
|
||||
config.analyse.avvVersion = '2000-01-01'
|
||||
|
||||
seedFixtures(aw)
|
||||
aw.seed('subscriptions', wartungDoc)
|
||||
@@ -60,6 +63,9 @@ const cookie = customerCookie('c1')
|
||||
const A = '/api/funktionen/analyse'
|
||||
const consent = () => ({ accepted: true, avvVersion: config.analyse.avvVersion })
|
||||
const adminPost = (id, body, jwt = 'jwt-admin') => post(`/api/admin/analyse/${id}`, body, { headers: { Authorization: `Bearer ${jwt}` } })
|
||||
const todayBerlin = () => new Intl.DateTimeFormat('sv-SE', { timeZone: 'Europe/Berlin' }).format(new Date())
|
||||
const tf = (extra = {}) => ({ textformDate: todayBerlin(), textformChannel: 'chat', audienceConfirmed: true, ...extra })
|
||||
const consentsOf = (p) => [...aw.docs('analyseConsents').values()].filter((d) => d.projectId === p).sort((a, b) => a.$seq - b.$seq)
|
||||
const featureDoc = (c, p) => sf.getFeatureDoc(c, p, 'analyse')
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -89,7 +95,7 @@ test('Schalter aus: alle Analyse-Routen 409', async () => {
|
||||
post(`${A}/deactivate`, { projectId: 'p1' }, { cookie }),
|
||||
rawGet(`${A}/stats?projectId=p1`, { cookie }),
|
||||
post(`${A}/heatmap-link`, { projectId: 'p1', path: '/', vp: 'd' }, { cookie }),
|
||||
adminPost('p1', { on: true, avvConfirmedVerbally: true }),
|
||||
adminPost('p1', { on: true, ...tf() }),
|
||||
]) {
|
||||
const res = await r
|
||||
assert.equal(res.status, 409)
|
||||
@@ -102,7 +108,7 @@ test('GET /api/funktionen: analyse-Zustand je Projekt, mit und ohne Schalter', a
|
||||
const body = await get('/api/funktionen', { cookie })
|
||||
const p = (id) => body.projects.find((x) => x.projectId === id)
|
||||
assert.equal(p('p1').features.news.state, 'off')
|
||||
assert.deepEqual(p('p1').analyse, { state: 'off', devMode: false, accentColor: '#111111', eligible: true, reason: null, reasonText: null, avvVersion: config.analyse.avvVersion })
|
||||
assert.deepEqual(p('p1').analyse, { state: 'off', devMode: false, accentColor: '#111111', eligible: true, reason: null, reasonText: null, avvVersion: config.analyse.avvVersion, controller: { company: 'Kunde Eins', address: '', email: 'k@example.invalid', ok: true, missing: [] } })
|
||||
assert.equal(p('p2').analyse.eligible, false)
|
||||
assert.equal(p('p2').analyse.reason, 'nicht_oeffentlich')
|
||||
assert.equal(p('p2').analyse.reasonText, an.ELIGIBILITY_MESSAGES.nicht_oeffentlich)
|
||||
@@ -189,18 +195,34 @@ test('Heatmap-Link: Prüfung, URL, Token', async () => {
|
||||
|
||||
test('Admin: 401, 400, 200', async () => {
|
||||
assert.equal((await post('/api/admin/analyse/p6', { on: true })).status, 401)
|
||||
assert.equal((await adminPost('p6', { on: true, avvConfirmedVerbally: true }, 'jwt-kunde')).status, 403)
|
||||
const noAvv = await adminPost('p6', { on: true })
|
||||
assert.equal(noAvv.status, 400)
|
||||
assert.equal(noAvv.body.error, 'Bitte bestätigen, dass der Kunde den AVV bestätigt hat')
|
||||
assert.equal((await adminPost('p6', { on: true, ...tf() }, 'jwt-kunde')).status, 403)
|
||||
const noTf = await adminPost('p6', { on: true, audienceConfirmed: true, textformChannel: 'chat' })
|
||||
assert.equal(noTf.status, 400)
|
||||
assert.equal((await adminPost('p6', { on: true, ...tf({ textformDate: undefined }) })).status, 400)
|
||||
assert.equal((await adminPost('p6', { on: true, ...tf({ textformChannel: undefined }) })).status, 400)
|
||||
assert.equal((await adminPost('p6', { on: true, ...tf({ textformChannel: 'telefon' }) })).status, 400)
|
||||
assert.equal((await adminPost('p6', { on: true, ...tf({ textformDate: '1999-01-01' }) })).status, 400, 'vor der Fassung')
|
||||
assert.equal((await adminPost('p6', { on: true, ...tf({ audienceConfirmed: undefined }) })).status, 400)
|
||||
const old = await adminPost('p6', { on: true, avvConfirmedVerbally: true, ...tf() })
|
||||
assert.equal(old.status, 400)
|
||||
assert.equal(old.body.error, 'Zustimmung in Textform nötig')
|
||||
assert.equal(await featureDoc('c1', 'p6'), null, 'nichts angelegt')
|
||||
assert.equal((await adminPost('p6', { on: 'ja' })).status, 400)
|
||||
assert.equal((await adminPost('nix', { on: true, avvConfirmedVerbally: true })).status, 404)
|
||||
const on = await adminPost('p6', { on: true, avvConfirmedVerbally: true })
|
||||
assert.equal((await adminPost('nix', { on: true, ...tf() })).status, 404)
|
||||
const on = await adminPost('p6', { on: true, ...tf() })
|
||||
assert.equal(on.status, 200)
|
||||
assert.deepEqual(on.body, { ok: true, state: 'active' })
|
||||
const d = await featureDoc('c1', 'p6')
|
||||
assert.equal(d.avvAcceptedBy, 'kenso@webklar.com')
|
||||
assert.equal(d.avvVersion, config.analyse.avvVersion)
|
||||
assert.equal(d.avvConsentDate, todayBerlin())
|
||||
assert.equal(d.avvConsentChannel, 'chat')
|
||||
const h = consentsOf('p6')
|
||||
assert.equal(h.length, 1)
|
||||
assert.equal(h[0].via, 'woms')
|
||||
assert.equal(h[0].acceptedBy, 'kenso@webklar.com')
|
||||
assert.equal(h[0].textformDate, todayBerlin())
|
||||
assert.equal(h[0].textformChannel, 'chat')
|
||||
const off = await adminPost('p6', { on: false })
|
||||
assert.deepEqual(off.body, { ok: true, state: 'off' })
|
||||
})
|
||||
@@ -217,9 +239,12 @@ test('Admin: Lesezugriff GET', async () => {
|
||||
assert.equal(off.body.eligible, true)
|
||||
assert.equal(off.body.reasonText, null)
|
||||
assert.equal(off.body.avvVersion, config.analyse.avvVersion)
|
||||
await adminPost('p1', { on: true, avvConfirmedVerbally: true })
|
||||
await adminPost('p1', { on: true, ...tf() })
|
||||
const on = await adminGet('p1')
|
||||
assert.equal(on.body.state, 'active')
|
||||
assert.equal(on.body.avvConsentDate, todayBerlin())
|
||||
assert.equal(on.body.avvConsentChannel, 'chat')
|
||||
assert.deepEqual(on.body.privacy, { ok: true, missing: [] })
|
||||
assert.equal(on.body.avvAcceptedBy, 'kenso@webklar.com')
|
||||
assert.ok(on.body.activatedAt)
|
||||
assert.ok(!('siteKey' in on.body) && !JSON.stringify(on.body).includes((await featureDoc('c1', 'p1')).siteKey))
|
||||
@@ -230,7 +255,7 @@ test('Admin: Lesezugriff GET', async () => {
|
||||
})
|
||||
|
||||
test('Fix a: Kunde ändert gesperrtes Dokument nicht, Mitarbeiter schon', async () => {
|
||||
await adminPost('p6', { on: true, avvConfirmedVerbally: true })
|
||||
await adminPost('p6', { on: true, ...tf() })
|
||||
const cur = await featureDoc('c1', 'p6')
|
||||
await sf.disable(cur.$id, { by: 'Test' })
|
||||
puts.length = 0
|
||||
@@ -242,7 +267,7 @@ test('Fix a: Kunde ändert gesperrtes Dokument nicht, Mitarbeiter schon', async
|
||||
})
|
||||
|
||||
test('Fix b: Abschalten durch Mitarbeiter meldet Sammeldienst active:false', async () => {
|
||||
await adminPost('p6', { on: true, avvConfirmedVerbally: true })
|
||||
await adminPost('p6', { on: true, ...tf() })
|
||||
const cur = await featureDoc('c1', 'p6')
|
||||
puts.length = 0
|
||||
await sf.disable(cur.$id, { by: 'Test' })
|
||||
@@ -275,3 +300,24 @@ test('Härtung: ungültige Hosts, Pfadzeichen, Fehler bei einem Projekt', async
|
||||
assert.equal(bad.body.error, 'Keine gültige Domain für die Analyse')
|
||||
assert.equal(puts.length, before)
|
||||
})
|
||||
|
||||
test('Kunden-Aktivierung schreibt Historie mit Sitzungsnamen; fehlende Angaben: controller + 409', async () => {
|
||||
aw.seed('customers', { $id: 'c9', appwriteUserId: 'u9', email: 'neun', name: '', status: 'active', portalAccessEnabled: true })
|
||||
aw.seed('websiteProjects', { $id: 'p9', customerId: 'c9', projectName: 'Leer', subdomain: 'leer', isPublic: true })
|
||||
aw.seed('subscriptions', { ...wartungDoc, $id: 'sub_w_c9', stripeSubscriptionId: 'sub_w_c9', customerId: 'c9' })
|
||||
const c9 = sessionCookie({ customerId: 'c9', appwriteUserId: 'u9', name: 'Neun', email: 'neun' })
|
||||
const view = (await get('/api/funktionen', { cookie: c9 })).projects.find((x) => x.projectId === 'p9').analyse
|
||||
assert.deepEqual(view.controller, { company: '', address: '', email: 'neun', ok: false, missing: ['firma', 'email'] })
|
||||
const r = await post(`${A}/activate`, { projectId: 'p9', consent: consent() }, { cookie: c9 })
|
||||
assert.equal(r.status, 409)
|
||||
assert.match(r.body.error, /fehlen in Deinen Kundendaten Firma oder Name/)
|
||||
assert.equal(consentsOf('p9').length, 0)
|
||||
const before = consentsOf('p3').length
|
||||
await post(`${A}/activate`, { projectId: 'p3', consent: consent() }, { cookie })
|
||||
const h = consentsOf('p3')
|
||||
assert.equal(h.length, before + 1)
|
||||
assert.equal(h.at(-1).acceptedBy, 'Kunde Eins')
|
||||
assert.equal(h.at(-1).via, 'portal')
|
||||
const admin = (await rawGet('/api/admin/analyse/p9', { headers: { Authorization: 'Bearer jwt-admin' } })).body
|
||||
assert.deepEqual(admin.privacy, { ok: false, missing: ['firma', 'email'] })
|
||||
})
|
||||
+64
-20
@@ -1,23 +1,36 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { ANALYSE_ATTRIBUTES, main } from '../scripts/analyse-setup.mjs'
|
||||
import { ANALYSE_ATTRIBUTES, CONSENT_ATTRIBUTES, CONSENT_INDEXES, main } from '../scripts/analyse-setup.mjs'
|
||||
import { config } from '../server/config.js'
|
||||
|
||||
function mockClient(existing = []) {
|
||||
const created = []
|
||||
function mockClient({ attrs = [], consentsExist = false, consentAttrs = [], indexes = [] } = {}) {
|
||||
const calls = { attrs: [], collections: [], indexes: [] }
|
||||
return {
|
||||
created,
|
||||
async listAttributes() {
|
||||
return existing.map((key) => ({ key }))
|
||||
calls,
|
||||
async collectionExists() {
|
||||
return consentsExist
|
||||
},
|
||||
async createAttribute(attr) {
|
||||
created.push(attr.key)
|
||||
async ensureCollection(id, name) {
|
||||
calls.collections.push({ id, name })
|
||||
return true
|
||||
},
|
||||
async listAttributes(id) {
|
||||
return (id === config.collections.portalFeatures ? attrs : consentAttrs).map((key) => ({ key }))
|
||||
},
|
||||
async createAttribute(id, attr) {
|
||||
calls.attrs.push(`${id}.${attr.key}`)
|
||||
},
|
||||
async waitForAttribute() {},
|
||||
async listIndexes() {
|
||||
return indexes.map((key) => ({ key }))
|
||||
},
|
||||
async createIndex(id, index) {
|
||||
calls.indexes.push(`${id}.${index.key}`)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
test('Schema: 8 Attribute mit Groessen, alle optional', () => {
|
||||
test('Schema: 12 Attribute mit Groessen, alle optional', () => {
|
||||
const sizes = Object.fromEntries(ANALYSE_ATTRIBUTES.map((a) => [a.key, a.size]))
|
||||
assert.deepEqual(sizes, {
|
||||
accentColor: 7,
|
||||
@@ -28,23 +41,54 @@ test('Schema: 8 Attribute mit Groessen, alle optional', () => {
|
||||
offSince: 40,
|
||||
privacyTicketId: 40,
|
||||
activatedMailAt: 40,
|
||||
avvConsentDate: 10,
|
||||
avvConsentChannel: 10,
|
||||
removalTicketId: 40,
|
||||
publishTicketId: 40,
|
||||
})
|
||||
assert.equal(ANALYSE_ATTRIBUTES.length, 12)
|
||||
assert.ok(ANALYSE_ATTRIBUTES.every((a) => a.required === false && a.type === 'string'))
|
||||
})
|
||||
|
||||
test('main legt nur fehlende Attribute an', async () => {
|
||||
const client = mockClient(['accentColor'])
|
||||
const result = await main({ client, log: () => {} })
|
||||
assert.equal(client.created.length, 7)
|
||||
assert.ok(!client.created.includes('accentColor'))
|
||||
assert.equal(result.missing.length, 7)
|
||||
test('Schema: Historien-Collection analyseConsents', () => {
|
||||
assert.equal(config.collections.analyseConsents, 'analyseConsents')
|
||||
const sizes = Object.fromEntries(CONSENT_ATTRIBUTES.map((a) => [a.key, a.size]))
|
||||
assert.deepEqual(sizes, {
|
||||
customerId: 36, projectId: 36, siteKey: 40, event: 8, avvVersion: 20, via: 8,
|
||||
acceptedAt: 40, acceptedBy: 100, textformDate: 10, textformChannel: 10,
|
||||
})
|
||||
assert.deepEqual(CONSENT_INDEXES.map((i) => i.attributes), [['projectId']])
|
||||
})
|
||||
|
||||
test('main --check legt nichts an', async () => {
|
||||
const client = mockClient(['accentColor'])
|
||||
test('main legt nur fehlende Attribute an und die fehlende Collection samt Index', async () => {
|
||||
const client = mockClient({ attrs: ['accentColor'] })
|
||||
const result = await main({ client, log: () => {} })
|
||||
assert.equal(client.calls.attrs.filter((k) => k.startsWith('portalFeatures.')).length, 11)
|
||||
assert.ok(!client.calls.attrs.includes('portalFeatures.accentColor'))
|
||||
assert.equal(client.calls.attrs.filter((k) => k.startsWith('analyseConsents.')).length, 10)
|
||||
assert.deepEqual(client.calls.collections, [{ id: 'analyseConsents', name: 'analyseConsents' }])
|
||||
assert.deepEqual(client.calls.indexes, ['analyseConsents.projectId_idx'])
|
||||
assert.equal(result.missing.length, 21)
|
||||
})
|
||||
|
||||
test('main ueberspringt vorhandene Collection, Attribute und Index', async () => {
|
||||
const client = mockClient({
|
||||
attrs: ANALYSE_ATTRIBUTES.map((a) => a.key),
|
||||
consentsExist: true,
|
||||
consentAttrs: CONSENT_ATTRIBUTES.map((a) => a.key),
|
||||
indexes: ['projectId_idx'],
|
||||
})
|
||||
const result = await main({ client, log: () => {} })
|
||||
assert.deepEqual(client.calls, { attrs: [], collections: [], indexes: [] })
|
||||
assert.equal(result.missing.length, 0)
|
||||
})
|
||||
|
||||
test('main --check legt nichts an und meldet missing', async () => {
|
||||
const client = mockClient({ attrs: ['accentColor'] })
|
||||
const result = await main({ check: true, client, log: () => {} })
|
||||
assert.equal(client.created.length, 0)
|
||||
assert.equal(result.missing.length, 7)
|
||||
assert.deepEqual(client.calls, { attrs: [], collections: [], indexes: [] })
|
||||
assert.equal(result.missing.length, 21)
|
||||
assert.equal(result.missingCollection, true)
|
||||
})
|
||||
|
||||
test('Konfiguration: analyse ist standardmaessig aus', () => {
|
||||
@@ -52,5 +96,5 @@ test('Konfiguration: analyse ist standardmaessig aus', () => {
|
||||
assert.equal(config.analyse.url, 'http://webklar-analytics:8091')
|
||||
assert.equal(config.analyse.adminToken, '')
|
||||
assert.equal(config.analyse.heatSecret, '')
|
||||
assert.equal(config.analyse.avvVersion, '2026-10-01')
|
||||
assert.equal(config.analyse.avvVersion, '2026-10-02')
|
||||
})
|
||||
+15
-4
@@ -2,13 +2,24 @@ import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
|
||||
const { config } = await import('../server/config.js')
|
||||
const root = (rel) => new URL(`../${rel}`, import.meta.url)
|
||||
|
||||
test('AVV Website-Analyse: Entwurf ist nicht öffentlich (geprüfte Fassung kommt mit der Rechtsrunde)', () => {
|
||||
assert.ok(!existsSync(root('public/rechtliches/avv-website-analyse.html')))
|
||||
test('AVV Website-Analyse: öffentliche Seite existiert, trägt Fassung und Entwurfshinweis, keine internen Kommentare', () => {
|
||||
const file = root('public/rechtliches/avv-website-analyse.html')
|
||||
assert.ok(existsSync(file))
|
||||
const html = readFileSync(file, 'utf8')
|
||||
// Die Entwurfsseite nennt noch Fassung 2026-10-01; Task 6 ersetzt sie durch die geprüfte Fassung 2026-10-02
|
||||
assert.ok(html.includes('Fassung 2026-10-01'))
|
||||
assert.match(html, /Entwurf – vor Verwendung anwaltlich prüfen lassen/)
|
||||
assert.doesNotMatch(html, /<!--/)
|
||||
assert.match(html, /<h2>12\. Haftung/)
|
||||
})
|
||||
|
||||
test('AVV-Link im Portal zeigt auf eine vorhandene Datei unter public/', () => {
|
||||
const fn = readFileSync(root('public/funktionen.js'), 'utf8')
|
||||
assert.equal(fn.match(/const AVV_URL = '([^']+)'/)?.[1], '/rechtliches/avv-website-analyse.html')
|
||||
const url = fn.match(/const AVV_URL = '([^']+)'/)?.[1]
|
||||
assert.equal(url, '/rechtliches/avv-website-analyse.html')
|
||||
assert.ok(existsSync(root(`public${url}`)))
|
||||
})
|
||||
|
||||
test('Portal: Dev-Modus-Hinweis an der Analyse-Kachel', () => {
|
||||
|
||||
@@ -403,6 +403,13 @@ test('Analyse: Standardfarbe, Firma fällt auf Name zurück, Adresse optional',
|
||||
assert.equal(body.analytics.privacy.address, '')
|
||||
})
|
||||
|
||||
test('Analyse: ungültige E-Mail ⇒ null, nur Name reicht als Firma', async (t) => {
|
||||
t.mock.method(console, 'warn', () => {})
|
||||
assert.equal((await submitFor(await analyseProject({ customer: { email: 'abc' } }))).analytics, null)
|
||||
const body = await submitFor(await analyseProject({ customer: { companyName: '', name: 'Nur Name' } }))
|
||||
assert.equal(body.analytics.privacy.company, 'Nur Name')
|
||||
})
|
||||
|
||||
test('Analyse: aus ⇒ null', async () => {
|
||||
assert.equal((await submitFor(await analyseProject({ feature: { state: 'off', enabled: false } }))).analytics, null)
|
||||
assert.equal((await submitFor(await analyseProject({ noFeature: true }))).analytics, null)
|
||||
|
||||
@@ -85,7 +85,12 @@ export const consent = {
|
||||
|
||||
/** Kunden-Session; Standard c1. */
|
||||
export const customerCookie = (customerId = 'c1') =>
|
||||
sessionCookie({ customerId, appwriteUserId: customers[customerId]?.appwriteUserId || `u_${customerId}` })
|
||||
sessionCookie({
|
||||
customerId,
|
||||
appwriteUserId: customers[customerId]?.appwriteUserId || `u_${customerId}`,
|
||||
name: customers[customerId]?.name || '', // wie beim echten Login (auth.js)
|
||||
email: customers[customerId]?.email || '',
|
||||
})
|
||||
|
||||
/** Admin-Session (Ticketsystem-Mitarbeiter im Portal), F11. */
|
||||
export const adminCookie = () =>
|
||||
|
||||
Reference in new issue
Block a user