backend-manager 5.9.7 → 5.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -3
- package/templates/_.env +42 -0
- package/templates/_.gitignore +60 -0
- package/templates/backend-manager-config.json +249 -0
- package/templates/database.rules.json +83 -0
- package/templates/firebase.json +66 -0
- package/templates/firestore.indexes.json +4 -0
- package/templates/firestore.rules +112 -0
- package/templates/public/404.html +26 -0
- package/templates/public/index.html +24 -0
- package/templates/remoteconfig.template.json +1 -0
- package/templates/storage-lifecycle-config-1-day.json +9 -0
- package/templates/storage-lifecycle-config-30-days.json +9 -0
- package/templates/storage.rules +8 -0
- package/test/_init/accounts-validation.js +58 -0
- package/test/_legacy/ai/index.js +231 -0
- package/test/_legacy/ai/test.jpg +0 -0
- package/test/_legacy/ai/test.pdf +0 -0
- package/test/_legacy/index.js +31 -0
- package/test/_legacy/payment-resolver/chargebee/orders/refunded.json +0 -0
- package/test/_legacy/payment-resolver/chargebee/orders/unpaid.json +98 -0
- package/test/_legacy/payment-resolver/chargebee/subscriptions/active.json +578 -0
- package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-in-trial.json +38 -0
- package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-skipped-to-active.json +135 -0
- package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-active-to-cancelled.json +42 -0
- package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-active.json +44 -0
- package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-cancelled.json +39 -0
- package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-refund.json +143 -0
- package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-suspended.json +48 -0
- package/test/_legacy/payment-resolver/coinbase/orders/regular.json +204 -0
- package/test/_legacy/payment-resolver/coinbase/subscriptions/cancelled.json +204 -0
- package/test/_legacy/payment-resolver/coinbase/subscriptions/paid-2.json +125 -0
- package/test/_legacy/payment-resolver/index.js +1558 -0
- package/test/_legacy/payment-resolver/paypal/orders/refunded.json +0 -0
- package/test/_legacy/payment-resolver/paypal/orders/regular.json +120 -0
- package/test/_legacy/payment-resolver/paypal/subscriptions/active-refund-previous-stmnt.json +323 -0
- package/test/_legacy/payment-resolver/paypal/subscriptions/active.json +192 -0
- package/test/_legacy/payment-resolver/paypal/subscriptions/trial-in-trial.json +125 -0
- package/test/_legacy/payment-resolver/paypal/subscriptions/trial-payment-not-complete.json +76 -0
- package/test/_legacy/payment-resolver/paypal/subscriptions/trial-payment-overdue-2.json +114 -0
- package/test/_legacy/payment-resolver/paypal/subscriptions/trial-payment-overdue.json +114 -0
- package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-active-to-cancelled.json +111 -0
- package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-active.json +132 -0
- package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-cancelled.json +107 -0
- package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-expired.json +91 -0
- package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-refund.json +147 -0
- package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-suspended.json +120 -0
- package/test/_legacy/payment-resolver/stripe/orders/refunded.json +0 -0
- package/test/_legacy/payment-resolver/stripe/orders/regular.json +91 -0
- package/test/_legacy/payment-resolver/stripe/subscriptions/active.json +421 -0
- package/test/_legacy/payment-resolver/stripe/subscriptions/trial-in-trial.json +349 -0
- package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-active-failed-authorization.json +430 -0
- package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-active.json +319 -0
- package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-cancelled.json +319 -0
- package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-refund.json +414 -0
- package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-suspended.json +319 -0
- package/test/_legacy/payment-resolver/stripe/subscriptions/unsure.json +339 -0
- package/test/_legacy/test-new +1178 -0
- package/test/_legacy/test.md +89 -0
- package/test/_legacy/usage.js +175 -0
- package/test/_legacy/user.js +31 -0
- package/test/ai/tools-live.js +170 -0
- package/test/boot/emulator-boots.js +37 -0
- package/test/content/blog-generate.js +160 -0
- package/test/email/campaign-send.js +45 -0
- package/test/email/consent-lifecycle.js +257 -0
- package/test/email/feedback-and-plain-send.js +52 -0
- package/test/email/fixtures/clean.json +30 -0
- package/test/email/fixtures/editorial.json +30 -0
- package/test/email/fixtures/field-report.json +53 -0
- package/test/email/marketing-lifecycle.js +132 -0
- package/test/email/newsletter-generate.js +819 -0
- package/test/email/newsletter-templates.js +491 -0
- package/test/email/templates.js +207 -0
- package/test/email/transactional-send.js +34 -0
- package/test/email/transactional.js +560 -0
- package/test/email/validation.js +710 -0
- package/test/events/auth-delete-race.js +201 -0
- package/test/events/payments/journey-payments-cancel-endpoint.js +100 -0
- package/test/events/payments/journey-payments-cancel.js +182 -0
- package/test/events/payments/journey-payments-discount.js +89 -0
- package/test/events/payments/journey-payments-failure.js +146 -0
- package/test/events/payments/journey-payments-legacy-product.js +149 -0
- package/test/events/payments/journey-payments-one-time-failure.js +111 -0
- package/test/events/payments/journey-payments-one-time.js +136 -0
- package/test/events/payments/journey-payments-plan-change.js +144 -0
- package/test/events/payments/journey-payments-refund-webhook.js +180 -0
- package/test/events/payments/journey-payments-suspend.js +181 -0
- package/test/events/payments/journey-payments-trial-cancel.js +130 -0
- package/test/events/payments/journey-payments-trial.js +171 -0
- package/test/events/payments/journey-payments-uid-resolution.js +132 -0
- package/test/events/payments/journey-payments-upgrade.js +135 -0
- package/test/fixtures/chargebee/invoice-one-time.json +27 -0
- package/test/fixtures/chargebee/subscription-active.json +44 -0
- package/test/fixtures/chargebee/subscription-cancelled.json +42 -0
- package/test/fixtures/chargebee/subscription-in-trial.json +41 -0
- package/test/fixtures/chargebee/subscription-legacy-plan.json +41 -0
- package/test/fixtures/chargebee/subscription-non-renewing.json +41 -0
- package/test/fixtures/chargebee/subscription-paused.json +42 -0
- package/test/fixtures/chargebee/webhook-payment-failed.json +51 -0
- package/test/fixtures/chargebee/webhook-subscription-created.json +47 -0
- package/test/fixtures/paypal/order-approved.json +62 -0
- package/test/fixtures/paypal/order-completed.json +110 -0
- package/test/fixtures/paypal/subscription-active.json +76 -0
- package/test/fixtures/paypal/subscription-cancelled.json +50 -0
- package/test/fixtures/paypal/subscription-suspended.json +65 -0
- package/test/fixtures/stripe/checkout-session-completed.json +130 -0
- package/test/fixtures/stripe/invoice-payment-failed.json +148 -0
- package/test/fixtures/stripe/invoice-subscription-payment-failed.json +28 -0
- package/test/fixtures/stripe/subscription-active.json +161 -0
- package/test/fixtures/stripe/subscription-canceled.json +161 -0
- package/test/fixtures/stripe/subscription-trialing.json +161 -0
- package/test/functions/admin/database-read.js +134 -0
- package/test/functions/admin/database-write.js +159 -0
- package/test/functions/admin/edit-post.js +366 -0
- package/test/functions/admin/firestore-query.js +207 -0
- package/test/functions/admin/firestore-read.js +129 -0
- package/test/functions/admin/firestore-write.js +105 -0
- package/test/functions/admin/get-stats.js +115 -0
- package/test/functions/admin/send-email.js +119 -0
- package/test/functions/admin/send-notification.js +208 -0
- package/test/functions/admin/write-repo-content.js +223 -0
- package/test/functions/general/add-marketing-contact.js +335 -0
- package/test/functions/general/fetch-post.js +54 -0
- package/test/functions/general/generate-uuid.js +114 -0
- package/test/functions/test/authenticate.js +64 -0
- package/test/functions/user/create-custom-token.js +73 -0
- package/test/functions/user/delete.js +135 -0
- package/test/functions/user/get-active-sessions.js +111 -0
- package/test/functions/user/get-subscription-info.js +99 -0
- package/test/functions/user/regenerate-api-keys.js +156 -0
- package/test/functions/user/resolve.js +52 -0
- package/test/functions/user/sign-out-all-sessions.js +94 -0
- package/test/functions/user/sign-up.js +252 -0
- package/test/functions/user/submit-feedback.js +104 -0
- package/test/functions/user/validate-settings.js +82 -0
- package/test/helpers/ai-request-payload.js +300 -0
- package/test/helpers/ai-test-provider.js +202 -0
- package/test/helpers/ai-tools-format.js +383 -0
- package/test/helpers/content/blog-auto-publisher.js +484 -0
- package/test/helpers/content/feed-parser.js +528 -0
- package/test/helpers/content/ghostii-blocks.js +134 -0
- package/test/helpers/content/ghostii-feed-integration.js +404 -0
- package/test/helpers/content/ghostii-write-article.js +243 -0
- package/test/helpers/environment.js +230 -0
- package/test/helpers/infer-contact.js +113 -0
- package/test/helpers/merge-line-files.js +271 -0
- package/test/helpers/payment/chargebee/parse-webhook.js +413 -0
- package/test/helpers/payment/chargebee/to-unified-one-time.js +147 -0
- package/test/helpers/payment/chargebee/to-unified-subscription.js +648 -0
- package/test/helpers/payment/paypal/parse-webhook.js +539 -0
- package/test/helpers/payment/paypal/to-unified-one-time.js +382 -0
- package/test/helpers/payment/paypal/to-unified-subscription.js +820 -0
- package/test/helpers/payment/stripe/parse-webhook.js +447 -0
- package/test/helpers/payment/stripe/to-unified-one-time.js +306 -0
- package/test/helpers/payment/stripe/to-unified-subscription.js +708 -0
- package/test/helpers/sanitize.js +222 -0
- package/test/helpers/slugify.js +394 -0
- package/test/helpers/storage.js +194 -0
- package/test/helpers/user.js +755 -0
- package/test/helpers/webhook-forward.js +418 -0
- package/test/mcp/discovery.js +53 -0
- package/test/mcp/oauth.js +161 -0
- package/test/mcp/protocol.js +268 -0
- package/test/mcp/roles.js +188 -0
- package/test/mcp/utils.js +245 -0
- package/test/routes/admin/create-post.js +364 -0
- package/test/routes/admin/database.js +131 -0
- package/test/routes/admin/deduplicate-image-alts.js +190 -0
- package/test/routes/admin/email.js +114 -0
- package/test/routes/admin/firestore-query.js +204 -0
- package/test/routes/admin/firestore.js +127 -0
- package/test/routes/admin/infer-contact.js +218 -0
- package/test/routes/admin/notification.js +198 -0
- package/test/routes/admin/post-resize-image.js +184 -0
- package/test/routes/admin/post.js +368 -0
- package/test/routes/admin/repo-content.js +223 -0
- package/test/routes/admin/stats.js +112 -0
- package/test/routes/content/post.js +54 -0
- package/test/routes/general/uuid.js +115 -0
- package/test/routes/marketing/campaign.js +125 -0
- package/test/routes/marketing/contact.js +370 -0
- package/test/routes/marketing/email-preferences.js +292 -0
- package/test/routes/marketing/push-send.js +32 -0
- package/test/routes/marketing/webhook-forward.js +61 -0
- package/test/routes/marketing/webhook.js +639 -0
- package/test/routes/payments/cancel.js +163 -0
- package/test/routes/payments/discount.js +80 -0
- package/test/routes/payments/dispute-alert.js +320 -0
- package/test/routes/payments/intent.js +336 -0
- package/test/routes/payments/portal.js +92 -0
- package/test/routes/payments/refund.js +179 -0
- package/test/routes/payments/trial-eligibility.js +71 -0
- package/test/routes/payments/webhook.js +107 -0
- package/test/routes/test/authenticate.js +59 -0
- package/test/routes/test/schema.js +554 -0
- package/test/routes/test/usage.js +342 -0
- package/test/routes/user/api-keys.js +156 -0
- package/test/routes/user/delete.js +136 -0
- package/test/routes/user/feedback.js +104 -0
- package/test/routes/user/sessions.js +199 -0
- package/test/routes/user/settings-validate.js +82 -0
- package/test/routes/user/signup.js +542 -0
- package/test/routes/user/subscription.js +99 -0
- package/test/routes/user/token.js +73 -0
- package/test/routes/user/user.js +157 -0
- package/test/rules/notifications.js +410 -0
- package/test/rules/payments-carts.js +371 -0
- package/test/rules/user.js +396 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test: Firestore Security Rules - User Documents
|
|
3
|
+
* Tests that security rules correctly protect user data
|
|
4
|
+
*
|
|
5
|
+
* Rules being tested:
|
|
6
|
+
* - Users can read their own document
|
|
7
|
+
* - Users can write to their own document (non-protected fields only)
|
|
8
|
+
* - Users cannot read/write other users' documents
|
|
9
|
+
* - Protected fields (auth, roles, flags, subscription, affiliate, api, metadata, usage, consent) cannot be written by users
|
|
10
|
+
*
|
|
11
|
+
* @see templates/firestore.rules
|
|
12
|
+
*/
|
|
13
|
+
module.exports = {
|
|
14
|
+
description: 'Firestore security rules for user documents',
|
|
15
|
+
type: 'group',
|
|
16
|
+
timeout: 30000,
|
|
17
|
+
|
|
18
|
+
tests: [
|
|
19
|
+
// Test 1: User can read their own document
|
|
20
|
+
{
|
|
21
|
+
name: 'user-can-read-own-doc',
|
|
22
|
+
auth: 'none', // We use rules context, not http auth
|
|
23
|
+
|
|
24
|
+
async run({ rules, accounts }) {
|
|
25
|
+
const uid = accounts.basic.uid;
|
|
26
|
+
const db = rules.asAccount('basic');
|
|
27
|
+
|
|
28
|
+
// Should succeed - reading own document
|
|
29
|
+
await rules.expectSuccess(
|
|
30
|
+
db.doc(`users/${uid}`).get()
|
|
31
|
+
);
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// Test 2: User cannot read another user's document
|
|
36
|
+
{
|
|
37
|
+
name: 'user-cannot-read-other-doc',
|
|
38
|
+
auth: 'none',
|
|
39
|
+
|
|
40
|
+
async run({ rules, accounts }) {
|
|
41
|
+
const otherUid = accounts.admin.uid;
|
|
42
|
+
const db = rules.asAccount('basic');
|
|
43
|
+
|
|
44
|
+
// Should fail - reading another user's document
|
|
45
|
+
await rules.expectFailure(
|
|
46
|
+
db.doc(`users/${otherUid}`).get()
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// Test 3: Unauthenticated user cannot read any user document
|
|
52
|
+
{
|
|
53
|
+
name: 'anonymous-cannot-read-user-doc',
|
|
54
|
+
auth: 'none',
|
|
55
|
+
|
|
56
|
+
async run({ rules, accounts }) {
|
|
57
|
+
const uid = accounts.basic.uid;
|
|
58
|
+
const db = rules.asAnonymous();
|
|
59
|
+
|
|
60
|
+
// Should fail - unauthenticated read
|
|
61
|
+
await rules.expectFailure(
|
|
62
|
+
db.doc(`users/${uid}`).get()
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// Test 4: User can write non-protected fields to own document
|
|
68
|
+
{
|
|
69
|
+
name: 'user-can-write-non-protected-fields',
|
|
70
|
+
auth: 'none',
|
|
71
|
+
|
|
72
|
+
async run({ rules, accounts }) {
|
|
73
|
+
const uid = accounts.basic.uid;
|
|
74
|
+
const db = rules.asAccount('basic');
|
|
75
|
+
|
|
76
|
+
// Should succeed - writing allowed fields
|
|
77
|
+
await rules.expectSuccess(
|
|
78
|
+
db.doc(`users/${uid}`).set({
|
|
79
|
+
profile: {
|
|
80
|
+
displayName: 'Test User',
|
|
81
|
+
bio: 'This is my bio',
|
|
82
|
+
},
|
|
83
|
+
preferences: {
|
|
84
|
+
theme: 'dark',
|
|
85
|
+
notifications: true,
|
|
86
|
+
},
|
|
87
|
+
}, { merge: true })
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// Test 5: User cannot write 'auth' field (protected)
|
|
93
|
+
{
|
|
94
|
+
name: 'user-cannot-write-auth-field',
|
|
95
|
+
auth: 'none',
|
|
96
|
+
|
|
97
|
+
async run({ rules, accounts }) {
|
|
98
|
+
const uid = accounts.basic.uid;
|
|
99
|
+
const db = rules.asAccount('basic');
|
|
100
|
+
|
|
101
|
+
// Should fail - auth is protected
|
|
102
|
+
await rules.expectFailure(
|
|
103
|
+
db.doc(`users/${uid}`).set({
|
|
104
|
+
auth: {
|
|
105
|
+
uid: 'hacked-uid',
|
|
106
|
+
email: 'hacked@example.com',
|
|
107
|
+
},
|
|
108
|
+
}, { merge: true })
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
// Test 6: User cannot write 'roles' field (protected)
|
|
114
|
+
{
|
|
115
|
+
name: 'user-cannot-write-roles-field',
|
|
116
|
+
auth: 'none',
|
|
117
|
+
|
|
118
|
+
async run({ rules, accounts }) {
|
|
119
|
+
const uid = accounts.basic.uid;
|
|
120
|
+
const db = rules.asAccount('basic');
|
|
121
|
+
|
|
122
|
+
// Should fail - roles is protected
|
|
123
|
+
await rules.expectFailure(
|
|
124
|
+
db.doc(`users/${uid}`).set({
|
|
125
|
+
roles: {
|
|
126
|
+
admin: true,
|
|
127
|
+
},
|
|
128
|
+
}, { merge: true })
|
|
129
|
+
);
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
// Test 7: User cannot write 'subscription' field (protected)
|
|
134
|
+
{
|
|
135
|
+
name: 'user-cannot-write-subscription-field',
|
|
136
|
+
auth: 'none',
|
|
137
|
+
|
|
138
|
+
async run({ rules, accounts }) {
|
|
139
|
+
const uid = accounts.basic.uid;
|
|
140
|
+
const db = rules.asAccount('basic');
|
|
141
|
+
|
|
142
|
+
// Should fail - subscription is protected
|
|
143
|
+
await rules.expectFailure(
|
|
144
|
+
db.doc(`users/${uid}`).set({
|
|
145
|
+
subscription: {
|
|
146
|
+
product: { id: 'premium' },
|
|
147
|
+
status: 'active',
|
|
148
|
+
},
|
|
149
|
+
}, { merge: true })
|
|
150
|
+
);
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// Test 8: User cannot write 'api' field (protected - contains privateKey)
|
|
155
|
+
{
|
|
156
|
+
name: 'user-cannot-write-api-field',
|
|
157
|
+
auth: 'none',
|
|
158
|
+
|
|
159
|
+
async run({ rules, accounts }) {
|
|
160
|
+
const uid = accounts.basic.uid;
|
|
161
|
+
const db = rules.asAccount('basic');
|
|
162
|
+
|
|
163
|
+
// Should fail - api is protected
|
|
164
|
+
await rules.expectFailure(
|
|
165
|
+
db.doc(`users/${uid}`).set({
|
|
166
|
+
api: {
|
|
167
|
+
privateKey: 'stolen-key',
|
|
168
|
+
clientId: 'fake-client',
|
|
169
|
+
},
|
|
170
|
+
}, { merge: true })
|
|
171
|
+
);
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
// Test 9: User cannot write 'flags' field (protected - system flags)
|
|
176
|
+
{
|
|
177
|
+
name: 'user-cannot-write-flags-field',
|
|
178
|
+
auth: 'none',
|
|
179
|
+
|
|
180
|
+
async run({ rules, accounts }) {
|
|
181
|
+
const uid = accounts.basic.uid;
|
|
182
|
+
const db = rules.asAccount('basic');
|
|
183
|
+
|
|
184
|
+
// Should fail - flags is protected
|
|
185
|
+
// Use a unique value to ensure isUpdatingField triggers
|
|
186
|
+
await rules.expectFailure(
|
|
187
|
+
db.doc(`users/${uid}`).set({
|
|
188
|
+
flags: {
|
|
189
|
+
hacked: true,
|
|
190
|
+
},
|
|
191
|
+
}, { merge: true })
|
|
192
|
+
);
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
// Test 10: User cannot write 'usage' field (protected - tracked by server)
|
|
197
|
+
{
|
|
198
|
+
name: 'user-cannot-write-usage-field',
|
|
199
|
+
auth: 'none',
|
|
200
|
+
|
|
201
|
+
async run({ rules, accounts }) {
|
|
202
|
+
const uid = accounts.basic.uid;
|
|
203
|
+
const db = rules.asAccount('basic');
|
|
204
|
+
|
|
205
|
+
// Should fail - usage is protected
|
|
206
|
+
await rules.expectFailure(
|
|
207
|
+
db.doc(`users/${uid}`).set({
|
|
208
|
+
usage: {
|
|
209
|
+
requests: 0, // Try to reset usage
|
|
210
|
+
},
|
|
211
|
+
}, { merge: true })
|
|
212
|
+
);
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
// Test 11: User cannot write 'affiliate' field (protected)
|
|
217
|
+
{
|
|
218
|
+
name: 'user-cannot-write-affiliate-field',
|
|
219
|
+
auth: 'none',
|
|
220
|
+
|
|
221
|
+
async run({ rules, accounts }) {
|
|
222
|
+
const uid = accounts.basic.uid;
|
|
223
|
+
const db = rules.asAccount('basic');
|
|
224
|
+
|
|
225
|
+
// Should fail - affiliate is protected
|
|
226
|
+
await rules.expectFailure(
|
|
227
|
+
db.doc(`users/${uid}`).set({
|
|
228
|
+
affiliate: {
|
|
229
|
+
code: 'STOLEN',
|
|
230
|
+
referrals: [],
|
|
231
|
+
},
|
|
232
|
+
}, { merge: true })
|
|
233
|
+
);
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
// Test 11.5: User cannot write 'consent' field (protected - server-only)
|
|
238
|
+
{
|
|
239
|
+
name: 'user-cannot-write-consent-field',
|
|
240
|
+
auth: 'none',
|
|
241
|
+
|
|
242
|
+
async run({ rules, accounts }) {
|
|
243
|
+
const uid = accounts.basic.uid;
|
|
244
|
+
const db = rules.asAccount('basic');
|
|
245
|
+
|
|
246
|
+
// Should fail - consent is protected (only signup route + webhook
|
|
247
|
+
// processors can mutate it server-side; a client write would let a
|
|
248
|
+
// user retroactively forge their own consent record).
|
|
249
|
+
// Use a value that can't match any prior state — earlier tests
|
|
250
|
+
// (email-preferences) may have set marketing.status to 'granted',
|
|
251
|
+
// and writing the SAME value is a no-op the rules correctly allow.
|
|
252
|
+
await rules.expectFailure(
|
|
253
|
+
db.doc(`users/${uid}`).set({
|
|
254
|
+
consent: {
|
|
255
|
+
marketing: { status: 'forged' },
|
|
256
|
+
},
|
|
257
|
+
}, { merge: true })
|
|
258
|
+
);
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
// Test 12: User cannot write to another user's document
|
|
263
|
+
{
|
|
264
|
+
name: 'user-cannot-write-other-doc',
|
|
265
|
+
auth: 'none',
|
|
266
|
+
|
|
267
|
+
async run({ rules, accounts }) {
|
|
268
|
+
const otherUid = accounts['premium-active'].uid;
|
|
269
|
+
const db = rules.asAccount('basic');
|
|
270
|
+
|
|
271
|
+
// Should fail - writing to another user's document
|
|
272
|
+
await rules.expectFailure(
|
|
273
|
+
db.doc(`users/${otherUid}`).set({
|
|
274
|
+
profile: { hacked: true },
|
|
275
|
+
}, { merge: true })
|
|
276
|
+
);
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
// Test 13: Unauthenticated user cannot write to any user document
|
|
281
|
+
{
|
|
282
|
+
name: 'anonymous-cannot-write-user-doc',
|
|
283
|
+
auth: 'none',
|
|
284
|
+
|
|
285
|
+
async run({ rules, accounts }) {
|
|
286
|
+
const uid = accounts.basic.uid;
|
|
287
|
+
const db = rules.asAnonymous();
|
|
288
|
+
|
|
289
|
+
// Should fail - unauthenticated write
|
|
290
|
+
await rules.expectFailure(
|
|
291
|
+
db.doc(`users/${uid}`).set({
|
|
292
|
+
profile: { displayName: 'Anonymous Hacker' },
|
|
293
|
+
}, { merge: true })
|
|
294
|
+
);
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
// Test 14: Admin can read any user's document
|
|
299
|
+
{
|
|
300
|
+
name: 'admin-can-read-any-user-doc',
|
|
301
|
+
auth: 'none',
|
|
302
|
+
|
|
303
|
+
async run({ rules, accounts }) {
|
|
304
|
+
const basicUid = accounts.basic.uid;
|
|
305
|
+
const db = rules.asAccount('admin');
|
|
306
|
+
|
|
307
|
+
// Should succeed - admin can read any user doc
|
|
308
|
+
await rules.expectSuccess(
|
|
309
|
+
db.doc(`users/${basicUid}`).get()
|
|
310
|
+
);
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
// Tests 15–18 exercise admin write/create/delete on a DEDICATED throwaway
|
|
315
|
+
// doc — never the shared seeded accounts. Mutating accounts.basic here
|
|
316
|
+
// poisons every suite that runs afterward (its api.privateKey and
|
|
317
|
+
// subscription are live fixtures for HTTP auth across the whole run).
|
|
318
|
+
|
|
319
|
+
// Test 15: Admin can create a user document
|
|
320
|
+
{
|
|
321
|
+
name: 'admin-can-create-user-doc',
|
|
322
|
+
auth: 'none',
|
|
323
|
+
|
|
324
|
+
async run({ rules }) {
|
|
325
|
+
const db = rules.asAccount('admin');
|
|
326
|
+
const newUid = '_test-new-user-by-admin';
|
|
327
|
+
|
|
328
|
+
// Should succeed - admin can create any user doc. The _test. email
|
|
329
|
+
// prefix keeps this doc blocked at the marketing validation layer
|
|
330
|
+
// (never reaches SendGrid/Beehiiv) even though a raw rules-context
|
|
331
|
+
// Firestore write fires no auth events anyway.
|
|
332
|
+
await rules.expectSuccess(
|
|
333
|
+
db.doc(`users/${newUid}`).set({
|
|
334
|
+
auth: { uid: newUid, email: '_test.new-user-by-admin@example.com' },
|
|
335
|
+
roles: {},
|
|
336
|
+
subscription: { product: { id: 'basic' }, status: 'active' },
|
|
337
|
+
})
|
|
338
|
+
);
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
// Test 16: Admin can write any user's document
|
|
343
|
+
{
|
|
344
|
+
name: 'admin-can-write-any-user-doc',
|
|
345
|
+
auth: 'none',
|
|
346
|
+
|
|
347
|
+
async run({ rules }) {
|
|
348
|
+
const db = rules.asAccount('admin');
|
|
349
|
+
|
|
350
|
+
// Should succeed - admin can write any user doc
|
|
351
|
+
await rules.expectSuccess(
|
|
352
|
+
db.doc('users/_test-new-user-by-admin').set({
|
|
353
|
+
profile: { updatedByAdmin: true },
|
|
354
|
+
}, { merge: true })
|
|
355
|
+
);
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
// Test 17: Admin can write protected fields
|
|
360
|
+
{
|
|
361
|
+
name: 'admin-can-write-protected-fields',
|
|
362
|
+
auth: 'none',
|
|
363
|
+
|
|
364
|
+
async run({ rules }) {
|
|
365
|
+
const db = rules.asAccount('admin');
|
|
366
|
+
|
|
367
|
+
// Should succeed - admin can write protected fields
|
|
368
|
+
await rules.expectSuccess(
|
|
369
|
+
db.doc('users/_test-new-user-by-admin').set({
|
|
370
|
+
roles: { premium: true },
|
|
371
|
+
subscription: { product: { id: 'pro' }, status: 'active' },
|
|
372
|
+
}, { merge: true })
|
|
373
|
+
);
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
// Test 18: Admin can delete a user document
|
|
378
|
+
{
|
|
379
|
+
name: 'admin-can-delete-user-doc',
|
|
380
|
+
auth: 'none',
|
|
381
|
+
|
|
382
|
+
async run({ rules }) {
|
|
383
|
+
const db = rules.asAccount('admin');
|
|
384
|
+
|
|
385
|
+
// Should succeed - admin can delete any user doc (the throwaway
|
|
386
|
+
// created in test 15, which also cleans it up)
|
|
387
|
+
await rules.expectSuccess(
|
|
388
|
+
db.doc('users/_test-new-user-by-admin').delete()
|
|
389
|
+
);
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
// Note: User create/delete tests are omitted because users don't create or delete
|
|
394
|
+
// their own documents - that's handled by system auth triggers (on-create/on-delete)
|
|
395
|
+
],
|
|
396
|
+
};
|