Finanzen: Start-Abruf nur bei altem Stand, unlesbare Buchungen ueberspringen, Mail bei Widerruf des Bankzugangs
I1: shouldRunStartupSync; der Abruf 2 Minuten nach dem Start entfaellt, wenn der letzte erfolgreiche Abruf weniger als 5,5 Stunden her ist (Deployments verbrauchten das Tageslimit der Bank). M4: eine unlesbare Buchung wird uebersprungen und gezaehlt, der Abruf endet ok. M10: 401/403 beim Abruf sendet T18 einmal (Stufe 0 in warnedThresholds). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3c5e06723f
commit
ef2ce49669
3 files changed
+168
-25
No files matched your search
+107
-15
@@ -9,6 +9,7 @@ import { startFakeStripe } from './support/fakeStripe.js'
|
||||
import { startFakeEnableBanking } from './support/fakeEnableBanking.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
import { schemaKeys } from './support/schema.js'
|
||||
import { mailSink } from './support/mailSink.js'
|
||||
|
||||
// Nachbauten starten und die Umgebung setzen, BEVOR config.js geladen wird (Muster wie aboRoutes.test.js)
|
||||
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 })
|
||||
@@ -30,12 +31,17 @@ useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
Object.assign(process.env, { ENABLE_BANKING_APP_ID: 'app-test', ENABLE_BANKING_KEY_PATH: keyPath, ENABLE_BANKING_API_BASE: fake.url })
|
||||
|
||||
const { config } = await import('../server/config.js')
|
||||
const { syncBank, transactionDocId, toTransactionDoc } = await import('../server/services/finance/bankSync.js')
|
||||
const { syncBank, transactionDocId, toTransactionDoc, shouldRunStartupSync, checkBankConsent } = await import('../server/services/finance/bankSync.js')
|
||||
const store = await import('../server/services/finance/bankStore.js')
|
||||
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
const { addDays, berlinDate } = await import('../server/services/finance/dates.js')
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
|
||||
const mails = mailSink()
|
||||
setMailTransportForTests(mails)
|
||||
|
||||
after(async () => {
|
||||
setMailTransportForTests(null)
|
||||
await Promise.all([fake.close(), aw.close(), st.close()])
|
||||
fs.rmSync(keyDir, { recursive: true, force: true })
|
||||
})
|
||||
@@ -91,6 +97,7 @@ async function reset({ transactions = nine(), bal = balances() } = {}) {
|
||||
for (const id of txDocs().keys()) await store.deleteTransaction(id)
|
||||
await deleteDocument(config.collections.bankConnections, 'finom').catch(() => {})
|
||||
fake.calls.length = 0
|
||||
mails.sent.length = 0
|
||||
fake.transactions.set(ACCOUNT, transactions)
|
||||
fake.balances.set(ACCOUNT, bal)
|
||||
}
|
||||
@@ -506,23 +513,11 @@ test('syncBank: Fehler bei den Umsätzen lässt den Saldo der Verbindung unverä
|
||||
assert.equal(connection().lastSyncOk, false)
|
||||
})
|
||||
|
||||
test('syncBank: wirft nie - kaputter Betrag der Bank, Netzwerkfehler, Appwrite-Fehler', async () => {
|
||||
// kaputter Betrag: nichts wird geschrieben, Fehler gemerkt
|
||||
const broken = nine()
|
||||
broken[4].transaction_amount.amount = 'abc'
|
||||
await reset({ transactions: broken })
|
||||
await connect()
|
||||
let result = await syncBank({ now: NOW })
|
||||
assert.equal(result.ok, false)
|
||||
assert.match(result.error, /Betrag/)
|
||||
assert.equal(txDocs().size, 0)
|
||||
assert.equal(connection().status, 'aktiv')
|
||||
assert.equal(connection().lastSyncOk, false)
|
||||
|
||||
test('syncBank: wirft nie - fehlerhafter Saldo, Netzwerkfehler, Appwrite-Fehler', async () => {
|
||||
// fehlerhafter Saldo
|
||||
await reset({ bal: [{ balance_type: 'ITBD', balance_amount: { amount: 'kaputt', currency: 'EUR' }, reference_date: '2026-10-01' }] })
|
||||
await connect()
|
||||
result = await syncBank({ now: NOW })
|
||||
let result = await syncBank({ now: NOW })
|
||||
assert.equal(result.ok, false)
|
||||
assert.equal(connection().lastSyncOk, false)
|
||||
|
||||
@@ -544,6 +539,103 @@ test('syncBank: wirft nie - kaputter Betrag der Bank, Netzwerkfehler, Appwrite-F
|
||||
assert.match(connection().lastError, /Appwrite gestoert/)
|
||||
})
|
||||
|
||||
test('syncBank: eine unlesbare Buchung wird übersprungen und gezählt, der Abruf endet ok (M4)', async () => {
|
||||
const broken = nine()
|
||||
broken[4].transaction_amount.amount = 'abc'
|
||||
broken[5].credit_debit_indicator = 'XXXX'
|
||||
await reset({ transactions: broken })
|
||||
await connect()
|
||||
const result = await syncBank({ now: NOW })
|
||||
assert.equal(result.ok, true)
|
||||
assert.equal(result.neu, 7)
|
||||
assert.equal(txDocs().size, 7, 'die sieben lesbaren Buchungen sind gespeichert')
|
||||
assert.ok(!txDocs().has(docId('r5')) && !txDocs().has(docId('r6')))
|
||||
const conn = connection()
|
||||
assert.equal(conn.status, 'aktiv')
|
||||
assert.equal(conn.lastSyncOk, true)
|
||||
assert.equal(conn.lastError, '2 Buchung(en) übersprungen (unlesbar).')
|
||||
assert.equal(conn.balanceBookedCents, 10893)
|
||||
|
||||
// der nächste Abruf ohne kaputte Buchung räumt die Meldung weg
|
||||
fake.transactions.set(ACCOUNT, nine())
|
||||
assert.equal((await syncBank({ now: NOW })).ok, true)
|
||||
assert.equal(connection().lastError, '')
|
||||
})
|
||||
|
||||
test('shouldRunStartupSync: frischer erfolgreicher Abruf (unter 5,5 Stunden) überspringt den Start-Abruf (I1)', () => {
|
||||
const at = (hoursAgo) => new Date(NOW.getTime() - hoursAgo * 3_600_000).toISOString()
|
||||
assert.equal(shouldRunStartupSync({ lastSyncOk: true, lastSyncAt: at(1) }, NOW), false)
|
||||
assert.equal(shouldRunStartupSync({ lastSyncOk: true, lastSyncAt: at(5.4) }, NOW), false)
|
||||
// ab 5,5 Stunden läuft der Abruf wieder (der 6-Stunden-Takt wäre sonst fast eine Stunde zu spät)
|
||||
assert.equal(shouldRunStartupSync({ lastSyncOk: true, lastSyncAt: at(5.5) }, NOW), true)
|
||||
assert.equal(shouldRunStartupSync({ lastSyncOk: true, lastSyncAt: at(20) }, NOW), true)
|
||||
// ein fehlgeschlagener oder nie gelaufener Abruf wird beim Start wiederholt
|
||||
assert.equal(shouldRunStartupSync({ lastSyncOk: false, lastSyncAt: at(1) }, NOW), true)
|
||||
assert.equal(shouldRunStartupSync({ lastSyncAt: at(1) }, NOW), true)
|
||||
assert.equal(shouldRunStartupSync({ lastSyncOk: true }, NOW), true)
|
||||
assert.equal(shouldRunStartupSync({ lastSyncOk: true, lastSyncAt: 'kein Datum' }, NOW), true)
|
||||
assert.equal(shouldRunStartupSync(null, NOW), true)
|
||||
})
|
||||
|
||||
test('syncBank: 401 -> einmalige Mail T18 "abgelaufen oder widerrufen", Stufe 0 gemerkt, checkBankConsent mailt nicht noch einmal (M10)', async () => {
|
||||
await reset()
|
||||
await connect({ warnedThresholds: '30,14' })
|
||||
fake.failNext({ path: /\/balances$/, status: 401, body: { code: 401, error: 'UNAUTHORIZED' } })
|
||||
const result = await syncBank({ now: NOW })
|
||||
assert.equal(result.ok, false)
|
||||
assert.equal(connection().status, 'abgelaufen')
|
||||
assert.equal(mails.sent.length, 1)
|
||||
const [mail] = mails.sent
|
||||
assert.equal(mail.subject, '[Finanzen] Bankzugang Finom ist abgelaufen oder wurde widerrufen')
|
||||
assert.match(mail.html, /Der Zugang ist bereits abgelaufen/, 'Block abgelaufen')
|
||||
assert.match(mail.html, /29\.03\.2027/)
|
||||
assert.ok(mail.html.includes(`${config.womsBaseUrl}/finance/einstellungen`))
|
||||
assert.deepEqual(connection().warnedThresholds.split(','), ['30', '14', '0'])
|
||||
|
||||
// weitere Abrufe (kein API-Aufruf mehr) und die tägliche Prüfung senden nichts
|
||||
await syncBank({ now: NOW })
|
||||
assert.deepEqual(await checkBankConsent({ now: new Date('2027-03-30T09:00:00Z') }), { sent: null })
|
||||
assert.equal(mails.sent.length, 1, 'keine zweite Mail')
|
||||
})
|
||||
|
||||
test('syncBank: 403 meldet ebenso, ein anderer Fehler (500) und ein bereits gemeldeter Ablauf nicht (M10)', async () => {
|
||||
await reset()
|
||||
await connect()
|
||||
fake.failNext({ path: /\/transactions$/, status: 403 })
|
||||
await syncBank({ now: NOW })
|
||||
assert.equal(mails.sent.length, 1)
|
||||
|
||||
await reset()
|
||||
await connect()
|
||||
fake.failNext({ path: /\/balances$/, status: 500 })
|
||||
await syncBank({ now: NOW })
|
||||
assert.equal(connection().status, 'aktiv')
|
||||
assert.equal(mails.sent.length, 0, '500 ist kein Ablauf')
|
||||
|
||||
// am Ablauftag war schon gewarnt (Stufe 0): der 401 danach löst keine zweite Mail aus
|
||||
await reset()
|
||||
await connect({ warnedThresholds: '30,14,3,0' })
|
||||
fake.failNext({ path: /\/balances$/, status: 401 })
|
||||
await syncBank({ now: NOW })
|
||||
assert.equal(connection().status, 'abgelaufen')
|
||||
assert.equal(mails.sent.length, 0)
|
||||
})
|
||||
|
||||
test('syncBank: kommt die Mail zum Ablauf nicht raus, bleibt Stufe 0 offen und der Abruf endet trotzdem sauber (M10)', async () => {
|
||||
await reset()
|
||||
await connect()
|
||||
setMailTransportForTests({ sendMail: async () => Promise.reject(new Error('SMTP kaputt')) })
|
||||
try {
|
||||
fake.failNext({ path: /\/balances$/, status: 401 })
|
||||
const result = await syncBank({ now: NOW })
|
||||
assert.equal(result.error, 'Bankzugang abgelaufen oder widerrufen')
|
||||
assert.equal(connection().status, 'abgelaufen')
|
||||
assert.ok(!String(connection().warnedThresholds || '').split(',').includes('0'))
|
||||
} finally {
|
||||
setMailTransportForTests(mails)
|
||||
}
|
||||
})
|
||||
|
||||
test('syncBank: wirft auch dann nicht, wenn die Verbindung nicht geladen werden kann', async () => {
|
||||
await reset()
|
||||
await connect()
|
||||
|
||||
Reference in new issue
Block a user