Files
Webklar-Kundenbereich/test/zeitenTeamBehalten.test.js
T
ClaudeandClaude Opus 5.5 811e054ca4 Öffnungszeiten: Ticket nachlegen lehnt eine Behalten-Zahlung ab (Stillstand an feature-admin)
bezahlteSitzung weist Sitzungen mit metadata.zweck behalten mit 409 ab. Sonst ginge die Sitzung über onFeaturePaid in bucheBehalten, das
feature-admin:<projekt> nimmt, während die Team-Route sie schon hält: Stillstand und dauerhaft belegter Schlüssel. Test in
test/zeitenTeamBehalten.test.js: 409 binnen Sekunden, Sperre frei, folgende Team-Aktion nicht blockiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:58:25 +00:00

131 lines
6.1 KiB
JavaScript

import { test, after, beforeEach, afterEach, mock } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { mailSink } from './support/mailSink.js'
import { schemaKeys } from './support/schema.js'
import { useTestEnv } from './support/testEnv.js'
import { ZEITEN_DATETIMES } from '../server/services/zeiten/schema.js'
// Team-Aktion "Ticket nachlegen" auf Oeffnungszeiten live mit einer Behalten-Zahlung am Dokument (Merge mit TP2, Fix Runde 1): ticketNachlegen
// holt die bezahlte Sitzung bei Stripe und fuettert sie dem Webhook-Weg. Eine Behalten-Sitzung (metadata.zweck 'behalten') ginge dort in
// bucheBehalten, das `feature-admin:<projekt>` nimmt, waehrend die Team-Route diese Sperre schon haelt (nicht wiederbetretbar => Stillstand,
// der den Schluessel dauerhaft blockiert). Sie wird darum mit 409 abgelehnt, ohne etwas zu veraendern.
const aw = await startAppwriteMock({
schema: {
portalFeatures: schemaKeys('portalFeatures'),
subscriptions: schemaKeys('subscriptions'),
featureConsents: schemaKeys('featureConsents'),
oeffnungszeiten: schemaKeys('oeffnungszeiten'),
zeitenErinnerungen: schemaKeys('zeitenErinnerungen'),
},
datetimes: {
...ZEITEN_DATETIMES,
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'k24At'],
featureConsents: ['acceptedAt'],
},
defaultLimit: 25,
})
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { startTestApp, post } = await import('./support/testApp.js')
const { seedFixtures } = await import('./support/fixtures.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { withLocks } = await import('../server/services/keyedLock.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const { zeitenDeps } = await import('../server/services/zeiten/neubau.js')
const { resetZeitenCacheForTests } = await import('../server/services/zeiten/cache.js')
const { default: teamRoutes } = await import('../server/routes/team/index.js')
const app = await startTestApp({ '/api/team': teamRoutes })
after(() => Promise.all([app.close(), aw.close(), st.close()]))
after(() => setMailTransportForTests(null))
seedFixtures(aw)
const STAFF = { headers: { Authorization: 'Bearer jwt-staff' } }
const doc = () => aw.docs('portalFeatures').get('f_p1_zeiten')
const tickets = () => [...aw.docs('workorders').values()]
const ticketAktion = () => post('/api/team/funktionen/c1/zeiten', { projectId: 'p1', action: 'ticket' }, STAFF)
/** Antwortet die Anfrage nicht binnen 4 s, ist sie haengengeblieben (Stillstand statt 409). */
const binnen4s = (promise) => Promise.race([promise, new Promise((resolve) => setTimeout(() => resolve('HANG'), 4000))])
let sink
let rebuilds
beforeEach(async () => {
for (const col of ['portalFeatures', 'oeffnungszeiten', 'zeitenErinnerungen', 'workorders', 'websiteProjects']) {
for (const id of [...aw.docs(col).keys()]) await deleteDocument(col, id)
}
resetZeitenCacheForTests()
st.checkoutSessions.clear()
aw.seed('customers', { $id: 'c1', appwriteUserId: 'u1', email: 'k@example.invalid', name: 'Kunde Eins', status: 'active', portalAccessEnabled: true })
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Projekt p1', subdomain: 'sub-p1', repoFullName: 'JUSN/p1', previewMode: 'auto', previewDetected: 'static' })
sink = mailSink()
setMailTransportForTests(sink)
rebuilds = []
zeitenDeps.rebuild = async (p) => {
rebuilds.push(p.$id)
return { queued: true }
}
mock.method(console, 'error', () => {})
mock.method(console, 'warn', () => {})
mock.method(console, 'log', () => {})
})
afterEach(() => mock.restoreAll())
/** Zustand, den bucheBehalten hinterlaesst: setup ohne Ticket, stripeSessionId = bezahlte Behalten-Sitzung, unlockedBy kauf. */
function behaltenDokument(sitzung = { payment_status: 'paid' }) {
aw.seed('portalFeatures', {
$id: 'f_p1_zeiten',
customerId: 'c1',
projectId: 'p1',
featureKey: 'zeiten',
state: 'setup',
enabled: false,
siteKey: 'abcdefghijklmnop',
unlockedBy: 'kauf',
unlockedByPurchase: true,
purchaseStatus: 'paid',
stripeSessionId: 'cs_beh',
ticketId: '',
einbau: 'angestossen',
})
st.checkoutSessions.set('cs_beh', {
id: 'cs_beh',
object: 'checkout.session',
mode: 'payment',
status: 'complete',
invoice: null,
metadata: { customerId: 'c1', projectId: 'p1', featureKey: 'zeiten', zweck: 'behalten' },
...sitzung,
})
}
test('ticket auf Zeiten mit bezahlter Behalten-Sitzung: sofort 409, nichts verändert, die Sperre feature-admin bleibt frei', async () => {
behaltenDokument()
const vorher = { ...doc() }
const res = await binnen4s(ticketAktion())
assert.notEqual(res, 'HANG', 'die Anfrage darf nicht an feature-admin haengenbleiben')
assert.equal(res.status, 409, JSON.stringify(res.body))
assert.match(res.body.error, /Behalten-Zahlung/)
assert.deepEqual(doc(), vorher, 'Dokument unveraendert')
assert.equal(tickets().length, 0, 'kein Ticket')
assert.deepEqual(rebuilds, [])
assert.equal(sink.sent.length, 0, 'keine Mail')
// die Sperre ist frei: ein Probelauf und danach eine weitere Team-Aktion am selben Projekt kommen sofort durch
assert.equal(await binnen4s(withLocks(['feature-admin:p1'], async () => 'frei')), 'frei')
const aus = await binnen4s(post('/api/team/funktionen/c1/zeiten', { projectId: 'p1', action: 'disable' }, STAFF))
assert.notEqual(aus, 'HANG', 'die folgende Team-Aktion darf nicht blockiert sein')
assert.equal(aus.status, 200, JSON.stringify(aus.body))
assert.equal(doc().state, 'disabled')
})
test('ticket auf Zeiten mit noch nicht bezahlter Behalten-Sitzung: ebenfalls 409 und kein Stillstand', async () => {
behaltenDokument({ payment_status: 'unpaid', status: 'open' })
const res = await binnen4s(ticketAktion())
assert.notEqual(res, 'HANG')
assert.equal(res.status, 409, JSON.stringify(res.body))
assert.equal(await binnen4s(withLocks(['feature-admin:p1'], async () => 'frei')), 'frei')
})