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-Baustein (server/services/aboStatus.js, Kernversion aus Block B): laeuft / bestellbar / aboMemo. // Fail-closed: ein Lesefehler wirft, er wird nie zu false bzw. ok:false. Die Umgebung steht vor dem ersten // Import von server/config.js. const aw = await startAppwriteMock({ schema: { subscriptions: schemaKeys('subscriptions') } }) const st = await startFakeStripe() after(() => Promise.all([aw.close(), st.close()])) useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) const { seedFixtures, subscriptionDoc, wartungDoc } = await import('./support/fixtures.js') const { deleteDocument } = await import('../server/services/appwriteAdmin.js') const { laeuft, bestellbar, aboMemo } = await import('../server/services/aboStatus.js') const DAY_MS = 86400000 const inTagen = (tage) => new Date(Date.now() + tage * DAY_MS).toISOString() // seedFixtures gibt c1 nur ein Hosting-Abo (sub_h): fuer Wartungsfaelle ueberschreibt der Test es mit einem Wartungs-Abo. const wartung = (patch = {}) => aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc, ...patch })) // Jeder Test beginnt mit den Standard-Abos (c1 Hosting, c2 Hosting, c3 keins); geloescht wird ueber die API beforeEach(async () => { for (const id of aw.docs('subscriptions').keys()) await deleteDocument('subscriptions', id) seedFixtures(aw) }) test('Wartungs-Abo active: laeuft true, bestellbar { ok: true } ohne Grund', async () => { wartung({ status: 'active' }) assert.equal(await laeuft('c1'), true) assert.deepEqual(await bestellbar('c1'), { ok: true }) }) test('Hosting-Abo (Standard von c1 und c2): laeuft false, bestellbar kein_abo', async () => { for (const customerId of ['c1', 'c2']) { assert.equal(await laeuft(customerId), false, customerId) assert.deepEqual(await bestellbar(customerId), { ok: false, grund: 'kein_abo' }, customerId) } }) test('Dokument ohne productKey zaehlt als Wartungs-Abo (wie routes/abo.js productKeyOf), ein unbekannter Schluessel nicht', async () => { const { productKey: _entfernt, ...ohneSchluessel } = wartungDoc aw.seed('subscriptions', subscriptionDoc('sub_h', ohneSchluessel)) assert.equal(await laeuft('c1'), true, 'Feld fehlt') assert.deepEqual(await bestellbar('c1'), { ok: true }) wartung({ productKey: null }) assert.equal(await laeuft('c1'), true, 'productKey null') assert.deepEqual(await bestellbar('c1'), { ok: true }) wartung({ productKey: 'mailStorage' }) assert.equal(await laeuft('c1'), false, 'anderes Produkt') assert.deepEqual(await bestellbar('c1'), { ok: false, grund: 'kein_abo' }) }) test('canceled: laeuft false, bestellbar kein_abo - auch mit vorgemerkter Kuendigung (kein Abo schlaegt gekuendigt)', async () => { wartung({ status: 'canceled' }) assert.equal(await laeuft('c1'), false) assert.deepEqual(await bestellbar('c1'), { ok: false, grund: 'kein_abo' }) wartung({ status: 'canceled', cancelAtPeriodEnd: true, cancelAt: inTagen(-1) }) assert.equal(await laeuft('c1'), false) assert.deepEqual(await bestellbar('c1'), { ok: false, grund: 'kein_abo' }) }) test('past_due und trialing: laeuft true, bestellbar nicht_aktiv', async () => { for (const status of ['past_due', 'trialing']) { wartung({ status }) assert.equal(await laeuft('c1'), true, status) assert.deepEqual(await bestellbar('c1'), { ok: false, grund: 'nicht_aktiv' }, status) } }) test('Kuendigung vorgemerkt (cancelAtPeriodEnd oder cancelAt): gekuendigt, vor nicht_aktiv und bei jedem laufenden Status', async () => { for (const status of ['active', 'trialing', 'past_due']) { wartung({ status, cancelAtPeriodEnd: true, cancelAt: null }) assert.equal(await laeuft('c1'), true, `${status} laeuft`) assert.deepEqual(await bestellbar('c1'), { ok: false, grund: 'gekuendigt' }, `${status} cancelAtPeriodEnd`) wartung({ status, cancelAtPeriodEnd: false, cancelAt: inTagen(12) }) assert.equal(await laeuft('c1'), true, `${status} laeuft`) assert.deepEqual(await bestellbar('c1'), { ok: false, grund: 'gekuendigt' }, `${status} cancelAt`) } }) test('kein Abo (c3), leere Kunden-ID: laeuft false, bestellbar kein_abo', async () => { assert.equal(await laeuft('c3'), false) assert.deepEqual(await bestellbar('c3'), { ok: false, grund: 'kein_abo' }) assert.equal(await laeuft(''), false) assert.deepEqual(await bestellbar(undefined), { ok: false, grund: 'kein_abo' }) }) test('Lesefehler (500 auf subscriptions): laeuft und bestellbar werfen, nie false und nie ok:false', async () => { wartung({ status: 'active' }) aw.failNext({ collection: 'subscriptions', status: 500 }) await assert.rejects(() => laeuft('c1'), /Nachbau: eingeplanter Fehler 500/) aw.failNext({ collection: 'subscriptions', status: 500 }) await assert.rejects(() => bestellbar('c1'), /Nachbau: eingeplanter Fehler 500/) // der Fehler war einmalig: danach liest dieselbe Frage wieder normal assert.equal(await laeuft('c1'), true) assert.deepEqual(await bestellbar('c1'), { ok: true }) }) test('aboMemo: zweiter Aufruf desselben Kunden kommt aus dem Speicher, ein frischer laeuft liest neu', async () => { wartung({ status: 'active' }) const memo = aboMemo() assert.equal(await memo.laeuft('c1'), true) aw.failNext({ collection: 'subscriptions', status: 500 }) assert.equal(await memo.laeuft('c1'), true, 'aus dem Speicher, das eingeplante Failure trifft ihn nicht') await assert.rejects(() => laeuft('c1'), /eingeplanter Fehler 500/, 'ein frischer laeuft liest neu und trifft den Fehler') }) test('aboMemo: bestellbar wird je Kunde gemerkt; laeuft und bestellbar teilen sich die Abo-Lesung desselben Kunden', async () => { wartung({ status: 'past_due' }) const memo = aboMemo() assert.deepEqual(await memo.bestellbar('c1'), { ok: false, grund: 'nicht_aktiv' }) aw.failNext({ collection: 'subscriptions', status: 500 }) assert.deepEqual(await memo.bestellbar('c1'), { ok: false, grund: 'nicht_aktiv' }, 'zweiter Aufruf aus dem Speicher') assert.equal(await memo.laeuft('c1'), true, 'laeuft nutzt dieselbe gelesene Abo-Zeile') await assert.rejects(() => bestellbar('c1'), /eingeplanter Fehler 500/, 'der eingeplante Fehler war noch frei') }) test('aboMemo: ein anderer Kunde wird neu gelesen, ein anderer aboMemo() ebenfalls', async () => { wartung({ status: 'active' }) const memo = aboMemo() assert.equal(await memo.laeuft('c1'), true) // anderer Kunde im selben Memo: eigene Lesung (trifft den eingeplanten Fehler) aw.failNext({ collection: 'subscriptions', status: 500 }) await assert.rejects(() => memo.laeuft('c3'), /eingeplanter Fehler 500/) assert.equal(await memo.laeuft('c3'), false, 'c3 hat kein Abo') // anderer aboMemo() fuer denselben Kunden: neu lesen, der alte Speicher gilt nicht aw.failNext({ collection: 'subscriptions', status: 500 }) await assert.rejects(() => aboMemo().laeuft('c1'), /eingeplanter Fehler 500/) assert.equal(await memo.laeuft('c1'), true, 'der erste Memo behaelt seinen Eintrag') }) test('aboMemo: ein Lesefehler bleibt nicht im Speicher haengen, der naechste Aufruf liest neu', async () => { wartung({ status: 'active' }) const memo = aboMemo() aw.failNext({ collection: 'subscriptions', status: 500 }) await assert.rejects(() => memo.laeuft('c1'), /eingeplanter Fehler 500/) aw.failNext({ collection: 'subscriptions', status: 500 }) await assert.rejects(() => memo.bestellbar('c1'), /eingeplanter Fehler 500/, 'wirft auch bestellbar, nie ok:false') assert.equal(await memo.laeuft('c1'), true) assert.deepEqual(await memo.bestellbar('c1'), { ok: true }) }) test('aboMemo: gleichzeitige Aufrufe desselben Kunden lesen nur einmal', async () => { wartung({ status: 'active' }) aw.resetListCalls() const memo = aboMemo() const antworten = await Promise.all([memo.laeuft('c1'), memo.bestellbar('c1'), memo.laeuft('c1')]) assert.deepEqual(antworten, [true, { ok: true }, true]) assert.equal(aw.listCalls('subscriptions').length, 1) })