Analyse: alle Beendigungswege schalten den Messdienst ab (Kunde/Projekt weg, Domain, Löschfrist, Widerruf Wechsel)
Messdienst: POST /a/admin/sites/:key/off setzt active=0 und offSince einmalig; Portal-Client stopSite. analyse.js: endAnalyse als gemeinsamer Kern, endAnalyseForCustomer, analyseOnProjectChanged, analyseDisabled (Sperre/Löschfrist) und täglicher Abgleich runAnalyseReconcile (Projekt/Kunde weg, Domains nachziehen, K18-Wiederholung, Messdienst für off/disabled, fehlende Entfernen-Tickets nachholen). Entfernen-Ticket je Abschaltung gekennzeichnet (kein Doppelticket, auch wenn das Speichern der ID scheiterte). Hooks: Kunde löschen, updateWebsiteProject, withdrawOrder (Wechsel), siteFeatures.disable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fbc40252a3
commit
78170567b4
17 files changed
+616
-70
No files matched your search
+184
-5
@@ -24,7 +24,7 @@ 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 { updateDocument, deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
|
||||
const mails = mailSink()
|
||||
@@ -35,8 +35,9 @@ after(() => {
|
||||
})
|
||||
|
||||
const puts = []
|
||||
const stops = []
|
||||
const rebuilds = []
|
||||
an.analyseDeps.client = { putSite: async (siteKey, body) => puts.push({ siteKey, ...body }) }
|
||||
an.analyseDeps.client = { putSite: async (siteKey, body) => puts.push({ siteKey, ...body }), stopSite: async (siteKey) => stops.push(siteKey) }
|
||||
an.analyseDeps.rebuild = async (project) => rebuilds.push(project.$id)
|
||||
|
||||
const consent = () => ({ accepted: true, avvVersion: config.analyse.avvVersion })
|
||||
@@ -67,6 +68,7 @@ aw.seed('websiteProjects', { $id: 'p5', customerId: 'c3', projectName: 'Ohne Abo
|
||||
|
||||
beforeEach(() => {
|
||||
puts.length = 0
|
||||
stops.length = 0
|
||||
rebuilds.length = 0
|
||||
mails.sent.length = 0
|
||||
})
|
||||
@@ -156,8 +158,8 @@ test('Ausschalten: off, offSince, Sammeldienst inaktiv, Neubau; erneut = nichts'
|
||||
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.equal(puts.length, 0)
|
||||
assert.deepEqual(stops, [d.siteKey])
|
||||
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 })
|
||||
@@ -466,7 +468,7 @@ test('Entfernen-Ticket: Kunde, Mitarbeiter, zweimal = ein Ticket, Aus/An/Aus = z
|
||||
assert.match(ticket.details, /fünf Werktagen/)
|
||||
assert.match(ticket.details, /Stundenkontingent/)
|
||||
await an.activateAnalyse({ customerId: 'c1', projectId: 'p22', ...staff() })
|
||||
const off2 = await an.deactivateAnalyse({ customerId: 'c1', projectId: 'p22', now: NOW, by: 'Kenso' })
|
||||
const off2 = await an.deactivateAnalyse({ customerId: 'c1', projectId: 'p22', now: new Date(NOW.getTime() + 3600000), by: 'Kenso' })
|
||||
assert.equal(removal().length, 2)
|
||||
assert.notEqual(off2.doc.removalTicketId, off1.doc.removalTicketId)
|
||||
})
|
||||
@@ -524,3 +526,180 @@ test('rebuildSafe-Ergebnis: kein_repo, preview_aus, fehler => Veröffentlichen-T
|
||||
}
|
||||
an.analyseDeps.rebuild = original
|
||||
})
|
||||
|
||||
// ---- Task 9: alle Beendigungswege ----
|
||||
const FULL = { avvVersion: '2026-10-02', avvAcceptedAt: '2026-10-03T08:00:00.000Z', avvAcceptedBy: 'x', activatedAt: '2026-10-03T08:00:00.000Z', activatedMailAt: '2026-10-03T08:00:01.000Z', privacyTicketId: 'pt' }
|
||||
async function seedFeature(customerId, projectId, state, extra = {}) {
|
||||
const d = await sf.ensureFeatureDoc(customerId, projectId, 'analyse')
|
||||
return updateDocument(config.collections.portalFeatures, d.$id, { ...sf.stateFields(state), accentColor: '#111111', ...FULL, ...extra })
|
||||
}
|
||||
const removalTopics = (name) => topics().filter((t) => t === `Website-Analyse von ${name} entfernen`)
|
||||
|
||||
test('Kunde gelöscht: endAnalyseForCustomer stoppt jeden aktiven Schlüssel, off, kein Ticket, keine Mail, Fehler werden geschluckt', async () => {
|
||||
aw.seed('customers', { $id: 'k1', name: 'Weg Eins', email: 'weg1@example.invalid' })
|
||||
aw.seed('websiteProjects', { $id: 'k1p1', customerId: 'k1', projectName: 'KP1', subdomain: 'kp1', isPublic: true, repoFullName: 'WEBklar/kp1' })
|
||||
aw.seed('websiteProjects', { $id: 'k1p2', customerId: 'k1', projectName: 'KP2', subdomain: 'kp2', isPublic: true })
|
||||
const a = await seedFeature('k1', 'k1p1', 'active')
|
||||
const b = await seedFeature('k1', 'k1p2', 'active')
|
||||
const real = an.analyseDeps.client.stopSite
|
||||
an.analyseDeps.client.stopSite = async (k) => { stops.push(k); if (k === b.siteKey) throw new Error('Sammeldienst weg') }
|
||||
const r = await an.endAnalyseForCustomer('k1', { reason: 'kunde_geloescht', mail: false })
|
||||
an.analyseDeps.client.stopSite = real
|
||||
assert.deepEqual(stops.sort(), [a.siteKey, b.siteKey].sort())
|
||||
assert.equal((await doc('k1', 'k1p1')).state, 'off')
|
||||
assert.ok((await doc('k1', 'k1p1')).offSince)
|
||||
assert.equal((await doc('k1', 'k1p2')).state, 'active', 'Fehler bei einem Dokument: Abgleich holt nach')
|
||||
assert.deepEqual(r.ended, [a.$id])
|
||||
assert.equal(removalTopics('KP1').length, 0)
|
||||
assert.equal(rebuilds.length, 0)
|
||||
assert.equal(mails.sent.length, 0)
|
||||
})
|
||||
|
||||
test('Abgleich: Projekt gelöscht, Kunde gelöscht => stopSite, off, offSince, kein Ticket', async () => {
|
||||
aw.seed('customers', { $id: 'k2', name: 'Weg Zwei', email: 'weg2@example.invalid' })
|
||||
aw.seed('websiteProjects', { $id: 'k2p', customerId: 'k2', projectName: 'KP3', subdomain: 'kp3', isPublic: true })
|
||||
aw.seed('websiteProjects', { $id: 'k3p', customerId: 'kgibtsnicht', projectName: 'KP4', subdomain: 'kp4', isPublic: true })
|
||||
const gone = await seedFeature('k2', 'k2p', 'active')
|
||||
const orphan = await seedFeature('kgibtsnicht', 'k3p', 'active')
|
||||
await deleteDocument(config.collections.websiteProjects, 'k2p')
|
||||
const now = new Date('2026-11-02T03:00:00.000Z')
|
||||
await an.runAnalyseReconcile({ now })
|
||||
assert.ok(stops.includes(gone.siteKey) && stops.includes(orphan.siteKey))
|
||||
for (const [c, p] of [['k2', 'k2p'], ['kgibtsnicht', 'k3p']]) {
|
||||
const d = await doc(c, p)
|
||||
assert.equal(d.state, 'off')
|
||||
assert.equal(d.offSince, now.toISOString())
|
||||
}
|
||||
assert.equal(removalTopics('KP3').length + removalTopics('KP4').length, 0)
|
||||
assert.equal(mails.sent.length, 0)
|
||||
})
|
||||
|
||||
test('Abgleich: geänderte liveDomain => putSite mit neuer Domainliste; off-Dokument => stopSite; K18-Wiederholung genau einmal', async () => {
|
||||
aw.seed('websiteProjects', { $id: 'k4p', customerId: 'c1', projectName: 'KP5', subdomain: 'kp5', liveDomain: 'neu-domain.de', isPublic: true })
|
||||
const live = await seedFeature('c1', 'k4p', 'active', { activatedMailAt: '' })
|
||||
aw.seed('websiteProjects', { $id: 'k5p', customerId: 'c1', projectName: 'KP6', subdomain: 'kp6', isPublic: true })
|
||||
const off = await seedFeature('c1', 'k5p', 'off', { offSince: '2026-10-03T09:00:00.000Z', removalTicketId: 'vorhanden' })
|
||||
await an.runAnalyseReconcile({ now: NOW })
|
||||
assert.deepEqual(puts.filter((p) => p.siteKey === live.siteKey), [{ siteKey: live.siteKey, projectId: 'k4p', domains: ['kp5.project.webklar.com', 'neu-domain.de', 'www.neu-domain.de'], active: true }])
|
||||
assert.ok(stops.includes(off.siteKey))
|
||||
assert.equal(mails.sent.length, 1)
|
||||
assert.ok((await doc('c1', 'k4p')).activatedMailAt)
|
||||
await an.runAnalyseReconcile({ now: NOW })
|
||||
assert.equal(mails.sent.length, 1, 'Marker gesetzt: keine zweite K18')
|
||||
})
|
||||
|
||||
test('Domain/Öffentlichkeit: analyseOnProjectChanged beendet bei nicht öffentlich (Ticket), putSite bei neuer Subdomain', async () => {
|
||||
aw.seed('websiteProjects', { $id: 'k6p', customerId: 'c1', projectName: 'KP7', subdomain: 'kp7', isPublic: true, repoFullName: 'WEBklar/kp7' })
|
||||
const d = await seedFeature('c1', 'k6p', 'active')
|
||||
await updateDocument(config.collections.websiteProjects, 'k6p', { subdomain: 'kp7neu' })
|
||||
await an.analyseOnProjectChanged('k6p')
|
||||
assert.deepEqual(puts, [{ siteKey: d.siteKey, projectId: 'k6p', domains: ['kp7neu.project.webklar.com'], active: true }])
|
||||
assert.equal(stops.length, 0)
|
||||
puts.length = 0
|
||||
await updateDocument(config.collections.websiteProjects, 'k6p', { isPublic: false })
|
||||
await an.analyseOnProjectChanged('k6p')
|
||||
assert.deepEqual(stops, [d.siteKey])
|
||||
const after = await doc('c1', 'k6p')
|
||||
assert.equal(after.state, 'off')
|
||||
assert.ok(after.offSince)
|
||||
assert.ok(after.removalTicketId)
|
||||
assert.equal(removalTopics('KP7').length, 1)
|
||||
assert.deepEqual(rebuilds, ['k6p'])
|
||||
await an.analyseOnProjectChanged('k6p') // kein aktives Dokument mehr
|
||||
assert.equal(removalTopics('KP7').length, 1)
|
||||
await an.analyseOnProjectChanged('gibts-nicht')
|
||||
})
|
||||
|
||||
test('Löschfrist/Sperre: siteFeatures.disable beendet Sammeldienst, baut neu und legt Entfernen-Ticket genau einmal an', async () => {
|
||||
aw.seed('websiteProjects', { $id: 'k7p', customerId: 'c1', projectName: 'KP8', subdomain: 'kp8', isPublic: true, repoFullName: 'WEBklar/kp8' })
|
||||
const d = await seedFeature('c1', 'k7p', 'active')
|
||||
const r = await sf.disable(d.$id, { adminNote: 'Abgeschaltet: Aufbewahrungsfrist abgelaufen' })
|
||||
assert.equal(r.state, 'disabled')
|
||||
assert.deepEqual(stops, [d.siteKey])
|
||||
assert.equal(puts.length, 0)
|
||||
const after = await doc('c1', 'k7p')
|
||||
assert.ok(after.offSince)
|
||||
assert.ok(after.removalTicketId)
|
||||
assert.deepEqual(rebuilds, ['k7p'])
|
||||
assert.equal(removalTopics('KP8').length, 1)
|
||||
await an.runAnalyseReconcile({ now: NOW })
|
||||
assert.equal(removalTopics('KP8').length, 1)
|
||||
assert.equal((await doc('c1', 'k7p')).state, 'disabled')
|
||||
})
|
||||
|
||||
test('Löschfrist ohne Projekt: stopSite, kein Ticket, kein Neubau, kein Fehler', async () => {
|
||||
aw.seed('websiteProjects', { $id: 'k8p', customerId: 'c1', projectName: 'KP9', subdomain: 'kp9', isPublic: true })
|
||||
const d = await seedFeature('c1', 'k8p', 'active')
|
||||
await deleteDocument(config.collections.websiteProjects, 'k8p')
|
||||
await sf.disable(d.$id, { adminNote: 'x' })
|
||||
assert.deepEqual(stops, [d.siteKey])
|
||||
assert.equal(removalTopics('KP9').length, 0)
|
||||
assert.equal(rebuilds.length, 0)
|
||||
})
|
||||
|
||||
test('Abgleich holt fehlendes Entfernen-Ticket nach (off/disabled), idempotent; Altfälle vor der Fassung nicht', async () => {
|
||||
aw.seed('websiteProjects', { $id: 'k9p', customerId: 'c1', projectName: 'KP10', subdomain: 'kp10', isPublic: true })
|
||||
aw.seed('websiteProjects', { $id: 'k10p', customerId: 'c1', projectName: 'KP11', subdomain: 'kp11', isPublic: true })
|
||||
aw.seed('websiteProjects', { $id: 'k11p', customerId: 'c1', projectName: 'KP12', subdomain: 'kp12', isPublic: true })
|
||||
await seedFeature('c1', 'k9p', 'off', { offSince: '2026-10-04T09:00:00.000Z' })
|
||||
await seedFeature('c1', 'k10p', 'disabled', { offSince: '', disabledAt: '2026-10-04T09:00:00.000Z' })
|
||||
await seedFeature('c1', 'k11p', 'off', { offSince: '2026-09-20T09:00:00.000Z' })
|
||||
await an.runAnalyseReconcile({ now: NOW })
|
||||
await an.runAnalyseReconcile({ now: NOW })
|
||||
assert.equal(removalTopics('KP10').length, 1)
|
||||
assert.equal(removalTopics('KP11').length, 1)
|
||||
assert.equal(removalTopics('KP12').length, 0, 'vor Livegang beendet')
|
||||
assert.ok((await doc('c1', 'k9p')).removalTicketId)
|
||||
})
|
||||
|
||||
test('Ticket angelegt, Speichern der ID scheiterte: Abgleich übernimmt das vorhandene Ticket (kein zweites)', async () => {
|
||||
aw.seed('websiteProjects', { $id: 'k12p', customerId: 'c1', projectName: 'KP13', subdomain: 'kp13', isPublic: true })
|
||||
const off = '2026-10-04T09:00:00.000Z'
|
||||
await seedFeature('c1', 'k12p', 'off', { offSince: off })
|
||||
// Zustand nach einem Fehler beim Speichern: Ticket fuer genau diese Abschaltung existiert, removalTicketId fehlt
|
||||
const orig = an.analyseDeps.ticket
|
||||
let created = null
|
||||
an.analyseDeps.ticket = async (a) => (created = await orig(a))
|
||||
const realUpdate = an.analyseDeps.saveRemovalTicketId
|
||||
an.analyseDeps.saveRemovalTicketId = async () => { throw new Error('Appwrite weg') }
|
||||
await an.runAnalyseReconcile({ now: NOW })
|
||||
an.analyseDeps.saveRemovalTicketId = realUpdate
|
||||
an.analyseDeps.ticket = orig
|
||||
assert.ok(created, 'Ticket entstand')
|
||||
assert.ok(!(await doc('c1', 'k12p')).removalTicketId, 'ID nicht gespeichert')
|
||||
await an.runAnalyseReconcile({ now: NOW })
|
||||
assert.equal(removalTopics('KP13').length, 1)
|
||||
assert.equal((await doc('c1', 'k12p')).removalTicketId, created.ticketId)
|
||||
})
|
||||
|
||||
test('Aus/An/Aus: zweite Abschaltung bekommt trotz offenem ersten Ticket ein eigenes (Marker je Abschaltung)', async () => {
|
||||
const tickets = [...aw.docs('workorders').values()].filter((t) => t.topic === 'Website-Analyse von Bäckerei Aus entfernen')
|
||||
assert.equal(tickets.length, 2)
|
||||
assert.notEqual(tickets[0].details, tickets[1].details)
|
||||
})
|
||||
|
||||
test('deleteCustomerWithPortalAccess: Analyse aus (kein Ticket, keine Mail), Löschen gelingt auch wenn stopSite wirft', async () => {
|
||||
const { deleteCustomerWithPortalAccess } = await import('../server/services/customerAdmin.js')
|
||||
aw.seed('customers', { $id: 'kd1', name: 'Löschen Eins', email: 'loe1@example.invalid' })
|
||||
aw.seed('customers', { $id: 'kd2', name: 'Löschen Zwei', email: 'loe2@example.invalid' })
|
||||
aw.seed('websiteProjects', { $id: 'kd1p', customerId: 'kd1', projectName: 'KD1', subdomain: 'kd1', isPublic: true, repoFullName: 'WEBklar/kd1' })
|
||||
aw.seed('websiteProjects', { $id: 'kd2p', customerId: 'kd2', projectName: 'KD2', subdomain: 'kd2', isPublic: true })
|
||||
const a = await seedFeature('kd1', 'kd1p', 'active')
|
||||
const b = await seedFeature('kd2', 'kd2p', 'active')
|
||||
await deleteCustomerWithPortalAccess('kd1')
|
||||
assert.deepEqual(stops, [a.siteKey])
|
||||
assert.equal((await doc('kd1', 'kd1p')).state, 'off')
|
||||
assert.equal(removalTopics('KD1').length, 0)
|
||||
assert.equal(mails.sent.length, 0)
|
||||
assert.equal(rebuilds.length, 0)
|
||||
assert.equal(aw.docs('customers').has('kd1'), false)
|
||||
const real = an.analyseDeps.client.stopSite
|
||||
an.analyseDeps.client.stopSite = async () => { throw new Error('weg') }
|
||||
await deleteCustomerWithPortalAccess('kd2')
|
||||
an.analyseDeps.client.stopSite = real
|
||||
assert.equal(aw.docs('customers').has('kd2'), false)
|
||||
assert.equal((await doc('kd2', 'kd2p')).state, 'active', 'bleibt für den Abgleich (Kunde fehlt)')
|
||||
await an.runAnalyseReconcile({ now: NOW })
|
||||
assert.equal((await doc('kd2', 'kd2p')).state, 'off')
|
||||
assert.ok(stops.includes(b.siteKey))
|
||||
})
|
||||
@@ -33,11 +33,13 @@ after(() => {
|
||||
})
|
||||
|
||||
const puts = []
|
||||
const stops = []
|
||||
const statsCalls = []
|
||||
let statsFail = false
|
||||
const realClient = createAnalyticsClient({ heatSecret: 'h'.repeat(32), adminToken: 't' })
|
||||
an.analyseDeps.client = {
|
||||
putSite: async (siteKey, body) => puts.push({ siteKey, ...body }),
|
||||
stopSite: async (siteKey) => stops.push(siteKey),
|
||||
getStats: async (siteKey, range) => {
|
||||
statsCalls.push({ siteKey, range })
|
||||
if (statsFail) throw Object.assign(new Error('x'), { status: 502 })
|
||||
@@ -71,6 +73,7 @@ const featureDoc = (c, p) => sf.getFeatureDoc(c, p, 'analyse')
|
||||
|
||||
beforeEach(() => {
|
||||
puts.length = 0
|
||||
stops.length = 0
|
||||
statsCalls.length = 0
|
||||
statsFail = false
|
||||
config.analyse.enabled = true
|
||||
@@ -132,7 +135,7 @@ test('Aktivieren und Deaktivieren (Kunde)', async () => {
|
||||
assert.equal(view.accentColor, '#112233')
|
||||
const off = await post(`${A}/deactivate`, { projectId: 'p1' }, { cookie })
|
||||
assert.deepEqual(off.body, { ok: true, state: 'off' })
|
||||
assert.equal(puts[1].active, false)
|
||||
assert.equal(stops.length, 1)
|
||||
const notElig = await post(`${A}/activate`, { projectId: 'p2', consent: consent() }, { cookie })
|
||||
assert.equal(notElig.status, 409)
|
||||
})
|
||||
@@ -270,18 +273,19 @@ test('Fix a: Kunde ändert gesperrtes Dokument nicht, Mitarbeiter schon', async
|
||||
assert.equal(staff.doc.state, 'off')
|
||||
})
|
||||
|
||||
test('Fix b: Abschalten durch Mitarbeiter meldet Sammeldienst active:false', async () => {
|
||||
test('Fix b: Abschalten durch Mitarbeiter meldet Sammeldienst per stopSite', async () => {
|
||||
await adminPost('p6', { on: true, ...tf() })
|
||||
const cur = await featureDoc('c1', 'p6')
|
||||
puts.length = 0
|
||||
stops.length = 0
|
||||
await sf.disable(cur.$id, { by: 'Test' })
|
||||
assert.equal(puts.length, 1)
|
||||
assert.deepEqual(puts[0], { siteKey: cur.siteKey, projectId: 'p6', domains: ['neu.project.webklar.com'], active: false })
|
||||
assert.deepEqual(stops, [cur.siteKey])
|
||||
assert.equal(puts.length, 0)
|
||||
// Ausfall des Sammeldienstes verhindert das Abschalten nicht
|
||||
const real = an.analyseDeps.client.putSite
|
||||
an.analyseDeps.client.putSite = async () => { throw new Error('weg') }
|
||||
const real = an.analyseDeps.client.stopSite
|
||||
an.analyseDeps.client.stopSite = async () => { throw new Error('weg') }
|
||||
const again = await sf.disable(cur.$id, { by: 'Test' })
|
||||
an.analyseDeps.client.putSite = real
|
||||
an.analyseDeps.client.stopSite = real
|
||||
assert.equal(again.state, 'disabled')
|
||||
})
|
||||
|
||||
|
||||
@@ -92,3 +92,15 @@ test('heatToken: von der Analytics-Verifikation akzeptiert, 30 Min gueltig', ()
|
||||
test('heatToken ohne heatSecret => 503', () => {
|
||||
assert.throws(() => mk({ heatSecret: '' }).heatToken({ siteKey: 'abcdefghij123456', path: '/', vp: 'd' }), (e) => e.status === 503)
|
||||
})
|
||||
|
||||
test('stopSite: POST /off mit Bearer, 404 zaehlt als erledigt, andere Fehler werfen', async () => {
|
||||
reset()
|
||||
await mk().stopSite('abcdefghij123456')
|
||||
assert.equal(calls[0].method, 'POST')
|
||||
assert.equal(calls[0].url, '/a/admin/sites/abcdefghij123456/off')
|
||||
assert.equal(calls[0].auth, 'Bearer tok-secret')
|
||||
reset(() => ({ status: 404, body: { error: 'x' } }))
|
||||
assert.deepEqual(await mk().stopSite('abcdefghij123456'), { ok: true, missing: true })
|
||||
reset(() => ({ status: 500, body: {} }))
|
||||
await assert.rejects(mk().stopSite('abcdefghij123456'), (e) => e.status === 500)
|
||||
})
|
||||
@@ -60,3 +60,15 @@ test('Vertrag: putSite -> Ereignisse -> getStats -> heatToken -> deleteSite', as
|
||||
await assert.rejects(client.getStats(SITE, '7'), (e) => e.status === 404)
|
||||
assert.equal(db.prepare('SELECT COUNT(*) AS n FROM events_raw').get().n, 0)
|
||||
})
|
||||
|
||||
test('Vertrag: stopSite setzt active=0 und offSince, unbekannt zaehlt als erledigt', async () => {
|
||||
const KEY2 = 'fedcba9876543210fedc'
|
||||
await client.putSite(KEY2, { projectId: 'p2', domains: [DOMAIN], active: true })
|
||||
await client.stopSite(KEY2)
|
||||
const row = db.prepare('SELECT active, offSince FROM sites WHERE siteKey = ?').get(KEY2)
|
||||
assert.equal(row.active, 0)
|
||||
assert.ok(row.offSince)
|
||||
await client.stopSite(KEY2) // zweiter Aufruf aendert nichts
|
||||
assert.equal(db.prepare('SELECT offSince FROM sites WHERE siteKey = ?').get(KEY2).offSince, row.offSince)
|
||||
assert.deepEqual(await client.stopSite('00000000000000000000'), { ok: true, missing: true })
|
||||
})
|
||||
@@ -30,6 +30,10 @@ before(async () => {
|
||||
{ repoFullName: 'knso/alt', projectName: 'alt', subdomain: 'alt' },
|
||||
{ repoFullName: 'knso/synced-sub', projectName: 'synced-sub', status: 'synced', subdomain: 'synced-sub' },
|
||||
{ repoFullName: 'knso/lead', projectName: 'lead', status: 'deployed', subdomain: 'lead' },
|
||||
{ $id: 'wp_an', repoFullName: 'knso/alt', projectName: 'Analyse-Projekt', subdomain: 'analyseprojekt', isPublic: true, projectType: 'website' },
|
||||
],
|
||||
portalFeatures: [
|
||||
{ $id: 'wp_an_feat', customerId: 'cx', projectId: 'wp_an', featureKey: 'analyse', state: 'active', enabled: true, siteKey: 'wpansitekey01234567' },
|
||||
],
|
||||
},
|
||||
})
|
||||
@@ -111,3 +115,30 @@ test('Repo-Liste hat eine Obergrenze', async () => {
|
||||
assert.equal(repos.length, 2000)
|
||||
assert.equal(capGitea.calls.filter((c) => c.path === '/api/v1/repos/search').length, 40)
|
||||
})
|
||||
|
||||
test('updateWebsiteProject: neue Subdomain => putSite mit neuer Domain; nicht öffentlich => stopSite; Hook-Fehler ändert das Ergebnis nicht', async () => {
|
||||
const { updateWebsiteProject } = await import('../server/services/giteaAdmin.js')
|
||||
const { analyseDeps } = await import('../server/services/analyse.js')
|
||||
const calls = []
|
||||
const real = analyseDeps.client
|
||||
analyseDeps.client = {
|
||||
putSite: async (k, b) => calls.push(['put', k, b]),
|
||||
stopSite: async (k) => calls.push(['stop', k]),
|
||||
}
|
||||
try {
|
||||
const r1 = await updateWebsiteProject('wp_an', { subdomain: 'analyseneu', projectType: 'website' })
|
||||
assert.equal(r1.subdomain, 'analyseneu')
|
||||
assert.deepEqual(calls, [['put', 'wpansitekey01234567', { projectId: 'wp_an', domains: ['analyseneu.project.webklar.com'], active: true }]])
|
||||
calls.length = 0
|
||||
const r2 = await updateWebsiteProject('wp_an', { projectType: 'preview' })
|
||||
assert.equal(r2.isPublic, false)
|
||||
assert.deepEqual(calls, [['stop', 'wpansitekey01234567']])
|
||||
const { updateDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
await updateDocument(config.collections.portalFeatures, 'wp_an_feat', { state: 'active', enabled: true })
|
||||
analyseDeps.client = { putSite: async () => { throw new Error('weg') }, stopSite: async () => { throw new Error('weg') } }
|
||||
const r3 = await updateWebsiteProject('wp_an', { subdomain: 'nochneu', projectType: 'website' })
|
||||
assert.equal(r3.subdomain, 'nochneu')
|
||||
} finally {
|
||||
analyseDeps.client = real
|
||||
}
|
||||
})
|
||||
@@ -15,6 +15,7 @@ const aw = await startAppwriteMock({
|
||||
subscriptions: schemaKeys('subscriptions'),
|
||||
contractEvents: schemaKeys('contractEvents'),
|
||||
hourCredits: schemaKeys('hourCredits'),
|
||||
portalFeatures: [...schemaKeys('portalFeatures'), 'siteKey', 'offSince', 'activatedAt', 'accentColor', 'removalTicketId'],
|
||||
},
|
||||
})
|
||||
const st = await startFakeStripe()
|
||||
@@ -28,6 +29,7 @@ const { withdrawalDeadline } = await import('../server/services/deadlines.js')
|
||||
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
|
||||
const { analyseDeps } = await import('../server/services/analyse.js')
|
||||
const app = await startTestApp({ '/api/abo': aboRoutes })
|
||||
// Mails landen in der Senke statt bei SMTP (F25): K03/K04 an den Kunden, T08/T09 ans Team sind pruefbar
|
||||
const mails = mailSink()
|
||||
@@ -396,6 +398,61 @@ test('Widerruf Wechsel in der Frist: Ereignis widerrufen, kein automatischer Pre
|
||||
assert.doesNotMatch(t09().text, /Abo in Stripe sofort beenden/)
|
||||
})
|
||||
|
||||
// Analyse hat nur mit Wartungs-Abo Bestand: der Widerruf des Wechsels beendet sie (Messdienst aus, K19)
|
||||
async function withActiveAnalyse(fn) {
|
||||
const stops = []
|
||||
const real = analyseDeps.client
|
||||
analyseDeps.client = { stopSite: async (k) => { stops.push(k) } }
|
||||
aw.seed('portalFeatures', { $id: 'wf_feat', customerId: 'c1', projectId: 'wf_p', featureKey: 'analyse', state: 'active', enabled: true, siteKey: 'wfsitekey0123456789' })
|
||||
try {
|
||||
return await fn(stops)
|
||||
} finally {
|
||||
analyseDeps.client = real
|
||||
}
|
||||
}
|
||||
const wechselIn = async (days) => {
|
||||
const consentAt = daysAgo(days)
|
||||
await recordContractEvent({
|
||||
customerId: 'c1', subscriptionId: 'sub_h', type: 'wechsel', productKey: 'wartung',
|
||||
details: { from: 'hosting', switchedAt: consentAt, effectiveAt: consentAt, paidAt: consentAt },
|
||||
consent: { ...consent, consentAt }, status: 'erledigt',
|
||||
})
|
||||
}
|
||||
|
||||
test('Widerruf Wechsel in der Frist beendet die Analyse des Kunden (stopSite, off, K19); Fehler lässt den Widerruf auf 200', async () => {
|
||||
await resetSubH({ units: 0 })
|
||||
await wechselIn(3)
|
||||
await withActiveAnalyse(async (stops) => {
|
||||
const res = await withdraw({ order: 'wechsel' })
|
||||
assert.equal(res.status, 200)
|
||||
assert.deepEqual(stops, ['wfsitekey0123456789'])
|
||||
const d = aw.docs('portalFeatures').get('wf_feat')
|
||||
assert.equal(d.state, 'off')
|
||||
assert.ok(d.offSince)
|
||||
assert.ok(bySubject('Deine Website-Analyse wurde beendet'))
|
||||
})
|
||||
// wirft der Messdienst, bleibt der Widerruf 200
|
||||
await resetSubH({ units: 0 })
|
||||
await wechselIn(3)
|
||||
await withActiveAnalyse(async () => {
|
||||
analyseDeps.client = { stopSite: async () => { throw new Error('Messdienst weg') } }
|
||||
const res = await withdraw({ order: 'wechsel' })
|
||||
assert.equal(res.status, 200)
|
||||
assert.equal((await events('wechsel'))[0].status, 'widerrufen')
|
||||
})
|
||||
})
|
||||
|
||||
test('Widerruf Wechsel nach Fristende oder Zusatzspeicher ändert die Analyse nicht', async () => {
|
||||
await resetSubH({ units: 0 })
|
||||
await wechselIn(30)
|
||||
await withActiveAnalyse(async (stops) => {
|
||||
assert.equal((await withdraw({ order: 'wechsel' })).status, 200)
|
||||
assert.equal((await withdraw({ order: 'zusatzspeicher' })).status, 200)
|
||||
assert.deepEqual(stops, [])
|
||||
assert.equal(aw.docs('portalFeatures').get('wf_feat').state, 'active')
|
||||
})
|
||||
})
|
||||
|
||||
test('Widerruf nach Fristende: 200, nichts geändert', async () => {
|
||||
await resetSubH({ units: 0 })
|
||||
const consentAt = daysAgo(30)
|
||||
|
||||
@@ -315,13 +315,13 @@ test('retention: Analyse-Dokument schaltet auch den Sammeldienst ab', async () =
|
||||
aw.seed('portalFeatures', { $id: 'ra_feat', customerId: 'ra', projectId: 'rap', featureKey: 'analyse', state: 'active', enabled: true, siteKey: 'abcdefghij234567' })
|
||||
const puts = []
|
||||
const ok = analyseDeps.client
|
||||
analyseDeps.client = { putSite: async (k, b) => { puts.push([k, b]) } }
|
||||
analyseDeps.client = { putSite: async (k, b) => { puts.push(['put', k, b]) }, stopSite: async (k) => { puts.push(['stop', k]) } }
|
||||
try {
|
||||
const plan = await findDueDeletions(JAN_2026)
|
||||
assert.ok(plan.news.find((n) => n.customerId === 'ra').featureIds.includes('ra_feat'))
|
||||
const result = await applyDeletions(plan)
|
||||
assert.deepEqual(result.failed, [])
|
||||
assert.deepEqual(puts, [['abcdefghij234567', { projectId: 'rap', domains: ['altprojekt.project.webklar.com'], active: false }]])
|
||||
assert.deepEqual(puts, [['stop', 'abcdefghij234567']])
|
||||
assert.equal([...aw.docs('portalFeatures').values()].find((d) => d.$id === 'ra_feat').state, 'disabled')
|
||||
} finally {
|
||||
analyseDeps.client = ok
|
||||
|
||||
Reference in new issue
Block a user