import { test, after } 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' // websiteProjects, workorders und customers legt das Setup-Skript nicht an: ohne Schema im Nachbau const aw = await startAppwriteMock({ schema: { portalFeatures: schemaKeys('portalFeatures') } }) const st = await startFakeStripe() after(() => Promise.all([aw.close(), st.close()])) useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) const sf = await import('../server/services/siteFeatures.js') aw.seed('customers', { $id: 'c1', name: 'Kunde Eins', location: 'Berlin' }) aw.seed('customers', { $id: 'c2', name: 'Kunde Zwei' }) aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Bäckerei', liveDomain: 'baeckerei.de' }) aw.seed('websiteProjects', { $id: 'p2', customerId: 'c1', projectName: 'Café', subdomain: 'cafe' }) aw.seed('websiteProjects', { $id: 'p3', customerId: 'c2', projectName: 'Fremd' }) aw.seed('websiteProjects', { $id: 'p_' + 'x'.repeat(40), customerId: 'c1', projectName: 'Lang' }) const consent = { text: 'Ich stimme zu', at: '2026-09-30T10:00:00.000Z', ip: '1.2.3.4', userAgent: 'UA' } const tickets = () => [...aw.docs('workorders').values()].filter((t) => String(t.topic).startsWith('News-Pinnwand einbauen')) test('ohne_dokument_ist_off', async () => { const list = await sf.listCustomerFeatures('c1') assert.equal(list.length, 3) for (const p of list) assert.equal(p.features.news.state, 'off') assert.equal(list.find((p) => p.projectId === 'p1').liveUrl, 'https://baeckerei.de') }) test('bezahlt_legt_ticket_an', async () => { const ordered = await sf.markOrdered({ customerId: 'c1', projectId: 'p1', key: 'news', sessionId: 'cs_1', consent }) assert.equal(ordered.state, 'ordered') assert.equal(ordered.enabled, false) assert.equal(ordered.consentIp, '1.2.3.4') assert.match(ordered.siteKey, /^[a-z2-7]{16}$/) const { doc, created } = await sf.markPaid({ sessionId: 'cs_1', invoiceId: 'in_1', invoiceUrl: 'https://x/inv' }) assert.equal(created, true) assert.equal(doc.state, 'setup') assert.equal(doc.enabled, false) assert.equal(doc.unlockedByPurchase, true) assert.ok(doc.ticketId) assert.equal(tickets().length, 1) assert.match(tickets()[0].topic, /^News-Pinnwand einbauen – Bäckerei/) assert.ok(tickets()[0].details.includes(`data-webklar-news="${doc.siteKey}"`)) const view = (await sf.listCustomerFeatures('c1')).find((p) => p.projectId === 'p1').features.news assert.equal(view.state, 'setup') assert.ok(view.ticketWoid) }) test('webhook_idempotent', async () => { const again = await sf.markPaid({ sessionId: 'cs_1', invoiceId: 'in_1', invoiceUrl: 'https://x/inv' }) assert.equal(again.created, false) assert.equal(tickets().length, 1) }) test('ticket_geschlossen_aktiviert', async () => { const doc = await sf.getFeatureDoc('c1', 'p1', 'news') assert.deepEqual(await sf.activateClosedTickets({ customerId: 'c1' }), []) assert.equal((await sf.getFeatureDoc('c1', 'p1', 'news')).state, 'setup') aw.seed('workorders', { ...aw.docs('workorders').get(doc.ticketId), status: 'Closed' }) assert.deepEqual(await sf.activateClosedTickets({ customerId: 'c2' }), []) const now = new Date('2026-10-01T08:00:00Z') const done = await sf.activateClosedTickets({ now }) assert.equal(done.length, 1) assert.equal(done[0].state, 'active') assert.equal(done[0].enabled, true) assert.equal(done[0].activatedAt, now.toISOString()) assert.equal((await sf.findActiveBySiteKey(doc.siteKey)).$id, doc.$id) }) test('fehlgeschlagene_zahlung_setzt_off', async () => { await sf.markOrdered({ customerId: 'c1', projectId: 'p2', key: 'news', sessionId: 'cs_2', consent }) const doc = await sf.markCheckoutFailed('cs_2') assert.equal(doc.state, 'off') assert.equal(doc.enabled, false) assert.equal(await sf.markCheckoutFailed('cs_unbekannt'), null) }) test('fremdes_projekt_403', async () => { await assert.rejects(() => sf.assertOwnProject('c1', 'p3'), { status: 403 }) await assert.rejects(() => sf.assertOwnProject('c1', 'gibts_nicht'), { status: 403 }) assert.equal((await sf.assertOwnProject('c1', 'p1')).$id, 'p1') }) test('sitekey_format', () => { const keys = new Set() for (let i = 0; i < 100; i += 1) { const k = sf.newSiteKey() assert.match(k, /^[a-z2-7]{16}$/) keys.add(k) } assert.equal(keys.size, 100) }) test('admin_aktiviert_ohne_zahlung', async () => { const before = tickets().length const doc = await sf.adminActivate('c1', 'p2', 'news', { by: 'kenso' }) assert.equal(doc.state, 'active') assert.equal(doc.enabled, true) assert.equal(doc.unlockedByPurchase, false) assert.equal(tickets().length, before) const off = await sf.disable(doc.$id, { by: 'kenso' }) assert.equal(off.state, 'disabled') assert.equal(off.enabled, false) }) test('lange_projekt_id_bekommt_gueltige_dokument_id', async () => { const doc = await sf.adminActivate('c1', 'p_' + 'x'.repeat(40), 'news', {}) assert.ok(doc.$id.length <= 36) }) test('bestellen_bei_aktiver_funktion_409', async () => { const doc = await sf.adminActivate('c1', 'p1', 'news', {}).catch(() => null) const active = doc || (await sf.getFeatureDoc('c1', 'p1', 'news')) assert.equal(active.state, 'active') await assert.rejects( () => sf.markOrdered({ customerId: 'c1', projectId: 'p1', key: 'news', sessionId: 'cs_x', consent }), { status: 409 } ) assert.equal((await sf.getFeatureDoc('c1', 'p1', 'news')).state, 'active') }) test('parallele_webhooks_ein_ticket', async () => { aw.seed('websiteProjects', { $id: 'p4', customerId: 'c1', projectName: 'Parallel' }) await sf.markOrdered({ customerId: 'c1', projectId: 'p4', key: 'news', sessionId: 'cs_par', consent }) const before = tickets().length const res = await Promise.all([1, 2, 3].map(() => sf.markPaid({ sessionId: 'cs_par', invoiceId: 'in_p', invoiceUrl: 'u' }))) assert.equal(res.filter((r) => r.created).length, 1) assert.equal(tickets().length, before + 1) assert.ok((await sf.getFeatureDoc('c1', 'p4', 'news')).ticketId) }) test('abgelaufen_fasst_setup_und_active_nicht_an', async () => { // cs_1 ist bezahlt ('setup' bzw. nach dem Schliessen des Tickets 'active'): ein spaetes expired aendert nichts const before = await sf.getFeatureDoc('c1', 'p1', 'news') assert.notEqual(before.state, 'ordered') const after = await sf.markCheckoutFailed('cs_1') assert.equal(after.state, before.state) assert.equal((await sf.getFeatureDoc('c1', 'p1', 'news')).state, before.state) assert.equal((await sf.getFeatureDoc('c1', 'p1', 'news')).purchaseStatus, 'paid') }) test('bezahlt_und_abgelaufen_gleichzeitig_bleiben_stimmig', async () => { const pid = 'p_race' aw.seed('websiteProjects', { $id: pid, customerId: 'c1', projectName: 'Rennen' }) await sf.markOrdered({ customerId: 'c1', projectId: pid, key: 'news', sessionId: 'cs_race', consent }) const ticketsBefore = tickets().length await Promise.all([ sf.markPaid({ sessionId: 'cs_race', invoiceId: 'in_r', invoiceUrl: '' }), sf.markCheckoutFailed('cs_race'), ]) const doc = await sf.getFeatureDoc('c1', pid, 'news') // entweder bezahlt (mit Ticket) oder zurueckgesetzt (ohne): nie 'setup' ohne Ticket oder 'off' mit Ticket if (doc.state === 'setup') { assert.ok(doc.ticketId) assert.equal(tickets().length, ticketsBefore + 1) } else { assert.equal(doc.state, 'off') assert.equal(doc.ticketId || '', '') assert.equal(tickets().length, ticketsBefore) } })