Files

122 lines
8.1 KiB
JavaScript

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'
// Abo-Zuordnung der Funktionen (V5): aboProdukt, aboDeckt, aboLaeuftFuer. Fail-closed: Lesefehler werden weitergereicht.
const aw = await startAppwriteMock({ schema: { subscriptions: schemaKeys('subscriptions'), contractEvents: schemaKeys('contractEvents') } })
const st = await startFakeStripe()
after(() => Promise.all([aw.close(), st.close()]))
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { subscriptionDoc, wartungDoc, hostingDoc } = await import('./support/fixtures.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { ENTHALTEN_IN, aboProdukt, aboDeckt, aboLaeuftFuer } = await import('../server/services/aboFunktionen.js')
const JETZT = new Date('2026-10-17T08:00:00.000Z')
const abo = (doc) => aw.seed('subscriptions', subscriptionDoc('sub_h', doc))
const ereignis = (id, type, contractAt, over = {}) =>
aw.seed('contractEvents', { $id: id, customerId: 'c1', subscriptionId: 'sub_h', type, productKey: 'wartung', status: 'erledigt', consentAt: contractAt, details: JSON.stringify({ contractAt }), ...over })
// aw.docs() liefert eine Kopie: geloescht wird ueber die API
const loesche = (collection, id) => deleteDocument(collection, id)
beforeEach(async () => {
for (const collection of ['subscriptions', 'contractEvents']) {
for (const id of aw.docs(collection).keys()) await deleteDocument(collection, id)
}
})
const FAELLE = [ // [Name, Abo-Dokument|null, aboProdukt-Teil, aboDeckt(termine), aboDeckt(analyse)]
['kein Abo', null, { produkt: null, laeuft: false, gekuendigt: false }, 'kein_abo', 'kein_abo'],
['Wartung aktiv', wartungDoc, { produkt: 'wartung', laeuft: true, gekuendigt: false }, null, null],
['Altbestand ohne productKey', { ...wartungDoc, productKey: undefined }, { produkt: 'wartung', laeuft: true, gekuendigt: false }, null, null],
['Hosting aktiv', hostingDoc, { produkt: 'hosting', laeuft: true, gekuendigt: false }, 'falsches_abo', null],
['Wartung zum Periodenende gekündigt', { ...wartungDoc, cancelAtPeriodEnd: true }, { produkt: 'wartung', laeuft: true, gekuendigt: true }, 'gekuendigt', 'gekuendigt'],
['Wartung mit cancelAt', { ...wartungDoc, cancelAt: '2026-12-01T00:00:00.000Z' }, { produkt: 'wartung', laeuft: true, gekuendigt: true }, 'gekuendigt', 'gekuendigt'],
['Wartung past_due', { ...wartungDoc, status: 'past_due' }, { produkt: 'wartung', laeuft: true, gekuendigt: false }, 'nicht_aktiv', 'nicht_aktiv'],
['Wartung trialing ohne Probezeit-Ende', { ...wartungDoc, status: 'trialing' }, { produkt: 'wartung', laeuft: true, gekuendigt: false }, 'nicht_aktiv', 'nicht_aktiv'],
['Wartung beendet', { ...wartungDoc, status: 'canceled' }, { produkt: null, laeuft: false, gekuendigt: false }, 'kein_abo', 'kein_abo'],
]
for (const [name, doc, teil, termine, analyse] of FAELLE) {
test(`Abo-Lage: ${name}`, async () => {
if (doc) abo(doc)
assert.deepEqual(await aboProdukt('c1', { now: JETZT }), { ...teil, widerrufsfristBis: null })
assert.deepEqual(await aboDeckt('c1', 'termine'), termine ? { ok: false, grund: termine } : { ok: true })
assert.deepEqual(await aboDeckt('c1', 'analyse'), analyse ? { ok: false, grund: analyse } : { ok: true })
})
}
test('Probezeit (Privatkunde bestellte ohne Häkchen, nach_frist): beginnt_spaeter mit Datum, nicht nicht_aktiv', async () => {
const ENDE = '2026-11-03T05:00:00.000Z' // 06:00 Berlin am Tag nach dem Fristende
const probe = { ...wartungDoc, status: 'trialing', startChoice: 'nach_frist', trialEnd: ENDE }
abo(probe)
assert.deepEqual(await aboProdukt('c1', { now: JETZT }), { produkt: 'wartung', laeuft: true, gekuendigt: false, widerrufsfristBis: null })
for (const key of ['termine', 'analyse']) assert.deepEqual(await aboDeckt('c1', key, { now: JETZT }), { ok: false, grund: 'beginnt_spaeter', ab: ENDE }, key)
for (const trialEnd of [null, '', 'kaputt', '2026-10-17T07:59:59.000Z']) { // kein oder abgelaufenes Ende: wie bisher nicht_aktiv
abo({ ...probe, trialEnd })
assert.deepEqual(await aboDeckt('c1', 'termine', { now: JETZT }), { ok: false, grund: 'nicht_aktiv' }, String(trialEnd))
}
abo({ ...probe, cancelAtPeriodEnd: true })
assert.equal((await aboDeckt('c1', 'termine', { now: JETZT })).grund, 'gekuendigt')
abo({ ...hostingDoc, status: 'trialing', trialEnd: ENDE })
assert.equal((await aboDeckt('c1', 'termine', { now: JETZT })).grund, 'falsches_abo') // Reihenfolge: Abo vor Beginn
assert.deepEqual(await aboDeckt('c1', 'analyse', { now: JETZT }), { ok: false, grund: 'beginnt_spaeter', ab: ENDE })
})
test('aboLaeuftFuer: gekündigt und past_due zählen noch, falsches Abo und beendet nicht', async () => {
abo({ ...wartungDoc, cancelAtPeriodEnd: true })
assert.equal(await aboLaeuftFuer('c1', 'termine'), true)
abo({ ...wartungDoc, status: 'past_due' })
assert.equal(await aboLaeuftFuer('c1', 'termine'), true)
abo({ ...hostingDoc })
assert.deepEqual([await aboLaeuftFuer('c1', 'termine'), await aboLaeuftFuer('c1', 'analyse')], [false, true])
abo({ ...wartungDoc, status: 'canceled' })
assert.equal(await aboLaeuftFuer('c1', 'news'), false)
})
test('widerrufener Wechsel: Spiegel sagt Wartung, es gilt Hosting', async () => {
abo(wartungDoc)
ereignis('ev_w', 'wechsel', '2026-10-10T09:00:00.000Z', { status: 'widerrufen' })
assert.equal((await aboProdukt('c1')).produkt, 'hosting')
assert.deepEqual(await aboDeckt('c1', 'shop'), { ok: false, grund: 'falsches_abo' })
assert.equal(await aboLaeuftFuer('c1', 'shop'), false)
})
test('widerrufsfristBis: spätestes offenes Ende von Abschluss und Wechsel; Speicher, fremde und abgelaufene zählen nicht', async () => {
abo(wartungDoc)
ereignis('ev_a', 'abschluss', '2026-10-17T07:00:00.000Z') // Sa + 14 Tage = Sa 31.10. (Feiertag), So 01.11. (Feiertag) -> Mo 02.11.
ereignis('ev_w', 'wechsel', '2026-10-10T09:00:00.000Z') // -> Mo 26.10., 23:59:59.999 MEZ
ereignis('ev_s', 'speicher', '2026-10-16T09:00:00.000Z', { details: JSON.stringify({ contractAt: '2026-10-16T09:00:00.000Z', fromUnits: 0, toUnits: 1 }) })
ereignis('ev_f', 'wechsel', '2026-10-15T09:00:00.000Z', { subscriptionId: 'sub_alt', status: 'widerrufen' })
assert.equal((await aboProdukt('c1', { now: JETZT })).widerrufsfristBis, '2026-11-02T22:59:59.999Z')
await loesche('contractEvents', 'ev_a')
assert.equal((await aboProdukt('c1', { now: JETZT })).widerrufsfristBis, '2026-10-26T22:59:59.999Z')
ereignis('ev_alt', 'abschluss', '2026-09-20T09:00:00.000Z')
await loesche('contractEvents', 'ev_w') // Frist endete Mo 05.10.
assert.equal((await aboProdukt('c1', { now: JETZT })).widerrufsfristBis, null)
})
test('widerrufsfristBis: offene Frist eines anderen Abos zählt nicht', async () => {
abo(wartungDoc)
ereignis('ev_x', 'abschluss', '2026-10-16T09:00:00.000Z', { subscriptionId: 'sub_alt' })
assert.equal((await aboProdukt('c1', { now: JETZT })).widerrufsfristBis, null)
})
test('Form: ENTHALTEN_IN eingefroren und genau die Schlüssel von V5; unbekannte Funktion ist ein Programmfehler', async () => {
assert.ok(Object.isFrozen(ENTHALTEN_IN))
assert.deepEqual(Object.keys(ENTHALTEN_IN), ['analyse', 'news', 'termine', 'shop', 'unterkunft', 'zeiten'])
assert.deepEqual(ENTHALTEN_IN, { analyse: ['hosting', 'wartung'], news: ['wartung'], termine: ['wartung'], shop: ['wartung'], unterkunft: ['wartung'], zeiten: ['wartung'] })
assert.deepEqual(Object.keys(await aboProdukt('c1')).sort(), ['gekuendigt', 'laeuft', 'produkt', 'widerrufsfristBis'])
await assert.rejects(() => aboDeckt('c1', 'gibtEsNicht'), TypeError)
await assert.rejects(() => aboLaeuftFuer('c1', 'gibtEsNicht'), TypeError)
})
test('fail-closed: ein Lesefehler wird weitergereicht, nie zu kein_abo', async () => {
abo(wartungDoc)
aw.failNext({ collection: 'subscriptions', method: 'GET', status: 500 })
await assert.rejects(() => aboDeckt('c1', 'termine'))
aw.failNext({ collection: 'contractEvents', method: 'GET', status: 500 })
await assert.rejects(() => aboProdukt('c1', { now: JETZT }))
})