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'), subscriptions: schemaKeys('subscriptions') }, }) const st = await startFakeStripe() useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) const { config } = await import('../server/config.js') const { startTestApp, get, post, postWebhook } = await import('./support/testApp.js') const { customerCookie, seedFixtures } = await import('./support/fixtures.js') const { default: funktionenRoutes } = await import('../server/routes/funktionen.js') const { default: aboRoutes } = await import('../server/routes/abo.js') const sf = await import('../server/services/siteFeatures.js') const { setMailTransportForTests } = await import('../server/services/mailer.js') const { mailSink } = await import('./support/mailSink.js') const app = await startTestApp({ '/api/funktionen': funktionenRoutes, '/api/abo': aboRoutes }) const mails = mailSink() setMailTransportForTests(mails) after(() => { setMailTransportForTests(null) return Promise.all([app.close(), aw.close(), st.close()]) }) seedFixtures(aw) aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Bäckerei', liveDomain: 'baeckerei.de' }) aw.seed('websiteProjects', { $id: 'p2', customerId: 'c1', projectName: 'Café' }) aw.seed('websiteProjects', { $id: 'p3', customerId: 'c1', projectName: 'Kiosk' }) aw.seed('websiteProjects', { $id: 'p4', customerId: 'c1', projectName: 'Bistro' }) aw.seed('websiteProjects', { $id: 'p5', customerId: 'c2', projectName: 'Fremd' }) const cookie = customerCookie('c1') const checkout = (projectId, consent = true) => post('/api/funktionen/news/checkout', { projectId, consent }, { cookie }) const sessions = () => [...st.checkoutSessions.values()] const workorders = () => [...aw.docs('workorders').values()].filter((t) => String(t.topic).startsWith('News-Pinnwand einbauen')) const subjects = () => mails.sent.map((m) => m.subject) const event = (type, session) => ({ id: `evt_${Math.random()}`, object: 'event', type, data: { object: session } }) const paidSession = (id, projectId, extra = {}) => ({ id, object: 'checkout.session', mode: 'payment', payment_status: 'paid', metadata: { customerId: 'c1', projectId, featureKey: 'news' }, ...extra, }) test('liste_zustaende', async () => { const body = await get('/api/funktionen', { cookie }) assert.equal(body.setupEstimate, 'meist innerhalb von 1–2 Wochen') assert.equal(body.projects.length, 4) for (const p of body.projects) assert.equal(p.features.news.state, 'off') }) test('checkout_ohne_zustimmung_400', async () => { assert.equal((await checkout('p1', false)).status, 400) assert.equal((await post('/api/funktionen/news/checkout', { projectId: 'p1' }, { cookie })).status, 400) assert.equal(sessions().length, 0) }) test('checkout_fremdes_projekt_403', async () => { assert.equal((await checkout('p5')).status, 403) assert.equal(sessions().length, 0) }) test('checkout_legt_session_an', async () => { const res = await checkout('p1') assert.equal(res.status, 200) assert.match(res.body.url, /^https:\/\/checkout\.stripe\.test\//) const [session] = sessions() assert.equal(session.mode, 'payment') assert.equal(session.metadata.featureKey, 'news') assert.equal(session.metadata.projectId, 'p1') assert.equal(session.metadata.customerId, 'c1') const call = st.calls.find((c) => c.method === 'POST' && c.path === '/v1/checkout/sessions') assert.equal(call.body['invoice_creation[enabled]'], 'true') assert.equal(call.body.success_url, `${config.publicBaseUrl}/dashboard.html#news?bestellt=1`) assert.equal(call.body.cancel_url, `${config.publicBaseUrl}/dashboard.html#news`) assert.ok(Number(call.body.expires_at) > Date.now() / 1000) const price = [...st.prices.values()].find((p) => p.id === call.body['line_items[0][price]']) assert.equal(price.unit_amount, 3000) const doc = await sf.getFeatureDoc('c1', 'p1', 'news') assert.equal(doc.state, 'ordered') assert.equal(doc.stripeSessionId, session.id) assert.equal(doc.consentText, config.funktionen.consentText) assert.ok(doc.consentAt) assert.ok(doc.consentUserAgent) // zweite Bestellung waehrend die erste offen ist: 409, keine neue Sitzung assert.equal((await checkout('p1')).status, 409) assert.equal(sessions().length, 1) }) test('abgelaufene_bestellung_darf_neu_gestartet_werden', async () => { const doc = await sf.getFeatureDoc('c1', 'p1', 'news') aw.seed('portalFeatures', { ...aw.docs('portalFeatures').get(doc.$id), orderedAt: new Date(Date.now() - 25 * 3600 * 1000).toISOString() }) assert.equal((await checkout('p1')).status, 200) assert.equal(sessions().length, 2) }) test('checkout_bei_setup_409', async () => { const sid = 'cs_setup' await sf.markOrdered({ customerId: 'c1', projectId: 'p2', key: 'news', sessionId: sid, consent: {} }) await sf.markPaid({ sessionId: sid }) const before = sessions().length assert.equal((await checkout('p2')).status, 409) assert.equal(sessions().length, before) }) test('sepa_wartet_auf_zahlung', async () => { const res = await checkout('p3') assert.equal(res.status, 200) const id = sessions().at(-1).id const mailsBefore = mails.sent.length const wosBefore = workorders().length const unpaid = paidSession(id, 'p3', { payment_status: 'unpaid' }) assert.equal((await postWebhook(event('checkout.session.completed', unpaid))).status, 200) assert.equal((await sf.getFeatureDoc('c1', 'p3', 'news')).state, 'ordered') assert.equal(workorders().length, wosBefore) assert.equal(mails.sent.length, mailsBefore) assert.equal((await postWebhook(event('checkout.session.async_payment_succeeded', paidSession(id, 'p3')))).status, 200) assert.equal((await sf.getFeatureDoc('c1', 'p3', 'news')).state, 'setup') assert.equal(workorders().length, wosBefore + 1) assert.deepEqual(subjects().slice(mailsBefore).sort(), ['Deine Bestellung: News-Pinnwand', 'Neue Funktion bestellt: News-Pinnwand']) const k15 = mails.sent.at(-2).subject.startsWith('Deine') ? mails.sent.at(-2) : mails.sent.at(-1) assert.equal(k15.to[0].address, 'k@example.invalid') assert.match(k15.text, /30,00 €/) }) test('webhook_idempotent', async () => { await checkout('p4') const id = sessions().at(-1).id const mailsBefore = mails.sent.length const wosBefore = workorders().length for (let i = 0; i < 2; i += 1) { assert.equal((await postWebhook(event('checkout.session.completed', paidSession(id, 'p4')))).status, 200) } assert.equal(workorders().length, wosBefore + 1) assert.equal(mails.sent.length - mailsBefore, 2) assert.equal(subjects().slice(mailsBefore).filter((s) => s.startsWith('Deine Bestellung')).length, 1) }) test('abgelaufen_zurueck_auf_off', async () => { aw.seed('websiteProjects', { $id: 'p6', customerId: 'c1', projectName: 'Werkstatt' }) await checkout('p6') const id = sessions().at(-1).id const expired = { id, object: 'checkout.session', mode: 'payment', metadata: { customerId: 'c1', projectId: 'p6', featureKey: 'news' } } assert.equal((await postWebhook(event('checkout.session.expired', expired))).status, 200) assert.equal((await sf.getFeatureDoc('c1', 'p6', 'news')).state, 'off') assert.equal((await checkout('p6')).status, 200) }) test('zahlung_fehlgeschlagen_mailt_team', async () => { aw.seed('websiteProjects', { $id: 'p7', customerId: 'c1', projectName: 'Garage' }) await checkout('p7') const id = sessions().at(-1).id const mailsBefore = mails.sent.length await postWebhook(event('checkout.session.async_payment_failed', paidSession(id, 'p7', { payment_status: 'unpaid' }))) assert.equal((await sf.getFeatureDoc('c1', 'p7', 'news')).state, 'off') assert.equal(mails.sent.length - mailsBefore, 1) assert.match(mails.sent.at(-1).text, /Zahlung fehlgeschlagen/) }) test('aktivierung_mailt', async () => { const doc = await sf.getFeatureDoc('c1', 'p3', 'news') assert.equal(doc.state, 'setup') aw.seed('workorders', { ...aw.docs('workorders').get(doc.ticketId), status: 'Closed' }) const mailsBefore = mails.sent.length const done = await sf.runActivation() assert.equal(done.length, 1) assert.equal((await sf.getFeatureDoc('c1', 'p3', 'news')).state, 'active') assert.equal(mails.sent.length - mailsBefore, 1) assert.equal(mails.sent.at(-1).subject, 'Deine News-Pinnwand ist bereit') assert.equal(mails.sent.at(-1).to[0].address, 'k@example.invalid') assert.deepEqual(await sf.runActivation(), []) assert.equal(mails.sent.length - mailsBefore, 1) }) test('parallele_checkouts_eine_sitzung', async () => { aw.seed('websiteProjects', { $id: 'p8', customerId: 'c1', projectName: 'Gärtnerei' }) const before = sessions().length const results = await Promise.all([checkout('p8'), checkout('p8')]) assert.deepEqual(results.map((r) => r.status).sort(), [200, 409]) assert.equal(sessions().length, before + 1) }) test('bezahlte_sitzung_mit_ueberschriebener_id_wird_zugeordnet', async () => { aw.seed('websiteProjects', { $id: 'p9', customerId: 'c1', projectName: 'Tischlerei' }) await checkout('p9') const paidId = sessions().at(-1).id const doc = await sf.getFeatureDoc('c1', 'p9', 'news') aw.seed('portalFeatures', { ...aw.docs('portalFeatures').get(doc.$id), stripeSessionId: 'cs_ueberschrieben' }) const wosBefore = workorders().length assert.equal((await postWebhook(event('checkout.session.completed', paidSession(paidId, 'p9')))).status, 200) const after = await sf.getFeatureDoc('c1', 'p9', 'news') assert.equal(after.state, 'setup') assert.equal(after.stripeSessionId, paidId) assert.equal(workorders().length, wosBefore + 1) }) test('parallele_aktivierung_mailt_einmal', async () => { aw.seed('websiteProjects', { $id: 'p10', customerId: 'c1', projectName: 'Schreinerei' }) await checkout('p10') const id = sessions().at(-1).id await postWebhook(event('checkout.session.completed', paidSession(id, 'p10'))) const doc = await sf.getFeatureDoc('c1', 'p10', 'news') aw.seed('workorders', { ...aw.docs('workorders').get(doc.ticketId), status: 'Closed' }) const mailsBefore = mails.sent.length const [a, b] = await Promise.all([sf.runActivation(), sf.runActivation({ customerId: 'c1' })]) assert.equal(a.length + b.length, 1) assert.equal(mails.sent.slice(mailsBefore).filter((m) => m.subject === 'Deine News-Pinnwand ist bereit').length, 1) })