413 lines
21 KiB
JavaScript
413 lines
21 KiB
JavaScript
import { readFileSync } from 'node:fs'
|
||
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, rawGet, 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 { default: teamRoutes } = await import('../server/routes/team/index.js')
|
||
const { default: publicNewsRoutes } = await import('../server/routes/publicNews.js')
|
||
const sf = await import('../server/services/siteFeatures.js')
|
||
const { deleteDocument } = await import('../server/services/appwriteAdmin.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,
|
||
'/api/team': teamRoutes,
|
||
'/api/public/news': publicNewsRoutes,
|
||
})
|
||
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.consentText, config.funktionen.consentText)
|
||
assert.equal(body.projects.length, 4)
|
||
for (const p of body.projects) assert.equal(p.features.news.state, 'off')
|
||
// Terminplaner: zweite Funktion je Projekt, Schalter TERMINE_ENABLED (Vorgabe off) - Einzelheiten in funktionenTermine.test.js
|
||
for (const p of body.projects) assert.equal(p.features.termine.state, 'off')
|
||
assert.deepEqual(body.termine, { verfuegbar: false })
|
||
})
|
||
|
||
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: dieselbe Zahlungsseite, keine neue Sitzung
|
||
const again = await checkout('p1')
|
||
assert.equal(again.status, 200)
|
||
assert.equal(again.body.url, res.body.url)
|
||
assert.equal(sessions().length, 1)
|
||
})
|
||
|
||
test('abgelaufene_sitzung_darf_neu_gestartet_werden', async () => {
|
||
sessions().at(-1).status = 'expired'
|
||
const res = await checkout('p1')
|
||
assert.equal(res.status, 200)
|
||
assert.equal(sessions().length, 2)
|
||
assert.equal((await sf.getFeatureDoc('c1', 'p1', 'news')).stripeSessionId, sessions().at(-1).id)
|
||
})
|
||
|
||
test('abgeschlossene_sitzung_409', async () => {
|
||
sessions().at(-1).status = 'complete'
|
||
const before = sessions().length
|
||
assert.equal((await checkout('p1')).status, 409)
|
||
assert.equal(sessions().length, before)
|
||
})
|
||
|
||
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), [200, 200])
|
||
assert.equal(results[0].body.url, results[1].body.url)
|
||
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)
|
||
})
|
||
|
||
// ------------------------------------------------- Ticketsystem (Team) ---
|
||
// Mitarbeiter ohne Admin-Label ueber den Team-Eingang (JWT)
|
||
aw.seed('employees', { $id: 'e_staff2', userId: 'u_staff2', displayName: 'Andrej', email: 'andrej@webklar.com' })
|
||
const STAFF = { headers: { Authorization: 'Bearer jwt-staff' } }
|
||
const adminPost = (customerId, body, opts = STAFF) => post(`/api/team/funktionen/${customerId}/news`, body, opts)
|
||
|
||
test('admin_aktivieren_ohne_zahlung', async () => {
|
||
aw.seed('websiteProjects', { $id: 'pa1', customerId: 'c1', projectName: 'Admin-Test' })
|
||
const wosBefore = workorders().length
|
||
const res = await adminPost('c1', { projectId: 'pa1', action: 'activate', note: 'Kulanz' })
|
||
assert.equal(res.status, 200)
|
||
const view = res.body.projects.find((p) => p.projectId === 'pa1').features.news
|
||
assert.equal(view.state, 'active')
|
||
assert.equal(view.ticketWoid, null)
|
||
assert.match(view.adminNote, /^\d{4}-\d\d-\d\dT[\d:.]+Z Andrej: activate – Kulanz$/)
|
||
const doc = await sf.getFeatureDoc('c1', 'pa1', 'news')
|
||
assert.equal(doc.enabled, true)
|
||
assert.equal(doc.unlockedByPurchase, false)
|
||
assert.equal(workorders().length, wosBefore)
|
||
const list = await get('/api/team/funktionen/c1', STAFF)
|
||
assert.equal(list.projects.find((p) => p.projectId === 'pa1').features.news.state, 'active')
|
||
})
|
||
|
||
test('admin_abschalten', async () => {
|
||
const doc = await sf.getFeatureDoc('c1', 'pa1', 'news')
|
||
const publicUrl = `/api/public/news/${doc.siteKey}`
|
||
assert.equal((await rawGet(publicUrl)).status, 200)
|
||
const res = await adminPost('c1', { projectId: 'pa1', action: 'disable' })
|
||
assert.equal(res.status, 200)
|
||
const view = res.body.projects[0].features.news
|
||
assert.equal(view.state, 'disabled')
|
||
assert.match(view.adminNote, /Andrej: disable \| .*Andrej: activate – Kulanz$/)
|
||
assert.ok(view.adminNote.length <= 300)
|
||
assert.equal((await sf.getFeatureDoc('c1', 'pa1', 'news')).enabled, false)
|
||
const { status, body } = await rawGet(publicUrl)
|
||
assert.ok(status === 404 || (status === 200 && (body.posts || []).length === 0), `öffentlich: ${status}`)
|
||
})
|
||
|
||
test('admin_ungueltig_und_fremd', async () => {
|
||
aw.seed('websiteProjects', { $id: 'pa4', customerId: 'c1', projectName: 'Ohne Dokument' })
|
||
assert.equal((await adminPost('c1', { projectId: 'pa1', action: 'loeschen' })).status, 400)
|
||
assert.equal((await adminPost('c1', { projectId: 'p5', action: 'activate' })).status, 403)
|
||
assert.equal((await adminPost('c1', { projectId: 'gibtsnicht', action: 'activate' })).status, 403)
|
||
assert.equal((await adminPost('c1', { projectId: 'pa4', action: 'disable' })).status, 409)
|
||
})
|
||
|
||
test('admin_notiz_bleibt_im_limit', async () => {
|
||
aw.seed('websiteProjects', { $id: 'pa2', customerId: 'c1', projectName: 'Notiz' })
|
||
for (let i = 0; i < 8; i += 1) {
|
||
await adminPost('c1', { projectId: 'pa2', action: i % 2 ? 'disable' : 'activate', note: 'x'.repeat(100) })
|
||
}
|
||
const note = (await sf.getFeatureDoc('c1', 'pa2', 'news')).adminNote
|
||
assert.ok(note.length <= 300)
|
||
assert.match(note, /^\S+ Andrej: disable – x+/)
|
||
})
|
||
|
||
test('kunde_darf_admin_route_nicht', async () => {
|
||
aw.seed('websiteProjects', { $id: 'pa5', customerId: 'c1', projectName: 'Kundenversuch' })
|
||
const res = await adminPost('c1', { projectId: 'pa5', action: 'activate' }, { cookie: customerCookie('c1') })
|
||
assert.ok(res.status === 401 || res.status === 403)
|
||
assert.equal(await sf.getFeatureDoc('c1', 'pa5', 'news'), null)
|
||
const list = await rawGet('/api/team/funktionen/c1', { cookie: customerCookie('c1') })
|
||
assert.ok(list.status === 401 || list.status === 403)
|
||
})
|
||
|
||
test('ticket_fehlt_markiert', async () => {
|
||
aw.seed('websiteProjects', { $id: 'pa3', customerId: 'c1', projectName: 'Ticketlos' })
|
||
await checkout('pa3')
|
||
await postWebhook(event('checkout.session.completed', paidSession(sessions().at(-1).id, 'pa3')))
|
||
const doc = await sf.getFeatureDoc('c1', 'pa3', 'news')
|
||
assert.equal(doc.state, 'setup')
|
||
const newsOf = async () =>
|
||
(await get('/api/team/funktionen/c1', STAFF)).projects.find((p) => p.projectId === 'pa3').features.news
|
||
assert.equal((await newsOf()).ticketMissing, false)
|
||
await deleteDocument(config.collections.workorders, doc.ticketId)
|
||
const missing = await newsOf()
|
||
assert.equal(missing.ticketMissing, true)
|
||
assert.equal(missing.ticketWoid, null)
|
||
})
|
||
|
||
// ------------------------------------------------ Schlussdurchgang ---
|
||
test('webhook_retry_setzt_ticketlose_einrichtung_fort', async () => {
|
||
aw.seed('websiteProjects', { $id: 'pr1', customerId: 'c1', projectName: 'Retry' })
|
||
await checkout('pr1')
|
||
const id = sessions().at(-1).id
|
||
const wosBefore = workorders().length
|
||
const mailsBefore = mails.sent.length
|
||
aw.failNext({ collection: 'workorders', method: 'POST', status: 400 })
|
||
const first = await postWebhook(event('checkout.session.completed', paidSession(id, 'pr1')))
|
||
assert.equal(first.status, 500)
|
||
const stuck = await sf.getFeatureDoc('c1', 'pr1', 'news')
|
||
assert.equal(stuck.state, 'setup')
|
||
assert.ok(!stuck.ticketId)
|
||
assert.equal(mails.sent.length, mailsBefore)
|
||
assert.equal((await postWebhook(event('checkout.session.completed', paidSession(id, 'pr1')))).status, 200)
|
||
const fixed = await sf.getFeatureDoc('c1', 'pr1', 'news')
|
||
assert.ok(fixed.ticketId)
|
||
assert.equal(workorders().length, wosBefore + 1)
|
||
assert.deepEqual(subjects().slice(mailsBefore).sort(), ['Deine Bestellung: News-Pinnwand', 'Neue Funktion bestellt: News-Pinnwand'])
|
||
// weiterer Retry: nichts mehr
|
||
await postWebhook(event('checkout.session.completed', paidSession(id, 'pr1')))
|
||
assert.equal(workorders().length, wosBefore + 1)
|
||
assert.equal(mails.sent.length - mailsBefore, 2)
|
||
})
|
||
|
||
test('rechnungslink_wird_nachgetragen', async () => {
|
||
aw.seed('websiteProjects', { $id: 'pi1', customerId: 'c1', projectName: 'Rechnung' })
|
||
await checkout('pi1')
|
||
const session = sessions().at(-1)
|
||
await postWebhook(event('checkout.session.completed', paidSession(session.id, 'pi1')))
|
||
const listed = async () => (await get('/api/funktionen', { cookie })).projects.find((p) => p.projectId === 'pi1').features.news
|
||
assert.equal((await listed()).invoiceUrl, null)
|
||
st.invoices.set('in_late', { id: 'in_late', object: 'invoice', hosted_invoice_url: 'https://invoice.stripe.test/late' })
|
||
session.invoice = 'in_late'
|
||
assert.equal((await listed()).invoiceUrl, 'https://invoice.stripe.test/late')
|
||
const doc = await sf.getFeatureDoc('c1', 'pi1', 'news')
|
||
assert.equal(doc.stripeInvoiceId, 'in_late')
|
||
// Stripe-Fehler lassen die Liste nicht scheitern
|
||
aw.seed('portalFeatures', { ...aw.docs('portalFeatures').get(doc.$id), invoiceUrl: '' })
|
||
st.failNext({ method: 'GET', path: /checkout\/sessions/, status: 400 })
|
||
assert.equal((await get('/api/funktionen', { cookie })).projects.length > 0, true)
|
||
})
|
||
|
||
test('bezahlt_ohne_passende_bestellung_meldet_dem_team', async () => {
|
||
aw.seed('websiteProjects', { $id: 'pu1', customerId: 'c1', projectName: 'Unklar' })
|
||
const mailsBefore = mails.sent.length
|
||
const res = await postWebhook(event('checkout.session.completed', paidSession('cs_verwaist', 'pu1')))
|
||
assert.equal(res.status, 200)
|
||
const notice = mails.sent.slice(mailsBefore)
|
||
assert.equal(notice.length, 1)
|
||
assert.equal(notice[0].subject, 'Neue Funktion bestellt: News-Pinnwand')
|
||
assert.match(notice[0].text, /cs_verwaist/)
|
||
assert.match(notice[0].text, /nicht zuzuordnen/)
|
||
assert.doesNotMatch(notice[0].text, /Zahlung fehlgeschlagen/)
|
||
assert.equal(workorders().filter((t) => t.topic.includes('Unklar')).length, 0)
|
||
})
|
||
|
||
test('abgeschaltete_funktion_nicht_buchbar', async () => {
|
||
aw.seed('websiteProjects', { $id: 'pd1', customerId: 'c1', projectName: 'Abgeschaltet' })
|
||
const doc = await sf.adminActivate('c1', 'pd1', 'news', { by: 'test' })
|
||
await sf.disable(doc.$id, { by: 'test' })
|
||
const before = sessions().length
|
||
const res = await checkout('pd1')
|
||
assert.equal(res.status, 409)
|
||
assert.equal(res.body.error, 'Die News-Pinnwand wurde für diese Website abgeschaltet. Schreib uns im Chat, wenn du sie wieder möchtest.')
|
||
assert.equal(sessions().length, before)
|
||
})
|
||
|
||
test('statische_quellen: news_label, minmax_grid, ticket_anleitung', () => {
|
||
const read = (p) => readFileSync(new URL(`../${p}`, import.meta.url), 'utf8')
|
||
assert.match(read('public/app.js'), /FEATURE_LABELS = \{[^}]*news: 'News-Pinnwand'/)
|
||
assert.doesNotMatch(read('public/app.js'), /FEATURE_LABELS\[f\.featureKey\] \|\| f\.featureKey/)
|
||
const embed = read('public/embed/news.js')
|
||
assert.ok(embed.includes('grid-template-columns:minmax(0,1.25fr) minmax(0,1fr)'))
|
||
assert.ok(embed.includes('.wkn-combo-in{grid-template-columns:minmax(0,1fr)}'))
|
||
assert.ok(!read('public/dashboard.html').includes('id="view-news" aria-live'))
|
||
})
|
||
|
||
test('ticket_details_nennen_die_anleitung', () => {
|
||
const doc = [...aw.docs('workorders').values()].find((t) => String(t.topic).startsWith('News-Pinnwand einbauen'))
|
||
assert.ok(doc.details.includes('Anleitung: docs/funktionen/news-einbau.md (Repo Webklar-Kundenbereich)'))
|
||
})
|
||
|
||
// Live-Fehler 30.09.2026: Appwrite lehnte die (zu lange) Sitzungs-ID ab, der Kunde sah die rohe Meldung
|
||
test('checkout_datenbankfehler_ohne_rohe_meldung', async () => {
|
||
aw.seed('websiteProjects', { $id: 'p-db', customerId: 'c1', projectName: 'Eisdiele' })
|
||
aw.failNext({ collection: 'portalFeatures', method: 'PATCH', status: 400, message: 'Invalid document structure: Attribute "stripeSessionId" has invalid type.' })
|
||
const res = await checkout('p-db')
|
||
assert.equal(res.status, 500, JSON.stringify(res.body))
|
||
assert.doesNotMatch(res.body.error, /Invalid document structure|stripeSessionId/)
|
||
assert.match(res.body.error, /Bestellung konnte gerade nicht gestartet werden/)
|
||
})
|