backend-manager 5.9.4 → 5.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/CLAUDE.md +6 -0
  2. package/package.json +8 -1
  3. package/src/cli/commands/install.js +4 -3
  4. package/src/cli/commands/setup-tests/backend-manager.js +2 -1
  5. package/src/cli/commands/setup-tests/firebase-admin.js +2 -1
  6. package/src/cli/commands/setup-tests/firebase-functions.js +2 -1
  7. package/src/cli/utils/safe-install.js +13 -0
  8. package/src/manager/routes/payments/cancel/post.js +22 -2
  9. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/metadata.json +14 -0
  10. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/newsletter.html +486 -0
  11. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/newsletter.md +41 -0
  12. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/newsletter.mjml +97 -0
  13. package/{test/email/fixtures/clean.json → src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/structure.json} +16 -4
  14. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/summary.md +1 -0
  15. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/metadata.json +14 -0
  16. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/newsletter.html +486 -0
  17. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/newsletter.md +41 -0
  18. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/newsletter.mjml +97 -0
  19. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/structure.json +42 -0
  20. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/summary.md +1 -0
  21. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/metadata.json +14 -0
  22. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/newsletter.html +486 -0
  23. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/newsletter.md +41 -0
  24. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/newsletter.mjml +97 -0
  25. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/structure.json +42 -0
  26. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/summary.md +1 -0
  27. package/src/test/fixtures/firebase-project/.temp/test-mode.json +6 -0
  28. package/src/test/fixtures/firebase-project/database-debug.log +12 -0
  29. package/src/test/fixtures/firebase-project/firestore-debug.log +136 -0
  30. package/src/test/fixtures/firebase-project/pubsub-debug.log +17 -0
  31. package/src/test/test-accounts.js +9 -0
  32. package/.codeclimate.yml +0 -32
  33. package/.nvmrc +0 -1
  34. package/CHANGELOG.md +0 -1432
  35. package/PROGRESS.md +0 -86
  36. package/TODO-2.md +0 -121
  37. package/TODO-CHARGEBLAST.md +0 -32
  38. package/TODO-WEBHOOK-KEY-LEGACY-REMOVAL.md +0 -15
  39. package/TODO-WEBHOOK-KEY-UPGRADE.md +0 -138
  40. package/TODO-email-auth.md +0 -14
  41. package/TODO.md +0 -187
  42. package/plans/mcp2.md +0 -247
  43. package/scripts/test-helper-providers.js +0 -162
  44. package/scripts/update-disposable-domains.js +0 -44
  45. package/templates/_.env +0 -42
  46. package/templates/_.gitignore +0 -60
  47. package/templates/backend-manager-config.json +0 -249
  48. package/templates/database.rules.json +0 -83
  49. package/templates/firebase.json +0 -66
  50. package/templates/firestore.indexes.json +0 -4
  51. package/templates/firestore.rules +0 -112
  52. package/templates/public/404.html +0 -26
  53. package/templates/public/index.html +0 -24
  54. package/templates/remoteconfig.template.json +0 -1
  55. package/templates/storage-lifecycle-config-1-day.json +0 -9
  56. package/templates/storage-lifecycle-config-30-days.json +0 -9
  57. package/templates/storage.rules +0 -8
  58. package/test/_init/accounts-validation.js +0 -58
  59. package/test/_legacy/ai/index.js +0 -231
  60. package/test/_legacy/ai/test.jpg +0 -0
  61. package/test/_legacy/ai/test.pdf +0 -0
  62. package/test/_legacy/index.js +0 -31
  63. package/test/_legacy/payment-resolver/chargebee/orders/refunded.json +0 -0
  64. package/test/_legacy/payment-resolver/chargebee/orders/unpaid.json +0 -98
  65. package/test/_legacy/payment-resolver/chargebee/subscriptions/active.json +0 -578
  66. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-in-trial.json +0 -38
  67. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-skipped-to-active.json +0 -135
  68. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-active-to-cancelled.json +0 -42
  69. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-active.json +0 -44
  70. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-cancelled.json +0 -39
  71. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-refund.json +0 -143
  72. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-suspended.json +0 -48
  73. package/test/_legacy/payment-resolver/coinbase/orders/regular.json +0 -204
  74. package/test/_legacy/payment-resolver/coinbase/subscriptions/cancelled.json +0 -204
  75. package/test/_legacy/payment-resolver/coinbase/subscriptions/paid-2.json +0 -125
  76. package/test/_legacy/payment-resolver/index.js +0 -1558
  77. package/test/_legacy/payment-resolver/paypal/orders/refunded.json +0 -0
  78. package/test/_legacy/payment-resolver/paypal/orders/regular.json +0 -120
  79. package/test/_legacy/payment-resolver/paypal/subscriptions/active-refund-previous-stmnt.json +0 -323
  80. package/test/_legacy/payment-resolver/paypal/subscriptions/active.json +0 -192
  81. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-in-trial.json +0 -125
  82. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-payment-not-complete.json +0 -76
  83. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-payment-overdue-2.json +0 -114
  84. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-payment-overdue.json +0 -114
  85. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-active-to-cancelled.json +0 -111
  86. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-active.json +0 -132
  87. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-cancelled.json +0 -107
  88. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-expired.json +0 -91
  89. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-refund.json +0 -147
  90. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-suspended.json +0 -120
  91. package/test/_legacy/payment-resolver/stripe/orders/refunded.json +0 -0
  92. package/test/_legacy/payment-resolver/stripe/orders/regular.json +0 -91
  93. package/test/_legacy/payment-resolver/stripe/subscriptions/active.json +0 -421
  94. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-in-trial.json +0 -349
  95. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-active-failed-authorization.json +0 -430
  96. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-active.json +0 -319
  97. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-cancelled.json +0 -319
  98. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-refund.json +0 -414
  99. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-suspended.json +0 -319
  100. package/test/_legacy/payment-resolver/stripe/subscriptions/unsure.json +0 -339
  101. package/test/_legacy/test-new +0 -1178
  102. package/test/_legacy/test.md +0 -89
  103. package/test/_legacy/usage.js +0 -175
  104. package/test/_legacy/user.js +0 -31
  105. package/test/ai/tools-live.js +0 -170
  106. package/test/boot/emulator-boots.js +0 -37
  107. package/test/content/blog-generate.js +0 -160
  108. package/test/email/campaign-send.js +0 -45
  109. package/test/email/consent-lifecycle.js +0 -257
  110. package/test/email/feedback-and-plain-send.js +0 -52
  111. package/test/email/fixtures/editorial.json +0 -30
  112. package/test/email/fixtures/field-report.json +0 -53
  113. package/test/email/marketing-lifecycle.js +0 -132
  114. package/test/email/newsletter-generate.js +0 -819
  115. package/test/email/newsletter-templates.js +0 -491
  116. package/test/email/templates.js +0 -207
  117. package/test/email/transactional-send.js +0 -34
  118. package/test/email/transactional.js +0 -560
  119. package/test/email/validation.js +0 -710
  120. package/test/events/auth-delete-race.js +0 -201
  121. package/test/events/payments/journey-payments-cancel-endpoint.js +0 -100
  122. package/test/events/payments/journey-payments-cancel.js +0 -182
  123. package/test/events/payments/journey-payments-discount.js +0 -89
  124. package/test/events/payments/journey-payments-failure.js +0 -146
  125. package/test/events/payments/journey-payments-legacy-product.js +0 -149
  126. package/test/events/payments/journey-payments-one-time-failure.js +0 -111
  127. package/test/events/payments/journey-payments-one-time.js +0 -136
  128. package/test/events/payments/journey-payments-plan-change.js +0 -144
  129. package/test/events/payments/journey-payments-refund-webhook.js +0 -180
  130. package/test/events/payments/journey-payments-suspend.js +0 -181
  131. package/test/events/payments/journey-payments-trial-cancel.js +0 -130
  132. package/test/events/payments/journey-payments-trial.js +0 -171
  133. package/test/events/payments/journey-payments-uid-resolution.js +0 -132
  134. package/test/events/payments/journey-payments-upgrade.js +0 -135
  135. package/test/fixtures/chargebee/invoice-one-time.json +0 -27
  136. package/test/fixtures/chargebee/subscription-active.json +0 -44
  137. package/test/fixtures/chargebee/subscription-cancelled.json +0 -42
  138. package/test/fixtures/chargebee/subscription-in-trial.json +0 -41
  139. package/test/fixtures/chargebee/subscription-legacy-plan.json +0 -41
  140. package/test/fixtures/chargebee/subscription-non-renewing.json +0 -41
  141. package/test/fixtures/chargebee/subscription-paused.json +0 -42
  142. package/test/fixtures/chargebee/webhook-payment-failed.json +0 -51
  143. package/test/fixtures/chargebee/webhook-subscription-created.json +0 -47
  144. package/test/fixtures/paypal/order-approved.json +0 -62
  145. package/test/fixtures/paypal/order-completed.json +0 -110
  146. package/test/fixtures/paypal/subscription-active.json +0 -76
  147. package/test/fixtures/paypal/subscription-cancelled.json +0 -50
  148. package/test/fixtures/paypal/subscription-suspended.json +0 -65
  149. package/test/fixtures/stripe/checkout-session-completed.json +0 -130
  150. package/test/fixtures/stripe/invoice-payment-failed.json +0 -148
  151. package/test/fixtures/stripe/invoice-subscription-payment-failed.json +0 -28
  152. package/test/fixtures/stripe/subscription-active.json +0 -161
  153. package/test/fixtures/stripe/subscription-canceled.json +0 -161
  154. package/test/fixtures/stripe/subscription-trialing.json +0 -161
  155. package/test/functions/admin/database-read.js +0 -134
  156. package/test/functions/admin/database-write.js +0 -159
  157. package/test/functions/admin/edit-post.js +0 -366
  158. package/test/functions/admin/firestore-query.js +0 -207
  159. package/test/functions/admin/firestore-read.js +0 -129
  160. package/test/functions/admin/firestore-write.js +0 -105
  161. package/test/functions/admin/get-stats.js +0 -115
  162. package/test/functions/admin/send-email.js +0 -119
  163. package/test/functions/admin/send-notification.js +0 -208
  164. package/test/functions/admin/write-repo-content.js +0 -223
  165. package/test/functions/general/add-marketing-contact.js +0 -335
  166. package/test/functions/general/fetch-post.js +0 -54
  167. package/test/functions/general/generate-uuid.js +0 -114
  168. package/test/functions/test/authenticate.js +0 -64
  169. package/test/functions/user/create-custom-token.js +0 -73
  170. package/test/functions/user/delete.js +0 -135
  171. package/test/functions/user/get-active-sessions.js +0 -111
  172. package/test/functions/user/get-subscription-info.js +0 -99
  173. package/test/functions/user/regenerate-api-keys.js +0 -156
  174. package/test/functions/user/resolve.js +0 -52
  175. package/test/functions/user/sign-out-all-sessions.js +0 -94
  176. package/test/functions/user/sign-up.js +0 -252
  177. package/test/functions/user/submit-feedback.js +0 -104
  178. package/test/functions/user/validate-settings.js +0 -82
  179. package/test/helpers/ai-request-payload.js +0 -300
  180. package/test/helpers/ai-test-provider.js +0 -202
  181. package/test/helpers/ai-tools-format.js +0 -383
  182. package/test/helpers/content/blog-auto-publisher.js +0 -484
  183. package/test/helpers/content/feed-parser.js +0 -528
  184. package/test/helpers/content/ghostii-blocks.js +0 -134
  185. package/test/helpers/content/ghostii-feed-integration.js +0 -404
  186. package/test/helpers/content/ghostii-write-article.js +0 -243
  187. package/test/helpers/environment.js +0 -230
  188. package/test/helpers/infer-contact.js +0 -113
  189. package/test/helpers/merge-line-files.js +0 -271
  190. package/test/helpers/payment/chargebee/parse-webhook.js +0 -413
  191. package/test/helpers/payment/chargebee/to-unified-one-time.js +0 -147
  192. package/test/helpers/payment/chargebee/to-unified-subscription.js +0 -648
  193. package/test/helpers/payment/paypal/parse-webhook.js +0 -539
  194. package/test/helpers/payment/paypal/to-unified-one-time.js +0 -382
  195. package/test/helpers/payment/paypal/to-unified-subscription.js +0 -820
  196. package/test/helpers/payment/stripe/parse-webhook.js +0 -447
  197. package/test/helpers/payment/stripe/to-unified-one-time.js +0 -306
  198. package/test/helpers/payment/stripe/to-unified-subscription.js +0 -708
  199. package/test/helpers/sanitize.js +0 -222
  200. package/test/helpers/slugify.js +0 -394
  201. package/test/helpers/storage.js +0 -194
  202. package/test/helpers/user.js +0 -755
  203. package/test/helpers/webhook-forward.js +0 -418
  204. package/test/mcp/discovery.js +0 -53
  205. package/test/mcp/oauth.js +0 -161
  206. package/test/mcp/protocol.js +0 -268
  207. package/test/mcp/roles.js +0 -188
  208. package/test/mcp/utils.js +0 -245
  209. package/test/routes/admin/create-post.js +0 -364
  210. package/test/routes/admin/database.js +0 -131
  211. package/test/routes/admin/deduplicate-image-alts.js +0 -190
  212. package/test/routes/admin/email.js +0 -114
  213. package/test/routes/admin/firestore-query.js +0 -204
  214. package/test/routes/admin/firestore.js +0 -127
  215. package/test/routes/admin/infer-contact.js +0 -218
  216. package/test/routes/admin/notification.js +0 -198
  217. package/test/routes/admin/post-resize-image.js +0 -184
  218. package/test/routes/admin/post.js +0 -368
  219. package/test/routes/admin/repo-content.js +0 -223
  220. package/test/routes/admin/stats.js +0 -112
  221. package/test/routes/content/post.js +0 -54
  222. package/test/routes/general/uuid.js +0 -115
  223. package/test/routes/marketing/campaign.js +0 -125
  224. package/test/routes/marketing/contact.js +0 -370
  225. package/test/routes/marketing/email-preferences.js +0 -292
  226. package/test/routes/marketing/push-send.js +0 -32
  227. package/test/routes/marketing/webhook-forward.js +0 -61
  228. package/test/routes/marketing/webhook.js +0 -639
  229. package/test/routes/payments/cancel.js +0 -140
  230. package/test/routes/payments/discount.js +0 -80
  231. package/test/routes/payments/dispute-alert.js +0 -320
  232. package/test/routes/payments/intent.js +0 -336
  233. package/test/routes/payments/portal.js +0 -92
  234. package/test/routes/payments/refund.js +0 -179
  235. package/test/routes/payments/trial-eligibility.js +0 -71
  236. package/test/routes/payments/webhook.js +0 -107
  237. package/test/routes/test/authenticate.js +0 -59
  238. package/test/routes/test/schema.js +0 -554
  239. package/test/routes/test/usage.js +0 -342
  240. package/test/routes/user/api-keys.js +0 -156
  241. package/test/routes/user/delete.js +0 -136
  242. package/test/routes/user/feedback.js +0 -104
  243. package/test/routes/user/sessions.js +0 -199
  244. package/test/routes/user/settings-validate.js +0 -82
  245. package/test/routes/user/signup.js +0 -542
  246. package/test/routes/user/subscription.js +0 -99
  247. package/test/routes/user/token.js +0 -73
  248. package/test/routes/user/user.js +0 -157
  249. package/test/rules/notifications.js +0 -410
  250. package/test/rules/payments-carts.js +0 -371
  251. package/test/rules/user.js +0 -396
