backend-manager 5.9.5 → 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 (244) hide show
  1. package/package.json +8 -1
  2. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/metadata.json +14 -0
  3. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/newsletter.html +486 -0
  4. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/newsletter.md +41 -0
  5. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/newsletter.mjml +97 -0
  6. package/{test/email/fixtures/clean.json → src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/structure.json} +16 -4
  7. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/summary.md +1 -0
  8. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/metadata.json +14 -0
  9. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/newsletter.html +486 -0
  10. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/newsletter.md +41 -0
  11. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/newsletter.mjml +97 -0
  12. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/structure.json +42 -0
  13. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/summary.md +1 -0
  14. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/metadata.json +14 -0
  15. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/newsletter.html +486 -0
  16. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/newsletter.md +41 -0
  17. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/newsletter.mjml +97 -0
  18. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/structure.json +42 -0
  19. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/summary.md +1 -0
  20. package/src/test/fixtures/firebase-project/.temp/test-mode.json +6 -0
  21. package/src/test/fixtures/firebase-project/database-debug.log +12 -0
  22. package/src/test/fixtures/firebase-project/firestore-debug.log +136 -0
  23. package/src/test/fixtures/firebase-project/pubsub-debug.log +17 -0
  24. package/.codeclimate.yml +0 -32
  25. package/.nvmrc +0 -1
  26. package/CHANGELOG.md +0 -1440
  27. package/PROGRESS.md +0 -93
  28. package/TODO-2.md +0 -121
  29. package/TODO-CHARGEBLAST.md +0 -32
  30. package/TODO-WEBHOOK-KEY-LEGACY-REMOVAL.md +0 -15
  31. package/TODO-WEBHOOK-KEY-UPGRADE.md +0 -138
  32. package/TODO-email-auth.md +0 -14
  33. package/TODO.md +0 -187
  34. package/plans/mcp2.md +0 -247
  35. package/plans/suspended-subscription-dead-zone.md +0 -97
  36. package/scripts/test-helper-providers.js +0 -162
  37. package/scripts/update-disposable-domains.js +0 -44
  38. package/templates/_.env +0 -42
  39. package/templates/_.gitignore +0 -60
  40. package/templates/backend-manager-config.json +0 -249
  41. package/templates/database.rules.json +0 -83
  42. package/templates/firebase.json +0 -66
  43. package/templates/firestore.indexes.json +0 -4
  44. package/templates/firestore.rules +0 -112
  45. package/templates/public/404.html +0 -26
  46. package/templates/public/index.html +0 -24
  47. package/templates/remoteconfig.template.json +0 -1
  48. package/templates/storage-lifecycle-config-1-day.json +0 -9
  49. package/templates/storage-lifecycle-config-30-days.json +0 -9
  50. package/templates/storage.rules +0 -8
  51. package/test/_init/accounts-validation.js +0 -58
  52. package/test/_legacy/ai/index.js +0 -231
  53. package/test/_legacy/ai/test.jpg +0 -0
  54. package/test/_legacy/ai/test.pdf +0 -0
  55. package/test/_legacy/index.js +0 -31
  56. package/test/_legacy/payment-resolver/chargebee/orders/refunded.json +0 -0
  57. package/test/_legacy/payment-resolver/chargebee/orders/unpaid.json +0 -98
  58. package/test/_legacy/payment-resolver/chargebee/subscriptions/active.json +0 -578
  59. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-in-trial.json +0 -38
  60. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-skipped-to-active.json +0 -135
  61. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-active-to-cancelled.json +0 -42
  62. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-active.json +0 -44
  63. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-cancelled.json +0 -39
  64. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-refund.json +0 -143
  65. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-suspended.json +0 -48
  66. package/test/_legacy/payment-resolver/coinbase/orders/regular.json +0 -204
  67. package/test/_legacy/payment-resolver/coinbase/subscriptions/cancelled.json +0 -204
  68. package/test/_legacy/payment-resolver/coinbase/subscriptions/paid-2.json +0 -125
  69. package/test/_legacy/payment-resolver/index.js +0 -1558
  70. package/test/_legacy/payment-resolver/paypal/orders/refunded.json +0 -0
  71. package/test/_legacy/payment-resolver/paypal/orders/regular.json +0 -120
  72. package/test/_legacy/payment-resolver/paypal/subscriptions/active-refund-previous-stmnt.json +0 -323
  73. package/test/_legacy/payment-resolver/paypal/subscriptions/active.json +0 -192
  74. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-in-trial.json +0 -125
  75. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-payment-not-complete.json +0 -76
  76. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-payment-overdue-2.json +0 -114
  77. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-payment-overdue.json +0 -114
  78. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-active-to-cancelled.json +0 -111
  79. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-active.json +0 -132
  80. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-cancelled.json +0 -107
  81. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-expired.json +0 -91
  82. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-refund.json +0 -147
  83. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-suspended.json +0 -120
  84. package/test/_legacy/payment-resolver/stripe/orders/refunded.json +0 -0
  85. package/test/_legacy/payment-resolver/stripe/orders/regular.json +0 -91
  86. package/test/_legacy/payment-resolver/stripe/subscriptions/active.json +0 -421
  87. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-in-trial.json +0 -349
  88. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-active-failed-authorization.json +0 -430
  89. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-active.json +0 -319
  90. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-cancelled.json +0 -319
  91. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-refund.json +0 -414
  92. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-suspended.json +0 -319
  93. package/test/_legacy/payment-resolver/stripe/subscriptions/unsure.json +0 -339
  94. package/test/_legacy/test-new +0 -1178
  95. package/test/_legacy/test.md +0 -89
  96. package/test/_legacy/usage.js +0 -175
  97. package/test/_legacy/user.js +0 -31
  98. package/test/ai/tools-live.js +0 -170
  99. package/test/boot/emulator-boots.js +0 -37
  100. package/test/content/blog-generate.js +0 -160
  101. package/test/email/campaign-send.js +0 -45
  102. package/test/email/consent-lifecycle.js +0 -257
  103. package/test/email/feedback-and-plain-send.js +0 -52
  104. package/test/email/fixtures/editorial.json +0 -30
  105. package/test/email/fixtures/field-report.json +0 -53
  106. package/test/email/marketing-lifecycle.js +0 -132
  107. package/test/email/newsletter-generate.js +0 -819
  108. package/test/email/newsletter-templates.js +0 -491
  109. package/test/email/templates.js +0 -207
  110. package/test/email/transactional-send.js +0 -34
  111. package/test/email/transactional.js +0 -560
  112. package/test/email/validation.js +0 -710
  113. package/test/events/auth-delete-race.js +0 -201
  114. package/test/events/payments/journey-payments-cancel-endpoint.js +0 -100
  115. package/test/events/payments/journey-payments-cancel.js +0 -182
  116. package/test/events/payments/journey-payments-discount.js +0 -89
  117. package/test/events/payments/journey-payments-failure.js +0 -146
  118. package/test/events/payments/journey-payments-legacy-product.js +0 -149
  119. package/test/events/payments/journey-payments-one-time-failure.js +0 -111
  120. package/test/events/payments/journey-payments-one-time.js +0 -136
  121. package/test/events/payments/journey-payments-plan-change.js +0 -144
  122. package/test/events/payments/journey-payments-refund-webhook.js +0 -180
  123. package/test/events/payments/journey-payments-suspend.js +0 -181
  124. package/test/events/payments/journey-payments-trial-cancel.js +0 -130
  125. package/test/events/payments/journey-payments-trial.js +0 -171
  126. package/test/events/payments/journey-payments-uid-resolution.js +0 -132
  127. package/test/events/payments/journey-payments-upgrade.js +0 -135
  128. package/test/fixtures/chargebee/invoice-one-time.json +0 -27
  129. package/test/fixtures/chargebee/subscription-active.json +0 -44
  130. package/test/fixtures/chargebee/subscription-cancelled.json +0 -42
  131. package/test/fixtures/chargebee/subscription-in-trial.json +0 -41
  132. package/test/fixtures/chargebee/subscription-legacy-plan.json +0 -41
  133. package/test/fixtures/chargebee/subscription-non-renewing.json +0 -41
  134. package/test/fixtures/chargebee/subscription-paused.json +0 -42
  135. package/test/fixtures/chargebee/webhook-payment-failed.json +0 -51
  136. package/test/fixtures/chargebee/webhook-subscription-created.json +0 -47
  137. package/test/fixtures/paypal/order-approved.json +0 -62
  138. package/test/fixtures/paypal/order-completed.json +0 -110
  139. package/test/fixtures/paypal/subscription-active.json +0 -76
  140. package/test/fixtures/paypal/subscription-cancelled.json +0 -50
  141. package/test/fixtures/paypal/subscription-suspended.json +0 -65
  142. package/test/fixtures/stripe/checkout-session-completed.json +0 -130
  143. package/test/fixtures/stripe/invoice-payment-failed.json +0 -148
  144. package/test/fixtures/stripe/invoice-subscription-payment-failed.json +0 -28
  145. package/test/fixtures/stripe/subscription-active.json +0 -161
  146. package/test/fixtures/stripe/subscription-canceled.json +0 -161
  147. package/test/fixtures/stripe/subscription-trialing.json +0 -161
  148. package/test/functions/admin/database-read.js +0 -134
  149. package/test/functions/admin/database-write.js +0 -159
  150. package/test/functions/admin/edit-post.js +0 -366
  151. package/test/functions/admin/firestore-query.js +0 -207
  152. package/test/functions/admin/firestore-read.js +0 -129
  153. package/test/functions/admin/firestore-write.js +0 -105
  154. package/test/functions/admin/get-stats.js +0 -115
  155. package/test/functions/admin/send-email.js +0 -119
  156. package/test/functions/admin/send-notification.js +0 -208
  157. package/test/functions/admin/write-repo-content.js +0 -223
  158. package/test/functions/general/add-marketing-contact.js +0 -335
  159. package/test/functions/general/fetch-post.js +0 -54
  160. package/test/functions/general/generate-uuid.js +0 -114
  161. package/test/functions/test/authenticate.js +0 -64
  162. package/test/functions/user/create-custom-token.js +0 -73
  163. package/test/functions/user/delete.js +0 -135
  164. package/test/functions/user/get-active-sessions.js +0 -111
  165. package/test/functions/user/get-subscription-info.js +0 -99
  166. package/test/functions/user/regenerate-api-keys.js +0 -156
  167. package/test/functions/user/resolve.js +0 -52
  168. package/test/functions/user/sign-out-all-sessions.js +0 -94
  169. package/test/functions/user/sign-up.js +0 -252
  170. package/test/functions/user/submit-feedback.js +0 -104
  171. package/test/functions/user/validate-settings.js +0 -82
  172. package/test/helpers/ai-request-payload.js +0 -300
  173. package/test/helpers/ai-test-provider.js +0 -202
  174. package/test/helpers/ai-tools-format.js +0 -383
  175. package/test/helpers/content/blog-auto-publisher.js +0 -484
  176. package/test/helpers/content/feed-parser.js +0 -528
  177. package/test/helpers/content/ghostii-blocks.js +0 -134
  178. package/test/helpers/content/ghostii-feed-integration.js +0 -404
  179. package/test/helpers/content/ghostii-write-article.js +0 -243
  180. package/test/helpers/environment.js +0 -230
  181. package/test/helpers/infer-contact.js +0 -113
  182. package/test/helpers/merge-line-files.js +0 -271
  183. package/test/helpers/payment/chargebee/parse-webhook.js +0 -413
  184. package/test/helpers/payment/chargebee/to-unified-one-time.js +0 -147
  185. package/test/helpers/payment/chargebee/to-unified-subscription.js +0 -648
  186. package/test/helpers/payment/paypal/parse-webhook.js +0 -539
  187. package/test/helpers/payment/paypal/to-unified-one-time.js +0 -382
  188. package/test/helpers/payment/paypal/to-unified-subscription.js +0 -820
  189. package/test/helpers/payment/stripe/parse-webhook.js +0 -447
  190. package/test/helpers/payment/stripe/to-unified-one-time.js +0 -306
  191. package/test/helpers/payment/stripe/to-unified-subscription.js +0 -708
  192. package/test/helpers/sanitize.js +0 -222
  193. package/test/helpers/slugify.js +0 -394
  194. package/test/helpers/storage.js +0 -194
  195. package/test/helpers/user.js +0 -755
  196. package/test/helpers/webhook-forward.js +0 -418
  197. package/test/mcp/discovery.js +0 -53
  198. package/test/mcp/oauth.js +0 -161
  199. package/test/mcp/protocol.js +0 -268
  200. package/test/mcp/roles.js +0 -188
  201. package/test/mcp/utils.js +0 -245
  202. package/test/routes/admin/create-post.js +0 -364
  203. package/test/routes/admin/database.js +0 -131
  204. package/test/routes/admin/deduplicate-image-alts.js +0 -190
  205. package/test/routes/admin/email.js +0 -114
  206. package/test/routes/admin/firestore-query.js +0 -204
  207. package/test/routes/admin/firestore.js +0 -127
  208. package/test/routes/admin/infer-contact.js +0 -218
  209. package/test/routes/admin/notification.js +0 -198
  210. package/test/routes/admin/post-resize-image.js +0 -184
  211. package/test/routes/admin/post.js +0 -368
  212. package/test/routes/admin/repo-content.js +0 -223
  213. package/test/routes/admin/stats.js +0 -112
  214. package/test/routes/content/post.js +0 -54
  215. package/test/routes/general/uuid.js +0 -115
  216. package/test/routes/marketing/campaign.js +0 -125
  217. package/test/routes/marketing/contact.js +0 -370
  218. package/test/routes/marketing/email-preferences.js +0 -292
  219. package/test/routes/marketing/push-send.js +0 -32
  220. package/test/routes/marketing/webhook-forward.js +0 -61
  221. package/test/routes/marketing/webhook.js +0 -639
  222. package/test/routes/payments/cancel.js +0 -163
  223. package/test/routes/payments/discount.js +0 -80
  224. package/test/routes/payments/dispute-alert.js +0 -320
  225. package/test/routes/payments/intent.js +0 -336
  226. package/test/routes/payments/portal.js +0 -92
  227. package/test/routes/payments/refund.js +0 -179
  228. package/test/routes/payments/trial-eligibility.js +0 -71
  229. package/test/routes/payments/webhook.js +0 -107
  230. package/test/routes/test/authenticate.js +0 -59
  231. package/test/routes/test/schema.js +0 -554
  232. package/test/routes/test/usage.js +0 -342
  233. package/test/routes/user/api-keys.js +0 -156
  234. package/test/routes/user/delete.js +0 -136
  235. package/test/routes/user/feedback.js +0 -104
  236. package/test/routes/user/sessions.js +0 -199
  237. package/test/routes/user/settings-validate.js +0 -82
  238. package/test/routes/user/signup.js +0 -542
  239. package/test/routes/user/subscription.js +0 -99
  240. package/test/routes/user/token.js +0 -73
  241. package/test/routes/user/user.js +0 -157
  242. package/test/rules/notifications.js +0 -410
  243. package/test/rules/payments-carts.js +0 -371
  244. 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
- };