@bernierllc/sender-identity-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/.env.test +8 -0
- package/.eslintrc.js +30 -0
- package/README.md +376 -0
- package/__tests__/SenderIdentityVerification.test.ts +461 -0
- package/__tests__/__mocks__/fetch-mock.ts +156 -0
- package/__tests__/additional-coverage.test.ts +129 -0
- package/__tests__/additional-error-coverage.test.ts +483 -0
- package/__tests__/branch-coverage.test.ts +509 -0
- package/__tests__/config.test.ts +119 -0
- package/__tests__/error-handling.test.ts +321 -0
- package/__tests__/final-branch-coverage.test.ts +372 -0
- package/__tests__/integration.real-api.test.ts +295 -0
- package/__tests__/providers.test.ts +331 -0
- package/__tests__/service-coverage.test.ts +412 -0
- package/dist/SenderIdentityVerification.d.ts +72 -0
- package/dist/SenderIdentityVerification.js +643 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.js +38 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +61 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +21 -0
- package/dist/providers/MailgunProvider.d.ts +13 -0
- package/dist/providers/MailgunProvider.js +35 -0
- package/dist/providers/SESProvider.d.ts +12 -0
- package/dist/providers/SESProvider.js +47 -0
- package/dist/providers/SMTPProvider.d.ts +12 -0
- package/dist/providers/SMTPProvider.js +30 -0
- package/dist/providers/SendGridProvider.d.ts +19 -0
- package/dist/providers/SendGridProvider.js +98 -0
- package/dist/templates/verification-email.d.ts +9 -0
- package/dist/templates/verification-email.js +67 -0
- package/dist/types.d.ts +139 -0
- package/dist/types.js +33 -0
- package/dist/utils/domain-extractor.d.ts +4 -0
- package/dist/utils/domain-extractor.js +20 -0
- package/jest.config.cjs +33 -0
- package/package.json +60 -0
- package/src/SenderIdentityVerification.ts +796 -0
- package/src/config.ts +81 -0
- package/src/errors.ts +64 -0
- package/src/global.d.ts +24 -0
- package/src/index.ts +24 -0
- package/src/providers/MailgunProvider.ts +35 -0
- package/src/providers/SESProvider.ts +51 -0
- package/src/providers/SMTPProvider.ts +29 -0
- package/src/providers/SendGridProvider.ts +108 -0
- package/src/templates/verification-email.ts +67 -0
- package/src/types.ts +163 -0
- package/src/utils/domain-extractor.ts +18 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,509 @@
|
|
|
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 { SenderIdentityVerification } from '../src/SenderIdentityVerification';
|
|
10
|
+
import { SenderIdentityConfig } from '../src/config';
|
|
11
|
+
import { EmailProvider, SenderStatus } from '../src/types';
|
|
12
|
+
|
|
13
|
+
describe('Branch Coverage Tests', () => {
|
|
14
|
+
let service: SenderIdentityVerification;
|
|
15
|
+
let config: SenderIdentityConfig;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
config = {
|
|
19
|
+
emailSenderConfig: {
|
|
20
|
+
provider: 'smtp'
|
|
21
|
+
},
|
|
22
|
+
domainVerificationConfig: {},
|
|
23
|
+
verificationBaseUrl: 'https://example.com',
|
|
24
|
+
verificationFromEmail: 'verify@example.com',
|
|
25
|
+
verificationFromName: 'Verification Service'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
service = new SenderIdentityVerification(config);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('SendVerificationEmail Branches', () => {
|
|
32
|
+
it('should handle sendVerificationEmail with undefined emailSender', async () => {
|
|
33
|
+
const createResult = await service.createSender({
|
|
34
|
+
email: 'no-sender@example.com',
|
|
35
|
+
name: 'Test User',
|
|
36
|
+
provider: EmailProvider.SMTP
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Should succeed even without emailSender configured
|
|
40
|
+
expect(createResult.success).toBe(true);
|
|
41
|
+
expect(createResult.data?.status).toBe(SenderStatus.VERIFICATION_SENT);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should handle sendVerificationEmail when crypto utils not available', async () => {
|
|
45
|
+
// Service uses fallback token generation when cryptoUtils is undefined
|
|
46
|
+
const createResult = await service.createSender({
|
|
47
|
+
email: 'no-crypto@example.com',
|
|
48
|
+
name: 'Test User',
|
|
49
|
+
provider: EmailProvider.SMTP
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(createResult.success).toBe(true);
|
|
53
|
+
expect(createResult.data?.verificationToken).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should resend verification and update attempts', async () => {
|
|
57
|
+
const createResult = await service.createSender({
|
|
58
|
+
email: 'resend-test@example.com',
|
|
59
|
+
name: 'Test User',
|
|
60
|
+
provider: EmailProvider.SMTP,
|
|
61
|
+
skipVerification: true
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!createResult.success || !createResult.data) {
|
|
65
|
+
throw new Error('Setup failed');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const sender = createResult.data;
|
|
69
|
+
sender.status = SenderStatus.PENDING;
|
|
70
|
+
sender.verificationAttempts = 1;
|
|
71
|
+
|
|
72
|
+
const resendResult = await service.resendVerification(sender.id);
|
|
73
|
+
|
|
74
|
+
expect(resendResult.success).toBe(true);
|
|
75
|
+
|
|
76
|
+
// Check that attempts were incremented
|
|
77
|
+
const senderResult = await service.getSender(sender.id);
|
|
78
|
+
expect(senderResult.data?.verificationAttempts).toBeGreaterThan(1);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('CreateSender Branches', () => {
|
|
83
|
+
it('should handle createSender without isDefault flag', async () => {
|
|
84
|
+
const createResult = await service.createSender({
|
|
85
|
+
email: 'no-default@example.com',
|
|
86
|
+
name: 'Test User',
|
|
87
|
+
provider: EmailProvider.SMTP,
|
|
88
|
+
skipVerification: true
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(createResult.success).toBe(true);
|
|
92
|
+
expect(createResult.data?.isDefault).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should handle createSender with custom replyTo', async () => {
|
|
96
|
+
const createResult = await service.createSender({
|
|
97
|
+
email: 'custom-reply@example.com',
|
|
98
|
+
name: 'Test User',
|
|
99
|
+
replyToEmail: 'reply@example.com',
|
|
100
|
+
replyToName: 'Reply User',
|
|
101
|
+
provider: EmailProvider.SMTP,
|
|
102
|
+
skipVerification: true
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(createResult.success).toBe(true);
|
|
106
|
+
expect(createResult.data?.replyToEmail).toBe('reply@example.com');
|
|
107
|
+
expect(createResult.data?.replyToName).toBe('Reply User');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle createSender with skipVerification true', async () => {
|
|
111
|
+
const createResult = await service.createSender({
|
|
112
|
+
email: 'skip-verify@example.com',
|
|
113
|
+
name: 'Test User',
|
|
114
|
+
provider: EmailProvider.SMTP,
|
|
115
|
+
skipVerification: true
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(createResult.success).toBe(true);
|
|
119
|
+
expect(createResult.data?.status).toBe(SenderStatus.VERIFIED);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should handle createSender with skipVerification false', async () => {
|
|
123
|
+
const createResult = await service.createSender({
|
|
124
|
+
email: 'dont-skip-verify@example.com',
|
|
125
|
+
name: 'Test User',
|
|
126
|
+
provider: EmailProvider.SMTP,
|
|
127
|
+
skipVerification: false
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(createResult.success).toBe(true);
|
|
131
|
+
expect(createResult.data?.status).toBe(SenderStatus.VERIFICATION_SENT);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('UpdateSender Branches', () => {
|
|
136
|
+
it('should handle updateSender with all optional fields undefined', async () => {
|
|
137
|
+
const createResult = await service.createSender({
|
|
138
|
+
email: 'update-undef@example.com',
|
|
139
|
+
name: 'Original Name',
|
|
140
|
+
provider: EmailProvider.SMTP,
|
|
141
|
+
skipVerification: true
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!createResult.success || !createResult.data) {
|
|
145
|
+
throw new Error('Setup failed');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const updateResult = await service.updateSender(createResult.data.id, {
|
|
149
|
+
// All fields undefined to test branches
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(updateResult.success).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should handle updateSender with name only', async () => {
|
|
156
|
+
const createResult = await service.createSender({
|
|
157
|
+
email: 'update-name@example.com',
|
|
158
|
+
name: 'Original Name',
|
|
159
|
+
provider: EmailProvider.SMTP,
|
|
160
|
+
skipVerification: true
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!createResult.success || !createResult.data) {
|
|
164
|
+
throw new Error('Setup failed');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const updateResult = await service.updateSender(createResult.data.id, {
|
|
168
|
+
name: 'New Name'
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(updateResult.success).toBe(true);
|
|
172
|
+
expect(updateResult.data?.name).toBe('New Name');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should handle updateSender with replyToName only', async () => {
|
|
176
|
+
const createResult = await service.createSender({
|
|
177
|
+
email: 'update-reply-name@example.com',
|
|
178
|
+
name: 'Original Name',
|
|
179
|
+
provider: EmailProvider.SMTP,
|
|
180
|
+
skipVerification: true
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (!createResult.success || !createResult.data) {
|
|
184
|
+
throw new Error('Setup failed');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const updateResult = await service.updateSender(createResult.data.id, {
|
|
188
|
+
replyToName: 'New Reply Name'
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(updateResult.success).toBe(true);
|
|
192
|
+
expect(updateResult.data?.replyToName).toBe('New Reply Name');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should handle updateSender with isDefault false', async () => {
|
|
196
|
+
const createResult = await service.createSender({
|
|
197
|
+
email: 'update-default-false@example.com',
|
|
198
|
+
name: 'Test User',
|
|
199
|
+
provider: EmailProvider.SMTP,
|
|
200
|
+
isDefault: true,
|
|
201
|
+
skipVerification: true
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (!createResult.success || !createResult.data) {
|
|
205
|
+
throw new Error('Setup failed');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const updateResult = await service.updateSender(createResult.data.id, {
|
|
209
|
+
isDefault: false
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(updateResult.success).toBe(true);
|
|
213
|
+
expect(updateResult.data?.isDefault).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should handle updateSender with isActive only', async () => {
|
|
217
|
+
const createResult = await service.createSender({
|
|
218
|
+
email: 'update-active@example.com',
|
|
219
|
+
name: 'Test User',
|
|
220
|
+
provider: EmailProvider.SMTP,
|
|
221
|
+
skipVerification: true
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
if (!createResult.success || !createResult.data) {
|
|
225
|
+
throw new Error('Setup failed');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const updateResult = await service.updateSender(createResult.data.id, {
|
|
229
|
+
isActive: false
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(updateResult.success).toBe(true);
|
|
233
|
+
expect(updateResult.data?.isActive).toBe(false);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('ListSenders Branches', () => {
|
|
238
|
+
beforeEach(async () => {
|
|
239
|
+
// Create test data
|
|
240
|
+
await service.createSender({
|
|
241
|
+
email: 'active1@example.com',
|
|
242
|
+
name: 'Active 1',
|
|
243
|
+
provider: EmailProvider.SMTP,
|
|
244
|
+
skipVerification: true
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
await service.createSender({
|
|
248
|
+
email: 'active2@example.com',
|
|
249
|
+
name: 'Active 2',
|
|
250
|
+
provider: EmailProvider.SENDGRID,
|
|
251
|
+
skipVerification: true
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should list senders without any filters', async () => {
|
|
256
|
+
const result = await service.listSenders({});
|
|
257
|
+
|
|
258
|
+
expect(result.success).toBe(true);
|
|
259
|
+
expect(result.data).toBeDefined();
|
|
260
|
+
expect(result.data!.length).toBeGreaterThan(0);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should list senders with only provider filter', async () => {
|
|
264
|
+
const result = await service.listSenders({
|
|
265
|
+
provider: EmailProvider.SMTP
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
expect(result.success).toBe(true);
|
|
269
|
+
expect(result.data!.every(s => s.provider === EmailProvider.SMTP)).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should list senders with only status filter', async () => {
|
|
273
|
+
const result = await service.listSenders({
|
|
274
|
+
status: SenderStatus.VERIFIED
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
expect(result.success).toBe(true);
|
|
278
|
+
expect(result.data!.every(s => s.status === SenderStatus.VERIFIED)).toBe(true);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should list senders with only domain filter', async () => {
|
|
282
|
+
const result = await service.listSenders({
|
|
283
|
+
domain: 'example.com'
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(result.success).toBe(true);
|
|
287
|
+
expect(result.data!.every(s => s.domain === 'example.com')).toBe(true);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should apply default limit when not specified', async () => {
|
|
291
|
+
const result = await service.listSenders({});
|
|
292
|
+
|
|
293
|
+
expect(result.success).toBe(true);
|
|
294
|
+
// Default limit is 50, but we should have fewer senders
|
|
295
|
+
expect(result.data!.length).toBeLessThanOrEqual(50);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should apply default offset when not specified', async () => {
|
|
299
|
+
const result = await service.listSenders({ limit: 1 });
|
|
300
|
+
|
|
301
|
+
expect(result.success).toBe(true);
|
|
302
|
+
expect(result.data).toBeDefined();
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe('CheckCompliance Branches', () => {
|
|
307
|
+
it('should check compliance without domain verification', async () => {
|
|
308
|
+
const createResult = await service.createSender({
|
|
309
|
+
email: 'no-domain-check@example.com',
|
|
310
|
+
name: 'Test User',
|
|
311
|
+
provider: EmailProvider.SMTP,
|
|
312
|
+
skipVerification: true
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
if (!createResult.success || !createResult.data) {
|
|
316
|
+
throw new Error('Setup failed');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const complianceResult = await service.checkCompliance(createResult.data.id);
|
|
320
|
+
|
|
321
|
+
expect(complianceResult.success).toBe(true);
|
|
322
|
+
// Without domain verification, these default to true
|
|
323
|
+
expect(complianceResult.data?.checks.domainVerified).toBe(true);
|
|
324
|
+
expect(complianceResult.data?.checks.spfValid).toBe(true);
|
|
325
|
+
expect(complianceResult.data?.checks.dkimValid).toBe(true);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should check compliance for non-SendGrid provider', async () => {
|
|
329
|
+
const createResult = await service.createSender({
|
|
330
|
+
email: 'smtp-compliance@example.com',
|
|
331
|
+
name: 'Test User',
|
|
332
|
+
provider: EmailProvider.SMTP,
|
|
333
|
+
skipVerification: true
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
if (!createResult.success || !createResult.data) {
|
|
337
|
+
throw new Error('Setup failed');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const complianceResult = await service.checkCompliance(createResult.data.id);
|
|
341
|
+
|
|
342
|
+
expect(complianceResult.success).toBe(true);
|
|
343
|
+
// Non-SendGrid providers don't get the warning about missing sender ID
|
|
344
|
+
expect(complianceResult.data?.warnings).toBeDefined();
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
describe('ResendVerification Branches', () => {
|
|
349
|
+
it('should handle resend when verificationSentAt is undefined', async () => {
|
|
350
|
+
const createResult = await service.createSender({
|
|
351
|
+
email: 'no-sent-time@example.com',
|
|
352
|
+
name: 'Test User',
|
|
353
|
+
provider: EmailProvider.SMTP,
|
|
354
|
+
skipVerification: true
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
if (!createResult.success || !createResult.data) {
|
|
358
|
+
throw new Error('Setup failed');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const sender = createResult.data;
|
|
362
|
+
sender.status = SenderStatus.PENDING;
|
|
363
|
+
sender.verificationSentAt = undefined;
|
|
364
|
+
|
|
365
|
+
const resendResult = await service.resendVerification(sender.id);
|
|
366
|
+
|
|
367
|
+
expect(resendResult.success).toBe(true);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should handle resend when verificationSentAt is old', async () => {
|
|
371
|
+
const createResult = await service.createSender({
|
|
372
|
+
email: 'old-sent-time@example.com',
|
|
373
|
+
name: 'Test User',
|
|
374
|
+
provider: EmailProvider.SMTP,
|
|
375
|
+
skipVerification: true
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
if (!createResult.success || !createResult.data) {
|
|
379
|
+
throw new Error('Setup failed');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const sender = createResult.data;
|
|
383
|
+
sender.status = SenderStatus.PENDING;
|
|
384
|
+
sender.verificationSentAt = new Date(Date.now() - 2 * 60 * 60 * 1000); // 2 hours ago
|
|
385
|
+
sender.verificationAttempts = 5;
|
|
386
|
+
|
|
387
|
+
const resendResult = await service.resendVerification(sender.id);
|
|
388
|
+
|
|
389
|
+
expect(resendResult.success).toBe(true);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe('VerifySender Branches', () => {
|
|
394
|
+
it('should handle verification with missing provider credentials (SendGrid)', async () => {
|
|
395
|
+
const noKeyConfig: SenderIdentityConfig = {
|
|
396
|
+
...config,
|
|
397
|
+
sendgridApiKey: undefined
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const noKeyService = new SenderIdentityVerification(noKeyConfig);
|
|
401
|
+
|
|
402
|
+
const createResult = await noKeyService.createSender({
|
|
403
|
+
email: 'no-sg-key@example.com',
|
|
404
|
+
name: 'Test User',
|
|
405
|
+
provider: EmailProvider.SENDGRID,
|
|
406
|
+
skipVerification: true
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
if (!createResult.success || !createResult.data) {
|
|
410
|
+
throw new Error('Setup failed');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const sender = createResult.data;
|
|
414
|
+
sender.verificationToken = 'test-token';
|
|
415
|
+
sender.status = SenderStatus.VERIFICATION_SENT;
|
|
416
|
+
|
|
417
|
+
const verifyResult = await noKeyService.verifySender('test-token');
|
|
418
|
+
|
|
419
|
+
expect(verifyResult.success).toBe(false);
|
|
420
|
+
expect(verifyResult.errors).toContain('SendGrid API key not configured');
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('should handle verification with missing SES credentials', async () => {
|
|
424
|
+
const noSesConfig: SenderIdentityConfig = {
|
|
425
|
+
...config,
|
|
426
|
+
sesAccessKey: undefined,
|
|
427
|
+
sesSecretKey: undefined,
|
|
428
|
+
sesRegion: undefined
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const noSesService = new SenderIdentityVerification(noSesConfig);
|
|
432
|
+
|
|
433
|
+
const createResult = await noSesService.createSender({
|
|
434
|
+
email: 'no-ses@example.com',
|
|
435
|
+
name: 'Test User',
|
|
436
|
+
provider: EmailProvider.SES,
|
|
437
|
+
skipVerification: true
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
if (!createResult.success || !createResult.data) {
|
|
441
|
+
throw new Error('Setup failed');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const sender = createResult.data;
|
|
445
|
+
sender.verificationToken = 'test-token';
|
|
446
|
+
sender.status = SenderStatus.VERIFICATION_SENT;
|
|
447
|
+
|
|
448
|
+
const verifyResult = await noSesService.verifySender('test-token');
|
|
449
|
+
|
|
450
|
+
expect(verifyResult.success).toBe(false);
|
|
451
|
+
expect(verifyResult.errors).toContain('SES credentials not configured');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('should handle verification with Mailgun (no API key needed)', async () => {
|
|
455
|
+
const noMailgunConfig: SenderIdentityConfig = {
|
|
456
|
+
...config,
|
|
457
|
+
mailgunApiKey: undefined
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const noMailgunService = new SenderIdentityVerification(noMailgunConfig);
|
|
461
|
+
|
|
462
|
+
const createResult = await noMailgunService.createSender({
|
|
463
|
+
email: 'no-mg@example.com',
|
|
464
|
+
name: 'Test User',
|
|
465
|
+
provider: EmailProvider.MAILGUN,
|
|
466
|
+
skipVerification: true
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
if (!createResult.success || !createResult.data) {
|
|
470
|
+
throw new Error('Setup failed');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const sender = createResult.data;
|
|
474
|
+
sender.verificationToken = 'test-token';
|
|
475
|
+
sender.status = SenderStatus.VERIFICATION_SENT;
|
|
476
|
+
|
|
477
|
+
const verifyResult = await noMailgunService.verifySender('test-token');
|
|
478
|
+
|
|
479
|
+
// Mailgun succeeds without API key
|
|
480
|
+
expect(verifyResult.success).toBe(true);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
describe('Validation Branches', () => {
|
|
485
|
+
it('should use fallback email validation when emailValidator is undefined', async () => {
|
|
486
|
+
// Service uses fallback regex when emailValidator is not available
|
|
487
|
+
const createResult = await service.createSender({
|
|
488
|
+
email: 'fallback-validation@example.com',
|
|
489
|
+
name: 'Test User',
|
|
490
|
+
provider: EmailProvider.SMTP,
|
|
491
|
+
skipVerification: true
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
expect(createResult.success).toBe(true);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('should reject invalid email with fallback validation', async () => {
|
|
498
|
+
const createResult = await service.createSender({
|
|
499
|
+
email: 'invalid@',
|
|
500
|
+
name: 'Test User',
|
|
501
|
+
provider: EmailProvider.SMTP,
|
|
502
|
+
skipVerification: true
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
expect(createResult.success).toBe(false);
|
|
506
|
+
expect(createResult.error).toContain('Invalid email');
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
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 { loadConfigFromEnv } from '../src/config';
|
|
10
|
+
|
|
11
|
+
describe('Configuration', () => {
|
|
12
|
+
const originalEnv = process.env;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
jest.resetModules();
|
|
16
|
+
process.env = { ...originalEnv };
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterAll(() => {
|
|
20
|
+
process.env = originalEnv;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('loadConfigFromEnv', () => {
|
|
24
|
+
it('should load SendGrid config from env', () => {
|
|
25
|
+
process.env.SENDGRID_API_KEY = 'test-key';
|
|
26
|
+
|
|
27
|
+
const config = loadConfigFromEnv();
|
|
28
|
+
|
|
29
|
+
expect(config.sendgridApiKey).toBe('test-key');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should load Mailgun config from env', () => {
|
|
33
|
+
process.env.MAILGUN_API_KEY = 'test-mailgun-key';
|
|
34
|
+
|
|
35
|
+
const config = loadConfigFromEnv();
|
|
36
|
+
|
|
37
|
+
expect(config.mailgunApiKey).toBe('test-mailgun-key');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should load SES config from env', () => {
|
|
41
|
+
process.env.AWS_SES_ACCESS_KEY = 'test-ses-key';
|
|
42
|
+
process.env.AWS_SES_SECRET_KEY = 'test-ses-secret';
|
|
43
|
+
process.env.AWS_SES_REGION = 'us-west-2';
|
|
44
|
+
|
|
45
|
+
const config = loadConfigFromEnv();
|
|
46
|
+
|
|
47
|
+
expect(config.sesAccessKey).toBe('test-ses-key');
|
|
48
|
+
expect(config.sesSecretKey).toBe('test-ses-secret');
|
|
49
|
+
expect(config.sesRegion).toBe('us-west-2');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should load verification config', () => {
|
|
53
|
+
process.env.SENDER_VERIFICATION_FROM_EMAIL = 'verify@example.com';
|
|
54
|
+
process.env.SENDER_VERIFICATION_FROM_NAME = 'Verification Team';
|
|
55
|
+
process.env.SENDER_VERIFICATION_BASE_URL = 'https://example.com/verify';
|
|
56
|
+
|
|
57
|
+
const config = loadConfigFromEnv();
|
|
58
|
+
|
|
59
|
+
expect(config.verificationFromEmail).toBe('verify@example.com');
|
|
60
|
+
expect(config.verificationFromName).toBe('Verification Team');
|
|
61
|
+
expect(config.verificationBaseUrl).toBe('https://example.com/verify');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should load database URL config', () => {
|
|
65
|
+
process.env.DATABASE_URL = 'postgres://localhost/test';
|
|
66
|
+
|
|
67
|
+
const config = loadConfigFromEnv();
|
|
68
|
+
|
|
69
|
+
expect(config.databaseUrl).toBe('postgres://localhost/test');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should use default values for missing optional config', () => {
|
|
73
|
+
const config = loadConfigFromEnv();
|
|
74
|
+
|
|
75
|
+
expect(config).toBeDefined();
|
|
76
|
+
expect(config.verificationFromEmail).toBe('noreply@example.com');
|
|
77
|
+
expect(config.verificationFromName).toBe('Email Verification');
|
|
78
|
+
expect(config.sendgridApiKey).toBeUndefined();
|
|
79
|
+
expect(config.mailgunApiKey).toBeUndefined();
|
|
80
|
+
expect(config.sesAccessKey).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should load all providers config together', () => {
|
|
84
|
+
process.env.SENDGRID_API_KEY = 'sg-key';
|
|
85
|
+
process.env.MAILGUN_API_KEY = 'mg-key';
|
|
86
|
+
process.env.AWS_SES_ACCESS_KEY = 'ses-key';
|
|
87
|
+
process.env.AWS_SES_SECRET_KEY = 'ses-secret';
|
|
88
|
+
process.env.AWS_SES_REGION = 'eu-west-1';
|
|
89
|
+
|
|
90
|
+
const config = loadConfigFromEnv();
|
|
91
|
+
|
|
92
|
+
expect(config.sendgridApiKey).toBe('sg-key');
|
|
93
|
+
expect(config.mailgunApiKey).toBe('mg-key');
|
|
94
|
+
expect(config.sesAccessKey).toBe('ses-key');
|
|
95
|
+
expect(config.sesSecretKey).toBe('ses-secret');
|
|
96
|
+
expect(config.sesRegion).toBe('eu-west-1');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should accept config overrides', () => {
|
|
100
|
+
const config = loadConfigFromEnv({
|
|
101
|
+
emailSenderConfig: {
|
|
102
|
+
provider: 'sendgrid',
|
|
103
|
+
apiKey: 'override-key'
|
|
104
|
+
},
|
|
105
|
+
verificationBaseUrl: 'https://override.com/verify'
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(config.emailSenderConfig.provider).toBe('sendgrid');
|
|
109
|
+
expect(config.emailSenderConfig.apiKey).toBe('override-key');
|
|
110
|
+
expect(config.verificationBaseUrl).toBe('https://override.com/verify');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should default SES region to us-east-1', () => {
|
|
114
|
+
const config = loadConfigFromEnv();
|
|
115
|
+
|
|
116
|
+
expect(config.sesRegion).toBe('us-east-1');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|