import { describe, it, expect } from 'vitest' import { EmailError, mapError, classify } from '../src/map-error.js' describe('classify (vendor error -> reason)', () => { it('429 -> transient', () => { expect(classify({ statusCode: 429 })).toBe('transient') expect(classify({ responseCode: 429 })).toBe('transient') }) it('5xx -> transient', () => { expect(classify({ statusCode: 500 })).toBe('transient') expect(classify({ statusCode: 503 })).toBe('transient') }) it('401/403 -> auth', () => { expect(classify({ statusCode: 401 })).toBe('auth') expect(classify({ statusCode: 403 })).toBe('auth') }) it('SMTP codes', () => { expect(classify({ code: 'EAUTH' })).toBe('auth') expect(classify({ code: 'EENVELOPE' })).toBe('invalidRecipient') expect(classify({ code: 'ETIMEDOUT' })).toBe('transient') expect(classify({ code: 'ECONNECTION' })).toBe('transient') expect(classify({ code: 'ESOCKET' })).toBe('transient') }) it('SES error names', () => { expect(classify({ name: 'Throttling' })).toBe('transient') expect(classify({ name: 'TooManyRequestsException' })).toBe('transient') expect(classify({ name: 'ServiceUnavailable' })).toBe('transient') expect(classify({ name: 'InvalidClientTokenId' })).toBe('auth') expect(classify({ name: 'SignatureDoesNotMatch' })).toBe('auth') expect(classify({ name: 'AccessDenied' })).toBe('auth') }) it('falls back to message heuristics', () => { expect(classify({ message: 'Request timed out' })).toBe('transient') expect(classify({ message: 'rate limit exceeded' })).toBe('transient') expect(classify({ message: 'invalid credential' })).toBe('auth') expect(classify({ message: 'No recipients defined' })).toBe('invalidRecipient') }) it('unknown by default', () => { expect(classify({})).toBe('unknown') expect(classify(null)).toBe('unknown') expect(classify('oops')).toBe('unknown') }) }) describe('mapError', () => { it('passes EmailError through untouched', () => { const e = new EmailError({ provider: 'ses', reason: 'auth', retryable: false }) expect(mapError(e, 'ses')).toBe(e) }) it('wraps vendor error and sets retryable from reason', () => { const wrapped = mapError({ statusCode: 503, message: 'boom' }, 'ses') expect(wrapped).toBeInstanceOf(EmailError) expect(wrapped.provider).toBe('ses') expect(wrapped.reason).toBe('transient') expect(wrapped.retryable).toBe(true) expect(wrapped.cause).toBeTruthy() }) it('auth / invalidRecipient are not retryable', () => { expect(mapError({ code: 'EAUTH' }, 'nodemailer').retryable).toBe(false) expect(mapError({ code: 'EENVELOPE' }, 'nodemailer').retryable).toBe(false) }) it('unknown is not retryable', () => { expect(mapError({}, 'fake').retryable).toBe(false) }) })