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

246 lines
14 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, after, beforeEach } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
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 aw = await startAppwriteMock({
schema: {
portalFeatures: [...schemaKeys('portalFeatures'), ...ANALYSE_KEYS],
subscriptions: schemaKeys('subscriptions'),
},
})
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
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 { updateDocument } = await import('../server/services/appwriteAdmin.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const mails = mailSink()
setMailTransportForTests(mails)
after(() => {
setMailTransportForTests(null)
return Promise.all([aw.close(), st.close()])
})
const puts = []
const rebuilds = []
an.analyseDeps.client = { putSite: async (siteKey, body) => puts.push({ siteKey, ...body }) }
an.analyseDeps.rebuild = async (project) => rebuilds.push(project.$id)
const consent = () => ({ accepted: true, avvVersion: config.analyse.avvVersion })
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: 'c3', name: 'Kunde Drei', email: 'drei@example.invalid' })
aw.seed('subscriptions', wartungDoc)
aw.seed('subscriptions', hosting2Doc)
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: 'p5', customerId: 'c3', projectName: 'Ohne Abo', subdomain: 'ohneabo', isPublic: true })
beforeEach(() => {
puts.length = 0
rebuilds.length = 0
mails.sent.length = 0
})
test('Berechtigung: Wartungs-Abo, öffentlich, Subdomain, Eigentum', async () => {
assert.equal((await an.analyseEligibility('c1', 'p1')).ok, true)
assert.deepEqual(await an.analyseEligibility('c1', 'p2'), { ok: false, reason: 'nicht_oeffentlich' })
assert.deepEqual(await an.analyseEligibility('c1', 'p3'), { ok: false, reason: 'keine_subdomain' })
assert.deepEqual(await an.analyseEligibility('c2', 'p4'), { ok: false, reason: 'kein_abo' }, 'Hosting-Abo reicht nicht')
assert.deepEqual(await an.analyseEligibility('c3', 'p5'), { ok: false, reason: 'kein_abo' })
assert.deepEqual(await an.analyseEligibility('c1', 'p4'), { ok: false, reason: 'nicht_dein_projekt' })
assert.deepEqual(await an.analyseEligibility('c1', 'gibts-nicht'), { ok: false, reason: 'nicht_dein_projekt' })
})
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: '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 })
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() })
assert.equal(changed, true)
assert.equal(d.featureKey, 'analyse')
assert.equal(d.state, 'active')
assert.equal(d.enabled, true)
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.ok(d.avvAcceptedAt && d.activatedAt)
assert.deepEqual(puts, [{ siteKey: d.siteKey, projectId: 'p1', domains: ['baeckerei.project.webklar.com', 'baeckerei.de'], active: true }])
assert.deepEqual(rebuilds, ['p1'])
const tickets = analyseTickets()
assert.equal(tickets.length, 1)
assert.equal(tickets[0].topic, 'Datenschutzerklärung von Bäckerei um Hinweis auf Analyse ergänzen')
assert.match(tickets[0].details, /Funktion: analyse/)
assert.match(tickets[0].details, /Domain\(s\): baeckerei\.project\.webklar\.com, baeckerei\.de/)
assert.match(tickets[0].details, /https:\/\/baeckerei\.de\/datenschutz-analyse\.html/)
assert.match(tickets[0].details, /Satz \+ Link in die bestehende Datenschutzerklärung einfügen/)
assert.equal((await doc('c1', 'p1')).privacyTicketId, tickets[0].$id)
assert.equal(mails.sent.length, 1)
assert.equal(mails.sent[0].subject, 'Deine Website-Analyse ist aktiv – Bäckerei')
assert.deepEqual(mails.sent[0].to.map((t) => t.address), ['eins@example.invalid'])
})
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() })
assert.equal(same.changed, false)
assert.equal(rebuilds.length, 0)
const recolored = await an.activateAnalyse({ customerId: 'c1', projectId: 'p1', accentColor: '#CC0000', consent: consent() })
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)
})
test('Ausschalten: off, offSince, Sammeldienst inaktiv, Neubau; erneut = nichts', async () => {
const now = new Date('2026-10-05T10:00:00.000Z')
const { doc: d, changed } = await an.deactivateAnalyse({ customerId: 'c1', projectId: 'p1', now })
assert.equal(changed, true)
assert.equal(d.state, 'off')
assert.equal(d.enabled, false)
assert.equal(d.offSince, '2026-10-05T10:00:00.000Z')
assert.equal(puts.length, 1)
assert.equal(puts[0].active, false)
assert.deepEqual(rebuilds, ['p1'])
assert.equal((await an.deactivateAnalyse({ customerId: 'c1', projectId: 'p1', now })).changed, false)
await assert.rejects(an.deactivateAnalyse({ customerId: 'c2', projectId: 'p1' }), { status: 403 })
})
test('Wieder einschalten: gleicher siteKey, offSince leer, kein neues Ticket', async () => {
const before = await doc('c1', 'p1')
const { doc: d } = await an.activateAnalyse({ customerId: 'c1', projectId: 'p1', consent: consent(), by: 'Kenso' })
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)
})
test('Abo-Ende-Job: aktive Analyse ohne Wartungs-Abo wird beendet, K19', async () => {
// c2 hat nur Hosting-Abo, c3 gar keins: beide Analysen von Hand "aktiv" (z. B. Abo wurde gewechselt)
for (const [c, p] of [['c2', 'p4'], ['c3', 'p5']]) {
const d = await sf.ensureFeatureDoc(c, p, 'analyse')
await updateDocument(config.collections.portalFeatures, d.$id, { ...sf.stateFields('active'), accentColor: '#111111' })
}
const now = new Date('2026-11-01T03:15:00.000Z')
const result = await an.runAboCheck({ now })
assert.equal(result.checked, 3)
assert.equal(result.deactivated.length, 2)
assert.equal((await doc('c1', 'p1')).state, 'active', 'Wartungs-Abo bleibt aktiv')
for (const [c, p] of [['c2', 'p4'], ['c3', 'p5']]) {
const d = await doc(c, p)
assert.equal(d.state, 'off')
assert.equal(d.offSince, now.toISOString())
}
assert.deepEqual(mails.sent.map((m) => m.subject).sort(), ['Deine Website-Analyse wurde beendet – Hosting-Kunde', 'Deine Website-Analyse wurde beendet – Ohne Abo'])
mails.sent.length = 0
assert.equal((await an.runAboCheck({ now })).deactivated.length, 0, 'zweiter Lauf: nichts mehr zu tun')
assert.equal(mails.sent.length, 0)
})
test('News-Jobs ignorieren Analyse-Dokumente', async () => {
const d = await sf.ensureFeatureDoc('c1', 'p2', 'analyse')
aw.seed('workorders', { $id: 't_an', status: 'Closed', woid: '1', topic: 'x' })
await updateDocument(config.collections.portalFeatures, d.$id, { ...sf.stateFields('setup'), ticketId: 't_an' })
assert.deepEqual(await sf.activateClosedTickets({ customerId: 'c1' }), [])
assert.deepEqual(await sf.runActivation({ customerId: 'c1' }), [])
assert.equal((await doc('c1', 'p2')).state, 'setup')
const active = await doc('c1', 'p1')
assert.equal(await sf.findActiveBySiteKey(active.siteKey.slice(0, 16)), null, 'Analyse-siteKey ist kein News-Schlüssel')
})
test('selbstheilend: Ticket scheitert einmal, Wiederholung holt Ticket, Neubau und Mail genau einmal nach', async () => {
aw.seed('websiteProjects', { $id: 'p6', customerId: 'c1', projectName: 'Heilung', subdomain: 'heilung', isPublic: true, repoFullName: 'WEBklar/heilung' })
const original = an.analyseDeps.ticket
let failOnce = true
an.analyseDeps.ticket = async (...args) => {
if (failOnce) { failOnce = false; throw new Error('Ticketsystem down') }
return original(...args)
}
const before = analyseTickets().length
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p6', consent: consent() }), /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() })
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() })
assert.equal(analyseTickets().length, before + 1)
assert.deepEqual(rebuilds, ['p6'])
assert.equal(mails.sent.length, 1)
an.analyseDeps.ticket = original
})
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 })
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
})
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() })
assert.equal(r.doc.state, 'active')
assert.equal(mails.sent.length, 1)
an.analyseDeps.rebuild = ok
})
test('parallel einschalten: ein Ticket, eine Mail', async () => {
aw.seed('websiteProjects', { $id: 'p9', customerId: 'c1', projectName: 'Parallel', subdomain: 'parallel', isPublic: true })
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() }),
])
assert.equal(analyseTickets().length, before + 1)
assert.equal(mails.sent.length, 1)
})
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' })
assert.equal(r.doc.state, 'active')
})