backend-manager 5.6.3 → 5.6.4
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
CHANGED
|
@@ -14,6 +14,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
14
14
|
- `Fixed` for any bug fixes.
|
|
15
15
|
- `Security` in case of vulnerabilities.
|
|
16
16
|
|
|
17
|
+
# [5.6.4] - 2026-06-11
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- **Consent side effects moved off shared test accounts (journey-account isolation).** The marketing webhook suite's revoke-event tests (`test/routes/marketing/webhook.js`) repeatedly write `consent.marketing.status = 'revoked'` to their target account — persistent side-effect data that previously landed on the shared `basic` account, leaving it revoked for the remainder of every run (and, since the v5.6.3 library consent gate, changing `sync()`/`add()` behavior for every later suite touching it). They now target a dedicated `journey-webhook-revoke` account. The extended-mode lifecycle suite (`test/email/marketing-lifecycle.js`) likewise now syncs a dedicated `journey-marketing-sync` account (`_test.allow_*` prefix) instead of the shared `consent-granted` sentinel (which the signup + consent-lifecycle suites rely on). Its cleanup step also now deletes the contact the suite actually created — previously it deleted the ADMIN account's contact, which (post-v5.6.3) revoked admin's doc consent mid-run AND left the synced contact behind in SendGrid/Beehiiv after every extended run. `docs/test-framework.md`'s journey-account rule now lists `consent.marketing` writes as a trigger. Validated: marketing route suites pass (46 passing / 10 env-gated skips / 0 failures).
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **Anonymous HMAC unsubscribe tests now actually run.** The self-test boot (`src/cli/commands/test.js`) injects a test-only `UNSUBSCRIBE_HMAC_KEY` into the process env (the emulated functions inherit it — same mechanism as the fixture webhook key), closing the fixture gap that left all 8 anon-HMAC tests in `test/routes/marketing/email-preferences.js` failing as "known env gap". Those tests are the route-level coverage for the v5.6.3 HMAC changes (signature validation, rate limiting, consent mirroring); marketing suites went from 38 passing + 8 failing to 46 passing + 0 failing.
|
|
24
|
+
|
|
17
25
|
# [5.6.3] - 2026-06-11
|
|
18
26
|
|
|
19
27
|
### Fixed
|
package/docs/test-framework.md
CHANGED
|
@@ -390,7 +390,7 @@ Security-rules tests use the `rules` client (`src/test/utils/firestore-rules-cli
|
|
|
390
390
|
|
|
391
391
|
## Test Account Isolation (CRITICAL)
|
|
392
392
|
|
|
393
|
-
**NEVER use shared accounts (`basic`, `admin`, `premium-active`, …) with the `test` processor or any operation that creates side-effect data** (orders, webhooks, subscriptions). The test processor auto-fires webhooks that upgrade a user's subscription asynchronously — using `basic` for a payment-intent test upgrades `basic` to a paid subscription and breaks every subsequent test that depends on `basic` being a basic user.
|
|
393
|
+
**NEVER use shared accounts (`basic`, `admin`, `premium-active`, …) with the `test` processor or any operation that creates side-effect data** (orders, webhooks, subscriptions, consent revocations). The test processor auto-fires webhooks that upgrade a user's subscription asynchronously — using `basic` for a payment-intent test upgrades `basic` to a paid subscription and breaks every subsequent test that depends on `basic` being a basic user.
|
|
394
394
|
|
|
395
395
|
**Rule: any test that creates persistent side-effect data MUST use a dedicated `journey-*` account.**
|
|
396
396
|
|
|
@@ -402,7 +402,7 @@ const response = await http.as('basic').post('payments/intent', { processor: 'te
|
|
|
402
402
|
const response = await http.as('journey-payments-intent-discount').post('payments/intent', { processor: 'test', ... });
|
|
403
403
|
```
|
|
404
404
|
|
|
405
|
-
**When to create a journey account:** the test uses `processor: 'test'`, creates docs in `payments-orders` / `payments-intents` / `payments-webhooks`, modifies subscription state,
|
|
405
|
+
**When to create a journey account:** the test uses `processor: 'test'`, creates docs in `payments-orders` / `payments-intents` / `payments-webhooks`, modifies subscription state, sends webhooks that trigger Firestore onWrite handlers, or **writes `consent.marketing` (grant/revoke)** — e.g. marketing webhook revoke events, or `DELETE /marketing/contact` (which mirrors `revoked` to the user doc). Revoked consent persists for the rest of the run and trips the email library's consent gate (`{ blocked: 'consent' }`) on every later `sync()`/`add()` of that account. Existing examples: `journey-webhook-revoke` (webhook revoke events), `journey-marketing-sync` (extended-mode live-provider sync + cleanup; `_test.allow_*` prefix). Add new ones to `src/test/test-accounts.js` (framework tests) or your project's `test/_init.js` `accounts` array (consumer tests).
|
|
406
406
|
|
|
407
407
|
**Shared accounts are safe for:** validation-only tests (missing fields, invalid input, auth rejection, unknown processor), read-only operations, and tests with no async side effects.
|
|
408
408
|
|
package/package.json
CHANGED
package/src/cli/commands/test.js
CHANGED
|
@@ -237,6 +237,11 @@ class TestCommand extends BaseCommand {
|
|
|
237
237
|
process.env.BACKEND_MANAGER_WEBHOOK_KEY = process.env.BACKEND_MANAGER_WEBHOOK_KEY || cfg.backend_manager?.webhookKey;
|
|
238
238
|
} catch (_) { /* fixture config unreadable — let the normal key check report it */ }
|
|
239
239
|
|
|
240
|
+
// Anonymous HMAC unsubscribe tests sign links with this shared secret; the
|
|
241
|
+
// emulated functions inherit it from this process env (same mechanism as the
|
|
242
|
+
// webhook key above). Test-only value — production sets its own env var.
|
|
243
|
+
process.env.UNSUBSCRIBE_HMAC_KEY = process.env.UNSUBSCRIBE_HMAC_KEY || '_test-unsubscribe-hmac-key';
|
|
244
|
+
|
|
240
245
|
this.ensureFixtureServiceAccount(fixture);
|
|
241
246
|
this.linkFixtureDeps(fixture);
|
|
242
247
|
this.log(chalk.cyan(` Self-test: booting bundled fixture project (${fixture})`));
|
|
@@ -530,6 +530,37 @@ const JOURNEY_ACCOUNTS = {
|
|
|
530
530
|
subscription: { product: { id: 'premium' }, status: 'cancelled', expires: getPastExpires() },
|
|
531
531
|
},
|
|
532
532
|
},
|
|
533
|
+
// Journey: marketing webhook revocation (test/routes/marketing/webhook.js). The
|
|
534
|
+
// SendGrid/Beehiiv revoke-event tests repeatedly write consent.marketing.status='revoked'
|
|
535
|
+
// to the target account — persistent side-effect data, so it must never be the shared
|
|
536
|
+
// `basic` account (revoked consent would persist for the rest of the run and trip the
|
|
537
|
+
// email library's consent gate for every later sync of that account).
|
|
538
|
+
'journey-webhook-revoke': {
|
|
539
|
+
id: 'journey-webhook-revoke',
|
|
540
|
+
uid: '_test-journey-webhook-revoke',
|
|
541
|
+
email: '_test.journey-webhook-revoke@{domain}',
|
|
542
|
+
properties: {
|
|
543
|
+
roles: {},
|
|
544
|
+
subscription: { product: { id: 'basic' }, status: 'active' },
|
|
545
|
+
personal: { name: { first: 'Webb', last: 'Revoke' } },
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
// Journey: live-provider sync round-trip (test/email/marketing-lifecycle.js, extended
|
|
549
|
+
// mode only). The `_test.allow_*` email prefix bypasses the `_test.*` marketing block so
|
|
550
|
+
// sync() reaches real SendGrid/Beehiiv; the suite's cleanup DELETE then removes the
|
|
551
|
+
// contact AND mirrors revoked consent to this account's doc. Dedicated account so that
|
|
552
|
+
// side effect stays isolated — the shared `consent-granted` sentinel is used by the
|
|
553
|
+
// signup and consent-lifecycle suites and must keep its granted state.
|
|
554
|
+
'journey-marketing-sync': {
|
|
555
|
+
id: 'journey-marketing-sync',
|
|
556
|
+
uid: '_test-journey-marketing-sync',
|
|
557
|
+
email: '_test.allow_journey-marketing-sync@{domain}',
|
|
558
|
+
properties: {
|
|
559
|
+
roles: {},
|
|
560
|
+
subscription: { product: { id: 'basic' }, status: 'active' },
|
|
561
|
+
personal: { name: { first: 'Lifecycle', last: 'Sync' } },
|
|
562
|
+
},
|
|
563
|
+
},
|
|
533
564
|
};
|
|
534
565
|
|
|
535
566
|
/**
|
|
@@ -61,8 +61,10 @@ module.exports = {
|
|
|
61
61
|
auth: 'admin',
|
|
62
62
|
|
|
63
63
|
async run({ http, assert, accounts, config, Manager }) {
|
|
64
|
-
//
|
|
65
|
-
|
|
64
|
+
// Dedicated journey account — its _test.allow_* prefix bypasses validation, and the
|
|
65
|
+
// cleanup step's DELETE revokes its doc consent, so it must not be a shared sentinel
|
|
66
|
+
// (consent-granted is used by the signup + consent-lifecycle suites).
|
|
67
|
+
const grantedUid = accounts['journey-marketing-sync'].uid;
|
|
66
68
|
const admin = Manager.libraries.admin;
|
|
67
69
|
await admin.firestore().doc(`users/${grantedUid}`).set({
|
|
68
70
|
consent: { marketing: { status: 'granted' } },
|
|
@@ -112,14 +114,17 @@ module.exports = {
|
|
|
112
114
|
},
|
|
113
115
|
},
|
|
114
116
|
|
|
115
|
-
// Step 4: Clean up the
|
|
117
|
+
// Step 4: Clean up the contact the sync step added to the live providers.
|
|
118
|
+
// DELETE also mirrors revoked consent to the matching user doc — which is why this
|
|
119
|
+
// targets the dedicated journey account (step 2 re-seeds granted before syncing, so
|
|
120
|
+
// the suite is self-healing across runs).
|
|
116
121
|
{
|
|
117
|
-
name: 'cleanup-synced-
|
|
122
|
+
name: 'cleanup-synced-contact',
|
|
118
123
|
auth: 'admin',
|
|
119
124
|
|
|
120
125
|
async run({ http, accounts }) {
|
|
121
126
|
await http.delete('backend-manager/marketing/contact', {
|
|
122
|
-
email: accounts.
|
|
127
|
+
email: accounts['journey-marketing-sync'].email,
|
|
123
128
|
}).catch(() => {});
|
|
124
129
|
},
|
|
125
130
|
},
|
|
@@ -18,8 +18,12 @@
|
|
|
18
18
|
* - Silent skip when email doesn't map to a user (shared SendGrid account scenario)
|
|
19
19
|
* - Batched events processed independently
|
|
20
20
|
* - Unsupported event types ignored
|
|
21
|
+
*
|
|
22
|
+
* All revoke-event tests target the dedicated `journey-webhook-revoke` account: they
|
|
23
|
+
* leave it with consent.marketing.status='revoked' (persistent side-effect data), which
|
|
24
|
+
* must never land on the shared `basic` account — revoked consent persists for the rest
|
|
25
|
+
* of the run and trips the email library's consent gate for later syncs of that account.
|
|
21
26
|
*/
|
|
22
|
-
const { TEST_ACCOUNTS } = require('../../../src/test/test-accounts.js');
|
|
23
27
|
|
|
24
28
|
// Helper — generate a unique sg_event_id per test
|
|
25
29
|
function sgEventId(name) {
|
|
@@ -105,8 +109,8 @@ module.exports = {
|
|
|
105
109
|
name: 'sendgrid-group-unsubscribe-writes-consent',
|
|
106
110
|
auth: 'none',
|
|
107
111
|
async run({ http, firestore, assert, accounts }) {
|
|
108
|
-
const uid = accounts.
|
|
109
|
-
const email = accounts.
|
|
112
|
+
const uid = accounts['journey-webhook-revoke'].uid;
|
|
113
|
+
const email = accounts['journey-webhook-revoke'].email;
|
|
110
114
|
const eventId = sgEventId('group-unsub');
|
|
111
115
|
const eventTimestamp = Math.floor(Date.now() / 1000);
|
|
112
116
|
|
|
@@ -130,8 +134,8 @@ module.exports = {
|
|
|
130
134
|
name: 'sendgrid-unsubscribe-event-handled',
|
|
131
135
|
auth: 'none',
|
|
132
136
|
async run({ http, firestore, assert, accounts }) {
|
|
133
|
-
const uid = accounts.
|
|
134
|
-
const email = accounts.
|
|
137
|
+
const uid = accounts['journey-webhook-revoke'].uid;
|
|
138
|
+
const email = accounts['journey-webhook-revoke'].email;
|
|
135
139
|
const eventId = sgEventId('unsub');
|
|
136
140
|
|
|
137
141
|
const response = await http.as('none').post(
|
|
@@ -152,8 +156,8 @@ module.exports = {
|
|
|
152
156
|
name: 'sendgrid-spamreport-event-handled',
|
|
153
157
|
auth: 'none',
|
|
154
158
|
async run({ http, firestore, assert, accounts }) {
|
|
155
|
-
const uid = accounts.
|
|
156
|
-
const email = accounts.
|
|
159
|
+
const uid = accounts['journey-webhook-revoke'].uid;
|
|
160
|
+
const email = accounts['journey-webhook-revoke'].email;
|
|
157
161
|
const eventId = sgEventId('spamreport');
|
|
158
162
|
|
|
159
163
|
const response = await http.as('none').post(
|
|
@@ -173,8 +177,8 @@ module.exports = {
|
|
|
173
177
|
name: 'sendgrid-hard-bounce-event-handled',
|
|
174
178
|
auth: 'none',
|
|
175
179
|
async run({ http, firestore, assert, accounts }) {
|
|
176
|
-
const uid = accounts.
|
|
177
|
-
const email = accounts.
|
|
180
|
+
const uid = accounts['journey-webhook-revoke'].uid;
|
|
181
|
+
const email = accounts['journey-webhook-revoke'].email;
|
|
178
182
|
const eventId = sgEventId('hard-bounce');
|
|
179
183
|
|
|
180
184
|
// Only hard bounces (bounce_classification='Invalid Address') revoke consent.
|
|
@@ -195,8 +199,8 @@ module.exports = {
|
|
|
195
199
|
name: 'sendgrid-dropped-hard-bounce-handled',
|
|
196
200
|
auth: 'none',
|
|
197
201
|
async run({ http, firestore, assert, accounts }) {
|
|
198
|
-
const uid = accounts.
|
|
199
|
-
const email = accounts.
|
|
202
|
+
const uid = accounts['journey-webhook-revoke'].uid;
|
|
203
|
+
const email = accounts['journey-webhook-revoke'].email;
|
|
200
204
|
const eventId = sgEventId('dropped');
|
|
201
205
|
|
|
202
206
|
// 'dropped' follows the same classification filter as 'bounce'.
|
|
@@ -219,7 +223,7 @@ module.exports = {
|
|
|
219
223
|
name: 'sendgrid-technical-bounce-ignored',
|
|
220
224
|
auth: 'none',
|
|
221
225
|
async run({ http, assert, accounts }) {
|
|
222
|
-
const email = accounts.
|
|
226
|
+
const email = accounts['journey-webhook-revoke'].email;
|
|
223
227
|
const eventId = sgEventId('technical-bounce');
|
|
224
228
|
|
|
225
229
|
// Technical bounces (DMARC, TLS, DNS) are sender-side issues — the recipient's
|
|
@@ -239,7 +243,7 @@ module.exports = {
|
|
|
239
243
|
name: 'sendgrid-bounce-without-classification-ignored',
|
|
240
244
|
auth: 'none',
|
|
241
245
|
async run({ http, assert, accounts }) {
|
|
242
|
-
const email = accounts.
|
|
246
|
+
const email = accounts['journey-webhook-revoke'].email;
|
|
243
247
|
const eventId = sgEventId('unclassified-bounce');
|
|
244
248
|
|
|
245
249
|
// No bounce_classification — can't confirm a hard bounce, so skip.
|
|
@@ -258,7 +262,7 @@ module.exports = {
|
|
|
258
262
|
name: 'sendgrid-delivered-event-ignored',
|
|
259
263
|
auth: 'none',
|
|
260
264
|
async run({ http, firestore, assert, accounts }) {
|
|
261
|
-
const email = accounts.
|
|
265
|
+
const email = accounts['journey-webhook-revoke'].email;
|
|
262
266
|
const eventId = sgEventId('delivered');
|
|
263
267
|
|
|
264
268
|
const response = await http.as('none').post(
|
|
@@ -276,7 +280,7 @@ module.exports = {
|
|
|
276
280
|
name: 'sendgrid-open-event-ignored',
|
|
277
281
|
auth: 'none',
|
|
278
282
|
async run({ http, assert, accounts }) {
|
|
279
|
-
const email = accounts.
|
|
283
|
+
const email = accounts['journey-webhook-revoke'].email;
|
|
280
284
|
const eventId = sgEventId('open');
|
|
281
285
|
|
|
282
286
|
const response = await http.as('none').post(
|
|
@@ -317,8 +321,8 @@ module.exports = {
|
|
|
317
321
|
name: 'sendgrid-batched-events-processed-independently',
|
|
318
322
|
auth: 'none',
|
|
319
323
|
async run({ http, firestore, assert, accounts }) {
|
|
320
|
-
const uid = accounts.
|
|
321
|
-
const email = accounts.
|
|
324
|
+
const uid = accounts['journey-webhook-revoke'].uid;
|
|
325
|
+
const email = accounts['journey-webhook-revoke'].email;
|
|
322
326
|
const e1 = sgEventId('batch-1');
|
|
323
327
|
const e2 = sgEventId('batch-2');
|
|
324
328
|
const e3 = sgEventId('batch-3');
|
|
@@ -348,8 +352,8 @@ module.exports = {
|
|
|
348
352
|
name: 'sendgrid-duplicate-event-reprocessed-idempotently',
|
|
349
353
|
auth: 'none',
|
|
350
354
|
async run({ http, firestore, assert, accounts }) {
|
|
351
|
-
const uid = accounts.
|
|
352
|
-
const email = accounts.
|
|
355
|
+
const uid = accounts['journey-webhook-revoke'].uid;
|
|
356
|
+
const email = accounts['journey-webhook-revoke'].email;
|
|
353
357
|
const eventId = sgEventId('duplicate');
|
|
354
358
|
|
|
355
359
|
// First delivery
|
|
@@ -380,8 +384,8 @@ module.exports = {
|
|
|
380
384
|
name: 'sendgrid-event-without-eventId-processed',
|
|
381
385
|
auth: 'none',
|
|
382
386
|
async run({ http, firestore, assert, accounts }) {
|
|
383
|
-
const uid = accounts.
|
|
384
|
-
const email = accounts.
|
|
387
|
+
const uid = accounts['journey-webhook-revoke'].uid;
|
|
388
|
+
const email = accounts['journey-webhook-revoke'].email;
|
|
385
389
|
|
|
386
390
|
const response = await http.as('none').post(
|
|
387
391
|
`backend-manager/marketing/webhook?provider=sendgrid&key=${process.env.BACKEND_MANAGER_WEBHOOK_KEY}`,
|
|
@@ -404,8 +408,8 @@ module.exports = {
|
|
|
404
408
|
name: 'beehiiv-subscription-unsubscribed-writes-consent',
|
|
405
409
|
auth: 'none',
|
|
406
410
|
async run({ http, firestore, assert, accounts, config, skip }) {
|
|
407
|
-
const uid = accounts.
|
|
408
|
-
const email = accounts.
|
|
411
|
+
const uid = accounts['journey-webhook-revoke'].uid;
|
|
412
|
+
const email = accounts['journey-webhook-revoke'].email;
|
|
409
413
|
const eventId = `_test-bh-unsub-${Date.now()}`;
|
|
410
414
|
const eventISO = new Date().toISOString();
|
|
411
415
|
const publicationId = config.marketing?.newsletter?.publicationId;
|
|
@@ -439,8 +443,8 @@ module.exports = {
|
|
|
439
443
|
name: 'beehiiv-subscription-deleted-handled',
|
|
440
444
|
auth: 'none',
|
|
441
445
|
async run({ http, firestore, assert, accounts, config }) {
|
|
442
|
-
const uid = accounts.
|
|
443
|
-
const email = accounts.
|
|
446
|
+
const uid = accounts['journey-webhook-revoke'].uid;
|
|
447
|
+
const email = accounts['journey-webhook-revoke'].email;
|
|
444
448
|
const eventId = `_test-bh-deleted-${Date.now()}`;
|
|
445
449
|
const publicationId = config.marketing?.newsletter?.publicationId;
|
|
446
450
|
|
|
@@ -468,8 +472,8 @@ module.exports = {
|
|
|
468
472
|
name: 'beehiiv-subscription-paused-handled',
|
|
469
473
|
auth: 'none',
|
|
470
474
|
async run({ http, firestore, assert, accounts, config }) {
|
|
471
|
-
const uid = accounts.
|
|
472
|
-
const email = accounts.
|
|
475
|
+
const uid = accounts['journey-webhook-revoke'].uid;
|
|
476
|
+
const email = accounts['journey-webhook-revoke'].email;
|
|
473
477
|
const eventId = `_test-bh-paused-${Date.now()}`;
|
|
474
478
|
const publicationId = config.marketing?.newsletter?.publicationId;
|
|
475
479
|
|
|
@@ -499,13 +503,13 @@ module.exports = {
|
|
|
499
503
|
// Send an event with a publication_id that does NOT match this brand's pub.
|
|
500
504
|
// Simulates the shared-devbeans scenario where the parent forwarder fans
|
|
501
505
|
// an event to brands that don't share the publication — they silent-skip.
|
|
502
|
-
const email = accounts.
|
|
506
|
+
const email = accounts['journey-webhook-revoke'].email;
|
|
503
507
|
const eventId = `_test-bh-pubmismatch-${Date.now()}`;
|
|
504
508
|
|
|
505
509
|
// Snapshot revokedAt BEFORE the request so we can prove the pub-mismatch
|
|
506
510
|
// handler didn't write anything new. (The basic account may already have
|
|
507
511
|
// a beehiiv-sourced revoke from a prior test that legitimately fired.)
|
|
508
|
-
const beforeDoc = await firestore.get(`users/${accounts.
|
|
512
|
+
const beforeDoc = await firestore.get(`users/${accounts['journey-webhook-revoke'].uid}`);
|
|
509
513
|
const beforeRevokedAt = beforeDoc?.consent?.marketing?.revokedAt || null;
|
|
510
514
|
|
|
511
515
|
const response = await http.as('none').post(
|
|
@@ -527,7 +531,7 @@ module.exports = {
|
|
|
527
531
|
|
|
528
532
|
// Reload the user doc and verify revokedAt is byte-equivalent to before —
|
|
529
533
|
// pub-mismatch must not write a new revoke entry.
|
|
530
|
-
const afterDoc = await firestore.get(`users/${accounts.
|
|
534
|
+
const afterDoc = await firestore.get(`users/${accounts['journey-webhook-revoke'].uid}`);
|
|
531
535
|
const afterRevokedAt = afterDoc?.consent?.marketing?.revokedAt || null;
|
|
532
536
|
|
|
533
537
|
assert.deepEqual(
|
|
@@ -568,7 +572,7 @@ module.exports = {
|
|
|
568
572
|
auth: 'none',
|
|
569
573
|
async run({ http, assert, accounts, config }) {
|
|
570
574
|
// 'subscription.created' (new signup) is NOT a revoke — should be ignored.
|
|
571
|
-
const email = accounts.
|
|
575
|
+
const email = accounts['journey-webhook-revoke'].email;
|
|
572
576
|
const publicationId = config.marketing?.newsletter?.publicationId;
|
|
573
577
|
const eventId = `_test-bh-created-${Date.now()}`;
|
|
574
578
|
|
|
@@ -593,8 +597,8 @@ module.exports = {
|
|
|
593
597
|
name: 'beehiiv-duplicate-event-reprocessed-idempotently',
|
|
594
598
|
auth: 'none',
|
|
595
599
|
async run({ http, firestore, assert, accounts, config, skip }) {
|
|
596
|
-
const uid = accounts.
|
|
597
|
-
const email = accounts.
|
|
600
|
+
const uid = accounts['journey-webhook-revoke'].uid;
|
|
601
|
+
const email = accounts['journey-webhook-revoke'].email;
|
|
598
602
|
const publicationId = config.marketing?.newsletter?.publicationId;
|
|
599
603
|
const eventId = `_test-bh-dup-${Date.now()}`;
|
|
600
604
|
|