216 lines
11 KiB
JavaScript
216 lines
11 KiB
JavaScript
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)
|
||
})
|