map-error.test.js
2.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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)
})
})