backend-manager 5.0.148 → 5.0.150
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 +63 -0
- package/CLAUDE.md +26 -0
- package/package.json +1 -1
- package/src/cli/commands/emulator.js +14 -4
- package/src/cli/commands/test.js +4 -10
- package/src/manager/cron/daily/ghostii-auto-publisher.js +25 -25
- package/src/manager/cron/frequent/abandoned-carts.js +7 -5
- package/src/manager/cron/frequent/email-queue.js +56 -0
- package/src/manager/events/auth/before-signin.js +3 -0
- package/src/manager/events/auth/on-delete.js +8 -0
- package/src/manager/events/firestore/payments-disputes/on-write.js +2 -1
- package/src/manager/events/firestore/payments-webhooks/on-write.js +9 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/send-email.js +7 -21
- package/src/manager/functions/core/actions/api/admin/get-stats.js +2 -2
- package/src/manager/functions/core/actions/api/admin/send-email.js +14 -14
- package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +21 -319
- package/src/manager/functions/core/actions/api/general/emails/general:download-app-link.js +1 -1
- package/src/manager/functions/core/actions/api/general/remove-marketing-contact.js +2 -186
- package/src/manager/functions/core/actions/api/general/send-email.js +1 -1
- package/src/manager/functions/core/actions/api/special/setup-electron-manager-client.js +2 -2
- package/src/manager/functions/core/actions/api/test/health.js +1 -0
- package/src/manager/helpers/api-manager.js +2 -2
- package/src/manager/helpers/user.js +3 -1
- package/src/manager/index.js +15 -10
- package/src/manager/libraries/email/constants.js +240 -0
- package/src/manager/libraries/email/index.js +136 -0
- package/src/manager/libraries/email/marketing/index.js +370 -0
- package/src/manager/libraries/email/providers/beehiiv.js +274 -0
- package/src/manager/libraries/email/providers/sendgrid.js +429 -0
- package/src/manager/libraries/{email.js → email/transactional/index.js} +91 -99
- package/src/manager/libraries/email/validation.js +168 -0
- package/src/manager/routes/admin/cron/post.js +3 -3
- package/src/manager/routes/admin/email/post.js +1 -1
- package/src/manager/routes/admin/stats/get.js +2 -2
- package/src/manager/routes/{app → brand}/get.js +1 -1
- package/src/manager/routes/general/email/templates/download-app-link.js +1 -1
- package/src/manager/routes/marketing/contact/delete.js +2 -165
- package/src/manager/routes/marketing/contact/post.js +42 -298
- package/src/manager/routes/marketing/contact/put.js +39 -0
- package/src/manager/routes/payments/cancel/post.js +11 -0
- package/src/manager/routes/special/electron-client/post.js +3 -3
- package/src/manager/routes/test/health/get.js +1 -0
- package/src/manager/routes/user/data-request/delete.js +2 -2
- package/src/manager/routes/user/data-request/get.js +2 -2
- package/src/manager/routes/user/data-request/post.js +2 -2
- package/src/manager/routes/user/delete.js +1 -1
- package/src/manager/routes/user/feedback/post.js +12 -8
- package/src/manager/routes/user/signup/post.js +48 -37
- package/src/manager/schemas/admin/email/post.js +4 -4
- package/src/manager/schemas/marketing/contact/delete.js +0 -1
- package/src/manager/schemas/marketing/contact/post.js +0 -1
- package/src/manager/schemas/marketing/contact/put.js +6 -0
- package/src/manager/schemas/special/electron-client/post.js +2 -2
- package/src/manager/schemas/user/feedback/post.js +2 -2
- package/src/test/run-tests.js +1 -1
- package/src/test/runner.js +22 -10
- package/src/test/test-accounts.js +9 -0
- package/src/test/utils/extended-mode-warning.js +11 -0
- package/templates/backend-manager-config.json +9 -0
- package/test/events/payments/journey-payments-cancel-endpoint.js +11 -0
- package/test/events/payments/journey-payments-trial-cancel.js +11 -0
- package/test/functions/admin/edit-post.js +2 -2
- package/test/functions/admin/write-repo-content.js +2 -2
- package/test/functions/general/add-marketing-contact.js +21 -62
- package/test/helpers/email-validation.js +420 -0
- package/test/helpers/email.js +119 -6
- package/test/helpers/marketing-lifecycle.js +121 -0
- package/test/helpers/user.js +2 -2
- package/test/routes/admin/create-post.js +2 -2
- package/test/routes/admin/post.js +2 -2
- package/test/routes/admin/repo-content.js +2 -2
- package/test/routes/marketing/contact.js +21 -61
- package/test/routes/payments/cancel.js +18 -0
- /package/src/manager/schemas/{app → brand}/get.js +0 -0
|
@@ -206,48 +206,9 @@ module.exports = {
|
|
|
206
206
|
},
|
|
207
207
|
},
|
|
208
208
|
|
|
209
|
-
// Test 7:
|
|
209
|
+
// Test 7: Mailbox verification (only runs if TEST_EXTENDED_MODE and ZEROBOUNCE_API_KEY are set)
|
|
210
210
|
{
|
|
211
|
-
name: '
|
|
212
|
-
auth: 'admin',
|
|
213
|
-
timeout: 30000,
|
|
214
|
-
|
|
215
|
-
async run({ http, assert, config, state }) {
|
|
216
|
-
const testEmail = TEST_EMAILS.valid(config.domain);
|
|
217
|
-
state.testEmail = testEmail;
|
|
218
|
-
|
|
219
|
-
const response = await http.command('general:add-marketing-contact', {
|
|
220
|
-
email: testEmail,
|
|
221
|
-
source: 'bem-test',
|
|
222
|
-
providers: ['sendgrid'], // Only SendGrid, not Beehiiv
|
|
223
|
-
// No firstName/lastName - should be inferred as "Rachel Greene"
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
assert.isSuccess(response, 'Add marketing contact with specific providers should succeed');
|
|
227
|
-
|
|
228
|
-
// Only check providers if TEST_EXTENDED_MODE is set (external APIs are called)
|
|
229
|
-
if (process.env.TEST_EXTENDED_MODE) {
|
|
230
|
-
// Should only have sendgrid result
|
|
231
|
-
if (response.data?.providers) {
|
|
232
|
-
assert.hasProperty(response.data.providers, 'sendgrid', 'Should have SendGrid result');
|
|
233
|
-
}
|
|
234
|
-
state.sendgridAdded = response.data?.providers?.sendgrid?.success;
|
|
235
|
-
// Beehiiv not called since we only specified sendgrid
|
|
236
|
-
}
|
|
237
|
-
},
|
|
238
|
-
|
|
239
|
-
async cleanup({ state, http }) {
|
|
240
|
-
if (!process.env.TEST_EXTENDED_MODE || !state.testEmail) {
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
await http.command('general:remove-marketing-contact', { email: state.testEmail });
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
|
|
248
|
-
// Test 8: ZeroBounce validation (only runs if TEST_EXTENDED_MODE and ZEROBOUNCE_API_KEY are set)
|
|
249
|
-
{
|
|
250
|
-
name: 'zerobounce-validation',
|
|
211
|
+
name: 'mailbox-validation',
|
|
251
212
|
auth: 'admin',
|
|
252
213
|
timeout: 30000,
|
|
253
214
|
skip: !process.env.TEST_EXTENDED_MODE || !process.env.ZEROBOUNCE_API_KEY
|
|
@@ -261,7 +222,6 @@ module.exports = {
|
|
|
261
222
|
const response = await http.command('general:add-marketing-contact', {
|
|
262
223
|
email: testEmail,
|
|
263
224
|
source: 'bem-test',
|
|
264
|
-
// No firstName/lastName - should be inferred as "Rachel Greene"
|
|
265
225
|
});
|
|
266
226
|
|
|
267
227
|
assert.isSuccess(response, 'Add marketing contact should succeed');
|
|
@@ -270,17 +230,17 @@ module.exports = {
|
|
|
270
230
|
assert.hasProperty(response, 'data.validation', 'Response should contain validation');
|
|
271
231
|
assert.hasProperty(response, 'data.validation.checks', 'Validation should contain checks');
|
|
272
232
|
|
|
273
|
-
//
|
|
274
|
-
assert.hasProperty(response, 'data.validation.checks.
|
|
233
|
+
// Mailbox check should be in checks when key is set
|
|
234
|
+
assert.hasProperty(response, 'data.validation.checks.mailbox', 'Should have mailbox check');
|
|
275
235
|
|
|
276
|
-
const
|
|
236
|
+
const mbResult = response.data.validation.checks.mailbox;
|
|
277
237
|
|
|
278
|
-
// If
|
|
279
|
-
if (
|
|
280
|
-
skip('
|
|
238
|
+
// If out of credits, skip test - not a failure
|
|
239
|
+
if (mbResult.error?.includes('out of credits')) {
|
|
240
|
+
skip('Mailbox verification out of credits');
|
|
281
241
|
}
|
|
282
242
|
|
|
283
|
-
assert.hasProperty(
|
|
243
|
+
assert.hasProperty(mbResult, 'status', 'Mailbox check should return status');
|
|
284
244
|
|
|
285
245
|
state.sendgridAdded = response.data?.providers?.sendgrid?.success;
|
|
286
246
|
state.beehiivAdded = response.data?.providers?.beehiiv?.success;
|
|
@@ -295,9 +255,9 @@ module.exports = {
|
|
|
295
255
|
},
|
|
296
256
|
},
|
|
297
257
|
|
|
298
|
-
// Test 9:
|
|
258
|
+
// Test 9: Mailbox verification rejects invalid email (only runs if TEST_EXTENDED_MODE and ZEROBOUNCE_API_KEY are set)
|
|
299
259
|
{
|
|
300
|
-
name: '
|
|
260
|
+
name: 'mailbox-rejects-invalid',
|
|
301
261
|
auth: 'admin',
|
|
302
262
|
timeout: 30000,
|
|
303
263
|
skip: !process.env.TEST_EXTENDED_MODE || !process.env.ZEROBOUNCE_API_KEY
|
|
@@ -305,30 +265,29 @@ module.exports = {
|
|
|
305
265
|
: false,
|
|
306
266
|
|
|
307
267
|
async run({ http, assert, skip }) {
|
|
308
|
-
// Use fake email that
|
|
268
|
+
// Use fake email that mailbox verification should flag as invalid
|
|
309
269
|
const testEmail = TEST_EMAILS.invalid();
|
|
310
270
|
|
|
311
271
|
const response = await http.command('general:add-marketing-contact', {
|
|
312
272
|
email: testEmail,
|
|
313
273
|
source: 'bem-test',
|
|
314
|
-
// No firstName/lastName - AI will try to infer from "test"
|
|
315
274
|
});
|
|
316
275
|
|
|
317
|
-
// Should still succeed (we fail open) but
|
|
276
|
+
// Should still succeed (we fail open) but mailbox should report invalid
|
|
318
277
|
assert.isSuccess(response, 'Request should succeed even with invalid email');
|
|
319
278
|
|
|
320
|
-
const
|
|
279
|
+
const mbResult = response.data?.validation?.checks?.mailbox;
|
|
321
280
|
|
|
322
|
-
// If
|
|
323
|
-
if (
|
|
324
|
-
skip('
|
|
281
|
+
// If out of credits, skip test - not a failure
|
|
282
|
+
if (mbResult?.error?.includes('out of credits')) {
|
|
283
|
+
skip('Mailbox verification out of credits');
|
|
325
284
|
}
|
|
326
285
|
|
|
327
|
-
//
|
|
328
|
-
if (
|
|
329
|
-
assert.hasProperty(
|
|
286
|
+
// Mailbox should return a status indicating the email is not valid
|
|
287
|
+
if (mbResult) {
|
|
288
|
+
assert.hasProperty(mbResult, 'status', 'Should have status');
|
|
330
289
|
// Status should NOT be 'valid' for this fake email
|
|
331
|
-
assert.notEqual(
|
|
290
|
+
assert.notEqual(mbResult.status, 'valid', 'Fake email should not be marked valid');
|
|
332
291
|
}
|
|
333
292
|
},
|
|
334
293
|
},
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test: Email validation library (libraries/email/validation.js)
|
|
3
|
+
* Unit tests for format, local part, disposable domain, and ZeroBounce checks
|
|
4
|
+
*
|
|
5
|
+
* Format, local part, and disposable tests always run (free, regex-based).
|
|
6
|
+
* Mailbox verification tests require TEST_EXTENDED_MODE + ZEROBOUNCE_API_KEY.
|
|
7
|
+
*/
|
|
8
|
+
const { validate, DEFAULT_CHECKS, ALL_CHECKS } = require('../../src/manager/libraries/email/validation.js');
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
description: 'Email validation',
|
|
12
|
+
type: 'group',
|
|
13
|
+
tests: [
|
|
14
|
+
// --- Format checks ---
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
name: 'format-valid-email-passes',
|
|
18
|
+
timeout: 5000,
|
|
19
|
+
|
|
20
|
+
async run({ assert }) {
|
|
21
|
+
const result = await validate('rachel.greene@gmail.com');
|
|
22
|
+
|
|
23
|
+
assert.equal(result.valid, true, 'Valid email should pass');
|
|
24
|
+
assert.propertyEquals(result, 'checks.format.valid', true, 'Format check should pass');
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
name: 'format-no-at-sign-fails',
|
|
30
|
+
timeout: 5000,
|
|
31
|
+
|
|
32
|
+
async run({ assert }) {
|
|
33
|
+
const result = await validate('not-a-valid-email');
|
|
34
|
+
|
|
35
|
+
assert.equal(result.valid, false, 'Missing @ should fail');
|
|
36
|
+
assert.propertyEquals(result, 'checks.format.valid', false, 'Format check should fail');
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
{
|
|
41
|
+
name: 'format-no-domain-fails',
|
|
42
|
+
timeout: 5000,
|
|
43
|
+
|
|
44
|
+
async run({ assert }) {
|
|
45
|
+
const result = await validate('user@');
|
|
46
|
+
|
|
47
|
+
assert.equal(result.valid, false, 'Missing domain should fail');
|
|
48
|
+
assert.propertyEquals(result, 'checks.format.valid', false, 'Format check should fail');
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
{
|
|
53
|
+
name: 'format-empty-string-fails',
|
|
54
|
+
timeout: 5000,
|
|
55
|
+
|
|
56
|
+
async run({ assert }) {
|
|
57
|
+
const result = await validate('');
|
|
58
|
+
|
|
59
|
+
assert.equal(result.valid, false, 'Empty string should fail');
|
|
60
|
+
assert.propertyEquals(result, 'checks.format.valid', false, 'Format check should fail');
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
name: 'format-spaces-fails',
|
|
66
|
+
timeout: 5000,
|
|
67
|
+
|
|
68
|
+
async run({ assert }) {
|
|
69
|
+
const result = await validate('user name@gmail.com');
|
|
70
|
+
|
|
71
|
+
assert.equal(result.valid, false, 'Spaces should fail');
|
|
72
|
+
assert.propertyEquals(result, 'checks.format.valid', false, 'Format check should fail');
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// --- Local part checks ---
|
|
77
|
+
|
|
78
|
+
{
|
|
79
|
+
name: 'localpart-test-blocked',
|
|
80
|
+
timeout: 5000,
|
|
81
|
+
|
|
82
|
+
async run({ assert }) {
|
|
83
|
+
const result = await validate('test@gmail.com');
|
|
84
|
+
|
|
85
|
+
assert.equal(result.valid, false, '"test" local part should be blocked');
|
|
86
|
+
assert.propertyEquals(result, 'checks.localPart.blocked', true, 'Should be flagged as blocked');
|
|
87
|
+
assert.propertyEquals(result, 'checks.localPart.localPart', 'test', 'Should include the local part');
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
{
|
|
92
|
+
name: 'localpart-noreply-blocked',
|
|
93
|
+
timeout: 5000,
|
|
94
|
+
|
|
95
|
+
async run({ assert }) {
|
|
96
|
+
const result = await validate('noreply@company.com');
|
|
97
|
+
|
|
98
|
+
assert.equal(result.valid, false, '"noreply" local part should be blocked');
|
|
99
|
+
assert.propertyEquals(result, 'checks.localPart.blocked', true, 'Should be flagged as blocked');
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
{
|
|
104
|
+
name: 'localpart-admin-blocked',
|
|
105
|
+
timeout: 5000,
|
|
106
|
+
|
|
107
|
+
async run({ assert }) {
|
|
108
|
+
const result = await validate('admin@company.com');
|
|
109
|
+
|
|
110
|
+
assert.equal(result.valid, false, '"admin" local part should be blocked');
|
|
111
|
+
assert.propertyEquals(result, 'checks.localPart.blocked', true, 'Should be flagged as blocked');
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
name: 'localpart-all-numeric-blocked',
|
|
117
|
+
timeout: 5000,
|
|
118
|
+
|
|
119
|
+
async run({ assert }) {
|
|
120
|
+
const result = await validate('123456@gmail.com');
|
|
121
|
+
|
|
122
|
+
assert.equal(result.valid, false, 'All-numeric local part should be blocked');
|
|
123
|
+
assert.propertyEquals(result, 'checks.localPart.blocked', true, 'Should be flagged as blocked');
|
|
124
|
+
assert.propertyEquals(result, 'checks.localPart.reason', 'Matches junk pattern', 'Should match junk pattern');
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
{
|
|
129
|
+
name: 'localpart-repeating-chars-blocked',
|
|
130
|
+
timeout: 5000,
|
|
131
|
+
|
|
132
|
+
async run({ assert }) {
|
|
133
|
+
const result = await validate('aaaa@gmail.com');
|
|
134
|
+
|
|
135
|
+
assert.equal(result.valid, false, 'Repeating chars should be blocked');
|
|
136
|
+
assert.propertyEquals(result, 'checks.localPart.blocked', true, 'Should be flagged as blocked');
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
{
|
|
141
|
+
name: 'localpart-keyboard-walk-blocked',
|
|
142
|
+
timeout: 5000,
|
|
143
|
+
|
|
144
|
+
async run({ assert }) {
|
|
145
|
+
const result = await validate('asdf@gmail.com');
|
|
146
|
+
|
|
147
|
+
assert.equal(result.valid, false, 'Keyboard walk should be blocked');
|
|
148
|
+
assert.propertyEquals(result, 'checks.localPart.blocked', true, 'Should be flagged as blocked');
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
{
|
|
153
|
+
name: 'localpart-test-prefix-blocked',
|
|
154
|
+
timeout: 5000,
|
|
155
|
+
|
|
156
|
+
async run({ assert }) {
|
|
157
|
+
const result = await validate('test.user@gmail.com');
|
|
158
|
+
|
|
159
|
+
assert.equal(result.valid, false, '"test." prefix should be blocked');
|
|
160
|
+
assert.propertyEquals(result, 'checks.localPart.blocked', true, 'Should be flagged as blocked');
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
{
|
|
165
|
+
name: 'localpart-letter-plus-numbers-blocked',
|
|
166
|
+
timeout: 5000,
|
|
167
|
+
|
|
168
|
+
async run({ assert }) {
|
|
169
|
+
const result = await validate('a123@gmail.com');
|
|
170
|
+
|
|
171
|
+
assert.equal(result.valid, false, 'Single letter + numbers should be blocked');
|
|
172
|
+
assert.propertyEquals(result, 'checks.localPart.blocked', true, 'Should be flagged as blocked');
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
{
|
|
177
|
+
name: 'localpart-plus-suffix-stripped-before-check',
|
|
178
|
+
timeout: 5000,
|
|
179
|
+
|
|
180
|
+
async run({ assert }) {
|
|
181
|
+
// "test+something" → strips to "test" → blocked
|
|
182
|
+
const result = await validate('test+newsletter@gmail.com');
|
|
183
|
+
|
|
184
|
+
assert.equal(result.valid, false, '"test+suffix" should still be blocked (strips +suffix first)');
|
|
185
|
+
assert.propertyEquals(result, 'checks.localPart.blocked', true, 'Should be blocked after stripping suffix');
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
{
|
|
190
|
+
name: 'localpart-bem-suffix-allowed-on-real-names',
|
|
191
|
+
timeout: 5000,
|
|
192
|
+
|
|
193
|
+
async run({ assert }) {
|
|
194
|
+
// "rachel.greene+bem" → strips to "rachel.greene" → allowed
|
|
195
|
+
const result = await validate('rachel.greene+bem@gmail.com');
|
|
196
|
+
|
|
197
|
+
assert.equal(result.valid, true, 'Real name with +bem suffix should pass');
|
|
198
|
+
assert.propertyEquals(result, 'checks.localPart.valid', true, 'Local part check should pass');
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
{
|
|
203
|
+
name: 'localpart-real-name-passes',
|
|
204
|
+
timeout: 5000,
|
|
205
|
+
|
|
206
|
+
async run({ assert }) {
|
|
207
|
+
const result = await validate('john.smith@company.com');
|
|
208
|
+
|
|
209
|
+
assert.equal(result.valid, true, 'Real name should pass');
|
|
210
|
+
assert.propertyEquals(result, 'checks.localPart.valid', true, 'Local part check should pass');
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
{
|
|
215
|
+
name: 'localpart-single-real-name-passes',
|
|
216
|
+
timeout: 5000,
|
|
217
|
+
|
|
218
|
+
async run({ assert }) {
|
|
219
|
+
const result = await validate('rachel@company.com');
|
|
220
|
+
|
|
221
|
+
assert.equal(result.valid, true, 'Single real name should pass');
|
|
222
|
+
assert.propertyEquals(result, 'checks.localPart.valid', true, 'Local part check should pass');
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
// --- Disposable domain checks ---
|
|
227
|
+
|
|
228
|
+
{
|
|
229
|
+
name: 'disposable-mailinator-blocked',
|
|
230
|
+
timeout: 5000,
|
|
231
|
+
|
|
232
|
+
async run({ assert }) {
|
|
233
|
+
const result = await validate('rachel.greene@mailinator.com');
|
|
234
|
+
|
|
235
|
+
assert.equal(result.valid, false, 'Mailinator should be invalid');
|
|
236
|
+
assert.propertyEquals(result, 'checks.disposable.blocked', true, 'Should be flagged as blocked');
|
|
237
|
+
assert.propertyEquals(result, 'checks.disposable.domain', 'mailinator.com', 'Should include blocked domain');
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
{
|
|
242
|
+
name: 'disposable-guerrillamail-blocked',
|
|
243
|
+
timeout: 5000,
|
|
244
|
+
|
|
245
|
+
async run({ assert }) {
|
|
246
|
+
const result = await validate('rachel.greene@guerrillamail.com');
|
|
247
|
+
|
|
248
|
+
assert.equal(result.valid, false, 'GuerrillaMail should be invalid');
|
|
249
|
+
assert.propertyEquals(result, 'checks.disposable.blocked', true, 'Should be flagged as blocked');
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
{
|
|
254
|
+
name: 'disposable-tempmail-blocked',
|
|
255
|
+
timeout: 5000,
|
|
256
|
+
|
|
257
|
+
async run({ assert }) {
|
|
258
|
+
const result = await validate('rachel.greene@temp-mail.org');
|
|
259
|
+
|
|
260
|
+
assert.equal(result.valid, false, 'temp-mail.org should be invalid');
|
|
261
|
+
assert.propertyEquals(result, 'checks.disposable.blocked', true, 'Should be flagged as blocked');
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
{
|
|
266
|
+
name: 'valid-gmail-passes-disposable',
|
|
267
|
+
timeout: 5000,
|
|
268
|
+
|
|
269
|
+
async run({ assert }) {
|
|
270
|
+
const result = await validate('rachel.greene@gmail.com');
|
|
271
|
+
|
|
272
|
+
assert.equal(result.valid, true, 'Gmail should be valid');
|
|
273
|
+
assert.propertyEquals(result, 'checks.disposable.valid', true, 'Should pass disposable check');
|
|
274
|
+
assert.propertyEquals(result, 'checks.disposable.blocked', false, 'Should not be blocked');
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
{
|
|
279
|
+
name: 'valid-custom-domain-passes',
|
|
280
|
+
timeout: 5000,
|
|
281
|
+
|
|
282
|
+
async run({ assert }) {
|
|
283
|
+
const result = await validate('ian@somiibo.com');
|
|
284
|
+
|
|
285
|
+
assert.equal(result.valid, true, 'Custom domain should be valid');
|
|
286
|
+
assert.propertyEquals(result, 'checks.disposable.valid', true, 'Should pass disposable check');
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
// --- Selective checks ---
|
|
291
|
+
|
|
292
|
+
{
|
|
293
|
+
name: 'checks-format-only',
|
|
294
|
+
timeout: 5000,
|
|
295
|
+
|
|
296
|
+
async run({ assert }) {
|
|
297
|
+
// "test@gmail.com" normally blocked by localPart, but only running format
|
|
298
|
+
const result = await validate('test@gmail.com', { checks: ['format'] });
|
|
299
|
+
|
|
300
|
+
assert.equal(result.valid, true, 'Should pass with only format check');
|
|
301
|
+
assert.propertyEquals(result, 'checks.format.valid', true, 'Format should pass');
|
|
302
|
+
assert.equal(result.checks.localPart, undefined, 'localPart should not run');
|
|
303
|
+
assert.equal(result.checks.disposable, undefined, 'disposable should not run');
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
{
|
|
308
|
+
name: 'checks-format-and-disposable-skips-localpart',
|
|
309
|
+
timeout: 5000,
|
|
310
|
+
|
|
311
|
+
async run({ assert }) {
|
|
312
|
+
// "test@gmail.com" would be blocked by localPart, but we only run format + disposable
|
|
313
|
+
const result = await validate('test@gmail.com', { checks: ['format', 'disposable'] });
|
|
314
|
+
|
|
315
|
+
assert.equal(result.valid, true, 'Should pass without localPart check');
|
|
316
|
+
assert.propertyEquals(result, 'checks.format.valid', true, 'Format should pass');
|
|
317
|
+
assert.propertyEquals(result, 'checks.disposable.blocked', false, 'Disposable should pass');
|
|
318
|
+
assert.equal(result.checks.localPart, undefined, 'localPart should not run');
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
{
|
|
323
|
+
name: 'checks-format-and-disposable-still-blocks-disposable',
|
|
324
|
+
timeout: 5000,
|
|
325
|
+
|
|
326
|
+
async run({ assert }) {
|
|
327
|
+
const result = await validate('rachel.greene@mailinator.com', { checks: ['format', 'disposable'] });
|
|
328
|
+
|
|
329
|
+
assert.equal(result.valid, false, 'Disposable should still fail');
|
|
330
|
+
assert.propertyEquals(result, 'checks.disposable.blocked', true, 'Should be blocked');
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
{
|
|
335
|
+
name: 'checks-default-matches-expected',
|
|
336
|
+
timeout: 5000,
|
|
337
|
+
|
|
338
|
+
async run({ assert }) {
|
|
339
|
+
assert.deepEqual(DEFAULT_CHECKS, ['format', 'disposable', 'localPart'], 'DEFAULT_CHECKS should be format + disposable + localPart');
|
|
340
|
+
assert.deepEqual(ALL_CHECKS, ['format', 'disposable', 'localPart', 'mailbox'], 'ALL_CHECKS should include mailbox');
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
// --- Mailbox verification behavior ---
|
|
345
|
+
|
|
346
|
+
{
|
|
347
|
+
name: 'mailbox-not-in-default-checks',
|
|
348
|
+
timeout: 5000,
|
|
349
|
+
|
|
350
|
+
async run({ assert }) {
|
|
351
|
+
const result = await validate('rachel.greene@gmail.com');
|
|
352
|
+
|
|
353
|
+
assert.equal(result.valid, true, 'Should be valid');
|
|
354
|
+
assert.equal(result.checks.mailbox, undefined, 'Mailbox should not run with default checks');
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
{
|
|
359
|
+
name: 'mailbox-skipped-without-api-key',
|
|
360
|
+
timeout: 5000,
|
|
361
|
+
|
|
362
|
+
async run({ assert }) {
|
|
363
|
+
const originalKey = process.env.ZEROBOUNCE_API_KEY;
|
|
364
|
+
delete process.env.ZEROBOUNCE_API_KEY;
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const result = await validate('rachel.greene@gmail.com', { checks: ALL_CHECKS });
|
|
368
|
+
|
|
369
|
+
assert.equal(result.valid, true, 'Should still be valid');
|
|
370
|
+
assert.hasProperty(result, 'checks.mailbox', 'Should have mailbox check');
|
|
371
|
+
assert.propertyEquals(result, 'checks.mailbox.skipped', true, 'Should be marked as skipped');
|
|
372
|
+
} finally {
|
|
373
|
+
if (originalKey) {
|
|
374
|
+
process.env.ZEROBOUNCE_API_KEY = originalKey;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
// --- Mailbox verification API checks (require TEST_EXTENDED_MODE + ZEROBOUNCE_API_KEY) ---
|
|
381
|
+
|
|
382
|
+
{
|
|
383
|
+
name: 'mailbox-valid-email-passes',
|
|
384
|
+
timeout: 15000,
|
|
385
|
+
skip: !process.env.TEST_EXTENDED_MODE || !process.env.ZEROBOUNCE_API_KEY
|
|
386
|
+
? 'TEST_EXTENDED_MODE or ZEROBOUNCE_API_KEY not set'
|
|
387
|
+
: false,
|
|
388
|
+
|
|
389
|
+
async run({ assert, skip }) {
|
|
390
|
+
const result = await validate('disposable@gmail.com', { checks: ALL_CHECKS });
|
|
391
|
+
|
|
392
|
+
if (result.checks.mailbox?.error?.includes('out of credits')) {
|
|
393
|
+
skip('Mailbox verification out of credits');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
assert.hasProperty(result, 'checks.mailbox', 'Should have mailbox check');
|
|
397
|
+
assert.hasProperty(result, 'checks.mailbox.status', 'Should have status');
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
{
|
|
402
|
+
name: 'mailbox-fake-domain-fails',
|
|
403
|
+
timeout: 15000,
|
|
404
|
+
skip: !process.env.TEST_EXTENDED_MODE || !process.env.ZEROBOUNCE_API_KEY
|
|
405
|
+
? 'TEST_EXTENDED_MODE or ZEROBOUNCE_API_KEY not set'
|
|
406
|
+
: false,
|
|
407
|
+
|
|
408
|
+
async run({ assert, skip }) {
|
|
409
|
+
const result = await validate('rachel.greene@thisfakedomain99999.com', { checks: ALL_CHECKS });
|
|
410
|
+
|
|
411
|
+
if (result.checks.mailbox?.error?.includes('out of credits')) {
|
|
412
|
+
skip('Mailbox verification out of credits');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
assert.hasProperty(result, 'checks.mailbox.status', 'Should have status');
|
|
416
|
+
assert.notEqual(result.checks.mailbox.status, 'valid', 'Fake domain should not be valid');
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
};
|