You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/apps/hq/test/bounces.test.ts

50 lines
2.4 KiB
TypeScript

// 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:'); await seedIfEmpty(db)
await saveAccount(db, 'us@sims.com', encrypt('rt', KEY))
await 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(await db.get(`SELECT bounced FROM email_log WHERE to_addr='ravi@acme.in'`)).toMatchObject({ bounced: 1 })
expect(await 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(await listReminders(db, { ruleKind: 'email_bounced' })).toHaveLength(1)
})
it('no-ops cleanly when Gmail is not connected', async () => {
const db = openDb(':memory:'); await 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 })
})
})