// apps/hq/test/bounces.test.ts import { describe, it, expect } from 'vitest' import { openDb } from '../src/db' import { seedIfEmpty } from '../src/seed' import { saveAccount, logEmail } from '../src/repos-email' import { encrypt } from '../src/crypto' import { listReminders } from '../src/repos-reminders' import { pollBounces, type BounceDeps } from '../src/bounces' const KEY = '11'.repeat(32) function bounceFetch(recipient: string): typeof fetch { return (async (url: string) => { const u = String(url) if (u.includes('/token')) return new Response(JSON.stringify({ access_token: 'at' }), { status: 200 }) if (u.includes('/messages/b1')) { return new Response(JSON.stringify({ snippet: `Delivery to ${recipient} failed permanently`, payload: { headers: [{ name: 'X-Failed-Recipients', value: recipient }] }, }), { status: 200 }) } return new Response(JSON.stringify({ messages: [{ id: 'b1' }] }), { status: 200 }) // list }) as typeof fetch } describe('pollBounces', () => { it('flips the sent email_log to bounced and raises an email_bounced reminder, idempotently', async () => { const db = openDb(':memory:'); seedIfEmpty(db) saveAccount(db, 'us@tecnostac.com', encrypt('rt', KEY)) logEmail(db, { to: 'ravi@acme.in', subject: 'INV/26-27-0001', status: 'sent', gmailMessageId: 'g1' }) const deps: BounceDeps = { gmail: { f: bounceFetch('ravi@acme.in'), clientId: 'cid', clientSecret: 'sec', keyHex: KEY }, now: () => '2026-07-10T00:00:00Z', } const out = await pollBounces(db, deps) expect(out).toEqual({ scanned: 1, bounced: 1 }) expect(db.prepare(`SELECT bounced FROM email_log WHERE to_addr='ravi@acme.in'`).get()).toMatchObject({ bounced: 1 }) expect(listReminders(db, { ruleKind: 'email_bounced' })).toHaveLength(1) // Re-poll: the row is already bounced=0→1, so nothing new flips and no duplicate reminder. const again = await pollBounces(db, deps) expect(again.bounced).toBe(0) expect(listReminders(db, { ruleKind: 'email_bounced' })).toHaveLength(1) }) it('no-ops cleanly when Gmail is not connected', async () => { const db = openDb(':memory:'); seedIfEmpty(db) const deps: BounceDeps = { gmail: { f: bounceFetch('x@y.z'), clientId: '', clientSecret: '', keyHex: KEY }, now: () => '2026-07-10T00:00:00Z' } expect(await pollBounces(db, deps)).toEqual({ scanned: 0, bounced: 0 }) }) })