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.
50 lines
2.4 KiB
TypeScript
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@tecnostac.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 })
|
|
})
|
|
})
|