Finanzen: Zeitgrenzen fuer Stripe-Lesen, Schutz vor doppelt angelegten Rechnungen
I2: Leseanfragen an Stripe mit timeout 15 s und einer Wiederholung (auch Folgeseiten), die Uebersicht wartet hoechstens 20 s auf Stripe und rechnet sonst ohne Stripe weiter. I3: idempotencyKey vom Aufrufer an stripe.invoices.create, Schutz im Prozess gegen gleichzeitiges und 30 s spaeteres Doppelanlegen (409), 502 mit invoiceId/Nummer wenn die fertige Rechnung nicht lesbar ist. M9: Warnung, wenn die Rechnungsliste bei 1000 abgeschnitten wird. M11: Ursache eines 502 an einer bestehenden Rechnung (Art, Status, Code) im Protokoll. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ef2ce49669
commit
daa451094a
8 files changed
+476
-48
No files matched your search
@@ -10,13 +10,18 @@ const aw = await startAppwriteMock({ schema: {} })
|
||||
const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const { euroDe } = await import('../server/services/finance/format.js')
|
||||
const { mapInvoice, listInvoices, listPayouts, invalidateFinanceCache } = await import('../server/services/finance/stripeFinance.js')
|
||||
const { mapInvoice, listInvoices, listPayouts, invalidateFinanceCache, setStripeReadOptionsForTests } = await import(
|
||||
'../server/services/finance/stripeFinance.js'
|
||||
)
|
||||
const { getStripe } = await import('../server/services/stripeBilling.js')
|
||||
|
||||
after(() => {
|
||||
mock.timers.reset()
|
||||
return Promise.all([aw.close(), st.close()])
|
||||
})
|
||||
beforeEach(() => {
|
||||
mock.restoreAll()
|
||||
setStripeReadOptionsForTests(null)
|
||||
mock.timers.reset()
|
||||
invalidateFinanceCache()
|
||||
st.calls.length = 0
|
||||
@@ -212,10 +217,66 @@ test('listInvoices: blaettert ueber alle Seiten', async () => {
|
||||
assert.equal(st.calls.filter((c) => c.path === '/v1/invoices').length, 3)
|
||||
})
|
||||
|
||||
test('listInvoices: hoechstens 1000 Rechnungen', async () => {
|
||||
test('listInvoices: hoechstens 1000 Rechnungen, bei der Obergrenze genau eine Warnung je Laden (M9)', async () => {
|
||||
for (let i = 1; i <= 1100; i += 1) st.invoices.set(`in_${i}`, rawInvoice({ id: `in_${i}` }))
|
||||
const warn = mock.method(console, 'warn', () => {})
|
||||
const list = await listInvoices()
|
||||
assert.equal(list.length, 1000)
|
||||
assert.deepEqual(warn.mock.calls.map((c) => c.arguments), [['[finanzen] Rechnungsliste bei 1000 abgeschnitten']])
|
||||
// aus dem Zwischenspeicher: kein neues Laden, keine zweite Warnung; ein neues Laden warnt wieder
|
||||
await listInvoices()
|
||||
assert.equal(warn.mock.callCount(), 1)
|
||||
await listInvoices({ force: true })
|
||||
assert.equal(warn.mock.callCount(), 2)
|
||||
})
|
||||
|
||||
test('listInvoices: unter der Obergrenze keine Warnung (M9)', async () => {
|
||||
for (let i = 1; i <= 999; i += 1) st.invoices.set(`in_${i}`, rawInvoice({ id: `in_${i}` }))
|
||||
const warn = mock.method(console, 'warn', () => {})
|
||||
assert.equal((await listInvoices()).length, 999)
|
||||
assert.equal(warn.mock.callCount(), 0)
|
||||
})
|
||||
|
||||
test('Lesen: Voreinstellung 15 s Zeitgrenze und eine Wiederholung je Anfrage an Stripe (I2)', async () => {
|
||||
const stripe = getStripe()
|
||||
const invoicesList = mock.method(stripe.invoices, 'list', () => ({ autoPagingToArray: async () => [] }))
|
||||
const payoutsList = mock.method(stripe.payouts, 'list', async () => ({ data: [] }))
|
||||
await listInvoices()
|
||||
await listPayouts()
|
||||
assert.deepEqual(invoicesList.mock.calls[0].arguments[1], { timeout: 15_000, maxNetworkRetries: 1 })
|
||||
assert.deepEqual(payoutsList.mock.calls[0].arguments[1], { timeout: 15_000, maxNetworkRetries: 1 })
|
||||
})
|
||||
|
||||
test('Lesen: antwortet Stripe nicht, endet die Anfrage an der Zeitgrenze statt nach 80 Sekunden (I2)', async () => {
|
||||
setStripeReadOptionsForTests({ timeout: 80, maxNetworkRetries: 0 })
|
||||
st.invoices.set('in_1', rawInvoice())
|
||||
st.payouts.set('po_1', { id: 'po_1', object: 'payout', amount: 100, arrival_date: noonBerlin('2026-09-12'), status: 'paid' })
|
||||
const isTimeout = (err) => err.type === 'StripeConnectionError' && /timeout/i.test(err.message)
|
||||
|
||||
st.delayNext({ method: 'GET', path: '/v1/invoices', ms: 700 })
|
||||
let started = Date.now()
|
||||
await assert.rejects(listInvoices(), isTimeout)
|
||||
assert.ok(Date.now() - started < 500, `Rechnungen: ${Date.now() - started} ms`)
|
||||
|
||||
st.delayNext({ method: 'GET', path: '/v1/payouts', ms: 700 })
|
||||
started = Date.now()
|
||||
await assert.rejects(listPayouts(), isTimeout)
|
||||
assert.ok(Date.now() - started < 500, `Auszahlungen: ${Date.now() - started} ms`)
|
||||
|
||||
// der Fehler wird nicht zwischengespeichert: der naechste Aufruf klappt
|
||||
assert.equal((await listInvoices()).length, 1)
|
||||
assert.equal((await listPayouts()).length, 1)
|
||||
})
|
||||
|
||||
test('Lesen: die Zeitgrenze gilt auch fuer die Folgeseiten der Rechnungsliste (I2)', async () => {
|
||||
for (let i = 1; i <= 150; i += 1) st.invoices.set(`in_${i}`, rawInvoice({ id: `in_${i}` }))
|
||||
setStripeReadOptionsForTests({ timeout: 80, maxNetworkRetries: 0 })
|
||||
// die erste Seite kommt sofort, die zweite haengt
|
||||
st.delayNext({ method: 'GET', path: '/v1/invoices', ms: 700, after: 1 })
|
||||
const started = Date.now()
|
||||
await assert.rejects(listInvoices(), (err) => err.type === 'StripeConnectionError')
|
||||
assert.ok(Date.now() - started < 500)
|
||||
assert.equal(st.calls.filter((c) => c.path === '/v1/invoices').length, 2)
|
||||
})
|
||||
|
||||
test('listInvoices: zweimal innerhalb von 60 s -> ein Stripe-Aufruf, danach invalidateFinanceCache und force', async () => {
|
||||
|
||||
Reference in new issue
Block a user