Skip to main content

max / makenotwork

24.2 KB · 696 lines History Blame Raw
1 //! DB-layer contract tests for `db::synckit::subscriptions`, the end-user
2 //! entitlement for first-party sync.
3 //!
4 //! This module decides who is allowed to sync and what storage they hold, and
5 //! its writes all arrive from Stripe webhooks, which redeliver. The pieces
6 //! pinned here are the ones a wrong answer costs money or data on:
7 //!
8 //! - `internal_write_allowed` is a status-only gate: exactly `active` opens
9 //! writes, every other lifecycle status (`trialing`, `past_due`, `unpaid`,
10 //! `incomplete`, `canceled`) denies, and the boundary is asserted on both
11 //! sides. A lapsed `current_period_end` is deliberately NOT consulted, so
12 //! that is pinned too rather than assumed.
13 //! - the gate is scoped to the exact (app, user) pair: another user's
14 //! subscription, or the same user's subscription on a different first-party
15 //! app, opens nothing.
16 //! - `create_app_sync_subscription` is the checkout webhook's write, so it is
17 //! exercised under replay: an identical redelivery is a no-op that returns
18 //! `false` and adds no second row, while each arm of the guard (a new Stripe
19 //! subscription id, or a non-active row) does reactivate.
20 //! - `update_app_sync_subscription_status`, `set_pending_storage_cap`,
21 //! `set_storage_cap_now` and `apply_pending_storage_cap` are asserted to hit
22 //! one row and leave every sibling row alone, which a missing key predicate
23 //! would break silently.
24 //!
25 //! Deleting this file would leave the paid-sync gate and the cap-change ladder
26 //! asserted only through HTTP and webhook flows, where a wrong row or a second
27 //! credit is invisible.
28 //!
29 //! Not covered here: `get_user_app_subscription`'s per-user/per-app scoping and
30 //! the terminal-`canceled` and epoch-period guards on the status setter, which
31 //! `db_synckit_accounts_layer` already asserts.
32
33 use crate::harness::db::TestDb;
34 use crate::harness::seed_user;
35
36 use makenotwork::db::synckit;
37 use makenotwork::db::synckit::NewAppSyncSubscription;
38 use makenotwork::db::{SyncAppId, UserId};
39
40 /// Seed a first-party (internal) sync app owned by `user`. Only internal apps
41 /// use the end-user subscription model, so every gate test needs one.
42 async fn seed_internal_app(pool: &sqlx::PgPool, user: UserId, name: &str) -> SyncAppId {
43 let app =
44 synckit::create_sync_app(pool, user, name, &format!("key_{name}_padding"), None, None)
45 .await
46 .expect("seed sync app")
47 .id;
48 sqlx::query("UPDATE sync_apps SET is_internal = true WHERE id = $1")
49 .bind(app)
50 .execute(pool)
51 .await
52 .expect("mark app first-party");
53 app
54 }
55
56 /// Insert a subscription through the production checkout write, asserting the
57 /// first delivery is a real insert.
58 async fn subscribe(
59 pool: &sqlx::PgPool,
60 user: UserId,
61 app: SyncAppId,
62 sub_id: &str,
63 interval: &str,
64 limit_bytes: i64,
65 ) {
66 let created = synckit::create_app_sync_subscription(
67 pool,
68 &NewAppSyncSubscription {
69 user_id: user,
70 app_id: app,
71 stripe_subscription_id: sub_id,
72 stripe_customer_id: "cus_seed",
73 interval,
74 storage_limit_bytes: limit_bytes,
75 },
76 )
77 .await
78 .expect("seed subscription");
79 assert!(created, "the first checkout delivery inserts a row");
80 }
81
82 /// How many subscription rows exist for the (user, app) pair. Used to prove a
83 /// replay adds nothing rather than merely returning `false`.
84 async fn row_count(pool: &sqlx::PgPool, user: UserId, app: SyncAppId) -> i64 {
85 sqlx::query_scalar::<_, i64>(
86 "SELECT COUNT(*) FROM app_sync_subscriptions WHERE user_id = $1 AND app_id = $2",
87 )
88 .bind(user)
89 .bind(app)
90 .fetch_one(pool)
91 .await
92 .expect("count subscription rows")
93 }
94
95 /// Read `canceled_at`, which the struct returned by `get_user_app_subscription`
96 /// does not carry but reactivation is contractually required to clear.
97 async fn canceled_at_is_set(pool: &sqlx::PgPool, sub_id: &str) -> bool {
98 sqlx::query_scalar::<_, Option<chrono::DateTime<chrono::Utc>>>(
99 "SELECT canceled_at FROM app_sync_subscriptions WHERE stripe_subscription_id = $1",
100 )
101 .bind(sub_id)
102 .fetch_one(pool)
103 .await
104 .expect("read canceled_at")
105 .is_some()
106 }
107
108 // ── internal_write_allowed: the entitlement boundary, both sides ─────────────
109
110 #[tokio::test]
111 async fn only_the_active_status_opens_writes() {
112 let db = TestDb::new().await;
113 let user = seed_user(&db.pool, "sks_status").await;
114 let app = seed_internal_app(&db.pool, user, "sksstatus").await;
115 subscribe(&db.pool, user, app, "sub_status", "monthly", 25_000_000_000).await;
116
117 assert!(
118 synckit::internal_write_allowed(&db.pool, app, user)
119 .await
120 .expect("gate query ok"),
121 "an active subscription is the one status that syncs"
122 );
123
124 // Every other Stripe lifecycle status sits on the closed side of the gate.
125 // `trialing` and `past_due` are the ones a plausible "not canceled" reading
126 // of the rule would let through, so they are asserted individually rather
127 // than as a family.
128 for lapsed in ["trialing", "past_due", "unpaid", "incomplete", "canceled"] {
129 synckit::update_app_sync_subscription_status(&db.pool, "sub_status", lapsed, None)
130 .await
131 .expect("status write ok");
132 assert!(
133 !synckit::internal_write_allowed(&db.pool, app, user)
134 .await
135 .expect("gate query ok"),
136 "status {lapsed} must not open first-party sync"
137 );
138 // `canceled` is terminal, so it has to be the last status tried: nothing
139 // after it could move the row back.
140 if lapsed == "canceled" {
141 break;
142 }
143 synckit::update_app_sync_subscription_status(&db.pool, "sub_status", "active", None)
144 .await
145 .expect("restore active ok");
146 assert!(
147 synckit::internal_write_allowed(&db.pool, app, user)
148 .await
149 .expect("gate query ok"),
150 "returning to active reopens writes after {lapsed}"
151 );
152 }
153 }
154
155 #[tokio::test]
156 async fn a_lapsed_billing_period_does_not_close_the_gate_on_its_own() {
157 let db = TestDb::new().await;
158 let user = seed_user(&db.pool, "sks_period").await;
159 let app = seed_internal_app(&db.pool, user, "sksperiod").await;
160 subscribe(&db.pool, user, app, "sub_period", "annual", 7_500_000_000).await;
161
162 // 2020-09-13, long past. The gate reads `status` alone, so entitlement
163 // survives a stale period and it is Stripe flipping the status (past_due,
164 // then canceled) that ends sync. Pinned because the alternative reading,
165 // "deny once the period end is behind us", would lock out every user during
166 // the ordinary gap between a renewal and its webhook.
167 synckit::update_app_sync_subscription_status(
168 &db.pool,
169 "sub_period",
170 "active",
171 Some(1_600_000_000),
172 )
173 .await
174 .expect("stamp a past period");
175
176 let sub = synckit::get_user_app_subscription(&db.pool, user, app)
177 .await
178 .expect("read subscription")
179 .expect("row exists");
180 assert_eq!(
181 sub.current_period_end,
182 chrono::DateTime::from_timestamp(1_600_000_000, 0),
183 "the raw Stripe seconds round-trip exactly"
184 );
185 assert!(
186 synckit::internal_write_allowed(&db.pool, app, user)
187 .await
188 .expect("gate query ok"),
189 "an active row with a lapsed period still syncs: status is the gate"
190 );
191
192 // And the far side of the boundary: the same row, one status change later.
193 synckit::update_app_sync_subscription_status(&db.pool, "sub_period", "past_due", None)
194 .await
195 .expect("status write ok");
196 assert!(
197 !synckit::internal_write_allowed(&db.pool, app, user)
198 .await
199 .expect("gate query ok"),
200 "the status is what closes it"
201 );
202 }
203
204 #[tokio::test]
205 async fn one_subscription_entitles_exactly_one_user_on_exactly_one_app() {
206 let db = TestDb::new().await;
207 let payer = seed_user(&db.pool, "sks_payer").await;
208 let freeloader = seed_user(&db.pool, "sks_freeloader").await;
209 let paid_app = seed_internal_app(&db.pool, payer, "skspaid").await;
210 let other_app = seed_internal_app(&db.pool, payer, "sksother").await;
211
212 subscribe(
213 &db.pool,
214 payer,
215 paid_app,
216 "sub_scoped",
217 "monthly",
218 25_000_000_000,
219 )
220 .await;
221
222 assert!(
223 synckit::internal_write_allowed(&db.pool, paid_app, payer)
224 .await
225 .expect("gate query ok"),
226 "the payer syncs the app they paid for"
227 );
228 // A gate that dropped the user predicate (EXISTS any active sub on the app)
229 // would let this through, and a gate that dropped the app predicate would
230 // let the next one through. Both are one deleted line away.
231 assert!(
232 !synckit::internal_write_allowed(&db.pool, paid_app, freeloader)
233 .await
234 .expect("gate query ok"),
235 "another user's subscription entitles nobody else"
236 );
237 assert!(
238 !synckit::internal_write_allowed(&db.pool, other_app, payer)
239 .await
240 .expect("gate query ok"),
241 "paying for one first-party app entitles nothing on another"
242 );
243
244 // The freeloader subscribing opens only their own pair, and leaves the
245 // payer's row untouched.
246 subscribe(
247 &db.pool,
248 freeloader,
249 paid_app,
250 "sub_scoped_two",
251 "monthly",
252 7_500_000_000,
253 )
254 .await;
255 assert!(
256 synckit::internal_write_allowed(&db.pool, paid_app, freeloader)
257 .await
258 .expect("gate query ok")
259 );
260 assert!(
261 !synckit::internal_write_allowed(&db.pool, other_app, freeloader)
262 .await
263 .expect("gate query ok"),
264 "the second subscription is scoped the same way as the first"
265 );
266 }
267
268 // ── create_app_sync_subscription: the checkout webhook, under replay ─────────
269
270 #[tokio::test]
271 async fn a_redelivered_checkout_webhook_writes_nothing_a_second_time() {
272 let db = TestDb::new().await;
273 let user = seed_user(&db.pool, "sks_replay").await;
274 let app = seed_internal_app(&db.pool, user, "sksreplay").await;
275 let params = NewAppSyncSubscription {
276 user_id: user,
277 app_id: app,
278 stripe_subscription_id: "sub_replay",
279 stripe_customer_id: "cus_replay",
280 interval: "annual",
281 storage_limit_bytes: 25_000_000_000,
282 };
283
284 assert!(
285 synckit::create_app_sync_subscription(&db.pool, &params)
286 .await
287 .expect("first checkout ok"),
288 "the first delivery inserts"
289 );
290 // Stripe redelivers; the guard WHERE makes an unchanged active row a no-op.
291 assert!(
292 !synckit::create_app_sync_subscription(&db.pool, &params)
293 .await
294 .expect("replay ok"),
295 "a redelivered checkout for an unchanged active row writes nothing"
296 );
297 assert!(
298 !synckit::create_app_sync_subscription(&db.pool, &params)
299 .await
300 .expect("second replay ok"),
301 "and it stays a no-op however many times Stripe retries"
302 );
303
304 assert_eq!(
305 row_count(&db.pool, user, app).await,
306 1,
307 "the replays added no second subscription row"
308 );
309 let sub = synckit::get_user_app_subscription(&db.pool, user, app)
310 .await
311 .expect("read subscription")
312 .expect("row exists");
313 assert_eq!(sub.stripe_subscription_id, "sub_replay");
314 assert_eq!(sub.interval, "annual");
315 assert_eq!(sub.status, "active");
316 assert_eq!(
317 sub.storage_limit_bytes,
318 Some(25_000_000_000),
319 "the cap the user paid for is unchanged by the replays"
320 );
321 }
322
323 #[tokio::test]
324 async fn a_cap_change_delivered_as_a_checkout_replay_is_refused() {
325 let db = TestDb::new().await;
326 let user = seed_user(&db.pool, "sks_capreplay").await;
327 let app = seed_internal_app(&db.pool, user, "skscapreplay").await;
328 subscribe(
329 &db.pool,
330 user,
331 app,
332 "sub_capreplay",
333 "monthly",
334 25_000_000_000,
335 )
336 .await;
337
338 // Same active row, same Stripe subscription, different cap and interval.
339 // Neither arm of the guard fires, so the checkout path declines to move the
340 // cap: cap changes are the `set_storage_cap_now` / `set_pending_storage_cap`
341 // ladder's job, and letting a stale redelivery rewrite a cap would undo a
342 // change the user already paid for.
343 let changed = synckit::create_app_sync_subscription(
344 &db.pool,
345 &NewAppSyncSubscription {
346 user_id: user,
347 app_id: app,
348 stripe_subscription_id: "sub_capreplay",
349 stripe_customer_id: "cus_seed",
350 interval: "annual",
351 storage_limit_bytes: 7_500_000_000,
352 },
353 )
354 .await
355 .expect("checkout write ok");
356 assert!(!changed, "an unchanged-subscription checkout is a no-op");
357
358 let sub = synckit::get_user_app_subscription(&db.pool, user, app)
359 .await
360 .expect("read subscription")
361 .expect("row exists");
362 assert_eq!(
363 sub.storage_limit_bytes,
364 Some(25_000_000_000),
365 "the cap on record is the one the ladder set, not the replayed one"
366 );
367 assert_eq!(sub.interval, "monthly", "and the interval is untouched too");
368 }
369
370 #[tokio::test]
371 async fn a_new_stripe_subscription_replaces_the_row_it_conflicts_with() {
372 let db = TestDb::new().await;
373 let user = seed_user(&db.pool, "sks_swap").await;
374 let app = seed_internal_app(&db.pool, user, "sksswap").await;
375 subscribe(
376 &db.pool,
377 user,
378 app,
379 "sub_swap_old",
380 "monthly",
381 7_500_000_000,
382 )
383 .await;
384
385 // The user re-checks-out on a different Stripe subscription (the old one
386 // ended at Stripe's side). The id differs, so the first arm of the guard
387 // fires and every billing field moves together.
388 let replaced = synckit::create_app_sync_subscription(
389 &db.pool,
390 &NewAppSyncSubscription {
391 user_id: user,
392 app_id: app,
393 stripe_subscription_id: "sub_swap_new",
394 stripe_customer_id: "cus_swap_new",
395 interval: "annual",
396 storage_limit_bytes: 25_000_000_000,
397 },
398 )
399 .await
400 .expect("re-checkout ok");
401 assert!(
402 replaced,
403 "a different Stripe subscription id is a real write"
404 );
405
406 assert_eq!(
407 row_count(&db.pool, user, app).await,
408 1,
409 "the (user, app) pair still holds exactly one subscription"
410 );
411 let sub = synckit::get_user_app_subscription(&db.pool, user, app)
412 .await
413 .expect("read subscription")
414 .expect("row exists");
415 assert_eq!(sub.stripe_subscription_id, "sub_swap_new");
416 assert_eq!(sub.interval, "annual");
417 assert_eq!(sub.storage_limit_bytes, Some(25_000_000_000));
418
419 // The old id is no longer a route to this row, and the new one is.
420 assert_eq!(
421 synckit::get_subscription_by_stripe_id(&db.pool, "sub_swap_old")
422 .await
423 .expect("lookup ok"),
424 None,
425 "a webhook on the retired subscription finds nothing to update"
426 );
427 assert_eq!(
428 synckit::get_subscription_by_stripe_id(&db.pool, "sub_swap_new")
429 .await
430 .expect("lookup ok"),
431 Some((user, app))
432 );
433 }
434
435 #[tokio::test]
436 async fn re_subscribing_after_a_cancellation_reactivates_at_checkout() {
437 let db = TestDb::new().await;
438 let user = seed_user(&db.pool, "sks_resub").await;
439 let app = seed_internal_app(&db.pool, user, "sksresub").await;
440 subscribe(&db.pool, user, app, "sub_resub", "monthly", 7_500_000_000).await;
441
442 synckit::update_app_sync_subscription_status(&db.pool, "sub_resub", "canceled", None)
443 .await
444 .expect("cancel ok");
445 assert!(
446 canceled_at_is_set(&db.pool, "sub_resub").await,
447 "cancellation stamps canceled_at"
448 );
449 assert!(
450 !synckit::internal_write_allowed(&db.pool, app, user)
451 .await
452 .expect("gate query ok"),
453 "a canceled subscription closes writes"
454 );
455
456 // The user pays again on the same Stripe subscription id. The second arm of
457 // the guard (status != 'active') fires, so the row reactivates at checkout
458 // rather than waiting for a later `customer.subscription.updated`.
459 let revived = synckit::create_app_sync_subscription(
460 &db.pool,
461 &NewAppSyncSubscription {
462 user_id: user,
463 app_id: app,
464 stripe_subscription_id: "sub_resub",
465 stripe_customer_id: "cus_seed",
466 interval: "annual",
467 storage_limit_bytes: 25_000_000_000,
468 },
469 )
470 .await
471 .expect("re-subscribe ok");
472 assert!(revived, "a paid re-subscribe is a write");
473
474 let sub = synckit::get_user_app_subscription(&db.pool, user, app)
475 .await
476 .expect("read subscription")
477 .expect("row exists");
478 assert_eq!(sub.status, "active");
479 assert_eq!(
480 sub.interval, "annual",
481 "the reactivating checkout carries the new interval"
482 );
483 assert_eq!(sub.storage_limit_bytes, Some(25_000_000_000));
484 assert!(
485 !canceled_at_is_set(&db.pool, "sub_resub").await,
486 "reactivation clears canceled_at rather than leaving a canceled-looking row"
487 );
488 assert!(
489 synckit::internal_write_allowed(&db.pool, app, user)
490 .await
491 .expect("gate query ok"),
492 "and sync is open again"
493 );
494
495 // The very next redelivery of that same checkout is a no-op again.
496 assert!(
497 !synckit::create_app_sync_subscription(
498 &db.pool,
499 &NewAppSyncSubscription {
500 user_id: user,
501 app_id: app,
502 stripe_subscription_id: "sub_resub",
503 stripe_customer_id: "cus_seed",
504 interval: "annual",
505 storage_limit_bytes: 25_000_000_000,
506 },
507 )
508 .await
509 .expect("replay ok"),
510 "the reactivated row is active, so the replay writes nothing"
511 );
512 }
513
514 // ── keyed writes: one row moves, its siblings do not ─────────────────────────
515
516 #[tokio::test]
517 async fn a_status_webhook_moves_only_the_subscription_it_names() {
518 let db = TestDb::new().await;
519 let alice = seed_user(&db.pool, "sks_alice").await;
520 let bob = seed_user(&db.pool, "sks_bob").await;
521 let app = seed_internal_app(&db.pool, alice, "skssiblings").await;
522 subscribe(&db.pool, alice, app, "sub_alice", "monthly", 25_000_000_000).await;
523 subscribe(&db.pool, bob, app, "sub_bob", "annual", 7_500_000_000).await;
524
525 synckit::update_app_sync_subscription_status(
526 &db.pool,
527 "sub_alice",
528 "past_due",
529 Some(1_800_000_000),
530 )
531 .await
532 .expect("status write ok");
533
534 let alice_sub = synckit::get_user_app_subscription(&db.pool, alice, app)
535 .await
536 .expect("read alice")
537 .expect("row exists");
538 assert_eq!(alice_sub.status, "past_due");
539 assert_eq!(
540 alice_sub.current_period_end,
541 chrono::DateTime::from_timestamp(1_800_000_000, 0)
542 );
543
544 let bob_sub = synckit::get_user_app_subscription(&db.pool, bob, app)
545 .await
546 .expect("read bob")
547 .expect("row exists");
548 assert_eq!(
549 bob_sub.status, "active",
550 "a webhook keyed on one Stripe id must not touch another subscriber"
551 );
552 assert_eq!(
553 bob_sub.current_period_end, None,
554 "and it stamps no period on the sibling row"
555 );
556 assert!(
557 synckit::internal_write_allowed(&db.pool, app, bob)
558 .await
559 .expect("gate query ok"),
560 "bob keeps syncing while alice's payment is late"
561 );
562 assert!(
563 !synckit::internal_write_allowed(&db.pool, app, alice)
564 .await
565 .expect("gate query ok")
566 );
567
568 // An id that names no row is a no-op, not an error and not a wildcard.
569 synckit::update_app_sync_subscription_status(&db.pool, "sub_nobody", "canceled", None)
570 .await
571 .expect("unknown-id status write ok");
572 assert_eq!(
573 synckit::get_user_app_subscription(&db.pool, bob, app)
574 .await
575 .expect("read bob")
576 .expect("row exists")
577 .status,
578 "active",
579 "an unmatched webhook leaves every row alone"
580 );
581 assert_eq!(
582 synckit::get_subscription_by_stripe_id(&db.pool, "sub_nobody")
583 .await
584 .expect("lookup ok"),
585 None,
586 "and an unknown Stripe id resolves to nothing"
587 );
588 assert_eq!(
589 synckit::get_subscription_by_stripe_id(&db.pool, "sub_bob")
590 .await
591 .expect("lookup ok"),
592 Some((bob, app)),
593 "the lookup picks the row its id names, not the first row it meets"
594 );
595 }
596
597 #[tokio::test]
598 async fn cap_writes_land_on_one_subscriber_at_a_time() {
599 let db = TestDb::new().await;
600 let alice = seed_user(&db.pool, "sks_capalice").await;
601 let bob = seed_user(&db.pool, "sks_capbob").await;
602 let app = seed_internal_app(&db.pool, alice, "skscaps").await;
603 subscribe(
604 &db.pool,
605 alice,
606 app,
607 "sub_capalice",
608 "monthly",
609 25_000_000_000,
610 )
611 .await;
612 subscribe(&db.pool, bob, app, "sub_capbob", "monthly", 7_500_000_000).await;
613
614 // Alice queues a decrease for the next cycle; Bob's row must not move.
615 synckit::set_pending_storage_cap(&db.pool, alice, app, 3_000_000_000)
616 .await
617 .expect("queue cap change");
618 let bob_sub = synckit::get_user_app_subscription(&db.pool, bob, app)
619 .await
620 .expect("read bob")
621 .expect("row exists");
622 assert_eq!(bob_sub.storage_limit_bytes, Some(7_500_000_000));
623 assert_eq!(
624 bob_sub.pending_storage_limit_bytes, None,
625 "one subscriber's queued change is not queued on another"
626 );
627
628 // Bob raises his cap now; the immediate write is likewise his alone.
629 synckit::set_storage_cap_now(&db.pool, bob, app, 40_000_000_000)
630 .await
631 .expect("raise cap now");
632 let alice_sub = synckit::get_user_app_subscription(&db.pool, alice, app)
633 .await
634 .expect("read alice")
635 .expect("row exists");
636 assert_eq!(
637 alice_sub.storage_limit_bytes,
638 Some(25_000_000_000),
639 "alice keeps the cap she paid for while bob buys more"
640 );
641 assert_eq!(
642 alice_sub.pending_storage_limit_bytes,
643 Some(3_000_000_000),
644 "and her queued decrease is still waiting"
645 );
646
647 // The renewal for Bob's subscription rolls only Bob's row, and since he has
648 // nothing queued, the `IS NOT NULL` guard leaves his active cap alone rather
649 // than nulling it out of the pending column.
650 synckit::apply_pending_storage_cap(&db.pool, "sub_capbob")
651 .await
652 .expect("roll bob");
653 assert_eq!(
654 synckit::get_user_app_subscription(&db.pool, bob, app)
655 .await
656 .expect("read bob")
657 .expect("row exists")
658 .storage_limit_bytes,
659 Some(40_000_000_000),
660 "a renewal with nothing queued must not wipe the active cap"
661 );
662 assert_eq!(
663 synckit::get_user_app_subscription(&db.pool, alice, app)
664 .await
665 .expect("read alice")
666 .expect("row exists")
667 .pending_storage_limit_bytes,
668 Some(3_000_000_000),
669 "and it did not promote alice's queued change early"
670 );
671
672 // Alice's own renewal promotes hers, once. A redelivered `invoice.paid`
673 // finds nothing pending and changes nothing.
674 synckit::apply_pending_storage_cap(&db.pool, "sub_capalice")
675 .await
676 .expect("roll alice");
677 synckit::apply_pending_storage_cap(&db.pool, "sub_capalice")
678 .await
679 .expect("redelivered roll");
680 let alice_sub = synckit::get_user_app_subscription(&db.pool, alice, app)
681 .await
682 .expect("read alice")
683 .expect("row exists");
684 assert_eq!(alice_sub.storage_limit_bytes, Some(3_000_000_000));
685 assert_eq!(alice_sub.pending_storage_limit_bytes, None);
686 assert_eq!(
687 synckit::get_user_app_subscription(&db.pool, bob, app)
688 .await
689 .expect("read bob")
690 .expect("row exists")
691 .storage_limit_bytes,
692 Some(40_000_000_000),
693 "bob's cap survived both of alice's rolls"
694 );
695 }
696