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>
131 lines
6.1 KiB
JavaScript
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')
|
|
})
|