@@ -1,45 +0,0 @@
1
- /**
2
- * Test: Single marketing campaign send
3
- * Verifies the full marketing pipeline end-to-end: prepare → render → audience → SendGrid Single Send.
4
- * Sends to test_admin segment only.
5
- */
6
- module.exports = {
7
- description: 'Marketing campaign send',
8
- auth: 'admin',
9
- skip: !process.env.TEST_EXTENDED_MODE ? 'TEST_EXTENDED_MODE not set' : false,
10
- timeout: 60000,
11
-
12
- async run({ http, assert }) {
13
- const response = await http.post('backend-manager/marketing/campaign', {
14
- name: '[TEST] Summer Sale — Free Users',
15
- subject: 'Summer Sale — {discount.percent}% Off Your First Month!',
16
- preheader: 'Limited time offer — upgrade to Premium today',
17
- template: 'card',
18
- data: {
19
- content: {
20
- title: 'Summer Sale!',
21
- message: [
22
- 'You\'ve been using **{brand.name}** on our free plan — and we think you\'ll love what\'s on the other side.',
23
- '',
24
- 'For a limited time, upgrade to Premium and get **{discount.percent}% off** your first month.',
25
- '',
26
- 'Use code **{discount.code}** at checkout.',
27
- ].join('\n'),
28
- button: { text: 'Upgrade Now →', url: '{brand.url}/pricing' },
29
- discountCode: 'UPGRADE15',
30
- },
31
- },
32
- test: true,
33
- sendAt: 'now',
34
- });
35
-
36
- assert.isSuccess(response, 'Should create and schedule campaign');
37
- assert.ok(response.data.id, 'Should have campaign ID');
38
-
39
- const sg = response.data?.providers?.campaigns;
40
- assert.ok(sg, 'Should have campaigns result');
41
- assert.equal(sg.success, true, 'Campaigns (SendGrid) should succeed');
42
- assert.ok(sg.id, 'Should have Single Send ID');
43
- assert.equal(sg.scheduled, true, 'Should be scheduled');
44
- },
45
- };
@@ -1,257 +0,0 @@
1
- /**
2
- * Test: Marketing Provider Lifecycle (end-to-end against live SendGrid + Beehiiv)
3
- *
4
- * Walks two long-lived test accounts through their full lifecycle and verifies
5
- * provider state at every transition:
6
- *
7
- * 1. Pre-check — both accounts should be absent from SendGrid + Beehiiv
8
- * 2. Sync — flip consent.marketing.status and call Marketing.sync()
9
- * 3. Verify — granted account present in both, declined account absent from both
10
- * 4. Unsubscribe — hit the email-preferences endpoint for the granted account
11
- * 5. Verify — granted account now absent from both
12
- *
13
- * The two accounts use the `_test.allow_*` prefix so they bypass the
14
- * blocked-local-patterns gate (which blocks plain `_test.*` from reaching
15
- * providers). They are the only accounts intentionally allowed to round-trip
16
- * through SendGrid + Beehiiv.
17
- *
18
- * Run with TEST_EXTENDED_MODE=true (no-op otherwise). Requires SENDGRID_API_KEY
19
- * and BEEHIIV_API_KEY in env. Total runtime is ~60-90s — most of it spent waiting
20
- * for SendGrid's async upsert/delete background jobs to surface.
21
- */
22
- const sendgridProvider = require('../../src/manager/libraries/email/providers/sendgrid.js');
23
- const beehiivProvider = require('../../src/manager/libraries/email/providers/beehiiv.js');
24
-
25
- const SETTLE_MS = 5000; // Beehiiv settles in 1-2s; SendGrid's background-job upsert can take 10-20s+
26
- const POLL_INTERVAL_MS = 2000;
27
- const POLL_MAX_MS = 90000; // SendGrid's delete-contact job can take 30-60s+ to surface
28
-
29
- function sleep(ms) {
30
- return new Promise((resolve) => setTimeout(resolve, ms));
31
- }
32
-
33
- /**
34
- * Poll a provider lookup until it matches the expected state (present/absent).
35
- * Returns the resolved value (contact object or null) once condition is met, or
36
- * the last value seen after timing out.
37
- *
38
- * @param {Function} fetchFn - async function that returns contact or null
39
- * @param {boolean} expectPresent - true = wait until present, false = wait until absent
40
- */
41
- async function pollProvider(fetchFn, expectPresent) {
42
- const start = Date.now();
43
- let lastValue = null;
44
-
45
- while (Date.now() - start < POLL_MAX_MS) {
46
- lastValue = await fetchFn();
47
- const present = !!lastValue;
48
- if (present === expectPresent) {
49
- return lastValue;
50
- }
51
- await sleep(POLL_INTERVAL_MS);
52
- }
53
-
54
- return lastValue;
55
- }
56
-
57
- module.exports = {
58
- description: 'Marketing provider lifecycle (live SendGrid + Beehiiv round-trip)',
59
- type: 'group',
60
- skip: !process.env.TEST_EXTENDED_MODE
61
- ? 'TEST_EXTENDED_MODE not set (this test hits live SendGrid + Beehiiv APIs)'
62
- : false,
63
- tests: [
64
- // ─────────────────────────────────────────────────────────────────────
65
- // Phase 1 — Pre-check: both accounts should be absent from providers
66
- // ─────────────────────────────────────────────────────────────────────
67
- {
68
- name: 'phase-1-pre-check-both-accounts-absent',
69
- auth: 'admin',
70
- timeout: 180000,
71
-
72
- async run({ accounts, assert }) {
73
- const granted = accounts['consent-granted'];
74
- const declined = accounts['consent-declined'];
75
-
76
- // Force-clean any leftovers from a prior run (e.g. the previous phase-2a
77
- // left a contact in SendGrid that THIS run's phase-1 needs to start
78
- // without). SendGrid's delete is an async job, so wait for absence.
79
- await sendgridProvider.removeContact(granted.email);
80
- await sendgridProvider.removeContact(declined.email);
81
- await beehiivProvider.removeContact(granted.email);
82
- await beehiivProvider.removeContact(declined.email);
83
-
84
- const grantedSg = await pollProvider(() => sendgridProvider.findContact(granted.email), false);
85
- const declinedSg = await pollProvider(() => sendgridProvider.findContact(declined.email), false);
86
- const grantedBh = await pollProvider(() => beehiivProvider.findContact(granted.email), false);
87
- const declinedBh = await pollProvider(() => beehiivProvider.findContact(declined.email), false);
88
-
89
- assert.equal(grantedSg, null, 'granted account should be absent from SendGrid');
90
- assert.equal(declinedSg, null, 'declined account should be absent from SendGrid');
91
- assert.equal(grantedBh, null, 'granted account should be absent from Beehiiv');
92
- assert.equal(declinedBh, null, 'declined account should be absent from Beehiiv');
93
- },
94
- },
95
-
96
- // ─────────────────────────────────────────────────────────────────────
97
- // Phase 2 — Sync granted account → both providers
98
- // ─────────────────────────────────────────────────────────────────────
99
- {
100
- name: 'phase-2a-granted-account-syncs-to-both-providers',
101
- auth: 'admin',
102
- timeout: 180000,
103
-
104
- async run({ accounts, assert, Manager, assistant, config }) {
105
- const granted = accounts['consent-granted'];
106
- const admin = Manager.libraries.admin;
107
-
108
- // Set consent.marketing.status = 'granted' on the user doc
109
- await admin.firestore().doc(`users/${granted.uid}`).set({
110
- consent: {
111
- marketing: {
112
- status: 'granted',
113
- grantedAt: {
114
- timestamp: new Date().toISOString(),
115
- timestampUNIX: Math.floor(Date.now() / 1000),
116
- source: 'test',
117
- ip: null,
118
- text: 'consent-lifecycle test',
119
- },
120
- },
121
- legal: {
122
- status: 'granted',
123
- grantedAt: {
124
- timestamp: new Date().toISOString(),
125
- timestampUNIX: Math.floor(Date.now() / 1000),
126
- source: 'test',
127
- ip: null,
128
- text: 'consent-lifecycle test',
129
- },
130
- },
131
- },
132
- }, { merge: true });
133
-
134
- // Trigger marketing sync via the Email() surface
135
- const result = await Manager.Email(assistant).sync(granted.uid);
136
- assert.ok(result, 'sync should return a result');
137
- assert.notEqual(result.blocked, 'validation', 'sync should not be blocked by validation (uses _test.allow_*)');
138
-
139
- // Poll providers until they reflect the upsert. SendGrid's upsert is
140
- // an async background job (returns a job_id, not the inserted contact)
141
- // so a single check 5s later isn't enough — it can take 10-20s to
142
- // surface. Beehiiv is usually instant but uses the same poll for symmetry.
143
- const sgContact = await pollProvider(() => sendgridProvider.findContact(granted.email), true);
144
- const bhContact = await pollProvider(() => beehiivProvider.findContact(granted.email), true);
145
-
146
- assert.ok(sgContact, 'granted account should now exist in SendGrid');
147
- if (config.marketing?.newsletter?.publicationId) {
148
- assert.ok(bhContact, 'granted account should now exist in Beehiiv');
149
- }
150
- },
151
- },
152
-
153
- // ─────────────────────────────────────────────────────────────────────
154
- // Phase 2b — Declined account: verify it stays out of providers when we
155
- // DON'T call sync (which is what the signup route does for declined
156
- // marketing consent).
157
- // ─────────────────────────────────────────────────────────────────────
158
- {
159
- name: 'phase-2b-declined-account-stays-out-of-providers',
160
- auth: 'admin',
161
- timeout: 180000,
162
-
163
- async run({ accounts, assert, Manager, assistant }) {
164
- const declined = accounts['consent-declined'];
165
- const admin = Manager.libraries.admin;
166
-
167
- // Set consent.marketing.status = 'revoked' on the user doc.
168
- // We deliberately do NOT call sync — the production signup route gates
169
- // on consent.marketing.status === 'granted' before calling sync, so a
170
- // declined user simply never gets a sync call. We verify that contract
171
- // by NOT calling sync and asserting the user stays absent.
172
- await admin.firestore().doc(`users/${declined.uid}`).set({
173
- consent: {
174
- marketing: {
175
- status: 'revoked',
176
- grantedAt: { timestamp: null, timestampUNIX: null, source: null, ip: null, text: null },
177
- revokedAt: {
178
- timestamp: new Date().toISOString(),
179
- timestampUNIX: Math.floor(Date.now() / 1000),
180
- source: 'test',
181
- ip: null,
182
- text: null,
183
- },
184
- },
185
- legal: {
186
- status: 'granted',
187
- grantedAt: {
188
- timestamp: new Date().toISOString(),
189
- timestampUNIX: Math.floor(Date.now() / 1000),
190
- source: 'test',
191
- ip: null,
192
- text: 'consent-lifecycle test',
193
- },
194
- },
195
- },
196
- }, { merge: true });
197
-
198
- await sleep(SETTLE_MS);
199
-
200
- const sgContact = await sendgridProvider.findContact(declined.email);
201
- const bhContact = await beehiivProvider.findContact(declined.email);
202
-
203
- assert.equal(sgContact, null, 'declined account should remain absent from SendGrid');
204
- assert.equal(bhContact, null, 'declined account should remain absent from Beehiiv');
205
- },
206
- },
207
-
208
- // ─────────────────────────────────────────────────────────────────────
209
- // Phase 3 — Unsubscribe: granted account is removed via Manager.Email().remove
210
- // ─────────────────────────────────────────────────────────────────────
211
- {
212
- name: 'phase-3-granted-account-unsubscribe-removes-from-both',
213
- auth: 'admin',
214
- timeout: 180000,
215
-
216
- async run({ accounts, assert, Manager, assistant }) {
217
- const granted = accounts['consent-granted'];
218
-
219
- // Trigger removal — simulates the email-preferences opt-out flow
220
- const result = await Manager.Email(assistant).remove(granted.email);
221
- assert.ok(result, 'remove should return a result');
222
-
223
- // Poll for absence — SendGrid's contact delete is also an async job.
224
- const sgContact = await pollProvider(() => sendgridProvider.findContact(granted.email), false);
225
- const bhContact = await pollProvider(() => beehiivProvider.findContact(granted.email), false);
226
-
227
- assert.equal(sgContact, null, 'granted account should now be absent from SendGrid');
228
- assert.equal(bhContact, null, 'granted account should now be absent from Beehiiv');
229
- },
230
- },
231
-
232
- // ─────────────────────────────────────────────────────────────────────
233
- // Phase 4 — Validation gate: _test.* (non-allow) is blocked by validate()
234
- // ─────────────────────────────────────────────────────────────────────
235
- {
236
- name: 'phase-4-non-allow-test-email-blocked-by-validation',
237
- auth: 'admin',
238
- timeout: 30000,
239
-
240
- async run({ assert, Manager, assistant }) {
241
- // _test.never-reaches-providers@... is NOT _test.allow_* → should be blocked
242
- const blockedEmail = '_test.never-reaches-providers@somiibo.com';
243
-
244
- const result = await Manager.Email(assistant).add({ email: blockedEmail });
245
-
246
- assert.equal(result.blocked, 'validation', 'non-allow _test.* email should be blocked by validation');
247
-
248
- // And the provider lookup should show nothing
249
- const sgContact = await sendgridProvider.findContact(blockedEmail);
250
- const bhContact = await beehiivProvider.findContact(blockedEmail);
251
-
252
- assert.equal(sgContact, null, 'blocked email should never appear in SendGrid');
253
- assert.equal(bhContact, null, 'blocked email should never appear in Beehiiv');
254
- },
255
- },
256
- ],
257
- };
@@ -1,52 +0,0 @@
1
- /**
2
- * Test: Send feedback + plain template emails
3
- * Quick visual test — sends one of each to verify rendering.
4
- */
5
- module.exports = {
6
- description: 'Feedback + plain template send',
7
- type: 'group',
8
- skip: !process.env.TEST_EXTENDED_MODE ? 'TEST_EXTENDED_MODE not set' : false,
9
- tests: [
10
- {
11
- name: 'feedback-template',
12
- auth: 'admin',
13
- timeout: 30000,
14
- async run({ http, assert, config }) {
15
- const response = await http.post('backend-manager/admin/email', {
16
- subject: '[TEST] Feedback template',
17
- to: `_test-email-send@${config.domain}`,
18
- template: 'feedback',
19
- sender: 'hello',
20
- copy: false,
21
- data: { signoff: { type: 'personal' } },
22
- });
23
-
24
- assert.isSuccess(response, 'Should send feedback email');
25
- assert.equal(response.data.status, 'sent', 'Status should be sent');
26
- },
27
- },
28
- {
29
- name: 'plain-template-with-markdown',
30
- auth: 'admin',
31
- timeout: 30000,
32
- async run({ http, assert, config }) {
33
- const response = await http.post('backend-manager/admin/email', {
34
- subject: '[TEST] Plain template with markdown',
35
- to: `_test-email-send@${config.domain}`,
36
- template: 'plain',
37
- sender: 'hello',
38
- copy: false,
39
- data: {
40
- content: {
41
- message: '# Quick Update\n\nHey Ian,\n\nJust wanted to let you know that your **account settings** have been updated successfully.\n\nHere is what changed:\n\n- Email notifications: **enabled**\n- Two-factor auth: **enabled**\n- API key: [regenerated](https://example.com/account)\n\nIf you didn\'t make these changes, please [contact support](https://example.com/support) immediately.\n\nThanks!',
42
- },
43
- signoff: { type: 'personal' },
44
- },
45
- });
46
-
47
- assert.isSuccess(response, 'Should send plain email');
48
- assert.equal(response.data.status, 'sent', 'Status should be sent');
49
- },
50
- },
51
- ],
52
- };
@@ -1,30 +0,0 @@
1
- {
2
- "_comment": "Predefined fixture for the `editorial` template. Loaded via NEWSLETTER_FIXTURE=editorial. Same classic content shape as clean.json — both templates render the same data.",
3
- "subject": "Identity is the new growth lever",
4
- "preheader": "Three platform shifts to lock in this week.",
5
- "summary": "LinkedIn, YouTube, and the broader creator stack are all tightening identity controls this week. The operators with documented attribution histories will outrun the rest. Documentation is no longer overhead — it's the moat.",
6
- "tags": ["linkedin", "youtube", "platform-policy", "creator-economy", "operator-tools"],
7
- "intro": "Identity matters because trust is the new growth lever. Teams that handle this well will spend less time cleaning up later, and the patterns emerging this week tell you exactly where to focus.",
8
- "sections": [
9
- {
10
- "title": "LinkedIn ships verified-profile gates for business accounts",
11
- "body": "LinkedIn now requires verified identity attestations on any business profile claiming more than 500 followers. The rollout is fast and uneven — some accounts hit the gate inside two days of profile activity, others sit unchallenged for weeks. The pattern that matters is this: accounts with documented attribution histories sail through verification in under twelve hours. Accounts without get queued. The practical implication is to lock down your attribution log this week, not next.",
12
- "image_prompt": "Abstract geometric illustration of a verification checkmark inside a layered profile card."
13
- },
14
- {
15
- "title": "Documentation is the new operator moat",
16
- "body": "Operator playbooks — once dismissed as overhead — are now the difference between an account that recovers from a flag and one that gets permanently restricted. The teams already running on documented processes recover in days. The ones without burn weeks negotiating with support queues. Treat your playbook as a compliance artifact, not a knowledge-management nice-to-have.",
17
- "image_prompt": "Stack of geometric document layers casting clean shadows on a flat surface."
18
- },
19
- {
20
- "title": "YouTube tests creator-attribution metadata on uploads",
21
- "body": "A subset of channels saw a new upload checkbox this week asking whether content was assisted by automation. The checkbox is optional today. Reading the room: it will not stay optional. Creators with clear internal workflows already labeled will adapt in an afternoon. Everyone else will spend a quarter retrofitting.",
22
- "image_prompt": "Minimalist play button morphing into a label tag, flat geometric style."
23
- }
24
- ],
25
- "signoff": "Best,\nThe Somiibo Team",
26
- "citations": [
27
- { "note": "LinkedIn flagged 12,000 impersonation attempts in the verified-profile beta.", "source": "Reported by LinkedIn product team, May 2026" },
28
- { "note": "Operator playbook adoption up 38% week-over-week in Q2.", "source": "Per platform analytics, May 2026" }
29
- ]
30
- }
@@ -1,53 +0,0 @@
1
- {
2
- "_comment": "Predefined fixture for the `field-report` template. Loaded via NEWSLETTER_FIXTURE=field-report. Wire-service content shape: tldr + dateline + dispatches[{kicker, headline, byline, location, lede, dispatch, dataPoints, image_prompt}].",
3
- "subject": "LinkedIn tightens identity — operators document everything",
4
- "preheader": "Three filings from this week's platform front lines.",
5
- "summary": "Wire from the platform front: LinkedIn is rolling identity verification across every business profile, YouTube is piloting upload-time attribution metadata, and the operators with clean documentation are outrunning everyone else.",
6
- "tags": ["linkedin", "youtube", "platform-policy", "verification", "operator-playbook"],
7
- "tldr": "LinkedIn is rolling out verified-account scrutiny across every business profile. Operators with paper trails will outrun the rest. Documentation is the new growth lever.",
8
- "dateline": "OAKLAND",
9
- "signoff": "— Stay sharp,\nThe Somiibo Desk",
10
- "citations": [
11
- { "note": "LinkedIn flagged 12,000 impersonation attempts in the verified-profile beta.", "source": "Reported by LinkedIn product team, May 2026" },
12
- { "note": "Operator playbook adoption up 38% week-over-week in Q2.", "source": "Per platform analytics, May 2026" }
13
- ],
14
- "dispatches": [
15
- {
16
- "kicker": "LEAD DISPATCH",
17
- "headline": "LinkedIn ships verified-profile gates for every business account",
18
- "byline": "Filed by The Somiibo platform desk",
19
- "location": "OAKLAND",
20
- "lede": "A wave of identity-verification gates is rolling out across LinkedIn business profiles this week — and the platform is not asking politely.",
21
- "dispatch": "LinkedIn now requires verified identity attestations on any business profile claiming more than 500 followers. The rollout is fast and uneven: some accounts hit the gate inside two days of profile activity, others sit unchallenged for weeks. The pattern that matters is this — accounts with documented attribution histories sail through verification in under twelve hours. Accounts without get queued. The practical implication is to lock down your attribution log this week, not next.",
22
- "dataPoints": [
23
- { "label": "PROFILES FLAGGED", "value": "12.4K" },
24
- { "label": "AVG REVIEW TIME", "value": "14HR" },
25
- { "label": "WoW APPROVAL", "value": "+38%" }
26
- ],
27
- "image_prompt": "Abstract geometric illustration of a verification checkmark inside a layered profile card."
28
- },
29
- {
30
- "kicker": "FIELD NOTES",
31
- "headline": "Documentation is the new operator moat",
32
- "byline": "Filed by The Somiibo growth desk",
33
- "location": "REMOTE",
34
- "lede": "The teams surviving this round of platform scrutiny share one habit: they write everything down.",
35
- "dispatch": "Operator playbooks — once dismissed as overhead — are now the difference between an account that recovers from a flag and one that gets permanently restricted. The teams already running on documented processes recover in days. The ones without burn weeks negotiating with support queues. Treat your playbook as a compliance artifact, not a knowledge-management nice-to-have.",
36
- "dataPoints": [
37
- { "label": "RECOVERY (DOCUMENTED)", "value": "4D" },
38
- { "label": "RECOVERY (UNDOC)", "value": "21D" }
39
- ],
40
- "image_prompt": "Stack of geometric document layers casting clean shadows on a flat surface."
41
- },
42
- {
43
- "kicker": "WATCH",
44
- "headline": "YouTube tests creator-attribution metadata on uploads",
45
- "byline": "Filed by The Somiibo signals desk",
46
- "location": "NEW YORK",
47
- "lede": "A small pilot suggests YouTube will soon ask creators to declare automated tooling and cross-posting workflows at upload time.",
48
- "dispatch": "A subset of channels saw a new upload checkbox this week asking whether content was assisted by automation. The checkbox is optional today. Reading the room: it will not stay optional. Creators with clear internal workflows already labeled will adapt in an afternoon. Everyone else will spend a quarter retrofitting.",
49
- "dataPoints": [],
50
- "image_prompt": "Minimalist play button morphing into a label tag, flat geometric style."
51
- }
52
- ]
53
- }
@@ -1,132 +0,0 @@
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('backend-manager/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, config }) {
35
- const response = await http.post('backend-manager/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.campaigns', 'Should have SendGrid result');
47
- assert.propertyEquals(response, 'data.providers.campaigns.success', true, 'SendGrid add should succeed');
48
- }
49
-
50
- if (process.env.BEEHIIV_API_KEY && config.marketing?.newsletter?.publicationId) {
51
- assert.hasProperty(response, 'data.providers.newsletter', 'Should have Beehiiv result');
52
- assert.propertyEquals(response, 'data.providers.newsletter.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, config, Manager }) {
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;
68
- const admin = Manager.libraries.admin;
69
- await admin.firestore().doc(`users/${grantedUid}`).set({
70
- consent: { marketing: { status: 'granted' } },
71
- }, { merge: true });
72
-
73
- const response = await http.put('backend-manager/marketing/contact', {
74
- uid: grantedUid,
75
- });
76
-
77
- assert.isSuccess(response, 'Sync contact should succeed');
78
- assert.propertyEquals(response, 'data.success', true, 'success should be true');
79
-
80
- if (process.env.SENDGRID_API_KEY) {
81
- assert.hasProperty(response, 'data.providers.campaigns', 'Should have SendGrid result');
82
- assert.propertyEquals(response, 'data.providers.campaigns.success', true, 'SendGrid sync should succeed');
83
- }
84
-
85
- if (process.env.BEEHIIV_API_KEY && config.marketing?.newsletter?.publicationId) {
86
- assert.hasProperty(response, 'data.providers.newsletter', 'Should have Beehiiv result');
87
- assert.propertyEquals(response, 'data.providers.newsletter.success', true, 'Beehiiv sync should succeed');
88
- }
89
- },
90
- },
91
-
92
- // Step 3: Remove the contact via DELETE /marketing/contact
93
- {
94
- name: 'remove-contact',
95
- auth: 'admin',
96
-
97
- async run({ http, assert, state, config }) {
98
- const response = await http.delete('backend-manager/marketing/contact', {
99
- email: state.testEmail,
100
- });
101
-
102
- assert.isSuccess(response, 'Remove contact should succeed');
103
- assert.propertyEquals(response, 'data.success', true, 'success should be true');
104
-
105
- if (process.env.SENDGRID_API_KEY) {
106
- assert.hasProperty(response, 'data.providers.campaigns', 'Should have SendGrid result');
107
- assert.propertyEquals(response, 'data.providers.campaigns.success', true, 'SendGrid remove should succeed');
108
- }
109
-
110
- if (process.env.BEEHIIV_API_KEY && config.marketing?.newsletter?.publicationId) {
111
- assert.hasProperty(response, 'data.providers.newsletter', 'Should have Beehiiv result');
112
- assert.propertyEquals(response, 'data.providers.newsletter.success', true, 'Beehiiv remove should succeed');
113
- }
114
- },
115
- },
116
-
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).
121
- {
122
- name: 'cleanup-synced-contact',
123
- auth: 'admin',
124
-
125
- async run({ http, accounts }) {
126
- await http.delete('backend-manager/marketing/contact', {
127
- email: accounts['journey-marketing-sync'].email,
128
- }).catch(() => {});
129
- },
130
- },
131
- ],
132
- };