* feat(invoicing): opt-in invoice email from the company's own sending domain Companies holding the custom_sender_domain capability grant can register their own domain (Resend sending-only profile), publish DKIM/SPF, and once verified every invoice email (send, reminders, recurring, payment confirmation, MCP/v1 sends) leaves as "<name> <faktura@their-domain>" instead of the platform sender. Reply-To is unchanged. - New table company_sending_domains (RLS: members read, owner/admin write; audit trigger), types, archive-export classification. - New capability key custom_sender_domain: manually granted per company, deliberately outside PAID_CAPABILITIES (never trial-seeded, never written by the Stripe sync). Without the grant the settings section is hidden and nothing changes. - Email extension: sending-domain routes (GET/POST/PATCH/DELETE, verify), Resend domain lifecycle without orphan adoption, domain.updated handling on the delivery webhook, explicit From support in the Resend adapter. - Core resolveInvoiceSender(): verified + enabled + entitled, else the platform sender; never throws. - Settings -> Invoicing: "Avsändare vid fakturautskick" section (sv/en). - Unit tests for the resolver, domain helpers, routes, From header; pg-real test for RLS and constraints. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoicing): harden sending-domain writes, sender fallback, review findings Skeptic refutations: - Tenant JWTs could insert/update company_sending_domains with status = 'verified' and an arbitrary domain through PostgREST (RLS only checked membership), then send invoice mail as that domain. New migration 20260822130000 adds a BEFORE trigger: tenants may only open a pending claim and edit sender_local_part/sender_name/enabled; domain and verification state are service-role only. claim/verify helpers now take a service-role writer for those columns; the route's RLS client still does the insert. - A company domain Resend later rejects made every invoice send fail: the Resend adapter retries once as the platform sender when an explicit company From is rejected (nothing was sent, so no double send). Review findings: - domain.updated webhook: discriminated outcome; DB errors answer 500 so Svix retries, unknown domains are acknowledged. - Display names are RFC 5322-quoted only when they carry specials. - Sender local part is a strict dot-atom (no trailing/consecutive dots), in code and in the CHECK constraint; resend_domain_id index is UNIQUE. - IME composition guard on the claim input; event bus reset in tests; settings section skips its request for non-admins. Deferred (needs a product call): persisting the effective From address in the invoice delivery log touches the hardened evidence triggers; recorded in DECISIONS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoicing): bind sending-domain verification to the claimed domain; fix pg test Skeptic re-check found a TOCTOU: during the claim's Resend round-trip a tenant could delete and re-insert its pending row under the same id with a reserved domain, and the service-role writer updated by id alone. Now: - the claim's verification-state write filters on (id, company_id, domain, resend_domain_id IS NULL) and rolls back on zero rows; - verify and the domain.updated webhook compare Resend's domain name with the row before writing verified; - resolveInvoiceSender refuses reserved platform domains and non-hostnames at send time (reserved-domain logic moved to lib/email/domain-name.ts and shared with the claim validator). pg-real: the case-insensitive uniqueness assertion now expects the domain_shape CHECK (lowercase enforced) for an uppercase variant and the unique index for a same-case duplicate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
428 lines
18 KiB
TypeScript
428 lines
18 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import {
|
|
validateClaimableSendingDomain,
|
|
mapResendSendingStatus,
|
|
isSendingOnlyProfile,
|
|
claimSendingDomain,
|
|
checkSendingDomainVerification,
|
|
updateSendingDomainSettings,
|
|
removeSendingDomain,
|
|
applySendingDomainStatusFromWebhook,
|
|
} from '@/extensions/general/email/lib/sending-domains'
|
|
import { createQueuedMockSupabase } from '@/tests/helpers'
|
|
import type { SupabaseClient } from '@supabase/supabase-js'
|
|
|
|
const { domainsMock } = vi.hoisted(() => ({
|
|
domainsMock: {
|
|
create: vi.fn(),
|
|
get: vi.fn(),
|
|
verify: vi.fn(),
|
|
remove: vi.fn(),
|
|
list: vi.fn(),
|
|
},
|
|
}))
|
|
|
|
vi.mock('resend', () => ({
|
|
Resend: class {
|
|
domains = domainsMock
|
|
},
|
|
}))
|
|
|
|
const DKIM_RECORD = {
|
|
record: 'DKIM',
|
|
name: 'resend._domainkey.hansbolag.example',
|
|
value: 'p=MIGf...',
|
|
type: 'TXT',
|
|
ttl: 'Auto',
|
|
status: 'not_started',
|
|
}
|
|
|
|
const SENDING_ONLY = { sending: 'enabled', receiving: 'disabled' }
|
|
|
|
const ROW = {
|
|
id: 'row-1',
|
|
company_id: 'company-1',
|
|
domain: 'hansbolag.example',
|
|
status: 'pending',
|
|
sender_local_part: 'faktura',
|
|
sender_name: null,
|
|
enabled: true,
|
|
resend_domain_id: 'rd_1',
|
|
dns_records: [DKIM_RECORD],
|
|
verified_at: null,
|
|
last_checked_at: null,
|
|
created_at: '2026-08-22T00:00:00Z',
|
|
updated_at: '2026-08-22T00:00:00Z',
|
|
}
|
|
|
|
function sb(): ReturnType<typeof createQueuedMockSupabase> & { client: SupabaseClient } {
|
|
const m = createQueuedMockSupabase()
|
|
return Object.assign(m, { client: m.supabase as unknown as SupabaseClient })
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
process.env.RESEND_API_KEY = 'test-key'
|
|
process.env.RESEND_FROM_EMAIL = 'noreply@platform.example'
|
|
process.env.RESEND_INBOUND_DOMAIN = 'inbox.platform.example'
|
|
process.env.NEXT_PUBLIC_APP_URL = 'https://app.platform.example'
|
|
})
|
|
afterEach(() => {
|
|
delete process.env.RESEND_INBOUND_DOMAIN
|
|
})
|
|
|
|
describe('validateClaimableSendingDomain', () => {
|
|
it('blocks public mailbox providers', () => {
|
|
expect(validateClaimableSendingDomain('gmail.com')).toMatch(/Publika/)
|
|
})
|
|
|
|
it("blocks the platform's own sender domain, the inbound domain and the app host (and subdomains)", () => {
|
|
expect(validateClaimableSendingDomain('platform.example')).toMatch(/reserverad/)
|
|
expect(validateClaimableSendingDomain('mail.platform.example')).toMatch(/reserverad/)
|
|
expect(validateClaimableSendingDomain('inbox.platform.example')).toMatch(/reserverad/)
|
|
expect(validateClaimableSendingDomain('app.platform.example')).toMatch(/reserverad/)
|
|
})
|
|
|
|
it('allows an ordinary company domain', () => {
|
|
expect(validateClaimableSendingDomain('hansbolag.example')).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('mapResendSendingStatus', () => {
|
|
it('maps verified and temporary_failure to verified, failures to failed, the rest to pending', () => {
|
|
expect(mapResendSendingStatus('verified')).toBe('verified')
|
|
expect(mapResendSendingStatus('temporary_failure')).toBe('verified')
|
|
expect(mapResendSendingStatus('failed')).toBe('failed')
|
|
expect(mapResendSendingStatus('partially_failed')).toBe('failed')
|
|
expect(mapResendSendingStatus('pending')).toBe('pending')
|
|
expect(mapResendSendingStatus('not_started')).toBe('pending')
|
|
expect(mapResendSendingStatus('partially_verified')).toBe('pending')
|
|
})
|
|
})
|
|
|
|
describe('isSendingOnlyProfile', () => {
|
|
it('accepts sending-only and rejects receiving or unknown', () => {
|
|
expect(isSendingOnlyProfile(SENDING_ONLY)).toBe(true)
|
|
expect(isSendingOnlyProfile({ sending: 'enabled', receiving: 'enabled' })).toBe(false)
|
|
expect(isSendingOnlyProfile({ sending: 'disabled', receiving: 'enabled' })).toBe(false)
|
|
expect(isSendingOnlyProfile(null)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('claimSendingDomain', () => {
|
|
it('rejects an invalid domain without touching the DB or Resend', async () => {
|
|
const { client, calls } = sb()
|
|
const result = await claimSendingDomain(client, client, 'company-1', 'nodots')
|
|
expect(result).toEqual({ ok: false, status: 400, error: expect.stringMatching(/Ogiltig/) })
|
|
expect(calls).toHaveLength(0)
|
|
expect(domainsMock.create).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects a reserved domain', async () => {
|
|
const { client } = sb()
|
|
const result = await claimSendingDomain(client, client, 'company-1', 'platform.example')
|
|
expect(result.ok).toBe(false)
|
|
expect(domainsMock.create).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('inserts the row, registers a sending-only domain in Resend, and stores the DNS records', async () => {
|
|
const { client, enqueue, findCall } = sb()
|
|
enqueue({ data: { ...ROW, resend_domain_id: null, dns_records: null } }) // insert
|
|
enqueue({ data: ROW }) // update
|
|
domainsMock.create.mockResolvedValue({ data: { id: 'rd_1' }, error: null })
|
|
domainsMock.get.mockResolvedValue({
|
|
data: { id: 'rd_1', status: 'not_started', records: [DKIM_RECORD], capabilities: SENDING_ONLY },
|
|
error: null,
|
|
})
|
|
|
|
const result = await claimSendingDomain(client, client, 'company-1', 'HansBolag.example')
|
|
expect(result.ok).toBe(true)
|
|
expect(domainsMock.create).toHaveBeenCalledWith({
|
|
name: 'hansbolag.example',
|
|
region: 'eu-west-1',
|
|
capabilities: { sending: 'enabled', receiving: 'disabled' },
|
|
})
|
|
const insertArgs = findCall('company_sending_domains', 'insert')
|
|
expect(insertArgs?.[0]).toEqual({ company_id: 'company-1', domain: 'hansbolag.example', status: 'pending' })
|
|
const updateArgs = findCall('company_sending_domains', 'update')
|
|
expect(updateArgs?.[0]).toMatchObject({ resend_domain_id: 'rd_1', dns_records: [DKIM_RECORD], status: 'pending' })
|
|
})
|
|
|
|
it('writes the verification state through the service-role writer, not the tenant client', async () => {
|
|
const tenant = sb()
|
|
const writer = sb()
|
|
tenant.enqueue({ data: { ...ROW, resend_domain_id: null, dns_records: null } }) // insert (RLS client)
|
|
writer.enqueue({ data: ROW }) // update (service role)
|
|
domainsMock.create.mockResolvedValue({ data: { id: 'rd_1' }, error: null })
|
|
domainsMock.get.mockResolvedValue({
|
|
data: { id: 'rd_1', status: 'not_started', records: [DKIM_RECORD], capabilities: SENDING_ONLY },
|
|
error: null,
|
|
})
|
|
|
|
const result = await claimSendingDomain(tenant.client, writer.client, 'company-1', 'hansbolag.example')
|
|
expect(result.ok).toBe(true)
|
|
expect(tenant.findCall('company_sending_domains', 'update')).toBeUndefined()
|
|
expect(writer.findCall('company_sending_domains', 'update')?.[0]).toMatchObject({ resend_domain_id: 'rd_1' })
|
|
// Still scoped to the company on the service-role path, and bound only to
|
|
// the row that still carries the registered domain and no Resend id (a
|
|
// concurrent tenant delete + re-insert under the same id matches nothing).
|
|
expect(writer.findCalls('company_sending_domains', 'eq')).toEqual(
|
|
expect.arrayContaining([
|
|
['id', 'row-1'],
|
|
['company_id', 'company-1'],
|
|
['domain', 'hansbolag.example'],
|
|
]),
|
|
)
|
|
expect(writer.findCall('company_sending_domains', 'is')).toEqual(['resend_domain_id', null])
|
|
})
|
|
|
|
it('maps a unique-violation on company_id to a 409 about the existing domain', async () => {
|
|
const { client, enqueue } = sb()
|
|
enqueue({ data: null, error: { code: '23505', message: 'duplicate key idx_company_sending_domains_company' } })
|
|
const result = await claimSendingDomain(client, client, 'company-1', 'hansbolag.example')
|
|
expect(result).toEqual({ ok: false, status: 409, error: expect.stringMatching(/redan en avsändardomän/) })
|
|
})
|
|
|
|
it('never adopts an existing Resend domain: "already exists" rolls back and returns 409', async () => {
|
|
const { client, enqueue, findCalls } = sb()
|
|
enqueue({ data: { ...ROW, resend_domain_id: null } }) // insert
|
|
enqueue({ data: null }) // rollback delete
|
|
domainsMock.create.mockResolvedValue({ data: null, error: { message: 'Domain already exists' } })
|
|
|
|
const result = await claimSendingDomain(client, client, 'company-1', 'hansbolag.example')
|
|
expect(result).toEqual({ ok: false, status: 409, error: expect.stringMatching(/finns redan/) })
|
|
expect(domainsMock.list).not.toHaveBeenCalled()
|
|
expect(findCalls('company_sending_domains', 'delete')).toHaveLength(1)
|
|
})
|
|
|
|
it('removes the Resend domain it just created when persisting the DNS records fails', async () => {
|
|
const { client, enqueue } = sb()
|
|
enqueue({ data: { ...ROW, resend_domain_id: null } }) // insert
|
|
enqueue({ data: null, error: { message: 'db down' } }) // update fails
|
|
enqueue({ data: null }) // rollback delete
|
|
domainsMock.create.mockResolvedValue({ data: { id: 'rd_1' }, error: null })
|
|
domainsMock.get.mockResolvedValue({ data: { id: 'rd_1', status: 'pending', records: [], capabilities: SENDING_ONLY }, error: null })
|
|
domainsMock.remove.mockResolvedValue({ data: null, error: null })
|
|
|
|
const result = await claimSendingDomain(client, client, 'company-1', 'hansbolag.example')
|
|
expect(result.ok).toBe(false)
|
|
expect(domainsMock.remove).toHaveBeenCalledWith('rd_1')
|
|
})
|
|
})
|
|
|
|
describe('checkSendingDomainVerification', () => {
|
|
it('404s without a row', async () => {
|
|
const { client, enqueue } = sb()
|
|
enqueue({ data: null })
|
|
const result = await checkSendingDomainVerification(client, client, 'company-1')
|
|
expect(result).toMatchObject({ ok: false, status: 404 })
|
|
})
|
|
|
|
it('verifies, then persists verified + verified_at', async () => {
|
|
const { client, enqueue, findCall } = sb()
|
|
enqueue({ data: ROW })
|
|
enqueue({ data: { ...ROW, status: 'verified' } })
|
|
domainsMock.verify.mockResolvedValue({ data: {}, error: null })
|
|
domainsMock.get.mockResolvedValue({
|
|
data: {
|
|
id: 'rd_1',
|
|
name: 'hansbolag.example',
|
|
status: 'verified',
|
|
records: [{ ...DKIM_RECORD, status: 'verified' }],
|
|
capabilities: SENDING_ONLY,
|
|
},
|
|
error: null,
|
|
})
|
|
|
|
const result = await checkSendingDomainVerification(client, client, 'company-1')
|
|
expect(result.ok).toBe(true)
|
|
expect(domainsMock.verify).toHaveBeenCalledWith('rd_1')
|
|
const updateArgs = findCall('company_sending_domains', 'update')?.[0] as Record<string, unknown>
|
|
expect(updateArgs.status).toBe('verified')
|
|
expect(typeof updateArgs.verified_at).toBe('string')
|
|
})
|
|
|
|
it('refuses to flip when Resend reports a different domain name than the row (swapped row)', async () => {
|
|
const { client, enqueue, findCall } = sb()
|
|
enqueue({ data: { ...ROW, domain: 'platform.example' } })
|
|
domainsMock.verify.mockResolvedValue({ data: {}, error: null })
|
|
domainsMock.get.mockResolvedValue({
|
|
data: { id: 'rd_1', name: 'hansbolag.example', status: 'verified', records: [], capabilities: SENDING_ONLY },
|
|
error: null,
|
|
})
|
|
const result = await checkSendingDomainVerification(client, client, 'company-1')
|
|
expect(result).toMatchObject({ ok: false, status: 409 })
|
|
expect(findCall('company_sending_domains', 'update')).toBeUndefined()
|
|
})
|
|
|
|
it('refuses to flip a domain without the sending capability', async () => {
|
|
const { client, enqueue } = sb()
|
|
enqueue({ data: ROW })
|
|
domainsMock.verify.mockResolvedValue({ data: {}, error: null })
|
|
domainsMock.get.mockResolvedValue({
|
|
data: { id: 'rd_1', status: 'verified', records: [], capabilities: { sending: 'disabled', receiving: 'enabled' } },
|
|
error: null,
|
|
})
|
|
const result = await checkSendingDomainVerification(client, client, 'company-1')
|
|
expect(result).toMatchObject({ ok: false, status: 409 })
|
|
})
|
|
})
|
|
|
|
describe('updateSendingDomainSettings', () => {
|
|
it('normalizes the local part, strips header characters from the name, and toggles enabled', async () => {
|
|
const { client, enqueue, findCall } = sb()
|
|
enqueue({ data: ROW })
|
|
enqueue({ data: { ...ROW, sender_local_part: 'ekonomi', sender_name: 'Hans Bolag', enabled: false } })
|
|
const result = await updateSendingDomainSettings(client, 'company-1', {
|
|
sender_local_part: 'Ekonomi',
|
|
sender_name: 'Hans <Bolag>\r\n',
|
|
enabled: false,
|
|
})
|
|
expect(result.ok).toBe(true)
|
|
expect(findCall('company_sending_domains', 'update')?.[0]).toEqual({
|
|
sender_local_part: 'ekonomi',
|
|
sender_name: 'Hans Bolag',
|
|
enabled: false,
|
|
})
|
|
})
|
|
|
|
it('rejects an invalid local part with 400', async () => {
|
|
const { client, enqueue } = sb()
|
|
enqueue({ data: ROW })
|
|
const result = await updateSendingDomainSettings(client, 'company-1', { sender_local_part: 'no spaces' })
|
|
expect(result).toMatchObject({ ok: false, status: 400 })
|
|
})
|
|
|
|
it('clears the sender name with null', async () => {
|
|
const { client, enqueue, findCall } = sb()
|
|
enqueue({ data: { ...ROW, sender_name: 'Old' } })
|
|
enqueue({ data: ROW })
|
|
const result = await updateSendingDomainSettings(client, 'company-1', { sender_name: null })
|
|
expect(result.ok).toBe(true)
|
|
expect(findCall('company_sending_domains', 'update')?.[0]).toEqual({ sender_name: null })
|
|
})
|
|
})
|
|
|
|
describe('removeSendingDomain', () => {
|
|
it('deletes a sending-only Resend domain, then the row', async () => {
|
|
const { client, enqueue, findCalls } = sb()
|
|
enqueue({ data: ROW })
|
|
enqueue({ data: null }) // delete
|
|
domainsMock.get.mockResolvedValue({
|
|
data: { id: 'rd_1', name: 'hansbolag.example', capabilities: SENDING_ONLY },
|
|
error: null,
|
|
})
|
|
domainsMock.remove.mockResolvedValue({ data: null, error: null })
|
|
|
|
const result = await removeSendingDomain(client, 'company-1')
|
|
expect(result).toEqual({ ok: true, data: { removed: true } })
|
|
expect(domainsMock.remove).toHaveBeenCalledWith('rd_1')
|
|
expect(findCalls('company_sending_domains', 'delete')).toHaveLength(1)
|
|
})
|
|
|
|
it("never deletes the platform's own sender domain from Resend, even when a row points at it", async () => {
|
|
const { client, enqueue } = sb()
|
|
enqueue({ data: { ...ROW, domain: 'platform.example', resend_domain_id: 'rd_platform' } })
|
|
enqueue({ data: null }) // delete row
|
|
domainsMock.get.mockResolvedValue({
|
|
data: { id: 'rd_platform', name: 'platform.example', capabilities: SENDING_ONLY },
|
|
error: null,
|
|
})
|
|
const result = await removeSendingDomain(client, 'company-1')
|
|
expect(result.ok).toBe(true)
|
|
expect(domainsMock.remove).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('leaves a receiving-capable Resend domain alone', async () => {
|
|
const { client, enqueue } = sb()
|
|
enqueue({ data: ROW })
|
|
enqueue({ data: null })
|
|
domainsMock.get.mockResolvedValue({
|
|
data: { id: 'rd_1', name: 'hansbolag.example', capabilities: { sending: 'enabled', receiving: 'enabled' } },
|
|
error: null,
|
|
})
|
|
const result = await removeSendingDomain(client, 'company-1')
|
|
expect(result.ok).toBe(true)
|
|
expect(domainsMock.remove).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('applySendingDomainStatusFromWebhook', () => {
|
|
it('returns no_match for an unknown Resend domain id', async () => {
|
|
const { client, enqueue } = sb()
|
|
enqueue({ data: null })
|
|
await expect(applySendingDomainStatusFromWebhook(client, { id: 'rd_other', status: 'verified' })).resolves.toBe(
|
|
'no_match',
|
|
)
|
|
expect(domainsMock.get).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('returns error (so the webhook is retried) when the lookup or the update fails', async () => {
|
|
const lookupFail = sb()
|
|
lookupFail.enqueue({ data: null, error: { message: 'db down' } })
|
|
await expect(
|
|
applySendingDomainStatusFromWebhook(lookupFail.client, { id: 'rd_1', status: 'failed' }),
|
|
).resolves.toBe('error')
|
|
|
|
const updateFail = sb()
|
|
updateFail.enqueue({ data: { id: 'row-1', verified_at: null } })
|
|
updateFail.enqueue({ data: null, error: { message: 'db down' } })
|
|
await expect(
|
|
applySendingDomainStatusFromWebhook(updateFail.client, { id: 'rd_1', status: 'failed' }),
|
|
).resolves.toBe('error')
|
|
})
|
|
|
|
it('keeps the stored status when Resend names a different domain than the row (swapped row)', async () => {
|
|
const { client, enqueue, findCall } = sb()
|
|
enqueue({ data: { id: 'row-1', domain: 'platform.example', verified_at: null } })
|
|
enqueue({ data: null }) // update (records/last_checked only)
|
|
domainsMock.get.mockResolvedValue({
|
|
data: { id: 'rd_1', name: 'hansbolag.example', capabilities: SENDING_ONLY },
|
|
error: null,
|
|
})
|
|
const ok = await applySendingDomainStatusFromWebhook(client, { id: 'rd_1', status: 'verified' })
|
|
expect(ok).toBe('applied')
|
|
const update = findCall('company_sending_domains', 'update')?.[0] as Record<string, unknown>
|
|
expect(update.status).toBeUndefined()
|
|
})
|
|
|
|
it('flips to verified only after confirming the sending capability with Resend', async () => {
|
|
const { client, enqueue, findCall } = sb()
|
|
enqueue({ data: { id: 'row-1', domain: 'hansbolag.example', verified_at: null } })
|
|
enqueue({ data: null }) // update
|
|
domainsMock.get.mockResolvedValue({
|
|
data: { id: 'rd_1', name: 'hansbolag.example', capabilities: SENDING_ONLY },
|
|
error: null,
|
|
})
|
|
const ok = await applySendingDomainStatusFromWebhook(client, { id: 'rd_1', status: 'verified', records: [DKIM_RECORD] })
|
|
expect(ok).toBe('applied')
|
|
const update = findCall('company_sending_domains', 'update')?.[0] as Record<string, unknown>
|
|
expect(update.status).toBe('verified')
|
|
expect(update.dns_records).toEqual([DKIM_RECORD])
|
|
expect(typeof update.verified_at).toBe('string')
|
|
})
|
|
|
|
it('keeps the stored status when Resend cannot confirm sending', async () => {
|
|
const { client, enqueue, findCall } = sb()
|
|
enqueue({ data: { id: 'row-1', verified_at: null } })
|
|
enqueue({ data: null })
|
|
domainsMock.get.mockResolvedValue({ data: null, error: { message: 'nope' } })
|
|
const ok = await applySendingDomainStatusFromWebhook(client, { id: 'rd_1', status: 'verified' })
|
|
expect(ok).toBe('applied')
|
|
const update = findCall('company_sending_domains', 'update')?.[0] as Record<string, unknown>
|
|
expect(update.status).toBeUndefined()
|
|
})
|
|
|
|
it('records a failed status without calling Resend', async () => {
|
|
const { client, enqueue, findCall } = sb()
|
|
enqueue({ data: { id: 'row-1', verified_at: '2026-08-01T00:00:00Z' } })
|
|
enqueue({ data: null })
|
|
const ok = await applySendingDomainStatusFromWebhook(client, { id: 'rd_1', status: 'failed' })
|
|
expect(ok).toBe('applied')
|
|
expect(domainsMock.get).not.toHaveBeenCalled()
|
|
const update = findCall('company_sending_domains', 'update')?.[0] as Record<string, unknown>
|
|
expect(update.status).toBe('failed')
|
|
expect(update.verified_at).toBe('2026-08-01T00:00:00Z')
|
|
})
|
|
})
|