import { test, after, beforeEach } 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' import { mailSink } from './support/mailSink.js' // Nachbauten starten und die Umgebung setzen, BEVOR config.js geladen wird (Muster wie financeBankSync.test.js) const aw = await startAppwriteMock({ schema: { bankConnections: schemaKeys('bankConnections'), bankTransactions: schemaKeys('bankTransactions') }, }) const st = await startFakeStripe() useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) const { config } = await import('../server/config.js') const { 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 { setMailTransportForTests } = await import('../server/services/mailer.js') const mails = mailSink() setMailTransportForTests(mails) after(async () => { setMailTransportForTests(null) await Promise.all([aw.close(), st.close()]) }) // "Jetzt" der Pruefung: 01.10.2026 11:00 Berliner Zeit const NOW = new Date('2026-10-01T09:00:00Z') const DAY_MS = 86_400_000 const SESSION = 'sess-geheim-4711' const ACCOUNT = 'konto-uid-4711' const connection = () => aw.docs(config.collections.bankConnections).get('finom') /** validUntil, das von NOW aus gesehen in tage Tagen (zur selben Uhrzeit) liegt. */ const inDays = (tage, from = NOW) => new Date(from.getTime() + tage * DAY_MS).toISOString() const connect = (patch = {}) => store.saveConnection({ status: 'aktiv', sessionId: SESSION, accountUid: ACCOUNT, warnedThresholds: '', ...patch }) beforeEach(async () => { await deleteDocument(config.collections.bankConnections, 'finom').catch(() => {}) mails.sent.length = 0 setMailTransportForTests(mails) }) test('29 Tage: Warnung bei 30 Tagen, am selben Tag nicht noch einmal', async () => { await connect({ validUntil: inDays(29) }) assert.deepEqual(await checkBankConsent({ now: NOW }), { sent: 30 }) assert.equal(mails.sent.length, 1) const [mail] = mails.sent assert.equal(mail.subject, '[Finanzen] Bankzugang Finom läuft in 29 Tagen ab') assert.match(mail.html, /30\.10\.2026/) assert.ok(mail.html.includes(`${config.womsBaseUrl}/finance/einstellungen`), 'Link zu WOMS Finanzen Einstellungen') assert.doesNotMatch(mail.html, /Der Zugang ist bereits abgelaufen/) assert.equal(connection().warnedThresholds, '30') assert.deepEqual(await checkBankConsent({ now: NOW }), { sent: null }) assert.deepEqual(await checkBankConsent({ now: new Date(NOW.getTime() + 3 * 3_600_000) }), { sent: null }) assert.equal(mails.sent.length, 1, 'keine zweite Mail') }) test('der Beispielwert 29.03.2027 steht als gueltig_bis in der Mail', async () => { await connect({ validUntil: '2027-03-29T00:00:00.000Z' }) assert.deepEqual(await checkBankConsent({ now: new Date('2027-03-10T09:00:00Z') }), { sent: 30 }) assert.match(mails.sent[0].html, /29\.03\.2027/) assert.match(mails.sent[0].text, /29\.03\.2027/) }) test('13 Tage: Schwelle 14 (die darueberliegende 30 gilt damit als erledigt)', async () => { await connect({ validUntil: inDays(13) }) assert.deepEqual(await checkBankConsent({ now: NOW }), { sent: 14 }) assert.equal(mails.sent[0].subject, '[Finanzen] Bankzugang Finom läuft in 13 Tagen ab') assert.equal(connection().warnedThresholds, '30,14') // am naechsten Tag weder 14 noch 30 assert.deepEqual(await checkBankConsent({ now: new Date(NOW.getTime() + DAY_MS) }), { sent: null }) assert.equal(mails.sent.length, 1) }) test('mehr als 30 Tage: keine Mail', async () => { await connect({ validUntil: inDays(31) }) assert.deepEqual(await checkBankConsent({ now: NOW }), { sent: null }) assert.equal(mails.sent.length, 0) assert.equal(connection().warnedThresholds, '') }) test('Schwellen nacheinander: 30, 14, 3, am Ablauftag 0, jeweils einmal', async () => { // Ablauf am 10.11. um 14:00 Berliner Zeit: am Ablauftag um 11:00 gilt der Zugang noch await connect({ validUntil: new Date(NOW.getTime() + 40 * DAY_MS + 3 * 3_600_000).toISOString() }) const sentAt = [] for (let day = 0; day <= 41; day += 1) { const now = new Date(NOW.getTime() + day * DAY_MS) const { sent } = await checkBankConsent({ now }) if (sent !== null) sentAt.push([40 - day, sent]) } // [verbleibende Tage, Schwelle]; 41 = der Tag nach dem Ablauf: 0 war schon gemeldet assert.deepEqual(sentAt, [[30, 30], [14, 14], [3, 3], [0, 0]]) assert.deepEqual( mails.sent.map((m) => m.subject), [ '[Finanzen] Bankzugang Finom läuft in 30 Tagen ab', '[Finanzen] Bankzugang Finom läuft in 14 Tagen ab', '[Finanzen] Bankzugang Finom läuft in 3 Tagen ab', '[Finanzen] Bankzugang Finom läuft heute ab', ] ) assert.equal(connection().warnedThresholds, '30,14,3,0') }) test('Betreff: 2 Tage, morgen, heute (noch gueltig) und abgelaufen', async () => { const cases = [ [2, 3, 'läuft in 2 Tagen ab'], [1, 3, 'läuft morgen ab'], [0, 0, 'läuft heute ab'], ] for (const [tage, threshold, text] of cases) { await deleteDocument(config.collections.bankConnections, 'finom').catch(() => {}) mails.sent.length = 0 // gleicher Berliner Tag wie NOW, aber noch in der Zukunft (13:00 Berliner Zeit) const validUntil = tage === 0 ? '2026-10-01T11:00:00.000Z' : inDays(tage) await connect({ validUntil }) assert.deepEqual(await checkBankConsent({ now: NOW }), { sent: threshold }, text) assert.equal(mails.sent[0].subject, `[Finanzen] Bankzugang Finom ${text}`) assert.doesNotMatch(mails.sent[0].html, /Der Zugang ist bereits abgelaufen/, text) } }) test('abgelaufen: Schwelle 0, Betreff "ist abgelaufen", Block abgelaufen, nur einmal', async () => { await connect({ validUntil: inDays(-3) }) assert.deepEqual(await checkBankConsent({ now: NOW }), { sent: 0 }) const [mail] = mails.sent assert.equal(mail.subject, '[Finanzen] Bankzugang Finom ist abgelaufen') assert.match(mail.html, /Der Zugang ist bereits abgelaufen/) assert.match(mail.html, /28\.09\.2026/) assert.equal(connection().warnedThresholds, '30,14,3,0') assert.deepEqual(await checkBankConsent({ now: NOW }), { sent: null }) assert.equal(mails.sent.length, 1) }) test('Ablaufzeit heute schon vorbei: abgelaufen, nicht "heute"', async () => { await connect({ validUntil: '2026-10-01T06:00:00.000Z' }) assert.deepEqual(await checkBankConsent({ now: NOW }), { sent: 0 }) assert.equal(mails.sent[0].subject, '[Finanzen] Bankzugang Finom ist abgelaufen') }) test('der Berliner Kalendertag zaehlt (Ablauf kurz nach Mitternacht, spaeter Abend davor)', async () => { // 23:30 Berliner Zeit am 01.10.; Ablauf 02.10. 00:30 Berliner Zeit -> morgen await connect({ validUntil: '2026-10-01T22:30:00.000Z' }) assert.deepEqual(await checkBankConsent({ now: new Date('2026-10-01T21:30:00Z') }), { sent: 3 }) assert.equal(mails.sent[0].subject, '[Finanzen] Bankzugang Finom läuft morgen ab') assert.match(mails.sent[0].html, /02\.10\.2026/, 'gueltig_bis ist der Berliner Tag') }) test('keine Verbindung, kein oder kaputtes validUntil: nichts zu tun', async () => { assert.deepEqual(await checkBankConsent({ now: NOW }), { sent: null }) await connect({ validUntil: '' }) assert.deepEqual(await checkBankConsent({ now: NOW }), { sent: null }) await connect({ validUntil: 'morgen' }) assert.deepEqual(await checkBankConsent({ now: NOW }), { sent: null }) assert.equal(mails.sent.length, 0) }) test('Versand scheitert: Schwelle bleibt offen und wird beim naechsten Lauf nachgeholt', async () => { await connect({ validUntil: inDays(29) }) setMailTransportForTests({ async sendMail() { throw new Error('SMTP down') }, }) const originalError = console.error console.error = () => {} try { assert.deepEqual(await checkBankConsent({ now: NOW }), { sent: null }) } finally { console.error = originalError } assert.equal(connection().warnedThresholds, '') setMailTransportForTests(mails) assert.deepEqual(await checkBankConsent({ now: NOW }), { sent: 30 }) assert.equal(mails.sent.length, 1) }) test('ohne Mailversand (SMTP nicht eingerichtet) bleibt die Schwelle offen', async () => { await connect({ validUntil: inDays(29) }) setMailTransportForTests(null) // useTestEnv leert SMTP_HOST: sendAdminNotice meldet "nicht konfiguriert" assert.deepEqual(await checkBankConsent({ now: NOW }), { sent: null }) assert.equal(connection().warnedThresholds, '') }) test('keine Sitzungs-ID, kein Kontoschluessel in der Mail', async () => { await connect({ validUntil: inDays(2) }) await checkBankConsent({ now: NOW }) const [mail] = mails.sent for (const secret of [SESSION, ACCOUNT]) { assert.ok(!mail.html.includes(secret) && !mail.text.includes(secret) && !mail.subject.includes(secret)) } }) test('Mail an das Team (chatNotify) mit Text- und HTML-Fassung', async () => { await connect({ validUntil: inDays(2) }) await checkBankConsent({ now: NOW }) const [mail] = mails.sent assert.deepEqual(mail.to.map((t) => t.address).sort(), [...config.chatNotify.emails].sort()) assert.match(mail.text, /Neu verbinden/) assert.match(mail.text, /Kontostand und Umsätze/) assert.match(mail.html, /Neu verbinden/) }) test('Neuverbindung loescht die Warnstufen: die naechste Frist warnt wieder bei 30', async () => { await connect({ validUntil: inDays(2), warnedThresholds: '30,14,3' }) assert.deepEqual(await checkBankConsent({ now: NOW }), { sent: null }) // Erneuerung wie in completeBankAuth: neue Frist, Warnstufen leer await store.saveConnection({ validUntil: inDays(179), warnedThresholds: '' }) assert.deepEqual(await checkBankConsent({ now: NOW }), { sent: null }, 'noch weit weg') assert.deepEqual(await checkBankConsent({ now: new Date(NOW.getTime() + 150 * DAY_MS) }), { sent: 30 }) }) test('Frist zwischen Pruefung und Speichern erneuert: die neuen Warnstufen bleiben leer', async () => { await connect({ validUntil: inDays(29) }) // Der Versand dauert; waehrenddessen wird der Zugang erneuert (neue Frist, Warnstufen geleert) setMailTransportForTests({ async sendMail(mail) { await store.saveConnection({ validUntil: inDays(179), warnedThresholds: '' }) return mails.sendMail(mail) }, }) assert.deepEqual(await checkBankConsent({ now: NOW }), { sent: 30 }) assert.equal(connection().warnedThresholds, '', 'die Erneuerung wurde nicht mit der alten Schwelle ueberschrieben') })