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,755 +0,0 @@
1
- /**
2
- * Test: helpers/user
3
- * Unit tests for Manager.User() schema-driven normalization
4
- *
5
- * Tests the declarative schema resolver: defaults, passthrough, templates, type coercion
6
- */
7
- const User = require('../../src/manager/helpers/user.js');
8
-
9
- // Mock Manager with minimal Utilities
10
- const Manager = {
11
- Utilities: () => ({
12
- randomId: ({ size }) => 'test123',
13
- }),
14
- };
15
-
16
- function createUser(settings) {
17
- return new User(Manager, settings).properties;
18
- }
19
-
20
- module.exports = {
21
- description: 'User() schema resolver',
22
- type: 'group',
23
-
24
- tests: [
25
- // ─── Empty / new user ───
26
-
27
- {
28
- name: 'empty-settings-gets-all-defaults',
29
- async run({ assert }) {
30
- const user = createUser({});
31
-
32
- // Auth
33
- assert.equal(user.auth.uid, null, 'auth.uid should be null');
34
- assert.equal(user.auth.email, null, 'auth.email should be null');
35
- assert.equal(user.auth.temporary, false, 'auth.temporary should be false');
36
-
37
- // Subscription
38
- assert.equal(user.subscription.product.id, 'basic', 'subscription.product.id should be basic');
39
- assert.equal(user.subscription.product.name, 'Basic', 'subscription.product.name should be Basic');
40
- assert.equal(user.subscription.status, 'active', 'subscription.status should be active');
41
- assert.equal(user.subscription.trial.claimed, false, 'subscription.trial.claimed should be false');
42
- assert.equal(user.subscription.cancellation.pending, false, 'subscription.cancellation.pending should be false');
43
-
44
- // Timestamps should exist
45
- assert.ok(user.subscription.expires.timestamp, 'subscription.expires.timestamp should exist');
46
- assert.equal(typeof user.subscription.expires.timestampUNIX, 'number', 'subscription.expires.timestampUNIX should be number');
47
-
48
- // Roles
49
- assert.equal(user.roles.admin, false, 'roles.admin should be false');
50
- assert.equal(user.roles.betaTester, false, 'roles.betaTester should be false');
51
- assert.equal(user.roles.developer, false, 'roles.developer should be false');
52
-
53
- // Flags
54
- assert.equal(user.flags.signupProcessed, false, 'flags.signupProcessed should be false');
55
-
56
- // Affiliate
57
- assert.equal(typeof user.affiliate.code, 'string', 'affiliate.code should be string');
58
- assert.ok(user.affiliate.code.length > 0, 'affiliate.code should not be empty');
59
- assert.ok(Array.isArray(user.affiliate.referrals), 'affiliate.referrals should be array');
60
-
61
- // Metadata
62
- assert.ok(user.metadata.updated.timestamp, 'metadata.updated.timestamp should exist');
63
- assert.ok(user.metadata.created.timestamp, 'metadata.created.timestamp should exist');
64
- assert.equal(user.activity.geolocation.latitude, 0, 'geolocation.latitude should be 0');
65
- assert.equal(user.activity.geolocation.longitude, 0, 'geolocation.longitude should be 0');
66
- assert.equal(user.activity.client.mobile, false, 'client.mobile should be false');
67
-
68
- // API keys
69
- assert.equal(typeof user.api.clientId, 'string', 'api.clientId should be string');
70
- assert.ok(user.api.clientId.length > 0, 'api.clientId should not be empty');
71
- assert.equal(typeof user.api.privateKey, 'string', 'api.privateKey should be string');
72
- assert.ok(user.api.privateKey.length > 0, 'api.privateKey should not be empty');
73
-
74
- // Usage
75
- assert.deepEqual(user.usage, {}, 'usage should be empty object by default');
76
-
77
- // Personal
78
- assert.equal(user.personal.name.first, null, 'personal.name.first should be null');
79
- assert.equal(user.personal.name.last, null, 'personal.name.last should be null');
80
- assert.equal(user.personal.telephone.countryCode, 0, 'telephone.countryCode should be 0');
81
-
82
- // OAuth2
83
- assert.deepEqual(user.oauth2, {}, 'oauth2 should be empty object');
84
-
85
- // Attribution
86
- assert.equal(user.attribution.affiliate.code, null, 'attribution.affiliate.code should be null');
87
- assert.deepEqual(user.attribution.utm.tags, {}, 'attribution.utm.tags should be empty object');
88
- },
89
- },
90
-
91
- {
92
- name: 'undefined-settings-gets-all-defaults',
93
- async run({ assert }) {
94
- const user = new User(Manager).properties;
95
-
96
- assert.equal(user.auth.uid, null, 'auth.uid should be null');
97
- assert.equal(user.subscription.product.id, 'basic', 'subscription.product.id should be basic');
98
- assert.equal(user.roles.admin, false, 'roles.admin should be false');
99
- },
100
- },
101
-
102
- // ─── Preserving real user data ───
103
-
104
- {
105
- name: 'real-data-takes-precedence-over-defaults',
106
- async run({ assert }) {
107
- const user = createUser({
108
- auth: { uid: 'user123', email: 'test@test.com', temporary: true },
109
- subscription: { product: { id: 'pro', name: 'Pro' }, status: 'cancelled' },
110
- roles: { admin: true, betaTester: true, developer: false },
111
- personal: { name: { first: 'Ian', last: 'W' } },
112
- });
113
-
114
- assert.equal(user.auth.uid, 'user123', 'auth.uid should be preserved');
115
- assert.equal(user.auth.email, 'test@test.com', 'auth.email should be preserved');
116
- assert.equal(user.auth.temporary, true, 'auth.temporary should be preserved');
117
- assert.equal(user.subscription.product.id, 'pro', 'subscription.product.id should be preserved');
118
- assert.equal(user.subscription.status, 'cancelled', 'subscription.status should be preserved');
119
- assert.equal(user.roles.admin, true, 'roles.admin should be preserved');
120
- assert.equal(user.personal.name.first, 'Ian', 'personal.name.first should be preserved');
121
- },
122
- },
123
-
124
- // ─── $passthrough: oauth2 ───
125
-
126
- {
127
- name: 'oauth2-passthrough-preserves-provider-data',
128
- async run({ assert }) {
129
- const googleToken = {
130
- access_token: 'ya29.xxx',
131
- refresh_token: '1//xxx',
132
- expiry_date: 1700000000,
133
- };
134
- const msToken = {
135
- access_token: 'eyJ.xxx',
136
- refresh_token: 'M.xxx',
137
- };
138
-
139
- const user = createUser({
140
- oauth2: {
141
- google: {
142
- token: googleToken,
143
- identity: { email: 'g@gmail.com', name: 'Test' },
144
- },
145
- microsoft: {
146
- token: msToken,
147
- },
148
- },
149
- });
150
-
151
- assert.equal(user.oauth2.google.token.access_token, 'ya29.xxx', 'google access_token preserved');
152
- assert.equal(user.oauth2.google.token.refresh_token, '1//xxx', 'google refresh_token preserved');
153
- assert.equal(user.oauth2.google.identity.email, 'g@gmail.com', 'google identity preserved');
154
- assert.equal(user.oauth2.microsoft.token.access_token, 'eyJ.xxx', 'microsoft token preserved');
155
- },
156
- },
157
-
158
- {
159
- name: 'oauth2-empty-when-not-provided',
160
- async run({ assert }) {
161
- const user = createUser({});
162
- assert.deepEqual(user.oauth2, {}, 'oauth2 should be empty object when not provided');
163
- },
164
- },
165
-
166
- // ─── $passthrough: roles (with defined defaults) ───
167
-
168
- {
169
- name: 'roles-passthrough-preserves-custom-roles',
170
- async run({ assert }) {
171
- const user = createUser({
172
- roles: { admin: true, customRole: true, moderator: false },
173
- });
174
-
175
- assert.equal(user.roles.admin, true, 'admin should be true');
176
- assert.equal(user.roles.customRole, true, 'customRole should be preserved');
177
- assert.equal(user.roles.moderator, false, 'moderator should be preserved');
178
- assert.equal(user.roles.betaTester, false, 'betaTester should get default');
179
- assert.equal(user.roles.developer, false, 'developer should get default');
180
- },
181
- },
182
-
183
- // ─── $passthrough: flags (with defined defaults) ───
184
-
185
- {
186
- name: 'flags-passthrough-preserves-custom-flags',
187
- async run({ assert }) {
188
- const user = createUser({
189
- flags: { signupProcessed: true, featureX: true, betaOptIn: false },
190
- });
191
-
192
- assert.equal(user.flags.signupProcessed, true, 'signupProcessed preserved');
193
- assert.equal(user.flags.featureX, true, 'featureX preserved');
194
- assert.equal(user.flags.betaOptIn, false, 'betaOptIn preserved');
195
- },
196
- },
197
-
198
- // ─── $template: usage with dynamic keys ───
199
-
200
- {
201
- name: 'usage-template-preserves-dynamic-keys',
202
- async run({ assert }) {
203
- const user = createUser({
204
- usage: {
205
- requests: { monthly: 10, total: 100, last: { id: 'r1', timestamp: '2025-01-01T00:00:00.000Z', timestampUNIX: 1735689600 } },
206
- emails: { monthly: 5, total: 50, last: { id: 'e1', timestamp: '2025-01-02T00:00:00.000Z', timestampUNIX: 1735776000 } },
207
- sends: { monthly: 3, total: 30 },
208
- },
209
- });
210
-
211
- // Defined key (requests)
212
- assert.equal(user.usage.requests.monthly, 10, 'requests.monthly preserved');
213
- assert.equal(user.usage.requests.total, 100, 'requests.total preserved');
214
- assert.equal(user.usage.requests.last.id, 'r1', 'requests.last.id preserved');
215
-
216
- // Dynamic key (emails) — full data
217
- assert.equal(user.usage.emails.monthly, 5, 'emails.monthly preserved');
218
- assert.equal(user.usage.emails.total, 50, 'emails.total preserved');
219
- assert.equal(user.usage.emails.last.id, 'e1', 'emails.last.id preserved');
220
-
221
- // Dynamic key (sends) — partial data, template fills in missing
222
- assert.equal(user.usage.sends.monthly, 3, 'sends.monthly preserved');
223
- assert.equal(user.usage.sends.total, 30, 'sends.total preserved');
224
- assert.equal(user.usage.sends.last.id, null, 'sends.last.id defaulted to null');
225
- assert.ok(user.usage.sends.last.timestamp, 'sends.last.timestamp defaulted');
226
- assert.equal(typeof user.usage.sends.last.timestampUNIX, 'number', 'sends.last.timestampUNIX defaulted to number');
227
- },
228
- },
229
-
230
- {
231
- name: 'usage-empty-when-no-keys-provided',
232
- async run({ assert }) {
233
- const user = createUser({});
234
-
235
- assert.deepEqual(user.usage, {}, 'usage should be empty object when no keys provided');
236
- },
237
- },
238
-
239
- // ─── $passthrough: utm.tags ───
240
-
241
- {
242
- name: 'utm-tags-passthrough-preserves-all-tags',
243
- async run({ assert }) {
244
- const user = createUser({
245
- attribution: {
246
- utm: {
247
- tags: { source: 'google', medium: 'cpc', campaign: 'summer' },
248
- timestamp: '2025-06-01T00:00:00.000Z',
249
- },
250
- },
251
- });
252
-
253
- assert.equal(user.attribution.utm.tags.source, 'google', 'utm source preserved');
254
- assert.equal(user.attribution.utm.tags.medium, 'cpc', 'utm medium preserved');
255
- assert.equal(user.attribution.utm.tags.campaign, 'summer', 'utm campaign preserved');
256
- assert.equal(user.attribution.utm.timestamp, '2025-06-01T00:00:00.000Z', 'utm timestamp preserved');
257
- },
258
- },
259
-
260
- // ─── Type coercion ───
261
-
262
- {
263
- name: 'coerces-number-from-string',
264
- async run({ assert }) {
265
- const user = createUser({
266
- activity: { geolocation: { latitude: '42.5', longitude: '-73.2' } },
267
- personal: { telephone: { countryCode: '44', national: '7911123456' } },
268
- });
269
-
270
- assert.equal(user.activity.geolocation.latitude, 42.5, 'string "42.5" coerced to number');
271
- assert.equal(user.activity.geolocation.longitude, -73.2, 'string "-73.2" coerced to number');
272
- assert.equal(user.personal.telephone.countryCode, 44, 'string "44" coerced to number');
273
- },
274
- },
275
-
276
- {
277
- name: 'coerces-boolean-from-number',
278
- async run({ assert }) {
279
- const user = createUser({
280
- auth: { temporary: 1 },
281
- activity: { client: { mobile: 0 } },
282
- });
283
-
284
- assert.equal(user.auth.temporary, true, '1 coerced to true');
285
- assert.equal(user.activity.client.mobile, false, '0 coerced to false');
286
- },
287
- },
288
-
289
- {
290
- name: 'coerces-boolean-from-string',
291
- async run({ assert }) {
292
- const user = createUser({
293
- auth: { temporary: 'true' },
294
- roles: { admin: 'false' },
295
- });
296
-
297
- assert.equal(user.auth.temporary, true, '"true" coerced to true');
298
- assert.equal(user.roles.admin, false, '"false" coerced to false');
299
- },
300
- },
301
-
302
- {
303
- name: 'invalid-coercion-falls-back-to-default',
304
- async run({ assert }) {
305
- const user = createUser({
306
- activity: { geolocation: { latitude: 'not-a-number', longitude: undefined } },
307
- personal: { telephone: { national: 'abc' } },
308
- });
309
-
310
- assert.equal(user.activity.geolocation.latitude, 0, 'invalid string falls back to default 0');
311
- assert.equal(user.activity.geolocation.longitude, 0, 'undefined falls back to default 0');
312
- assert.equal(user.personal.telephone.national, 0, 'non-numeric string falls back to default 0');
313
- },
314
- },
315
-
316
- // ─── Nullable fields ───
317
-
318
- {
319
- name: 'nullable-fields-preserve-null',
320
- async run({ assert }) {
321
- const user = createUser({
322
- auth: { uid: null, email: null },
323
- personal: { name: { first: null, last: null }, gender: null },
324
- activity: { geolocation: { ip: null, country: null } },
325
- });
326
-
327
- assert.equal(user.auth.uid, null, 'null uid preserved');
328
- assert.equal(user.auth.email, null, 'null email preserved');
329
- assert.equal(user.personal.name.first, null, 'null first name preserved');
330
- assert.equal(user.personal.gender, null, 'null gender preserved');
331
- assert.equal(user.activity.geolocation.ip, null, 'null ip preserved');
332
- },
333
- },
334
-
335
- {
336
- name: 'non-nullable-fields-replace-null-with-default',
337
- async run({ assert }) {
338
- const user = createUser({
339
- auth: { temporary: null },
340
- roles: { admin: null },
341
- activity: { geolocation: { latitude: null } },
342
- });
343
-
344
- // These are non-nullable, so null should be replaced with the schema default
345
- assert.equal(user.auth.temporary, false, 'null temporary replaced with false');
346
- assert.equal(user.roles.admin, false, 'null admin replaced with false');
347
- assert.equal(user.activity.geolocation.latitude, 0, 'null latitude replaced with 0');
348
- },
349
- },
350
-
351
- // ─── Fragmented / incomplete users ───
352
-
353
- {
354
- name: 'partial-subscription-fills-missing-fields',
355
- async run({ assert }) {
356
- const user = createUser({
357
- subscription: { product: { id: 'premium' } },
358
- });
359
-
360
- assert.equal(user.subscription.product.id, 'premium', 'provided id preserved');
361
- assert.equal(user.subscription.product.name, 'Basic', 'missing name gets default');
362
- assert.equal(user.subscription.status, 'active', 'missing status gets default');
363
- assert.equal(user.subscription.trial.claimed, false, 'missing trial.claimed gets default');
364
- assert.ok(user.subscription.expires.timestamp, 'missing expires gets default timestamp');
365
- },
366
- },
367
-
368
- {
369
- name: 'partial-activity-fills-missing-fields',
370
- async run({ assert }) {
371
- const user = createUser({
372
- activity: { geolocation: { country: 'US' } },
373
- });
374
-
375
- assert.equal(user.activity.geolocation.country, 'US', 'provided country preserved');
376
- assert.equal(user.activity.geolocation.ip, null, 'missing ip defaults to null');
377
- assert.equal(user.activity.geolocation.latitude, 0, 'missing latitude defaults to 0');
378
- assert.ok(user.metadata.updated.timestamp, 'missing updated gets default');
379
- assert.ok(user.metadata.created.timestamp, 'missing created gets default');
380
- },
381
- },
382
-
383
- {
384
- name: 'deeply-nested-partial-payment-fills-correctly',
385
- async run({ assert }) {
386
- const user = createUser({
387
- subscription: {
388
- payment: { processor: 'stripe', resourceId: 'sub_123' },
389
- },
390
- });
391
-
392
- assert.equal(user.subscription.payment.processor, 'stripe', 'processor preserved');
393
- assert.equal(user.subscription.payment.orderId, null, 'missing orderId defaults to null');
394
- assert.equal(user.subscription.payment.resourceId, 'sub_123', 'resourceId preserved');
395
- assert.equal(user.subscription.payment.frequency, null, 'missing frequency defaults to null');
396
- assert.ok(user.subscription.payment.startDate.timestamp, 'missing startDate gets default');
397
- assert.equal(user.subscription.payment.updatedBy.event.name, null, 'missing event.name defaults to null');
398
- },
399
- },
400
-
401
- // ─── Top-level structure ───
402
-
403
- {
404
- name: 'all-top-level-keys-present',
405
- async run({ assert }) {
406
- const user = createUser({});
407
- const expectedKeys = [
408
- 'auth', 'subscription', 'roles', 'flags', 'affiliate',
409
- 'activity', 'api', 'usage', 'personal', 'oauth2', 'attribution', 'consent', 'metadata',
410
- ];
411
-
412
- for (const key of expectedKeys) {
413
- assert.ok(key in user, `Top-level key "${key}" should exist`);
414
- }
415
- },
416
- },
417
-
418
- {
419
- name: 'no-extra-top-level-keys',
420
- async run({ assert }) {
421
- const user = createUser({});
422
- const expectedKeys = [
423
- 'auth', 'subscription', 'roles', 'flags', 'affiliate',
424
- 'activity', 'api', 'usage', 'personal', 'oauth2', 'attribution', 'consent', 'metadata',
425
- ];
426
-
427
- for (const key of Object.keys(user)) {
428
- assert.ok(expectedKeys.includes(key), `Unexpected top-level key "${key}"`);
429
- }
430
- },
431
- },
432
-
433
- // ─── Consent (legal + marketing) ───
434
-
435
- {
436
- name: 'consent-defaults-revoked-with-null-leaves',
437
- async run({ assert }) {
438
- const user = createUser({});
439
-
440
- // Legal
441
- assert.equal(user.consent.legal.status, 'revoked', 'consent.legal.status defaults to revoked');
442
- assert.equal(user.consent.legal.grantedAt.timestamp, null, 'legal.grantedAt.timestamp defaults to null');
443
- assert.equal(user.consent.legal.grantedAt.timestampUNIX, null, 'legal.grantedAt.timestampUNIX defaults to null');
444
- assert.equal(user.consent.legal.grantedAt.source, null, 'legal.grantedAt.source defaults to null');
445
- assert.equal(user.consent.legal.grantedAt.ip, null, 'legal.grantedAt.ip defaults to null');
446
- assert.equal(user.consent.legal.grantedAt.text, null, 'legal.grantedAt.text defaults to null');
447
-
448
- // Marketing
449
- assert.equal(user.consent.marketing.status, 'revoked', 'consent.marketing.status defaults to revoked');
450
- assert.equal(user.consent.marketing.grantedAt.timestamp, null, 'marketing.grantedAt.timestamp defaults to null');
451
- assert.equal(user.consent.marketing.grantedAt.timestampUNIX, null, 'marketing.grantedAt.timestampUNIX defaults to null');
452
- assert.equal(user.consent.marketing.grantedAt.source, null, 'marketing.grantedAt.source defaults to null');
453
- assert.equal(user.consent.marketing.grantedAt.ip, null, 'marketing.grantedAt.ip defaults to null');
454
- assert.equal(user.consent.marketing.grantedAt.text, null, 'marketing.grantedAt.text defaults to null');
455
- assert.equal(user.consent.marketing.revokedAt.timestamp, null, 'marketing.revokedAt.timestamp defaults to null');
456
- assert.equal(user.consent.marketing.revokedAt.timestampUNIX, null, 'marketing.revokedAt.timestampUNIX defaults to null');
457
- assert.equal(user.consent.marketing.revokedAt.source, null, 'marketing.revokedAt.source defaults to null');
458
- assert.equal(user.consent.marketing.revokedAt.ip, null, 'marketing.revokedAt.ip defaults to null');
459
- assert.equal(user.consent.marketing.revokedAt.text, null, 'marketing.revokedAt.text defaults to null');
460
- },
461
- },
462
-
463
- {
464
- name: 'consent-objects-always-present-even-on-empty-input',
465
- async run({ assert }) {
466
- const user = createUser({});
467
-
468
- assert.ok(user.consent, 'consent object exists');
469
- assert.ok(user.consent.legal, 'consent.legal exists');
470
- assert.ok(user.consent.legal.grantedAt, 'consent.legal.grantedAt object exists (not null)');
471
- assert.ok(user.consent.marketing, 'consent.marketing exists');
472
- assert.ok(user.consent.marketing.grantedAt, 'consent.marketing.grantedAt object exists (not null)');
473
- assert.ok(user.consent.marketing.revokedAt, 'consent.marketing.revokedAt object exists (not null)');
474
- },
475
- },
476
-
477
- {
478
- name: 'consent-granted-marketing-preserves-fields',
479
- async run({ assert }) {
480
- const user = createUser({
481
- consent: {
482
- legal: {
483
- status: 'granted',
484
- grantedAt: {
485
- timestamp: '2026-05-15T12:00:00.000Z',
486
- timestampUNIX: 1779235200,
487
- source: 'signup',
488
- ip: '1.2.3.4',
489
- text: 'I agree to the Terms of Service and Privacy Policy.',
490
- },
491
- },
492
- marketing: {
493
- status: 'granted',
494
- grantedAt: {
495
- timestamp: '2026-05-15T12:00:00.000Z',
496
- timestampUNIX: 1779235200,
497
- source: 'signup',
498
- ip: '1.2.3.4',
499
- text: 'Send me product updates and newsletters.',
500
- },
501
- },
502
- },
503
- });
504
-
505
- assert.equal(user.consent.legal.status, 'granted', 'legal.status preserved');
506
- assert.equal(user.consent.legal.grantedAt.timestamp, '2026-05-15T12:00:00.000Z', 'legal grantedAt.timestamp preserved');
507
- assert.equal(user.consent.legal.grantedAt.timestampUNIX, 1779235200, 'legal grantedAt.timestampUNIX preserved');
508
- assert.equal(user.consent.legal.grantedAt.source, 'signup', 'legal grantedAt.source preserved');
509
- assert.equal(user.consent.legal.grantedAt.ip, '1.2.3.4', 'legal grantedAt.ip preserved');
510
- assert.equal(
511
- user.consent.legal.grantedAt.text,
512
- 'I agree to the Terms of Service and Privacy Policy.',
513
- 'legal grantedAt.text preserved'
514
- );
515
-
516
- assert.equal(user.consent.marketing.status, 'granted', 'marketing.status preserved');
517
- assert.equal(user.consent.marketing.grantedAt.source, 'signup', 'marketing grantedAt.source preserved');
518
- assert.equal(user.consent.marketing.grantedAt.text, 'Send me product updates and newsletters.', 'marketing grantedAt.text preserved');
519
-
520
- // revokedAt still defaults to all nulls (not touched by input)
521
- assert.equal(user.consent.marketing.revokedAt.timestamp, null, 'marketing revokedAt.timestamp defaults to null');
522
- assert.equal(user.consent.marketing.revokedAt.source, null, 'marketing revokedAt.source defaults to null');
523
- },
524
- },
525
-
526
- {
527
- name: 'consent-marketing-declined-at-signup',
528
- async run({ assert }) {
529
- const user = createUser({
530
- consent: {
531
- legal: {
532
- status: 'granted',
533
- grantedAt: {
534
- timestamp: '2026-05-15T12:00:00.000Z',
535
- timestampUNIX: 1779235200,
536
- source: 'signup',
537
- ip: '1.2.3.4',
538
- text: 'I agree to the Terms of Service and Privacy Policy.',
539
- },
540
- },
541
- marketing: {
542
- status: 'revoked',
543
- revokedAt: {
544
- timestamp: '2026-05-15T12:00:00.000Z',
545
- timestampUNIX: 1779235200,
546
- source: 'signup',
547
- ip: '1.2.3.4',
548
- text: null,
549
- },
550
- },
551
- },
552
- });
553
-
554
- assert.equal(user.consent.legal.status, 'granted', 'legal.status granted');
555
- assert.equal(user.consent.marketing.status, 'revoked', 'marketing.status revoked');
556
- assert.equal(user.consent.marketing.grantedAt.timestamp, null, 'marketing grantedAt.timestamp is null (never granted)');
557
- assert.equal(user.consent.marketing.grantedAt.source, null, 'marketing grantedAt.source is null');
558
- assert.equal(user.consent.marketing.revokedAt.timestamp, '2026-05-15T12:00:00.000Z', 'marketing revokedAt.timestamp preserved');
559
- assert.equal(user.consent.marketing.revokedAt.source, 'signup', 'marketing revokedAt.source preserved');
560
- assert.equal(user.consent.marketing.revokedAt.ip, '1.2.3.4', 'marketing revokedAt.ip preserved');
561
- assert.equal(user.consent.marketing.revokedAt.text, null, 'marketing revokedAt.text is null for decline');
562
- },
563
- },
564
-
565
- {
566
- name: 'consent-revoked-then-regranted-keeps-prior-revokedAt',
567
- async run({ assert }) {
568
- const user = createUser({
569
- consent: {
570
- marketing: {
571
- status: 'granted',
572
- grantedAt: {
573
- timestamp: '2026-07-01T00:00:00.000Z',
574
- timestampUNIX: 1783785600,
575
- source: 'account-page',
576
- ip: '5.6.7.8',
577
- text: 'Send me product updates and newsletters.',
578
- },
579
- revokedAt: {
580
- timestamp: '2026-06-12T00:00:00.000Z',
581
- timestampUNIX: 1781251200,
582
- source: 'sendgrid-webhook',
583
- ip: null,
584
- text: null,
585
- },
586
- },
587
- },
588
- });
589
-
590
- assert.equal(user.consent.marketing.status, 'granted', 'status is granted (latest action)');
591
- assert.equal(user.consent.marketing.grantedAt.timestamp, '2026-07-01T00:00:00.000Z', 'grantedAt reflects most recent grant');
592
- assert.equal(user.consent.marketing.grantedAt.source, 'account-page', 'grantedAt.source is account-page');
593
- // revokedAt still reflects the prior revoke — informational
594
- assert.equal(user.consent.marketing.revokedAt.timestamp, '2026-06-12T00:00:00.000Z', 'revokedAt preserves prior revoke');
595
- assert.equal(user.consent.marketing.revokedAt.source, 'sendgrid-webhook', 'revokedAt.source preserves prior source');
596
- },
597
- },
598
-
599
- {
600
- name: 'consent-partial-input-fills-missing-leaves-with-null',
601
- async run({ assert }) {
602
- const user = createUser({
603
- consent: {
604
- marketing: {
605
- status: 'granted',
606
- grantedAt: {
607
- timestamp: '2026-05-15T12:00:00.000Z',
608
- source: 'signup',
609
- },
610
- },
611
- },
612
- });
613
-
614
- assert.equal(user.consent.marketing.status, 'granted', 'marketing.status preserved');
615
- assert.equal(user.consent.marketing.grantedAt.timestamp, '2026-05-15T12:00:00.000Z', 'grantedAt.timestamp preserved');
616
- assert.equal(user.consent.marketing.grantedAt.source, 'signup', 'grantedAt.source preserved');
617
- // Missing leaves default to null
618
- assert.equal(user.consent.marketing.grantedAt.timestampUNIX, null, 'missing grantedAt.timestampUNIX defaults to null');
619
- assert.equal(user.consent.marketing.grantedAt.ip, null, 'missing grantedAt.ip defaults to null');
620
- assert.equal(user.consent.marketing.grantedAt.text, null, 'missing grantedAt.text defaults to null');
621
- // Legal not provided — gets full defaults
622
- assert.equal(user.consent.legal.status, 'revoked', 'untouched legal defaults to revoked');
623
- assert.equal(user.consent.legal.grantedAt.timestamp, null, 'untouched legal grantedAt.timestamp is null');
624
- },
625
- },
626
-
627
- {
628
- name: 'consent-round-trip-preserves-shape',
629
- async run({ assert }) {
630
- const grantedDoc = {
631
- consent: {
632
- legal: {
633
- status: 'granted',
634
- grantedAt: {
635
- timestamp: '2026-05-15T12:00:00.000Z',
636
- timestampUNIX: 1779235200,
637
- source: 'signup',
638
- ip: '1.2.3.4',
639
- text: 'I agree to the Terms of Service.',
640
- },
641
- },
642
- marketing: {
643
- status: 'granted',
644
- grantedAt: {
645
- timestamp: '2026-05-15T12:00:00.000Z',
646
- timestampUNIX: 1779235200,
647
- source: 'signup',
648
- ip: '1.2.3.4',
649
- text: 'Send me updates.',
650
- },
651
- revokedAt: {
652
- timestamp: null,
653
- timestampUNIX: null,
654
- source: null,
655
- ip: null,
656
- text: null,
657
- },
658
- },
659
- },
660
- };
661
-
662
- // First pass — resolve from raw input
663
- const user1 = createUser(grantedDoc);
664
-
665
- // Second pass — resolve from the resolved doc (simulates Firestore read-back)
666
- const user2 = createUser(user1);
667
-
668
- assert.deepEqual(user1.consent, user2.consent, 'consent round-trips identically');
669
- },
670
- },
671
-
672
- // ─── Unique/generated values ───
673
-
674
- {
675
- name: 'api-keys-are-unique-per-call',
676
- async run({ assert }) {
677
- const u1 = createUser({});
678
- const u2 = createUser({});
679
-
680
- // privateKey uses UIDGenerator which should produce unique values
681
- assert.notEqual(u1.api.privateKey, u2.api.privateKey, 'privateKey should be unique per call');
682
- assert.notEqual(u1.api.clientId, u2.api.clientId, 'clientId should be unique per call');
683
- },
684
- },
685
-
686
- {
687
- name: 'existing-api-keys-not-overwritten',
688
- async run({ assert }) {
689
- const user = createUser({
690
- api: { clientId: 'my-client-id', privateKey: 'my-secret-key' },
691
- });
692
-
693
- assert.equal(user.api.clientId, 'my-client-id', 'existing clientId preserved');
694
- assert.equal(user.api.privateKey, 'my-secret-key', 'existing privateKey preserved');
695
- },
696
- },
697
-
698
- // ─── Complex real-world user ───
699
-
700
- {
701
- name: 'full-real-world-user-preserves-everything',
702
- async run({ assert }) {
703
- const user = createUser({
704
- auth: { uid: 'V4U9wR0AiLUQRxpcP7WhgA4FX9H2', email: 'ian@example.com', temporary: false },
705
- subscription: {
706
- product: { id: 'premium', name: 'Premium' },
707
- status: 'active',
708
- expires: { timestamp: '2026-12-31T00:00:00.000Z', timestampUNIX: 1798761600 },
709
- trial: { claimed: true, expires: { timestamp: '2024-01-01T00:00:00.000Z', timestampUNIX: 1704067200 } },
710
- payment: { processor: 'stripe', resourceId: 'sub_abc', frequency: 'annually' },
711
- },
712
- roles: { admin: true, betaTester: true, developer: true, superAdmin: true },
713
- flags: { signupProcessed: true, onboarded: true },
714
- affiliate: { code: 'IAN7', referrals: ['ref1', 'ref2'] },
715
- api: { clientId: 'uuid-123', privateKey: 'key-456' },
716
- oauth2: {
717
- google: {
718
- token: { access_token: 'ya29.real', refresh_token: '1//real', expiry_date: 1700000000 },
719
- identity: { email: 'ian@gmail.com', name: 'Ian W', picture: 'https://photo.url' },
720
- },
721
- },
722
- usage: {
723
- requests: { monthly: 100, total: 5000, last: { id: 'req-z', timestamp: '2025-12-01T00:00:00.000Z', timestampUNIX: 1764633600 } },
724
- emails: { monthly: 42, total: 2100, last: { id: 'em-z', timestamp: '2025-12-01T00:00:00.000Z', timestampUNIX: 1764633600 } },
725
- },
726
- personal: {
727
- name: { first: 'Ian', last: 'Wiedenman' },
728
- company: { name: 'ITW Creative Works', position: 'CEO' },
729
- birthday: { timestamp: '1990-05-15T00:00:00.000Z', timestampUNIX: 642988800 },
730
- },
731
- attribution: {
732
- affiliate: { code: 'PARTNER1', timestamp: '2024-06-01T00:00:00.000Z' },
733
- utm: { tags: { source: 'twitter', campaign: 'launch' }, url: 'https://example.com' },
734
- },
735
- });
736
-
737
- // Everything should be preserved exactly
738
- assert.equal(user.auth.uid, 'V4U9wR0AiLUQRxpcP7WhgA4FX9H2', 'uid preserved');
739
- assert.equal(user.subscription.product.id, 'premium', 'product preserved');
740
- assert.equal(user.roles.superAdmin, true, 'custom role preserved');
741
- assert.equal(user.flags.onboarded, true, 'custom flag preserved');
742
- assert.equal(user.affiliate.code, 'IAN7', 'affiliate code preserved');
743
- assert.deepEqual(user.affiliate.referrals, ['ref1', 'ref2'], 'referrals preserved');
744
- assert.equal(user.api.clientId, 'uuid-123', 'api clientId preserved');
745
- assert.equal(user.oauth2.google.token.access_token, 'ya29.real', 'oauth2 token preserved');
746
- assert.equal(user.oauth2.google.identity.email, 'ian@gmail.com', 'oauth2 identity preserved');
747
- assert.equal(user.usage.emails.monthly, 42, 'usage emails preserved');
748
- assert.equal(user.personal.name.first, 'Ian', 'name preserved');
749
- assert.equal(user.personal.company.name, 'ITW Creative Works', 'company preserved');
750
- assert.equal(user.personal.birthday.timestampUNIX, 642988800, 'birthday preserved');
751
- assert.equal(user.attribution.utm.tags.source, 'twitter', 'utm tags preserved');
752
- },
753
- },
754
- ],
755
- };