map-error.test.js 2.78 KB
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)
  })
})