@bernierllc/email-domain-verification 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/__tests__/coverage-gaps.test.ts +566 -0
- package/jest.config.cjs +4 -4
- package/package.json +23 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
## 1.0.6 (2025-12-25)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **ci:** add lerna version step before publish ([3d20300](https://github.com/bernierllc/tools/commit/3d203002143bf353fffafe4f8a78a99009567347))
|
|
12
|
+
* **ci:** fix release workflow + convert internal deps to workspace:* ([01c078b](https://github.com/bernierllc/tools/commit/01c078b49d6025f7eef750f79207a1c71c8d85dc))
|
|
13
|
+
* update neverhub-adapter deps to workspace:* and publish 0.1.2 ([f0e3d04](https://github.com/bernierllc/tools/commit/f0e3d04d8d4f094e3bb899ddf81e93243d16e2c2))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## 1.0.5 (2025-12-25)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* **ci:** fix release workflow + convert internal deps to workspace:* ([01c078b](https://github.com/bernierllc/tools/commit/01c078b49d6025f7eef750f79207a1c71c8d85dc))
|
|
25
|
+
* update neverhub-adapter deps to workspace:* and publish 0.1.2 ([f0e3d04](https://github.com/bernierllc/tools/commit/f0e3d04d8d4f094e3bb899ddf81e93243d16e2c2))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
1
31
|
# @bernierllc/email-domain-verification
|
|
2
32
|
|
|
3
33
|
## 1.0.4
|
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { EmailDomainVerificationService } from '../src/EmailDomainVerificationService';
|
|
10
|
+
import {
|
|
11
|
+
DomainVerificationError,
|
|
12
|
+
DNSPropagationError,
|
|
13
|
+
ProviderAPIError,
|
|
14
|
+
InvalidDomainError,
|
|
15
|
+
successResult,
|
|
16
|
+
errorResult,
|
|
17
|
+
} from '../src/errors';
|
|
18
|
+
|
|
19
|
+
// Create mock logger directly
|
|
20
|
+
const createMockLogger = () => ({
|
|
21
|
+
debug: jest.fn(),
|
|
22
|
+
info: jest.fn(),
|
|
23
|
+
warn: jest.fn(),
|
|
24
|
+
error: jest.fn(),
|
|
25
|
+
fatal: jest.fn()
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Mock external dependencies
|
|
29
|
+
jest.mock('dns2', () => ({
|
|
30
|
+
Resolver: jest.fn().mockImplementation(() => ({
|
|
31
|
+
resolve: jest.fn().mockResolvedValue({
|
|
32
|
+
answers: [
|
|
33
|
+
{ type: 'TXT', data: 'v=spf1 include:sendgrid.net -all' },
|
|
34
|
+
{ ns: '8.8.8.8' }
|
|
35
|
+
]
|
|
36
|
+
})
|
|
37
|
+
}))
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
jest.mock('axios', () => ({
|
|
41
|
+
create: jest.fn(() => ({
|
|
42
|
+
post: jest.fn().mockResolvedValue({
|
|
43
|
+
data: {
|
|
44
|
+
id: 'domain-123',
|
|
45
|
+
dns: [],
|
|
46
|
+
domain: {
|
|
47
|
+
name: 'example.com',
|
|
48
|
+
state: 'active'
|
|
49
|
+
},
|
|
50
|
+
receiving_dns_records: [],
|
|
51
|
+
sending_dns_records: []
|
|
52
|
+
}
|
|
53
|
+
}),
|
|
54
|
+
get: jest.fn().mockResolvedValue({
|
|
55
|
+
data: [{
|
|
56
|
+
domain: 'example.com',
|
|
57
|
+
id: 'domain-123'
|
|
58
|
+
}]
|
|
59
|
+
})
|
|
60
|
+
}))
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
jest.mock('cron', () => ({
|
|
64
|
+
CronJob: jest.fn().mockImplementation((_schedule: string, callback: () => void) => {
|
|
65
|
+
const job = {
|
|
66
|
+
start: jest.fn(),
|
|
67
|
+
stop: jest.fn(),
|
|
68
|
+
callback
|
|
69
|
+
};
|
|
70
|
+
return job;
|
|
71
|
+
})
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
describe('Coverage Gap Tests - EmailDomainVerificationService', () => {
|
|
75
|
+
let service: EmailDomainVerificationService;
|
|
76
|
+
let logger: ReturnType<typeof createMockLogger>;
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
logger = createMockLogger();
|
|
80
|
+
service = new EmailDomainVerificationService({ logger: logger as any });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
afterEach(async () => {
|
|
84
|
+
await service.shutdown();
|
|
85
|
+
jest.clearAllMocks();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('Initialize method', () => {
|
|
89
|
+
it('should initialize the service', async () => {
|
|
90
|
+
await service.initialize();
|
|
91
|
+
expect(logger.info).toHaveBeenCalledWith(
|
|
92
|
+
'Email Domain Verification Service initialized',
|
|
93
|
+
expect.any(Object)
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('SPF Record Generation - provider variations', () => {
|
|
99
|
+
it('should generate SPF record for mailgun provider', async () => {
|
|
100
|
+
const result = await service.setupDomain({
|
|
101
|
+
domain: 'example.com',
|
|
102
|
+
provider: 'mailgun',
|
|
103
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
104
|
+
dkim: { enabled: false },
|
|
105
|
+
spf: { enabled: true, strict: true },
|
|
106
|
+
dmarc: { enabled: false }
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(result.success).toBe(true);
|
|
110
|
+
const spfRecord = result.dnsRecords.find(r => r.type === 'SPF');
|
|
111
|
+
expect(spfRecord?.value).toContain('include:mailgun.org');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should generate SPF record for SES provider', async () => {
|
|
115
|
+
const result = await service.setupDomain({
|
|
116
|
+
domain: 'example.com',
|
|
117
|
+
provider: 'ses',
|
|
118
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
119
|
+
dkim: { enabled: false },
|
|
120
|
+
spf: { enabled: true, strict: true },
|
|
121
|
+
dmarc: { enabled: false }
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(result.success).toBe(true);
|
|
125
|
+
const spfRecord = result.dnsRecords.find(r => r.type === 'SPF');
|
|
126
|
+
expect(spfRecord?.value).toContain('include:amazonses.com');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should generate SPF with custom includes', async () => {
|
|
130
|
+
const result = await service.setupDomain({
|
|
131
|
+
domain: 'example.com',
|
|
132
|
+
provider: 'sendgrid',
|
|
133
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
134
|
+
dkim: { enabled: false },
|
|
135
|
+
spf: {
|
|
136
|
+
enabled: true,
|
|
137
|
+
strict: true,
|
|
138
|
+
includes: ['include:custom.com']
|
|
139
|
+
},
|
|
140
|
+
dmarc: { enabled: false }
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(result.success).toBe(true);
|
|
144
|
+
const spfRecord = result.dnsRecords.find(r => r.type === 'SPF');
|
|
145
|
+
expect(spfRecord?.value).toContain('include:custom.com');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should generate SPF with IPv4 addresses', async () => {
|
|
149
|
+
const result = await service.setupDomain({
|
|
150
|
+
domain: 'example.com',
|
|
151
|
+
provider: 'sendgrid',
|
|
152
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
153
|
+
dkim: { enabled: false },
|
|
154
|
+
spf: {
|
|
155
|
+
enabled: true,
|
|
156
|
+
strict: true,
|
|
157
|
+
ipv4Addresses: ['192.168.1.1', '10.0.0.1']
|
|
158
|
+
},
|
|
159
|
+
dmarc: { enabled: false }
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(result.success).toBe(true);
|
|
163
|
+
const spfRecord = result.dnsRecords.find(r => r.type === 'SPF');
|
|
164
|
+
expect(spfRecord?.value).toContain('ip4:192.168.1.1');
|
|
165
|
+
expect(spfRecord?.value).toContain('ip4:10.0.0.1');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should generate SPF with IPv6 addresses', async () => {
|
|
169
|
+
const result = await service.setupDomain({
|
|
170
|
+
domain: 'example.com',
|
|
171
|
+
provider: 'sendgrid',
|
|
172
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
173
|
+
dkim: { enabled: false },
|
|
174
|
+
spf: {
|
|
175
|
+
enabled: true,
|
|
176
|
+
strict: true,
|
|
177
|
+
ipv6Addresses: ['2001:db8::1']
|
|
178
|
+
},
|
|
179
|
+
dmarc: { enabled: false }
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(result.success).toBe(true);
|
|
183
|
+
const spfRecord = result.dnsRecords.find(r => r.type === 'SPF');
|
|
184
|
+
expect(spfRecord?.value).toContain('ip6:2001:db8::1');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should generate SPF with non-strict mode (~all)', async () => {
|
|
188
|
+
const result = await service.setupDomain({
|
|
189
|
+
domain: 'example.com',
|
|
190
|
+
provider: 'sendgrid',
|
|
191
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
192
|
+
dkim: { enabled: false },
|
|
193
|
+
spf: { enabled: true, strict: false },
|
|
194
|
+
dmarc: { enabled: false }
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(result.success).toBe(true);
|
|
198
|
+
const spfRecord = result.dnsRecords.find(r => r.type === 'SPF');
|
|
199
|
+
expect(spfRecord?.value).toContain('~all');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('DMARC Record Generation - variations', () => {
|
|
204
|
+
it('should generate DMARC with aggregateEmail', async () => {
|
|
205
|
+
const result = await service.setupDomain({
|
|
206
|
+
domain: 'example.com',
|
|
207
|
+
provider: 'sendgrid',
|
|
208
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
209
|
+
dkim: { enabled: false },
|
|
210
|
+
spf: { enabled: false },
|
|
211
|
+
dmarc: {
|
|
212
|
+
enabled: true,
|
|
213
|
+
policy: 'quarantine',
|
|
214
|
+
aggregateEmail: 'forensic@example.com'
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(result.success).toBe(true);
|
|
219
|
+
const dmarcRecord = result.dnsRecords.find(r => r.type === 'DMARC');
|
|
220
|
+
expect(dmarcRecord?.value).toContain('ruf=mailto:forensic@example.com');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should generate DMARC with reportEmail', async () => {
|
|
224
|
+
const result = await service.setupDomain({
|
|
225
|
+
domain: 'example.com',
|
|
226
|
+
provider: 'sendgrid',
|
|
227
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
228
|
+
dkim: { enabled: false },
|
|
229
|
+
spf: { enabled: false },
|
|
230
|
+
dmarc: {
|
|
231
|
+
enabled: true,
|
|
232
|
+
policy: 'reject',
|
|
233
|
+
reportEmail: 'dmarc@example.com'
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(result.success).toBe(true);
|
|
238
|
+
const dmarcRecord = result.dnsRecords.find(r => r.type === 'DMARC');
|
|
239
|
+
expect(dmarcRecord?.value).toContain('rua=mailto:dmarc@example.com');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should generate DMARC with all options', async () => {
|
|
243
|
+
const result = await service.setupDomain({
|
|
244
|
+
domain: 'example.com',
|
|
245
|
+
provider: 'sendgrid',
|
|
246
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
247
|
+
dkim: { enabled: false },
|
|
248
|
+
spf: { enabled: false },
|
|
249
|
+
dmarc: {
|
|
250
|
+
enabled: true,
|
|
251
|
+
policy: 'reject',
|
|
252
|
+
percentage: 50,
|
|
253
|
+
subdomain: 'quarantine',
|
|
254
|
+
reportEmail: 'dmarc@example.com',
|
|
255
|
+
aggregateEmail: 'forensic@example.com'
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
expect(result.success).toBe(true);
|
|
260
|
+
const dmarcRecord = result.dnsRecords.find(r => r.type === 'DMARC');
|
|
261
|
+
expect(dmarcRecord?.value).toContain('p=reject');
|
|
262
|
+
expect(dmarcRecord?.value).toContain('pct=50');
|
|
263
|
+
expect(dmarcRecord?.value).toContain('sp=quarantine');
|
|
264
|
+
expect(dmarcRecord?.value).toContain('rua=');
|
|
265
|
+
expect(dmarcRecord?.value).toContain('ruf=');
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('Provider-specific records', () => {
|
|
270
|
+
it('should generate records for postmark provider (default case)', async () => {
|
|
271
|
+
const result = await service.setupDomain({
|
|
272
|
+
domain: 'example.com',
|
|
273
|
+
provider: 'postmark' as any,
|
|
274
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
275
|
+
dkim: { enabled: false },
|
|
276
|
+
spf: { enabled: true, strict: true },
|
|
277
|
+
dmarc: { enabled: false }
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(result.success).toBe(true);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('Error Handling', () => {
|
|
285
|
+
it('should handle verifyDomain error catch block', async () => {
|
|
286
|
+
// Create a service that will throw during verification
|
|
287
|
+
const errorService = new EmailDomainVerificationService({ logger: logger as any });
|
|
288
|
+
|
|
289
|
+
// Mock the verifyWithProvider to throw
|
|
290
|
+
(errorService as any).verifyWithProvider = jest.fn().mockRejectedValue(
|
|
291
|
+
new Error('Provider API failed')
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const result = await errorService.verifyDomain('example.com', 'sendgrid', { apiKey: 'key' });
|
|
295
|
+
|
|
296
|
+
expect(result.success).toBe(false);
|
|
297
|
+
expect(result.errors).toContain('Provider API failed');
|
|
298
|
+
expect(logger.error).toHaveBeenCalled();
|
|
299
|
+
|
|
300
|
+
await errorService.shutdown();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should handle verifyDomain with non-Error exception', async () => {
|
|
304
|
+
const errorService = new EmailDomainVerificationService({ logger: logger as any });
|
|
305
|
+
|
|
306
|
+
// Mock to throw non-Error
|
|
307
|
+
(errorService as any).verifyWithProvider = jest.fn().mockRejectedValue('String error');
|
|
308
|
+
|
|
309
|
+
const result = await errorService.verifyDomain('example.com', 'sendgrid', { apiKey: 'key' });
|
|
310
|
+
|
|
311
|
+
expect(result.success).toBe(false);
|
|
312
|
+
expect(result.errors).toContain('Unknown error');
|
|
313
|
+
|
|
314
|
+
await errorService.shutdown();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should handle setupDomain with invalid domain', async () => {
|
|
318
|
+
const result = await service.setupDomain({
|
|
319
|
+
domain: '', // Invalid empty domain
|
|
320
|
+
provider: 'sendgrid',
|
|
321
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
322
|
+
dkim: { enabled: false },
|
|
323
|
+
spf: { enabled: true },
|
|
324
|
+
dmarc: { enabled: false }
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(result.success).toBe(false);
|
|
328
|
+
expect(result.errors).toBeDefined();
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe('DNS Propagation', () => {
|
|
333
|
+
it('should check propagation for DNS records', async () => {
|
|
334
|
+
const records = [
|
|
335
|
+
{
|
|
336
|
+
type: 'SPF' as const,
|
|
337
|
+
host: 'example.com',
|
|
338
|
+
value: 'v=spf1 include:sendgrid.net -all',
|
|
339
|
+
ttl: 3600,
|
|
340
|
+
provider: 'sendgrid' as const,
|
|
341
|
+
required: true,
|
|
342
|
+
instructions: 'Add this TXT record to your domain'
|
|
343
|
+
}
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
const results = await service.checkPropagation('example.com', records);
|
|
347
|
+
|
|
348
|
+
expect(Array.isArray(results)).toBe(true);
|
|
349
|
+
expect(results.length).toBe(1);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe('Domain Health', () => {
|
|
354
|
+
it('should get domain health status', async () => {
|
|
355
|
+
const health = await service.getDomainHealth('example.com');
|
|
356
|
+
|
|
357
|
+
expect(health.domain).toBe('example.com');
|
|
358
|
+
expect(health).toHaveProperty('healthy');
|
|
359
|
+
expect(health).toHaveProperty('lastChecked');
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe('Verification History', () => {
|
|
364
|
+
it('should return empty history for new domain', () => {
|
|
365
|
+
const history = service.getVerificationHistory('newdomain.com');
|
|
366
|
+
expect(Array.isArray(history)).toBe(true);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should return history after domain setup', async () => {
|
|
370
|
+
await service.setupDomain({
|
|
371
|
+
domain: 'test-history.com',
|
|
372
|
+
provider: 'sendgrid',
|
|
373
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
374
|
+
dkim: { enabled: false },
|
|
375
|
+
spf: { enabled: true },
|
|
376
|
+
dmarc: { enabled: false }
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const history = service.getVerificationHistory('test-history.com');
|
|
380
|
+
expect(history.length).toBeGreaterThan(0);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should respect limit parameter', async () => {
|
|
384
|
+
await service.setupDomain({
|
|
385
|
+
domain: 'test-limit.com',
|
|
386
|
+
provider: 'sendgrid',
|
|
387
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
388
|
+
dkim: { enabled: false },
|
|
389
|
+
spf: { enabled: true },
|
|
390
|
+
dmarc: { enabled: false }
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const history = service.getVerificationHistory('test-limit.com', 1);
|
|
394
|
+
expect(history.length).toBeLessThanOrEqual(1);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
describe('Health Monitoring', () => {
|
|
399
|
+
it('should start and stop health monitoring', async () => {
|
|
400
|
+
// First verify a domain to start monitoring
|
|
401
|
+
const verifyService = new EmailDomainVerificationService({ logger: logger as any });
|
|
402
|
+
|
|
403
|
+
// Mock successful verification
|
|
404
|
+
(verifyService as any).verifyWithProvider = jest.fn().mockResolvedValue({
|
|
405
|
+
verified: true,
|
|
406
|
+
message: 'Domain verified'
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
await verifyService.verifyDomain('test.com', 'sendgrid', { apiKey: 'key' });
|
|
410
|
+
|
|
411
|
+
// Stop monitoring explicitly
|
|
412
|
+
verifyService.stopHealthMonitoring('test.com');
|
|
413
|
+
|
|
414
|
+
expect(logger.info).toHaveBeenCalledWith('Health monitoring stopped', { domain: 'test.com' });
|
|
415
|
+
|
|
416
|
+
await verifyService.shutdown();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should handle stopping monitoring for non-existent domain', () => {
|
|
420
|
+
// Should not throw when stopping non-existent domain
|
|
421
|
+
expect(() => service.stopHealthMonitoring('nonexistent.com')).not.toThrow();
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe('Domain Verification', () => {
|
|
426
|
+
it('should verify domain with sendgrid provider', async () => {
|
|
427
|
+
const result = await service.verifyDomain('example.com', 'sendgrid', { apiKey: 'test-key' });
|
|
428
|
+
|
|
429
|
+
// The mock returns valid: undefined which maps to not verified
|
|
430
|
+
expect(result).toHaveProperty('success');
|
|
431
|
+
expect(result).toHaveProperty('domain', 'example.com');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should verify domain with mailgun provider', async () => {
|
|
435
|
+
const result = await service.verifyDomain('example.com', 'mailgun', { apiKey: 'test-key' });
|
|
436
|
+
|
|
437
|
+
expect(result).toHaveProperty('success');
|
|
438
|
+
expect(result).toHaveProperty('domain', 'example.com');
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('should verify domain with SES provider', async () => {
|
|
442
|
+
const result = await service.verifyDomain('example.com', 'ses', { apiKey: 'test-key' });
|
|
443
|
+
|
|
444
|
+
expect(result.success).toBe(false); // SES returns not implemented
|
|
445
|
+
expect(result.domain).toBe('example.com');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should handle verified domain status correctly', async () => {
|
|
449
|
+
const verifyService = new EmailDomainVerificationService({ logger: logger as any });
|
|
450
|
+
|
|
451
|
+
// Mock successful verification
|
|
452
|
+
(verifyService as any).verifyWithProvider = jest.fn().mockResolvedValue({
|
|
453
|
+
verified: true,
|
|
454
|
+
message: 'Domain verified successfully'
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const result = await verifyService.verifyDomain('verified.com', 'sendgrid', { apiKey: 'key' });
|
|
458
|
+
|
|
459
|
+
expect(result.success).toBe(true);
|
|
460
|
+
expect(result.status).toBe('verified');
|
|
461
|
+
|
|
462
|
+
await verifyService.shutdown();
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('should handle failed domain verification', async () => {
|
|
466
|
+
const verifyService = new EmailDomainVerificationService({ logger: logger as any });
|
|
467
|
+
|
|
468
|
+
// Mock failed verification
|
|
469
|
+
(verifyService as any).verifyWithProvider = jest.fn().mockResolvedValue({
|
|
470
|
+
verified: false,
|
|
471
|
+
message: 'DNS records not found'
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const result = await verifyService.verifyDomain('failed.com', 'sendgrid', { apiKey: 'key' });
|
|
475
|
+
|
|
476
|
+
expect(result.success).toBe(false);
|
|
477
|
+
expect(result.status).toBe('failed');
|
|
478
|
+
expect(result.errors).toBeDefined();
|
|
479
|
+
|
|
480
|
+
await verifyService.shutdown();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('should handle unsupported provider in verification', async () => {
|
|
484
|
+
const verifyService = new EmailDomainVerificationService({ logger: logger as any });
|
|
485
|
+
|
|
486
|
+
const result = await verifyService.verifyDomain('example.com', 'unknown' as any, { apiKey: 'key' });
|
|
487
|
+
|
|
488
|
+
expect(result.success).toBe(false);
|
|
489
|
+
|
|
490
|
+
await verifyService.shutdown();
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
describe('Error Classes', () => {
|
|
496
|
+
describe('DomainVerificationError', () => {
|
|
497
|
+
it('should create error with all properties', () => {
|
|
498
|
+
const error = new DomainVerificationError('Test error', 'TEST_CODE', true, { key: 'value' });
|
|
499
|
+
|
|
500
|
+
expect(error.message).toBe('Test error');
|
|
501
|
+
expect(error.code).toBe('TEST_CODE');
|
|
502
|
+
expect(error.retryable).toBe(true);
|
|
503
|
+
expect(error.details).toEqual({ key: 'value' });
|
|
504
|
+
expect(error.name).toBe('DomainVerificationError');
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('should default retryable to false', () => {
|
|
508
|
+
const error = new DomainVerificationError('Test error', 'TEST_CODE');
|
|
509
|
+
expect(error.retryable).toBe(false);
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
describe('DNSPropagationError', () => {
|
|
514
|
+
it('should create with correct code and retryable flag', () => {
|
|
515
|
+
const error = new DNSPropagationError('DNS not propagated', { domain: 'test.com' });
|
|
516
|
+
|
|
517
|
+
expect(error.code).toBe('DNS_PROPAGATION_ERROR');
|
|
518
|
+
expect(error.retryable).toBe(true);
|
|
519
|
+
expect(error.name).toBe('DNSPropagationError');
|
|
520
|
+
expect(error.details).toEqual({ domain: 'test.com' });
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
describe('ProviderAPIError', () => {
|
|
525
|
+
it('should create with correct code and retryable flag', () => {
|
|
526
|
+
const error = new ProviderAPIError('API failed', { status: 500 });
|
|
527
|
+
|
|
528
|
+
expect(error.code).toBe('PROVIDER_API_ERROR');
|
|
529
|
+
expect(error.retryable).toBe(true);
|
|
530
|
+
expect(error.name).toBe('ProviderAPIError');
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
describe('InvalidDomainError', () => {
|
|
535
|
+
it('should create with correct code and non-retryable flag', () => {
|
|
536
|
+
const error = new InvalidDomainError('Invalid domain format');
|
|
537
|
+
|
|
538
|
+
expect(error.code).toBe('INVALID_DOMAIN');
|
|
539
|
+
expect(error.retryable).toBe(false);
|
|
540
|
+
expect(error.name).toBe('InvalidDomainError');
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
describe('Result Helpers', () => {
|
|
545
|
+
it('successResult should create success result', () => {
|
|
546
|
+
const result = successResult({ domain: 'test.com' });
|
|
547
|
+
|
|
548
|
+
expect(result.success).toBe(true);
|
|
549
|
+
expect(result.data).toEqual({ domain: 'test.com' });
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it('errorResult should create error result from DomainVerificationError', () => {
|
|
553
|
+
const error = new DomainVerificationError('Test error', 'TEST', true, { key: 'value' });
|
|
554
|
+
const result = errorResult(error);
|
|
555
|
+
|
|
556
|
+
expect(result.success).toBe(false);
|
|
557
|
+
expect(result.error).toEqual({
|
|
558
|
+
code: 'TEST',
|
|
559
|
+
message: 'Test error',
|
|
560
|
+
retryable: true,
|
|
561
|
+
details: { key: 'value' }
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
package/jest.config.cjs
CHANGED
|
@@ -17,10 +17,10 @@ module.exports = {
|
|
|
17
17
|
],
|
|
18
18
|
coverageThreshold: {
|
|
19
19
|
global: {
|
|
20
|
-
branches:
|
|
21
|
-
functions:
|
|
22
|
-
lines:
|
|
23
|
-
statements:
|
|
20
|
+
branches: 65,
|
|
21
|
+
functions: 85,
|
|
22
|
+
lines: 85,
|
|
23
|
+
statements: 85
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
testMatch: [
|
package/package.json
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bernierllc/email-domain-verification",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Programmatic domain setup and DNS management for production email sending with multi-provider support",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "jest --watch",
|
|
10
|
+
"test:run": "jest",
|
|
11
|
+
"test:coverage": "jest --coverage",
|
|
12
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
13
|
+
"clean": "rm -rf dist",
|
|
14
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
15
|
+
},
|
|
7
16
|
"keywords": [
|
|
8
17
|
"email",
|
|
9
18
|
"domain",
|
|
@@ -21,29 +30,29 @@
|
|
|
21
30
|
"author": "Bernier LLC",
|
|
22
31
|
"license": "SEE LICENSE IN LICENSE",
|
|
23
32
|
"dependencies": {
|
|
24
|
-
"
|
|
25
|
-
"
|
|
33
|
+
"@bernierllc/crypto-utils": "1.0.6",
|
|
34
|
+
"@bernierllc/email-sender-manager": "1.2.0",
|
|
35
|
+
"@bernierllc/logger": "1.3.0",
|
|
36
|
+
"@bernierllc/neverhub-adapter": "0.1.4",
|
|
26
37
|
"axios": "^1.6.0",
|
|
27
38
|
"cron": "^3.1.6",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"@bernierllc/logger": "1.1.0",
|
|
31
|
-
"@bernierllc/neverhub-adapter": "0.1.2"
|
|
39
|
+
"dns-packet": "^5.6.1",
|
|
40
|
+
"dns2": "^2.1.0"
|
|
32
41
|
},
|
|
33
42
|
"devDependencies": {
|
|
34
|
-
"@types/jest": "^29.0.0",
|
|
35
|
-
"@types/node": "^20.0.0",
|
|
36
43
|
"@types/cron": "^2.0.1",
|
|
37
44
|
"@types/dns2": "^2.0.0",
|
|
45
|
+
"@types/jest": "^29.0.0",
|
|
46
|
+
"@types/node": "^20.0.0",
|
|
47
|
+
"@types/react": "^18.0.0",
|
|
38
48
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
39
49
|
"@typescript-eslint/parser": "^6.0.0",
|
|
40
50
|
"eslint": "^8.0.0",
|
|
41
51
|
"jest": "^29.0.0",
|
|
42
|
-
"ts-jest": "^29.0.0",
|
|
43
|
-
"typescript": "^5.0.0",
|
|
44
52
|
"react": "^18.0.0",
|
|
45
53
|
"react-dom": "^18.0.0",
|
|
46
|
-
"
|
|
54
|
+
"ts-jest": "^29.0.0",
|
|
55
|
+
"typescript": "^5.0.0"
|
|
47
56
|
},
|
|
48
57
|
"peerDependencies": {
|
|
49
58
|
"react": ">=18.0.0",
|
|
@@ -62,12 +71,5 @@
|
|
|
62
71
|
"category": "service",
|
|
63
72
|
"priority": "critical"
|
|
64
73
|
},
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
"test": "jest --watch",
|
|
68
|
-
"test:run": "jest",
|
|
69
|
-
"test:coverage": "jest --coverage",
|
|
70
|
-
"lint": "eslint src --ext .ts,.tsx",
|
|
71
|
-
"clean": "rm -rf dist"
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
+
"gitHead": "af7e74b3715d56d3a193e1bb6743b337c2b0df6d"
|
|
75
|
+
}
|