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
package/test/helpers/email.js
CHANGED
|
@@ -76,7 +76,7 @@ module.exports = {
|
|
|
76
76
|
async run({ http, assert }) {
|
|
77
77
|
const response = await http.post('admin/email', {
|
|
78
78
|
subject: 'BEM Test Email - Bad UID',
|
|
79
|
-
to: '
|
|
79
|
+
to: 'nonexistent_uid_12345',
|
|
80
80
|
copy: false,
|
|
81
81
|
});
|
|
82
82
|
|
|
@@ -141,7 +141,7 @@ module.exports = {
|
|
|
141
141
|
async run({ http, assert, accounts }) {
|
|
142
142
|
const response = await http.post('admin/email', {
|
|
143
143
|
subject: 'BEM Test Email - UID Recipient',
|
|
144
|
-
to:
|
|
144
|
+
to: accounts.admin.uid,
|
|
145
145
|
copy: false,
|
|
146
146
|
data: {
|
|
147
147
|
email: {
|
|
@@ -167,7 +167,7 @@ module.exports = {
|
|
|
167
167
|
to: [
|
|
168
168
|
`_test-receiver@${config.domain}`,
|
|
169
169
|
{ email: `_test-receiver-2@${config.domain}`, name: 'Receiver 2' },
|
|
170
|
-
|
|
170
|
+
accounts.admin.uid,
|
|
171
171
|
],
|
|
172
172
|
copy: false,
|
|
173
173
|
data: {
|
|
@@ -378,18 +378,131 @@ module.exports = {
|
|
|
378
378
|
assert.isSuccess(response, 'Should send email');
|
|
379
379
|
assert.equal(response.data.status, 'sent', 'Status should be sent');
|
|
380
380
|
|
|
381
|
-
const
|
|
381
|
+
const brandImages = response.data.options.dynamicTemplateData.brand.images;
|
|
382
382
|
|
|
383
383
|
// Any image that was an SVG should now be a PNG (-x.svg → -1024.png)
|
|
384
|
-
for (const [key, value] of Object.entries(
|
|
384
|
+
for (const [key, value] of Object.entries(brandImages)) {
|
|
385
385
|
assert.ok(
|
|
386
386
|
!String(value || '').endsWith('.svg'),
|
|
387
|
-
`
|
|
387
|
+
`brand.images.${key} should not be an SVG (got: ${value})`,
|
|
388
388
|
);
|
|
389
389
|
}
|
|
390
390
|
},
|
|
391
391
|
},
|
|
392
392
|
|
|
393
|
+
// --- Sender Resolution ---
|
|
394
|
+
|
|
395
|
+
{
|
|
396
|
+
name: 'sender-orders-resolves-from-and-asm',
|
|
397
|
+
auth: 'admin',
|
|
398
|
+
timeout: 30000,
|
|
399
|
+
|
|
400
|
+
async run({ http, assert, config }) {
|
|
401
|
+
const response = await http.post('admin/email', {
|
|
402
|
+
subject: 'BEM Test Email - Sender Orders',
|
|
403
|
+
to: `_test-receiver@${config.domain}`,
|
|
404
|
+
sender: 'orders',
|
|
405
|
+
copy: false,
|
|
406
|
+
data: {
|
|
407
|
+
email: {
|
|
408
|
+
subject: 'BEM Test Email - Sender Orders',
|
|
409
|
+
body: 'Testing sender resolution for orders.',
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
assert.isSuccess(response, 'Should send email with orders sender');
|
|
415
|
+
assert.equal(response.data.status, 'sent', 'Status should be sent');
|
|
416
|
+
assert.ok(response.data.options.from.email.startsWith('orders@'), 'From email should start with orders@');
|
|
417
|
+
assert.ok(response.data.options.from.name.includes('Orders'), 'From name should include Orders');
|
|
418
|
+
assert.ok(response.data.options.asm, 'Should have ASM group');
|
|
419
|
+
assert.ok(response.data.options.replyTo.startsWith('orders@'), 'replyTo should match from address');
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
{
|
|
424
|
+
name: 'sender-security-resolves-from-asm-and-unsubscribe',
|
|
425
|
+
auth: 'admin',
|
|
426
|
+
timeout: 30000,
|
|
427
|
+
|
|
428
|
+
async run({ http, assert, config }) {
|
|
429
|
+
const response = await http.post('admin/email', {
|
|
430
|
+
subject: 'BEM Test Email - Sender Security',
|
|
431
|
+
to: `_test-receiver@${config.domain}`,
|
|
432
|
+
sender: 'security',
|
|
433
|
+
copy: false,
|
|
434
|
+
data: {
|
|
435
|
+
email: {
|
|
436
|
+
subject: 'BEM Test Email - Sender Security',
|
|
437
|
+
body: 'Testing that security sender resolves correctly.',
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
assert.isSuccess(response, 'Should send email with security sender');
|
|
443
|
+
assert.equal(response.data.status, 'sent', 'Status should be sent');
|
|
444
|
+
assert.ok(response.data.options.from.email.startsWith('security@'), 'From email should start with security@');
|
|
445
|
+
assert.ok(response.data.options.asm, 'Should have ASM group');
|
|
446
|
+
assert.ok(response.data.options.headers['List-Unsubscribe'], 'Should have List-Unsubscribe header');
|
|
447
|
+
assert.ok(response.data.options.dynamicTemplateData.email.unsubscribeUrl, 'Should have unsubscribeUrl');
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
{
|
|
452
|
+
name: 'sender-explicit-from-overrides-sender',
|
|
453
|
+
auth: 'admin',
|
|
454
|
+
timeout: 30000,
|
|
455
|
+
|
|
456
|
+
async run({ http, assert, config }) {
|
|
457
|
+
const customFrom = { email: `custom@${config.domain}`, name: 'Custom Sender' };
|
|
458
|
+
|
|
459
|
+
const response = await http.post('admin/email', {
|
|
460
|
+
subject: 'BEM Test Email - From Override',
|
|
461
|
+
to: `_test-receiver@${config.domain}`,
|
|
462
|
+
sender: 'orders',
|
|
463
|
+
from: customFrom,
|
|
464
|
+
copy: false,
|
|
465
|
+
data: {
|
|
466
|
+
email: {
|
|
467
|
+
subject: 'BEM Test Email - From Override',
|
|
468
|
+
body: 'Testing that explicit from overrides sender.',
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
assert.isSuccess(response, 'Should send email with explicit from');
|
|
474
|
+
assert.equal(response.data.status, 'sent', 'Status should be sent');
|
|
475
|
+
assert.equal(response.data.options.from.email, customFrom.email, 'Explicit from should override sender');
|
|
476
|
+
assert.equal(response.data.options.from.name, customFrom.name, 'Explicit from name should override sender');
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
{
|
|
481
|
+
name: 'sender-unknown-falls-back-to-defaults',
|
|
482
|
+
auth: 'admin',
|
|
483
|
+
timeout: 30000,
|
|
484
|
+
|
|
485
|
+
async run({ http, assert, config }) {
|
|
486
|
+
const response = await http.post('admin/email', {
|
|
487
|
+
subject: 'BEM Test Email - Unknown Sender',
|
|
488
|
+
to: `_test-receiver@${config.domain}`,
|
|
489
|
+
sender: 'nonexistent',
|
|
490
|
+
copy: false,
|
|
491
|
+
data: {
|
|
492
|
+
email: {
|
|
493
|
+
subject: 'BEM Test Email - Unknown Sender',
|
|
494
|
+
body: 'Testing that unknown sender falls back to brand defaults.',
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
assert.isSuccess(response, 'Should send email with default from');
|
|
500
|
+
assert.equal(response.data.status, 'sent', 'Status should be sent');
|
|
501
|
+
assert.ok(response.data.options.from.email, 'Should have a from email (brand default)');
|
|
502
|
+
assert.ok(response.data.options.asm, 'Should have default ASM group');
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
|
|
393
506
|
{
|
|
394
507
|
name: 'sendat-iso-string-accepted',
|
|
395
508
|
auth: 'admin',
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test: Marketing lifecycle (add → sync → remove)
|
|
3
|
+
* End-to-end suite testing the full marketing contact flow via routes
|
|
4
|
+
*
|
|
5
|
+
* Requires TEST_EXTENDED_MODE=true and SENDGRID_API_KEY / BEEHIIV_API_KEY env vars.
|
|
6
|
+
* These tests hit real external APIs.
|
|
7
|
+
*/
|
|
8
|
+
module.exports = {
|
|
9
|
+
description: 'Marketing lifecycle (add → sync → remove)',
|
|
10
|
+
type: 'suite',
|
|
11
|
+
timeout: 60000,
|
|
12
|
+
skip: !process.env.TEST_EXTENDED_MODE ? 'TEST_EXTENDED_MODE not set' : false,
|
|
13
|
+
|
|
14
|
+
tests: [
|
|
15
|
+
// Step 0: Pre-clean test contacts from providers (in case a previous run left them)
|
|
16
|
+
{
|
|
17
|
+
name: 'pre-clean-test-contacts',
|
|
18
|
+
auth: 'admin',
|
|
19
|
+
|
|
20
|
+
async run({ http, config, state }) {
|
|
21
|
+
const testEmail = `lifecycle.test+bem@${config.domain}`;
|
|
22
|
+
state.testEmail = testEmail;
|
|
23
|
+
|
|
24
|
+
// Delete from all providers — handles "not found" gracefully
|
|
25
|
+
await http.delete('marketing/contact', { email: testEmail }).catch(() => {});
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// Step 1: Add a contact via POST /marketing/contact
|
|
30
|
+
{
|
|
31
|
+
name: 'add-contact',
|
|
32
|
+
auth: 'admin',
|
|
33
|
+
|
|
34
|
+
async run({ http, assert, state }) {
|
|
35
|
+
const response = await http.post('marketing/contact', {
|
|
36
|
+
email: state.testEmail,
|
|
37
|
+
firstName: 'Lifecycle',
|
|
38
|
+
lastName: 'Test',
|
|
39
|
+
source: 'bem-test-lifecycle',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
assert.isSuccess(response, 'Add contact should succeed');
|
|
43
|
+
assert.propertyEquals(response, 'data.success', true, 'success should be true');
|
|
44
|
+
|
|
45
|
+
if (process.env.SENDGRID_API_KEY) {
|
|
46
|
+
assert.hasProperty(response, 'data.providers.sendgrid', 'Should have SendGrid result');
|
|
47
|
+
assert.propertyEquals(response, 'data.providers.sendgrid.success', true, 'SendGrid add should succeed');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (process.env.BEEHIIV_API_KEY) {
|
|
51
|
+
assert.hasProperty(response, 'data.providers.beehiiv', 'Should have Beehiiv result');
|
|
52
|
+
assert.propertyEquals(response, 'data.providers.beehiiv.success', true, 'Beehiiv add should succeed');
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Step 2: Sync by UID via PUT /marketing/contact
|
|
58
|
+
// Tests the full sync pipeline: UID resolution → buildFields → upsert with custom fields
|
|
59
|
+
{
|
|
60
|
+
name: 'sync-contact-by-uid',
|
|
61
|
+
auth: 'admin',
|
|
62
|
+
|
|
63
|
+
async run({ http, assert, accounts }) {
|
|
64
|
+
// Sync the admin test account — exercises UID→doc resolution + buildFields
|
|
65
|
+
const response = await http.put('marketing/contact', {
|
|
66
|
+
uid: accounts.admin.uid,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
assert.isSuccess(response, 'Sync contact should succeed');
|
|
70
|
+
assert.propertyEquals(response, 'data.success', true, 'success should be true');
|
|
71
|
+
|
|
72
|
+
if (process.env.SENDGRID_API_KEY) {
|
|
73
|
+
assert.hasProperty(response, 'data.providers.sendgrid', 'Should have SendGrid result');
|
|
74
|
+
assert.propertyEquals(response, 'data.providers.sendgrid.success', true, 'SendGrid sync should succeed');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (process.env.BEEHIIV_API_KEY) {
|
|
78
|
+
assert.hasProperty(response, 'data.providers.beehiiv', 'Should have Beehiiv result');
|
|
79
|
+
assert.propertyEquals(response, 'data.providers.beehiiv.success', true, 'Beehiiv sync should succeed');
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// Step 3: Remove the contact via DELETE /marketing/contact
|
|
85
|
+
{
|
|
86
|
+
name: 'remove-contact',
|
|
87
|
+
auth: 'admin',
|
|
88
|
+
|
|
89
|
+
async run({ http, assert, state }) {
|
|
90
|
+
const response = await http.delete('marketing/contact', {
|
|
91
|
+
email: state.testEmail,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
assert.isSuccess(response, 'Remove contact should succeed');
|
|
95
|
+
assert.propertyEquals(response, 'data.success', true, 'success should be true');
|
|
96
|
+
|
|
97
|
+
if (process.env.SENDGRID_API_KEY) {
|
|
98
|
+
assert.hasProperty(response, 'data.providers.sendgrid', 'Should have SendGrid result');
|
|
99
|
+
assert.propertyEquals(response, 'data.providers.sendgrid.success', true, 'SendGrid remove should succeed');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (process.env.BEEHIIV_API_KEY) {
|
|
103
|
+
assert.hasProperty(response, 'data.providers.beehiiv', 'Should have Beehiiv result');
|
|
104
|
+
assert.propertyEquals(response, 'data.providers.beehiiv.success', true, 'Beehiiv remove should succeed');
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// Step 4: Clean up the admin test contact that sync added
|
|
110
|
+
{
|
|
111
|
+
name: 'cleanup-synced-admin-contact',
|
|
112
|
+
auth: 'admin',
|
|
113
|
+
|
|
114
|
+
async run({ http, accounts }) {
|
|
115
|
+
await http.delete('marketing/contact', {
|
|
116
|
+
email: accounts.admin.email,
|
|
117
|
+
}).catch(() => {});
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
package/test/helpers/user.js
CHANGED
|
@@ -410,7 +410,7 @@ module.exports = {
|
|
|
410
410
|
const user = createUser({});
|
|
411
411
|
const expectedKeys = [
|
|
412
412
|
'auth', 'subscription', 'roles', 'flags', 'affiliate',
|
|
413
|
-
'activity', 'api', 'usage', 'personal', 'oauth2', 'attribution',
|
|
413
|
+
'activity', 'api', 'usage', 'personal', 'oauth2', 'attribution', 'metadata',
|
|
414
414
|
];
|
|
415
415
|
|
|
416
416
|
for (const key of expectedKeys) {
|
|
@@ -425,7 +425,7 @@ module.exports = {
|
|
|
425
425
|
const user = createUser({});
|
|
426
426
|
const expectedKeys = [
|
|
427
427
|
'auth', 'subscription', 'roles', 'flags', 'affiliate',
|
|
428
|
-
'activity', 'api', 'usage', 'personal', 'oauth2', 'attribution',
|
|
428
|
+
'activity', 'api', 'usage', 'personal', 'oauth2', 'attribution', 'metadata',
|
|
429
429
|
];
|
|
430
430
|
|
|
431
431
|
for (const key of Object.keys(user)) {
|
|
@@ -113,13 +113,13 @@ module.exports = {
|
|
|
113
113
|
return;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
if (!config.
|
|
116
|
+
if (!config.github?.repo_website) {
|
|
117
117
|
assert.fail('githubRepoWebsite not configured');
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
// Parse owner/repo for cleanup later
|
|
122
|
-
const repoMatch = config.
|
|
122
|
+
const repoMatch = config.github?.repo_website.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
123
123
|
if (!repoMatch) {
|
|
124
124
|
assert.fail('Could not parse githubRepoWebsite');
|
|
125
125
|
return;
|
|
@@ -115,7 +115,7 @@ module.exports = {
|
|
|
115
115
|
skip: !process.env.GITHUB_TOKEN ? 'GITHUB_TOKEN env var not set' : false,
|
|
116
116
|
|
|
117
117
|
async run({ assert, state, config }) {
|
|
118
|
-
if (!config.
|
|
118
|
+
if (!config.github?.repo_website) {
|
|
119
119
|
assert.fail('githubRepoWebsite not configured');
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
@@ -123,7 +123,7 @@ module.exports = {
|
|
|
123
123
|
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
|
|
124
124
|
|
|
125
125
|
// Parse owner/repo from githubRepoWebsite
|
|
126
|
-
const repoMatch = config.
|
|
126
|
+
const repoMatch = config.github?.repo_website.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
127
127
|
if (!repoMatch) {
|
|
128
128
|
assert.fail('Could not parse githubRepoWebsite');
|
|
129
129
|
return;
|
|
@@ -155,14 +155,14 @@ module.exports = {
|
|
|
155
155
|
timeout: 60000,
|
|
156
156
|
|
|
157
157
|
async run({ state, config }) {
|
|
158
|
-
if (!process.env.GITHUB_TOKEN || !config.
|
|
158
|
+
if (!process.env.GITHUB_TOKEN || !config.github?.repo_website) {
|
|
159
159
|
return;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
|
|
163
163
|
|
|
164
164
|
// Parse owner/repo from githubRepoWebsite (e.g., 'https://github.com/owner/repo')
|
|
165
|
-
const repoMatch = config.
|
|
165
|
+
const repoMatch = config.github?.repo_website.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
166
166
|
if (!repoMatch) {
|
|
167
167
|
return;
|
|
168
168
|
}
|
|
@@ -208,46 +208,9 @@ module.exports = {
|
|
|
208
208
|
},
|
|
209
209
|
},
|
|
210
210
|
|
|
211
|
-
// Test 7:
|
|
211
|
+
// Test 7: Mailbox verification (only runs if TEST_EXTENDED_MODE and ZEROBOUNCE_API_KEY are set)
|
|
212
212
|
{
|
|
213
|
-
name: 'add-
|
|
214
|
-
auth: 'admin',
|
|
215
|
-
timeout: 30000,
|
|
216
|
-
|
|
217
|
-
async run({ http, assert, config, state }) {
|
|
218
|
-
const testEmail = TEST_EMAILS.valid(config.domain);
|
|
219
|
-
state.testEmail = testEmail;
|
|
220
|
-
|
|
221
|
-
const response = await http.post('marketing/contact', {
|
|
222
|
-
email: testEmail,
|
|
223
|
-
source: 'bem-test',
|
|
224
|
-
providers: ['sendgrid'], // Only SendGrid, not Beehiiv
|
|
225
|
-
// No firstName/lastName - should be inferred as "Rachel Greene"
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
assert.isSuccess(response, 'Add marketing contact with specific providers should succeed');
|
|
229
|
-
|
|
230
|
-
// Provider calls only happen in extended mode
|
|
231
|
-
if (process.env.TEST_EXTENDED_MODE) {
|
|
232
|
-
// Should only have sendgrid result (since we specified providers: ['sendgrid'])
|
|
233
|
-
assert.hasProperty(response.data.providers, 'sendgrid', 'Should have SendGrid result');
|
|
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.delete('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: 'add-zerobounce-validation',
|
|
213
|
+
name: 'add-mailbox-validation',
|
|
251
214
|
auth: 'admin',
|
|
252
215
|
timeout: 30000,
|
|
253
216
|
skip: !process.env.TEST_EXTENDED_MODE || !process.env.ZEROBOUNCE_API_KEY
|
|
@@ -261,7 +224,6 @@ module.exports = {
|
|
|
261
224
|
const response = await http.post('marketing/contact', {
|
|
262
225
|
email: testEmail,
|
|
263
226
|
source: 'bem-test',
|
|
264
|
-
// No firstName/lastName - should be inferred as "Rachel Greene"
|
|
265
227
|
});
|
|
266
228
|
|
|
267
229
|
assert.isSuccess(response, 'Add marketing contact should succeed');
|
|
@@ -270,17 +232,17 @@ module.exports = {
|
|
|
270
232
|
assert.hasProperty(response, 'data.validation', 'Response should contain validation');
|
|
271
233
|
assert.hasProperty(response, 'data.validation.checks', 'Validation should contain checks');
|
|
272
234
|
|
|
273
|
-
//
|
|
274
|
-
assert.hasProperty(response, 'data.validation.checks.
|
|
235
|
+
// Mailbox check should be in checks when key is set
|
|
236
|
+
assert.hasProperty(response, 'data.validation.checks.mailbox', 'Should have mailbox check');
|
|
275
237
|
|
|
276
|
-
const
|
|
238
|
+
const mbResult = response.data.validation.checks.mailbox;
|
|
277
239
|
|
|
278
|
-
// If
|
|
279
|
-
if (
|
|
280
|
-
skip('
|
|
240
|
+
// If out of credits, skip test - not a failure
|
|
241
|
+
if (mbResult.error?.includes('out of credits')) {
|
|
242
|
+
skip('Mailbox verification out of credits');
|
|
281
243
|
}
|
|
282
244
|
|
|
283
|
-
assert.hasProperty(
|
|
245
|
+
assert.hasProperty(mbResult, 'status', 'Mailbox check should return status');
|
|
284
246
|
|
|
285
247
|
state.sendgridAdded = response.data?.providers?.sendgrid?.success;
|
|
286
248
|
state.beehiivAdded = response.data?.providers?.beehiiv?.success;
|
|
@@ -295,9 +257,9 @@ module.exports = {
|
|
|
295
257
|
},
|
|
296
258
|
},
|
|
297
259
|
|
|
298
|
-
// Test 9:
|
|
260
|
+
// Test 9: Mailbox verification rejects invalid email (only runs if TEST_EXTENDED_MODE and ZEROBOUNCE_API_KEY are set)
|
|
299
261
|
{
|
|
300
|
-
name: 'add-
|
|
262
|
+
name: 'add-mailbox-rejects-invalid',
|
|
301
263
|
auth: 'admin',
|
|
302
264
|
timeout: 30000,
|
|
303
265
|
skip: !process.env.TEST_EXTENDED_MODE || !process.env.ZEROBOUNCE_API_KEY
|
|
@@ -305,30 +267,28 @@ module.exports = {
|
|
|
305
267
|
: false,
|
|
306
268
|
|
|
307
269
|
async run({ http, assert, skip }) {
|
|
308
|
-
// Use fake email that
|
|
270
|
+
// Use fake email that mailbox verification should flag as invalid
|
|
309
271
|
const testEmail = TEST_EMAILS.invalid();
|
|
310
272
|
|
|
311
273
|
const response = await http.post('marketing/contact', {
|
|
312
274
|
email: testEmail,
|
|
313
275
|
source: 'bem-test',
|
|
314
|
-
// No firstName/lastName - AI will try to infer from "test"
|
|
315
276
|
});
|
|
316
277
|
|
|
317
|
-
// Should still succeed (we fail open) but
|
|
278
|
+
// Should still succeed (we fail open) but mailbox should report invalid
|
|
318
279
|
assert.isSuccess(response, 'Request should succeed even with invalid email');
|
|
319
280
|
|
|
320
|
-
const
|
|
281
|
+
const mbResult = response.data?.validation?.checks?.mailbox;
|
|
321
282
|
|
|
322
|
-
// If
|
|
323
|
-
if (
|
|
324
|
-
skip('
|
|
283
|
+
// If out of credits, skip test - not a failure
|
|
284
|
+
if (mbResult?.error?.includes('out of credits')) {
|
|
285
|
+
skip('Mailbox verification out of credits');
|
|
325
286
|
}
|
|
326
287
|
|
|
327
|
-
//
|
|
328
|
-
if (
|
|
329
|
-
assert.hasProperty(
|
|
330
|
-
|
|
331
|
-
assert.notEqual(zbResult.status, 'valid', 'Fake email should not be marked valid');
|
|
288
|
+
// Mailbox should return a status indicating the email is not valid
|
|
289
|
+
if (mbResult) {
|
|
290
|
+
assert.hasProperty(mbResult, 'status', 'Should have status');
|
|
291
|
+
assert.notEqual(mbResult.status, 'valid', 'Fake email should not be marked valid');
|
|
332
292
|
}
|
|
333
293
|
},
|
|
334
294
|
},
|
|
@@ -66,6 +66,18 @@ module.exports = {
|
|
|
66
66
|
},
|
|
67
67
|
},
|
|
68
68
|
|
|
69
|
+
{
|
|
70
|
+
name: 'rejects-subscription-younger-than-24-hours',
|
|
71
|
+
async run({ http, assert }) {
|
|
72
|
+
// cancel-too-young starts with startDate set to now (< 24 hours old)
|
|
73
|
+
const response = await http.as('cancel-too-young').post('payments/cancel', {
|
|
74
|
+
confirmed: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
assert.isError(response, 400, 'Should reject subscription younger than 24 hours');
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
|
|
69
81
|
{
|
|
70
82
|
name: 'rejects-unknown-processor',
|
|
71
83
|
async run({ http, assert }) {
|
|
@@ -101,6 +113,12 @@ module.exports = {
|
|
|
101
113
|
&& userDoc?.subscription?.status === 'active';
|
|
102
114
|
}, 15000, 500);
|
|
103
115
|
|
|
116
|
+
// Backdate startDate so the 24-hour guard doesn't block cancellation
|
|
117
|
+
const twoDaysAgo = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000);
|
|
118
|
+
await firestore.set(`users/${uid}`, {
|
|
119
|
+
subscription: { payment: { startDate: { timestamp: twoDaysAgo.toISOString(), timestampUNIX: twoDaysAgo.getTime() } } },
|
|
120
|
+
}, { merge: true });
|
|
121
|
+
|
|
104
122
|
// Step 2: Call the cancel endpoint
|
|
105
123
|
const cancelResponse = await http.as('route-cancel-success').post('payments/cancel', {
|
|
106
124
|
confirmed: true,
|
|
File without changes
|