| 319 |
319 |
|
"a revoked key must not activate"
|
| 320 |
320 |
|
);
|
| 321 |
321 |
|
}
|
|
322 |
+ |
|
|
323 |
+ |
// ── db::creator_tiers — storage-cap compare-and-set (Run 13 Test) ──
|
|
324 |
+ |
// The storage-cap enforcement was "illusory coverage": the atomic
|
|
325 |
+ |
// conditional-UPDATE was exercised only indirectly through upload flows. Pin the
|
|
326 |
+ |
// CAS contract at the DB layer: it must never let the cap be exceeded, including
|
|
327 |
+ |
// under a concurrent race.
|
|
328 |
+ |
|
|
329 |
+ |
#[tokio::test]
|
|
330 |
+ |
async fn storage_increment_succeeds_under_cap() {
|
|
331 |
+ |
let mut h = TestHarness::new().await;
|
|
332 |
+ |
let user = h.create_creator("storagecap_under").await;
|
|
333 |
+ |
|
|
334 |
+ |
db::creator_tiers::try_increment_storage(&h.db, user, 400, 1000)
|
|
335 |
+ |
.await
|
|
336 |
+ |
.expect("an increment within the cap must succeed");
|
|
337 |
+ |
|
|
338 |
+ |
let used: i64 = sqlx::query_scalar("SELECT storage_used_bytes FROM users WHERE id = $1")
|
|
339 |
+ |
.bind(user)
|
|
340 |
+ |
.fetch_one(&h.db)
|
|
341 |
+ |
.await
|
|
342 |
+ |
.unwrap();
|
|
343 |
+ |
assert_eq!(used, 400);
|
|
344 |
+ |
}
|
|
345 |
+ |
|
|
346 |
+ |
#[tokio::test]
|
|
347 |
+ |
async fn storage_increment_past_cap_is_rejected_and_leaves_count_unchanged() {
|
|
348 |
+ |
let mut h = TestHarness::new().await;
|
|
349 |
+ |
let user = h.create_creator("storagecap_reject").await;
|
|
350 |
+ |
|
|
351 |
+ |
db::creator_tiers::try_increment_storage(&h.db, user, 800, 1000)
|
|
352 |
+ |
.await
|
|
353 |
+ |
.unwrap();
|
|
354 |
+ |
// 800 + 300 = 1100 > 1000 → rejected.
|
|
355 |
+ |
assert!(
|
|
356 |
+ |
db::creator_tiers::try_increment_storage(&h.db, user, 300, 1000).await.is_err(),
|
|
357 |
+ |
"an increment past the cap must fail"
|
|
358 |
+ |
);
|
|
359 |
+ |
|
|
360 |
+ |
let used: i64 = sqlx::query_scalar("SELECT storage_used_bytes FROM users WHERE id = $1")
|
|
361 |
+ |
.bind(user)
|
|
362 |
+ |
.fetch_one(&h.db)
|
|
363 |
+ |
.await
|
|
364 |
+ |
.unwrap();
|
|
365 |
+ |
assert_eq!(used, 800, "a rejected increment must not change the stored count");
|
|
366 |
+ |
}
|
|
367 |
+ |
|
|
368 |
+ |
#[tokio::test]
|
|
369 |
+ |
async fn storage_increment_concurrent_never_exceeds_cap() {
|
|
370 |
+ |
let mut h = TestHarness::new().await;
|
|
371 |
+ |
let user = h.create_creator("storagecap_race").await;
|
|
372 |
+ |
|
|
373 |
+ |
// Cap 900, starting at 0. Two concurrent +500 increments: only one fits
|
|
374 |
+ |
// (0+500 ok → 500; the other then sees 500, 500+500=1000 > 900 → rejected).
|
|
375 |
+ |
let (p1, p2) = (h.db.clone(), h.db.clone());
|
|
376 |
+ |
let a = tokio::spawn(async move {
|
|
377 |
+ |
db::creator_tiers::try_increment_storage(&p1, user, 500, 900).await
|
|
378 |
+ |
});
|
|
379 |
+ |
let b = tokio::spawn(async move {
|
|
380 |
+ |
db::creator_tiers::try_increment_storage(&p2, user, 500, 900).await
|
|
381 |
+ |
});
|
|
382 |
+ |
let (ra, rb) = (a.await.unwrap(), b.await.unwrap());
|
|
383 |
+ |
|
|
384 |
+ |
let succeeded = [ra.is_ok(), rb.is_ok()].iter().filter(|x| **x).count();
|
|
385 |
+ |
assert_eq!(succeeded, 1, "exactly one of two racing over-cap increments may succeed");
|
|
386 |
+ |
|
|
387 |
+ |
let used: i64 = sqlx::query_scalar("SELECT storage_used_bytes FROM users WHERE id = $1")
|
|
388 |
+ |
.bind(user)
|
|
389 |
+ |
.fetch_one(&h.db)
|
|
390 |
+ |
.await
|
|
391 |
+ |
.unwrap();
|
|
392 |
+ |
assert_eq!(used, 500, "the cap holds under a race: total is 500, never 1000");
|
|
393 |
+ |
}
|