@bernierllc/email-domain-verification 1.0.0
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/.eslintrc.cjs +29 -0
- package/README.md +397 -0
- package/__mocks__/@bernierllc/crypto-utils.ts +15 -0
- package/__mocks__/@bernierllc/logger.ts +42 -0
- package/__mocks__/@bernierllc/neverhub-adapter.ts +17 -0
- package/__tests__/EmailDomainVerificationService.test.ts +582 -0
- package/coverage/clover.xml +263 -0
- package/coverage/coverage-final.json +3 -0
- package/coverage/lcov-report/EmailDomainVerificationService.ts.html +3061 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/errors.ts.html +322 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +131 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +485 -0
- package/dist/EmailDomainVerificationService.d.ts +179 -0
- package/dist/EmailDomainVerificationService.js +822 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.js +66 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +28 -0
- package/dist/types.d.ts +209 -0
- package/dist/types.js +9 -0
- package/jest.config.cjs +44 -0
- package/jest.setup.js +19 -0
- package/package.json +72 -0
- package/src/EmailDomainVerificationService.ts +992 -0
- package/src/dns2.d.ts +29 -0
- package/src/errors.ts +79 -0
- package/src/index.ts +11 -0
- package/src/types.ts +281 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,582 @@
|
|
|
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 { DNSRecord } from '../src/types';
|
|
11
|
+
|
|
12
|
+
// Create mock logger directly to avoid type conflicts with real logger
|
|
13
|
+
const createMockLogger = () => ({
|
|
14
|
+
debug: jest.fn(),
|
|
15
|
+
info: jest.fn(),
|
|
16
|
+
warn: jest.fn(),
|
|
17
|
+
error: jest.fn(),
|
|
18
|
+
fatal: jest.fn()
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Mock external dependencies only (logger and neverhub-adapter are mocked via moduleNameMapper)
|
|
22
|
+
jest.mock('dns2', () => ({
|
|
23
|
+
Resolver: jest.fn().mockImplementation(() => ({
|
|
24
|
+
resolve: jest.fn().mockResolvedValue({
|
|
25
|
+
answers: [
|
|
26
|
+
{ type: 'TXT', data: 'v=spf1 include:sendgrid.net -all' },
|
|
27
|
+
{ ns: '8.8.8.8' }
|
|
28
|
+
]
|
|
29
|
+
})
|
|
30
|
+
}))
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
jest.mock('axios', () => ({
|
|
34
|
+
create: jest.fn(() => ({
|
|
35
|
+
post: jest.fn().mockResolvedValue({
|
|
36
|
+
data: {
|
|
37
|
+
id: 'domain-123',
|
|
38
|
+
dns: [],
|
|
39
|
+
domain: {
|
|
40
|
+
name: 'example.com',
|
|
41
|
+
state: 'active'
|
|
42
|
+
},
|
|
43
|
+
receiving_dns_records: [],
|
|
44
|
+
sending_dns_records: []
|
|
45
|
+
}
|
|
46
|
+
}),
|
|
47
|
+
get: jest.fn().mockResolvedValue({
|
|
48
|
+
data: [{
|
|
49
|
+
domain: 'example.com',
|
|
50
|
+
id: 'domain-123'
|
|
51
|
+
}]
|
|
52
|
+
})
|
|
53
|
+
}))
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
jest.mock('cron', () => ({
|
|
57
|
+
CronJob: jest.fn().mockImplementation((_schedule: string, _callback: () => void) => ({
|
|
58
|
+
start: jest.fn(),
|
|
59
|
+
stop: jest.fn()
|
|
60
|
+
}))
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
describe('EmailDomainVerificationService', () => {
|
|
64
|
+
let service: EmailDomainVerificationService;
|
|
65
|
+
let logger: ReturnType<typeof createMockLogger>;
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
logger = createMockLogger();
|
|
69
|
+
service = new EmailDomainVerificationService({ logger: logger as any });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(async () => {
|
|
73
|
+
await service.shutdown();
|
|
74
|
+
jest.clearAllMocks();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('setupDomain', () => {
|
|
78
|
+
it('should generate SPF record with provider includes', async () => {
|
|
79
|
+
const result = await service.setupDomain({
|
|
80
|
+
domain: 'example.com',
|
|
81
|
+
provider: 'sendgrid',
|
|
82
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
83
|
+
dkim: { enabled: false },
|
|
84
|
+
spf: { enabled: true, strict: true },
|
|
85
|
+
dmarc: { enabled: false }
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(result.success).toBe(true);
|
|
89
|
+
expect(result.dnsRecords.length).toBeGreaterThan(0);
|
|
90
|
+
|
|
91
|
+
const spfRecord = result.dnsRecords.find(r => r.type === 'SPF');
|
|
92
|
+
expect(spfRecord).toBeDefined();
|
|
93
|
+
expect(spfRecord!.value).toContain('include:sendgrid.net');
|
|
94
|
+
expect(spfRecord!.value).toContain('-all'); // strict mode
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should generate DKIM keypair and DNS record when enabled', async () => {
|
|
98
|
+
const result = await service.setupDomain({
|
|
99
|
+
domain: 'example.com',
|
|
100
|
+
provider: 'sendgrid',
|
|
101
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
102
|
+
dkim: { enabled: true, selector: 'default', keySize: 2048 },
|
|
103
|
+
spf: { enabled: false },
|
|
104
|
+
dmarc: { enabled: false }
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(result.success).toBe(true);
|
|
108
|
+
expect(result.dkimConfig).toBeDefined();
|
|
109
|
+
expect(result.dkimConfig!.selector).toBe('default');
|
|
110
|
+
expect(result.dkimConfig!.keySize).toBe(2048);
|
|
111
|
+
expect(result.dkimConfig!.privateKey).toMatch(/-----BEGIN PRIVATE KEY-----/);
|
|
112
|
+
expect(result.dkimConfig!.publicKey).toMatch(/-----BEGIN PUBLIC KEY-----/);
|
|
113
|
+
|
|
114
|
+
const dkimRecord = result.dnsRecords.find(r => r.type === 'DKIM');
|
|
115
|
+
expect(dkimRecord).toBeDefined();
|
|
116
|
+
expect(dkimRecord!.host).toBe('default._domainkey.example.com');
|
|
117
|
+
expect(dkimRecord!.value).toContain('v=DKIM1');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should generate DMARC record with correct policy', async () => {
|
|
121
|
+
const result = await service.setupDomain({
|
|
122
|
+
domain: 'example.com',
|
|
123
|
+
provider: 'sendgrid',
|
|
124
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
125
|
+
dkim: { enabled: false },
|
|
126
|
+
spf: { enabled: false },
|
|
127
|
+
dmarc: {
|
|
128
|
+
enabled: true,
|
|
129
|
+
policy: 'quarantine',
|
|
130
|
+
percentage: 50,
|
|
131
|
+
reportEmail: 'dmarc@example.com'
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(result.success).toBe(true);
|
|
136
|
+
|
|
137
|
+
const dmarcRecord = result.dnsRecords.find(r => r.type === 'DMARC');
|
|
138
|
+
expect(dmarcRecord).toBeDefined();
|
|
139
|
+
expect(dmarcRecord!.host).toBe('_dmarc.example.com');
|
|
140
|
+
expect(dmarcRecord!.value).toContain('p=quarantine');
|
|
141
|
+
expect(dmarcRecord!.value).toContain('pct=50');
|
|
142
|
+
expect(dmarcRecord!.value).toContain('rua=mailto:dmarc@example.com');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should reject invalid domain names', async () => {
|
|
146
|
+
const result = await service.setupDomain({
|
|
147
|
+
domain: 'invalid domain with spaces',
|
|
148
|
+
provider: 'sendgrid',
|
|
149
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
150
|
+
dkim: { enabled: false },
|
|
151
|
+
spf: { enabled: true },
|
|
152
|
+
dmarc: { enabled: false }
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(result.success).toBe(false);
|
|
156
|
+
expect(result.status).toBe('failed');
|
|
157
|
+
expect(result.errors).toBeDefined();
|
|
158
|
+
expect(result.errors![0]).toContain('Invalid domain name');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should generate provider-specific records for SendGrid', async () => {
|
|
162
|
+
const result = await service.setupDomain({
|
|
163
|
+
domain: 'example.com',
|
|
164
|
+
provider: 'sendgrid',
|
|
165
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
166
|
+
dkim: { enabled: false },
|
|
167
|
+
spf: { enabled: false },
|
|
168
|
+
dmarc: { enabled: false }
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(result.success).toBe(true);
|
|
172
|
+
const cnameRecords = result.dnsRecords.filter(r => r.type === 'CNAME');
|
|
173
|
+
expect(cnameRecords.length).toBeGreaterThan(0);
|
|
174
|
+
expect(cnameRecords[0].provider).toBe('sendgrid');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should generate provider-specific records for Mailgun', async () => {
|
|
178
|
+
const result = await service.setupDomain({
|
|
179
|
+
domain: 'example.com',
|
|
180
|
+
provider: 'mailgun',
|
|
181
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
182
|
+
dkim: { enabled: false },
|
|
183
|
+
spf: { enabled: false },
|
|
184
|
+
dmarc: { enabled: false }
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(result.success).toBe(true);
|
|
188
|
+
const txtRecords = result.dnsRecords.filter(r => r.type === 'TXT');
|
|
189
|
+
expect(txtRecords.length).toBeGreaterThan(0);
|
|
190
|
+
expect(txtRecords[0].value).toContain('mailgun.org');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should generate provider-specific records for SES', async () => {
|
|
194
|
+
const result = await service.setupDomain({
|
|
195
|
+
domain: 'example.com',
|
|
196
|
+
provider: 'ses',
|
|
197
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
198
|
+
dkim: { enabled: false },
|
|
199
|
+
spf: { enabled: false },
|
|
200
|
+
dmarc: { enabled: false }
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(result.success).toBe(true);
|
|
204
|
+
const txtRecords = result.dnsRecords.filter(r => r.type === 'TXT');
|
|
205
|
+
expect(txtRecords.length).toBeGreaterThan(0);
|
|
206
|
+
expect(txtRecords[0].host).toContain('_amazonses');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should include custom SPF includes', async () => {
|
|
210
|
+
const result = await service.setupDomain({
|
|
211
|
+
domain: 'example.com',
|
|
212
|
+
provider: 'sendgrid',
|
|
213
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
214
|
+
dkim: { enabled: false },
|
|
215
|
+
spf: {
|
|
216
|
+
enabled: true,
|
|
217
|
+
includes: ['include:custom-provider.com'],
|
|
218
|
+
ipv4Addresses: ['192.168.1.1'],
|
|
219
|
+
ipv6Addresses: ['2001:db8::1']
|
|
220
|
+
},
|
|
221
|
+
dmarc: { enabled: false }
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(result.success).toBe(true);
|
|
225
|
+
const spfRecord = result.dnsRecords.find(r => r.type === 'SPF');
|
|
226
|
+
expect(spfRecord!.value).toContain('include:custom-provider.com');
|
|
227
|
+
expect(spfRecord!.value).toContain('ip4:192.168.1.1');
|
|
228
|
+
expect(spfRecord!.value).toContain('ip6:2001:db8::1');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should use ~all when strict is false', async () => {
|
|
232
|
+
const result = await service.setupDomain({
|
|
233
|
+
domain: 'example.com',
|
|
234
|
+
provider: 'sendgrid',
|
|
235
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
236
|
+
dkim: { enabled: false },
|
|
237
|
+
spf: { enabled: true, strict: false },
|
|
238
|
+
dmarc: { enabled: false }
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
expect(result.success).toBe(true);
|
|
242
|
+
const spfRecord = result.dnsRecords.find(r => r.type === 'SPF');
|
|
243
|
+
expect(spfRecord!.value).toContain('~all');
|
|
244
|
+
expect(spfRecord!.value).not.toContain('-all');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('checkPropagation', () => {
|
|
249
|
+
it('should check DNS propagation across nameservers', async () => {
|
|
250
|
+
const records: DNSRecord[] = [{
|
|
251
|
+
type: 'SPF',
|
|
252
|
+
host: 'example.com',
|
|
253
|
+
value: 'v=spf1 include:sendgrid.net -all',
|
|
254
|
+
ttl: 3600,
|
|
255
|
+
provider: 'sendgrid',
|
|
256
|
+
required: true,
|
|
257
|
+
instructions: 'Add SPF record'
|
|
258
|
+
}];
|
|
259
|
+
|
|
260
|
+
const results = await service.checkPropagation('example.com', records);
|
|
261
|
+
|
|
262
|
+
expect(results).toHaveLength(1);
|
|
263
|
+
expect(results[0].domain).toBe('example.com');
|
|
264
|
+
expect(results[0].record).toEqual(records[0]);
|
|
265
|
+
expect(results[0].propagationPercentage).toBeGreaterThanOrEqual(0);
|
|
266
|
+
expect(results[0].propagationPercentage).toBeLessThanOrEqual(100);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should handle DNS query failures gracefully', async () => {
|
|
270
|
+
const records: DNSRecord[] = [{
|
|
271
|
+
type: 'DKIM',
|
|
272
|
+
host: 'nonexistent._domainkey.example.com',
|
|
273
|
+
value: 'v=DKIM1; k=rsa; p=...',
|
|
274
|
+
ttl: 3600,
|
|
275
|
+
provider: 'sendgrid',
|
|
276
|
+
required: true,
|
|
277
|
+
instructions: 'Add DKIM record'
|
|
278
|
+
}];
|
|
279
|
+
|
|
280
|
+
const results = await service.checkPropagation('example.com', records);
|
|
281
|
+
|
|
282
|
+
expect(results).toHaveLength(1);
|
|
283
|
+
expect(results[0].propagated).toBe(false);
|
|
284
|
+
// Should not throw error - graceful degradation
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should return propagation results for multiple records', async () => {
|
|
288
|
+
const records: DNSRecord[] = [
|
|
289
|
+
{
|
|
290
|
+
type: 'SPF',
|
|
291
|
+
host: 'example.com',
|
|
292
|
+
value: 'v=spf1 include:sendgrid.net -all',
|
|
293
|
+
ttl: 3600,
|
|
294
|
+
provider: 'sendgrid',
|
|
295
|
+
required: true,
|
|
296
|
+
instructions: 'Add SPF record'
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
type: 'DMARC',
|
|
300
|
+
host: '_dmarc.example.com',
|
|
301
|
+
value: 'v=DMARC1; p=none; pct=100; sp=none;',
|
|
302
|
+
ttl: 3600,
|
|
303
|
+
provider: 'sendgrid',
|
|
304
|
+
required: true,
|
|
305
|
+
instructions: 'Add DMARC record'
|
|
306
|
+
}
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
const results = await service.checkPropagation('example.com', records);
|
|
310
|
+
|
|
311
|
+
expect(results).toHaveLength(2);
|
|
312
|
+
expect(results[0].record.type).toBe('SPF');
|
|
313
|
+
expect(results[1].record.type).toBe('DMARC');
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe('verifyDomain', () => {
|
|
318
|
+
it('should verify domain with provider', async () => {
|
|
319
|
+
// Mock successful verification
|
|
320
|
+
jest.spyOn(service as any, 'verifyWithProvider').mockResolvedValue({
|
|
321
|
+
verified: true,
|
|
322
|
+
message: 'Domain verified'
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const result = await service.verifyDomain(
|
|
326
|
+
'example.com',
|
|
327
|
+
'sendgrid',
|
|
328
|
+
{ apiKey: 'test-key' }
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
expect(result.success).toBe(true);
|
|
332
|
+
expect(result.status).toBe('verified');
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should handle verification failure', async () => {
|
|
336
|
+
// Mock failed verification
|
|
337
|
+
jest.spyOn(service as any, 'verifyWithProvider').mockResolvedValue({
|
|
338
|
+
verified: false,
|
|
339
|
+
message: 'Verification failed'
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const result = await service.verifyDomain(
|
|
343
|
+
'example.com',
|
|
344
|
+
'sendgrid',
|
|
345
|
+
{ apiKey: 'test-key' }
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
expect(result.success).toBe(false);
|
|
349
|
+
expect(result.status).toBe('failed');
|
|
350
|
+
expect(result.errors).toBeDefined();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should start health monitoring after successful verification', async () => {
|
|
354
|
+
// Mock successful verification
|
|
355
|
+
jest.spyOn(service as any, 'verifyWithProvider').mockResolvedValue({
|
|
356
|
+
verified: true,
|
|
357
|
+
message: 'Domain verified'
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const result = await service.verifyDomain(
|
|
361
|
+
'example.com',
|
|
362
|
+
'sendgrid',
|
|
363
|
+
{ apiKey: 'test-key' }
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
expect(result.success).toBe(true);
|
|
367
|
+
expect(result.status).toBe('verified');
|
|
368
|
+
|
|
369
|
+
// Health monitoring should be started
|
|
370
|
+
const jobs = (service as any).healthCheckJobs;
|
|
371
|
+
expect(jobs.has('example.com')).toBe(true);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe('getDomainHealth', () => {
|
|
376
|
+
it('should return domain health status', async () => {
|
|
377
|
+
const health = await service.getDomainHealth('example.com');
|
|
378
|
+
|
|
379
|
+
expect(health.domain).toBe('example.com');
|
|
380
|
+
expect(health.healthy).toBeDefined();
|
|
381
|
+
expect(health.verificationStatus).toBeDefined();
|
|
382
|
+
expect(health.lastChecked).toBeInstanceOf(Date);
|
|
383
|
+
expect(health.records).toBeDefined();
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe('getVerificationHistory', () => {
|
|
388
|
+
it('should track verification events in history', async () => {
|
|
389
|
+
await service.setupDomain({
|
|
390
|
+
domain: 'example.com',
|
|
391
|
+
provider: 'sendgrid',
|
|
392
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
393
|
+
dkim: { enabled: false },
|
|
394
|
+
spf: { enabled: true },
|
|
395
|
+
dmarc: { enabled: false }
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const history = service.getVerificationHistory('example.com');
|
|
399
|
+
|
|
400
|
+
expect(history).toHaveLength(1);
|
|
401
|
+
expect(history[0].domain).toBe('example.com');
|
|
402
|
+
expect(history[0].eventType).toBe('initiated');
|
|
403
|
+
expect(history[0].newStatus).toBe('pending');
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('should limit history entries based on limit parameter', async () => {
|
|
407
|
+
// Add multiple entries
|
|
408
|
+
for (let i = 0; i < 10; i++) {
|
|
409
|
+
await service.setupDomain({
|
|
410
|
+
domain: 'example.com',
|
|
411
|
+
provider: 'sendgrid',
|
|
412
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
413
|
+
dkim: { enabled: false },
|
|
414
|
+
spf: { enabled: true },
|
|
415
|
+
dmarc: { enabled: false }
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const history = service.getVerificationHistory('example.com', 5);
|
|
420
|
+
|
|
421
|
+
expect(history.length).toBeLessThanOrEqual(5);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('should return empty array for domain with no history', () => {
|
|
425
|
+
const history = service.getVerificationHistory('nonexistent.com');
|
|
426
|
+
|
|
427
|
+
expect(history).toEqual([]);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe('NeverHub Integration', () => {
|
|
432
|
+
it('should publish events to NeverHub when passed in constructor', async () => {
|
|
433
|
+
const mockNeverHub = {
|
|
434
|
+
register: jest.fn(),
|
|
435
|
+
publishEvent: jest.fn()
|
|
436
|
+
} as any;
|
|
437
|
+
|
|
438
|
+
const serviceWithNH = new EmailDomainVerificationService({
|
|
439
|
+
logger: logger as any,
|
|
440
|
+
neverhub: mockNeverHub
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// When neverhub is passed in constructor, it should publish events
|
|
444
|
+
await serviceWithNH.setupDomain({
|
|
445
|
+
domain: 'test-registration.com',
|
|
446
|
+
provider: 'sendgrid',
|
|
447
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
448
|
+
dkim: { enabled: false },
|
|
449
|
+
spf: { enabled: true },
|
|
450
|
+
dmarc: { enabled: false }
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Events should be published via the passed NeverHub adapter
|
|
454
|
+
expect(mockNeverHub.publishEvent).toHaveBeenCalled();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('should publish events to NeverHub', async () => {
|
|
458
|
+
const mockNeverHub = {
|
|
459
|
+
register: jest.fn(),
|
|
460
|
+
publishEvent: jest.fn()
|
|
461
|
+
} as any;
|
|
462
|
+
|
|
463
|
+
const serviceWithNH = new EmailDomainVerificationService({
|
|
464
|
+
logger: logger as any,
|
|
465
|
+
neverhub: mockNeverHub
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
await serviceWithNH.setupDomain({
|
|
469
|
+
domain: 'example.com',
|
|
470
|
+
provider: 'sendgrid',
|
|
471
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
472
|
+
dkim: { enabled: false },
|
|
473
|
+
spf: { enabled: true },
|
|
474
|
+
dmarc: { enabled: false }
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
expect(mockNeverHub.publishEvent).toHaveBeenCalledWith({
|
|
478
|
+
type: 'domain.verification.initiated',
|
|
479
|
+
data: expect.objectContaining({
|
|
480
|
+
domain: 'example.com',
|
|
481
|
+
provider: 'sendgrid'
|
|
482
|
+
}),
|
|
483
|
+
source: '@bernierllc/email-domain-verification',
|
|
484
|
+
timestamp: expect.any(String)
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('should work without NeverHub', async () => {
|
|
489
|
+
const serviceWithoutNH = new EmailDomainVerificationService({ logger: logger as any });
|
|
490
|
+
|
|
491
|
+
const result = await serviceWithoutNH.setupDomain({
|
|
492
|
+
domain: 'example.com',
|
|
493
|
+
provider: 'sendgrid',
|
|
494
|
+
providerCredentials: { apiKey: 'test-key' },
|
|
495
|
+
dkim: { enabled: false },
|
|
496
|
+
spf: { enabled: true },
|
|
497
|
+
dmarc: { enabled: false }
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
expect(result.success).toBe(true);
|
|
501
|
+
// No errors thrown - graceful degradation
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
describe('Health Monitoring', () => {
|
|
506
|
+
it('should start health monitoring after successful verification', async () => {
|
|
507
|
+
// Mock successful verification
|
|
508
|
+
jest.spyOn(service as any, 'verifyWithProvider').mockResolvedValue({
|
|
509
|
+
verified: true,
|
|
510
|
+
message: 'Domain verified'
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const result = await service.verifyDomain(
|
|
514
|
+
'example.com',
|
|
515
|
+
'sendgrid',
|
|
516
|
+
{ apiKey: 'test-key' }
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
expect(result.success).toBe(true);
|
|
520
|
+
expect(result.status).toBe('verified');
|
|
521
|
+
|
|
522
|
+
// Health monitoring should be started
|
|
523
|
+
const jobs = (service as any).healthCheckJobs;
|
|
524
|
+
expect(jobs.has('example.com')).toBe(true);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('should stop health monitoring on shutdown', async () => {
|
|
528
|
+
// Mock successful verification
|
|
529
|
+
jest.spyOn(service as any, 'verifyWithProvider').mockResolvedValue({
|
|
530
|
+
verified: true,
|
|
531
|
+
message: 'Domain verified'
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
await service.verifyDomain('example.com', 'sendgrid', { apiKey: 'test-key' });
|
|
535
|
+
|
|
536
|
+
const jobs = (service as any).healthCheckJobs;
|
|
537
|
+
const initialSize = jobs.size;
|
|
538
|
+
|
|
539
|
+
await service.shutdown();
|
|
540
|
+
|
|
541
|
+
expect(jobs.size).toBe(0);
|
|
542
|
+
expect(initialSize).toBeGreaterThan(0);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('should allow manual stop of health monitoring', async () => {
|
|
546
|
+
// Mock successful verification
|
|
547
|
+
jest.spyOn(service as any, 'verifyWithProvider').mockResolvedValue({
|
|
548
|
+
verified: true,
|
|
549
|
+
message: 'Domain verified'
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
await service.verifyDomain('example.com', 'sendgrid', { apiKey: 'test-key' });
|
|
553
|
+
|
|
554
|
+
const jobs = (service as any).healthCheckJobs;
|
|
555
|
+
expect(jobs.has('example.com')).toBe(true);
|
|
556
|
+
|
|
557
|
+
service.stopHealthMonitoring('example.com');
|
|
558
|
+
|
|
559
|
+
expect(jobs.has('example.com')).toBe(false);
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
describe('Shutdown', () => {
|
|
564
|
+
it('should cleanup all resources on shutdown', async () => {
|
|
565
|
+
// Start multiple health monitors
|
|
566
|
+
jest.spyOn(service as any, 'verifyWithProvider').mockResolvedValue({
|
|
567
|
+
verified: true,
|
|
568
|
+
message: 'Domain verified'
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
await service.verifyDomain('example1.com', 'sendgrid', { apiKey: 'test-key' });
|
|
572
|
+
await service.verifyDomain('example2.com', 'sendgrid', { apiKey: 'test-key' });
|
|
573
|
+
|
|
574
|
+
const jobs = (service as any).healthCheckJobs;
|
|
575
|
+
expect(jobs.size).toBe(2);
|
|
576
|
+
|
|
577
|
+
await service.shutdown();
|
|
578
|
+
|
|
579
|
+
expect(jobs.size).toBe(0);
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
});
|