backend-manager 5.6.3 → 5.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/CLAUDE.md +4 -3
- package/PROGRESS.md +34 -0
- package/docs/ai-library.md +62 -11
- package/docs/cdp-debugging.md +44 -0
- package/docs/cli-output.md +22 -10
- package/docs/mcp.md +166 -43
- package/docs/test-framework.md +2 -2
- package/package.json +1 -1
- package/plans/mcp2.md +247 -0
- package/src/cli/commands/mcp.js +8 -2
- package/src/cli/commands/serve.js +155 -29
- package/src/cli/commands/setup-tests/base-test.js +8 -0
- package/src/cli/commands/setup-tests/firebase-auth.js +26 -0
- package/src/cli/commands/setup-tests/firebase-cli.js +9 -13
- package/src/cli/commands/setup-tests/index.js +4 -0
- package/src/cli/commands/setup-tests/java-installed.js +26 -0
- package/src/cli/commands/setup.js +2 -1
- package/src/cli/commands/test.js +13 -0
- package/src/cli/index.js +14 -0
- package/src/cli/utils/ui.js +27 -5
- package/src/manager/index.js +8 -3
- package/src/manager/libraries/ai/index.js +45 -1
- package/src/manager/libraries/ai/providers/anthropic-format.js +234 -0
- package/src/manager/libraries/ai/providers/anthropic.js +28 -49
- package/src/manager/libraries/ai/providers/claude-code.js +21 -47
- package/src/manager/libraries/ai/providers/openai.js +154 -19
- package/src/manager/libraries/ai/providers/test.js +242 -0
- package/src/manager/libraries/email/data/disposable-domains.json +465 -0
- package/src/mcp/client.js +48 -13
- package/src/mcp/handler.js +222 -69
- package/src/mcp/index.js +48 -18
- package/src/mcp/tools.js +150 -0
- package/src/mcp/utils.js +108 -0
- package/src/test/fixtures/firebase-project/firebase.json +1 -1
- package/src/test/test-accounts.js +31 -0
- package/test/ai/tools-live.js +170 -0
- package/test/email/marketing-lifecycle.js +10 -5
- package/test/helpers/ai-test-provider.js +202 -0
- package/test/helpers/ai-tools-format.js +350 -0
- package/test/mcp/discovery.js +53 -0
- package/test/mcp/oauth.js +161 -0
- package/test/mcp/protocol.js +268 -0
- package/test/mcp/roles.js +168 -0
- package/test/mcp/utils.js +245 -0
- package/test/routes/marketing/webhook.js +37 -33
- package/.claude/settings.local.json +0 -12
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test: MCP utility functions
|
|
3
|
+
* Tests resolveAuthInfo, filterToolsByRole, loadConsumerTools, buildToolMap
|
|
4
|
+
*
|
|
5
|
+
* Run: npx mgr test bem:mcp/utils
|
|
6
|
+
*/
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
description: 'MCP utility functions',
|
|
11
|
+
type: 'group',
|
|
12
|
+
|
|
13
|
+
tests: [
|
|
14
|
+
// --- resolveAuthInfo ---
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
name: 'resolveAuthInfo: admin key returns admin role',
|
|
18
|
+
async run({ assert }) {
|
|
19
|
+
const { resolveAuthInfo } = require('../../src/mcp/utils.js');
|
|
20
|
+
const saved = process.env.BACKEND_MANAGER_KEY;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
process.env.BACKEND_MANAGER_KEY = 'test-admin-key';
|
|
24
|
+
const result = resolveAuthInfo('test-admin-key');
|
|
25
|
+
|
|
26
|
+
assert.equal(result.role, 'admin', 'Should be admin');
|
|
27
|
+
assert.equal(result.authType, 'adminKey', 'Should be adminKey type');
|
|
28
|
+
assert.equal(result.token, 'test-admin-key', 'Token should match');
|
|
29
|
+
} finally {
|
|
30
|
+
process.env.BACKEND_MANAGER_KEY = saved;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
name: 'resolveAuthInfo: non-admin token returns user role',
|
|
37
|
+
async run({ assert }) {
|
|
38
|
+
const { resolveAuthInfo } = require('../../src/mcp/utils.js');
|
|
39
|
+
const result = resolveAuthInfo('some-user-api-key');
|
|
40
|
+
|
|
41
|
+
assert.equal(result.role, 'user', 'Should be user');
|
|
42
|
+
assert.equal(result.authType, 'userToken', 'Should be userToken type');
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
{
|
|
47
|
+
name: 'resolveAuthInfo: empty token returns public role',
|
|
48
|
+
async run({ assert }) {
|
|
49
|
+
const { resolveAuthInfo } = require('../../src/mcp/utils.js');
|
|
50
|
+
const result = resolveAuthInfo('');
|
|
51
|
+
|
|
52
|
+
assert.equal(result.role, 'public', 'Should be public');
|
|
53
|
+
assert.equal(result.authType, 'none', 'Should be none type');
|
|
54
|
+
assert.equal(result.token, '', 'Token should be empty');
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
{
|
|
59
|
+
name: 'resolveAuthInfo: null/undefined token returns public role',
|
|
60
|
+
async run({ assert }) {
|
|
61
|
+
const { resolveAuthInfo } = require('../../src/mcp/utils.js');
|
|
62
|
+
|
|
63
|
+
assert.equal(resolveAuthInfo(null).role, 'public', 'null should be public');
|
|
64
|
+
assert.equal(resolveAuthInfo(undefined).role, 'public', 'undefined should be public');
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
name: 'resolveAuthInfo: returns public when BACKEND_MANAGER_KEY is not set',
|
|
70
|
+
async run({ assert }) {
|
|
71
|
+
const { resolveAuthInfo } = require('../../src/mcp/utils.js');
|
|
72
|
+
const saved = process.env.BACKEND_MANAGER_KEY;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
delete process.env.BACKEND_MANAGER_KEY;
|
|
76
|
+
const result = resolveAuthInfo('any-token');
|
|
77
|
+
|
|
78
|
+
assert.equal(result.role, 'user', 'Non-empty token with no config key should be user');
|
|
79
|
+
} finally {
|
|
80
|
+
process.env.BACKEND_MANAGER_KEY = saved;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// --- filterToolsByRole ---
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
name: 'filterToolsByRole: admin sees all roles',
|
|
89
|
+
async run({ assert }) {
|
|
90
|
+
const { filterToolsByRole } = require('../../src/mcp/utils.js');
|
|
91
|
+
const tools = [
|
|
92
|
+
{ name: 'a', role: 'admin' },
|
|
93
|
+
{ name: 'b', role: 'user' },
|
|
94
|
+
{ name: 'c', role: 'public' },
|
|
95
|
+
];
|
|
96
|
+
const result = filterToolsByRole(tools, 'admin');
|
|
97
|
+
|
|
98
|
+
assert.equal(result.length, 3, 'Admin should see all 3');
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
name: 'filterToolsByRole: user sees user + public only',
|
|
104
|
+
async run({ assert }) {
|
|
105
|
+
const { filterToolsByRole } = require('../../src/mcp/utils.js');
|
|
106
|
+
const tools = [
|
|
107
|
+
{ name: 'a', role: 'admin' },
|
|
108
|
+
{ name: 'b', role: 'user' },
|
|
109
|
+
{ name: 'c', role: 'public' },
|
|
110
|
+
];
|
|
111
|
+
const result = filterToolsByRole(tools, 'user');
|
|
112
|
+
|
|
113
|
+
assert.equal(result.length, 2, 'User should see 2');
|
|
114
|
+
assert.ok(result.some((t) => t.name === 'b'), 'Should include user tool');
|
|
115
|
+
assert.ok(result.some((t) => t.name === 'c'), 'Should include public tool');
|
|
116
|
+
assert.ok(!result.some((t) => t.name === 'a'), 'Should exclude admin tool');
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
{
|
|
121
|
+
name: 'filterToolsByRole: public sees public only',
|
|
122
|
+
async run({ assert }) {
|
|
123
|
+
const { filterToolsByRole } = require('../../src/mcp/utils.js');
|
|
124
|
+
const tools = [
|
|
125
|
+
{ name: 'a', role: 'admin' },
|
|
126
|
+
{ name: 'b', role: 'user' },
|
|
127
|
+
{ name: 'c', role: 'public' },
|
|
128
|
+
];
|
|
129
|
+
const result = filterToolsByRole(tools, 'public');
|
|
130
|
+
|
|
131
|
+
assert.equal(result.length, 1, 'Public should see 1');
|
|
132
|
+
assert.equal(result[0].name, 'c', 'Should be the public tool');
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
{
|
|
137
|
+
name: 'filterToolsByRole: tools without role default to admin',
|
|
138
|
+
async run({ assert }) {
|
|
139
|
+
const { filterToolsByRole } = require('../../src/mcp/utils.js');
|
|
140
|
+
const tools = [{ name: 'no-role' }];
|
|
141
|
+
|
|
142
|
+
assert.equal(filterToolsByRole(tools, 'admin').length, 1, 'Admin should see role-less tool');
|
|
143
|
+
assert.equal(filterToolsByRole(tools, 'user').length, 0, 'User should not see role-less tool');
|
|
144
|
+
assert.equal(filterToolsByRole(tools, 'public').length, 0, 'Public should not see role-less tool');
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
{
|
|
149
|
+
name: 'filterToolsByRole: unknown role treated as public',
|
|
150
|
+
async run({ assert }) {
|
|
151
|
+
const { filterToolsByRole } = require('../../src/mcp/utils.js');
|
|
152
|
+
const tools = [
|
|
153
|
+
{ name: 'a', role: 'admin' },
|
|
154
|
+
{ name: 'b', role: 'user' },
|
|
155
|
+
{ name: 'c', role: 'public' },
|
|
156
|
+
];
|
|
157
|
+
const result = filterToolsByRole(tools, 'garbage');
|
|
158
|
+
|
|
159
|
+
assert.equal(result.length, 1, 'Unknown role should see public only');
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
// --- loadConsumerTools ---
|
|
164
|
+
|
|
165
|
+
{
|
|
166
|
+
name: 'loadConsumerTools: returns empty array when no cwd',
|
|
167
|
+
async run({ assert }) {
|
|
168
|
+
const { loadConsumerTools } = require('../../src/mcp/utils.js');
|
|
169
|
+
|
|
170
|
+
assert.equal(loadConsumerTools(null).length, 0, 'null cwd');
|
|
171
|
+
assert.equal(loadConsumerTools('').length, 0, 'empty cwd');
|
|
172
|
+
assert.equal(loadConsumerTools(undefined).length, 0, 'undefined cwd');
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
{
|
|
177
|
+
name: 'loadConsumerTools: returns empty array for non-existent directory',
|
|
178
|
+
async run({ assert }) {
|
|
179
|
+
const { loadConsumerTools } = require('../../src/mcp/utils.js');
|
|
180
|
+
const result = loadConsumerTools('/tmp/does-not-exist-12345');
|
|
181
|
+
|
|
182
|
+
assert.equal(result.length, 0, 'Should return empty array');
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
// --- buildToolMap ---
|
|
187
|
+
|
|
188
|
+
{
|
|
189
|
+
name: 'buildToolMap: consumer tools override built-ins with same name',
|
|
190
|
+
async run({ assert }) {
|
|
191
|
+
const { buildToolMap } = require('../../src/mcp/utils.js');
|
|
192
|
+
const builtin = [{ name: 'tool_a', description: 'original' }];
|
|
193
|
+
const consumer = [{ name: 'tool_a', description: 'override', _consumer: true }];
|
|
194
|
+
|
|
195
|
+
const map = buildToolMap(builtin, consumer);
|
|
196
|
+
assert.equal(map.get('tool_a').description, 'override', 'Consumer should override');
|
|
197
|
+
assert.equal(map.get('tool_a')._consumer, true, 'Should be marked as consumer');
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
{
|
|
202
|
+
name: 'buildToolMap: merges non-overlapping tools',
|
|
203
|
+
async run({ assert }) {
|
|
204
|
+
const { buildToolMap } = require('../../src/mcp/utils.js');
|
|
205
|
+
const builtin = [{ name: 'a' }, { name: 'b' }];
|
|
206
|
+
const consumer = [{ name: 'c' }];
|
|
207
|
+
|
|
208
|
+
const map = buildToolMap(builtin, consumer);
|
|
209
|
+
assert.equal(map.size, 3, 'Should have 3 tools total');
|
|
210
|
+
assert.ok(map.has('a'), 'Should have a');
|
|
211
|
+
assert.ok(map.has('b'), 'Should have b');
|
|
212
|
+
assert.ok(map.has('c'), 'Should have c');
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
// --- Real tools verification ---
|
|
217
|
+
|
|
218
|
+
{
|
|
219
|
+
name: 'all 19 built-in tools have a role assigned',
|
|
220
|
+
async run({ assert }) {
|
|
221
|
+
const tools = require('../../src/mcp/tools.js');
|
|
222
|
+
|
|
223
|
+
assert.equal(tools.length, 25, 'Should have 25 tools');
|
|
224
|
+
|
|
225
|
+
const missing = tools.filter((t) => !t.role);
|
|
226
|
+
assert.equal(missing.length, 0, `All tools should have roles, missing: ${missing.map((t) => t.name).join(', ')}`);
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
{
|
|
231
|
+
name: 'role distribution matches expected counts',
|
|
232
|
+
async run({ assert }) {
|
|
233
|
+
const tools = require('../../src/mcp/tools.js');
|
|
234
|
+
|
|
235
|
+
const admin = tools.filter((t) => t.role === 'admin');
|
|
236
|
+
const user = tools.filter((t) => t.role === 'user');
|
|
237
|
+
const pub = tools.filter((t) => t.role === 'public');
|
|
238
|
+
|
|
239
|
+
assert.equal(admin.length, 22, `Should have 22 admin tools, got ${admin.length}`);
|
|
240
|
+
assert.equal(user.length, 2, `Should have 2 user tools, got ${user.length}`);
|
|
241
|
+
assert.equal(pub.length, 1, `Should have 1 public tool, got ${pub.length}`);
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
};
|
|
@@ -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
|
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(grep -B2 -A20 \"Generate a newsletter preview\" /tmp/bem-test-run-3.log | head -50)",
|
|
5
|
-
"Bash(sed 's/\\\\x1b\\\\[[0-9;]*m//g')",
|
|
6
|
-
"Read(//tmp/**)",
|
|
7
|
-
"Bash(TEST_EXTENDED_MODE=1 npx mgr test 2>&1 | sed 's/\\\\x1b\\\\[[0-9;]*m//g' > /tmp/bem-run-6.log; grep -E \"passing|failing|skipped\" /tmp/bem-run-6.log | tail -5)",
|
|
8
|
-
"Bash(awk -F'`' '{print $2}')",
|
|
9
|
-
"Bash(TEST_EXTENDED_MODE=1 npx mgr test 2>&1 | sed 's/\\\\x1b\\\\[[0-9;]*m//g' > /tmp/bem-cleanup-run2.log; grep -E \"\\(passing|failing|skipped\\)\" /tmp/bem-cleanup-run2.log | tail -5)"
|
|
10
|
-
]
|
|
11
|
-
}
|
|
12
|
-
}
|