@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.
Files changed (51) hide show
  1. package/.env.test +8 -0
  2. package/.eslintrc.js +30 -0
  3. package/README.md +376 -0
  4. package/__tests__/SenderIdentityVerification.test.ts +461 -0
  5. package/__tests__/__mocks__/fetch-mock.ts +156 -0
  6. package/__tests__/additional-coverage.test.ts +129 -0
  7. package/__tests__/additional-error-coverage.test.ts +483 -0
  8. package/__tests__/branch-coverage.test.ts +509 -0
  9. package/__tests__/config.test.ts +119 -0
  10. package/__tests__/error-handling.test.ts +321 -0
  11. package/__tests__/final-branch-coverage.test.ts +372 -0
  12. package/__tests__/integration.real-api.test.ts +295 -0
  13. package/__tests__/providers.test.ts +331 -0
  14. package/__tests__/service-coverage.test.ts +412 -0
  15. package/dist/SenderIdentityVerification.d.ts +72 -0
  16. package/dist/SenderIdentityVerification.js +643 -0
  17. package/dist/config.d.ts +31 -0
  18. package/dist/config.js +38 -0
  19. package/dist/errors.d.ts +27 -0
  20. package/dist/errors.js +61 -0
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.js +21 -0
  23. package/dist/providers/MailgunProvider.d.ts +13 -0
  24. package/dist/providers/MailgunProvider.js +35 -0
  25. package/dist/providers/SESProvider.d.ts +12 -0
  26. package/dist/providers/SESProvider.js +47 -0
  27. package/dist/providers/SMTPProvider.d.ts +12 -0
  28. package/dist/providers/SMTPProvider.js +30 -0
  29. package/dist/providers/SendGridProvider.d.ts +19 -0
  30. package/dist/providers/SendGridProvider.js +98 -0
  31. package/dist/templates/verification-email.d.ts +9 -0
  32. package/dist/templates/verification-email.js +67 -0
  33. package/dist/types.d.ts +139 -0
  34. package/dist/types.js +33 -0
  35. package/dist/utils/domain-extractor.d.ts +4 -0
  36. package/dist/utils/domain-extractor.js +20 -0
  37. package/jest.config.cjs +33 -0
  38. package/package.json +60 -0
  39. package/src/SenderIdentityVerification.ts +796 -0
  40. package/src/config.ts +81 -0
  41. package/src/errors.ts +64 -0
  42. package/src/global.d.ts +24 -0
  43. package/src/index.ts +24 -0
  44. package/src/providers/MailgunProvider.ts +35 -0
  45. package/src/providers/SESProvider.ts +51 -0
  46. package/src/providers/SMTPProvider.ts +29 -0
  47. package/src/providers/SendGridProvider.ts +108 -0
  48. package/src/templates/verification-email.ts +67 -0
  49. package/src/types.ts +163 -0
  50. package/src/utils/domain-extractor.ts +18 -0
  51. 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
+ });