Skip to main content

max / makenotwork

Test collections and items::bulk db cold spots Extend the db-layer testing pass to the last audit cold spots (14 tests): - items::bulk: pin the IDOR seal on every bulk mutation (a non-owner publish/unpublish/delete/reprice/tag touches zero rows), soft-delete idempotency, tag ON CONFLICT dedup, move_item sort-order swap, and duplicate_item's savepoint slug-collision retry (Phase 3 TOCTOU) — two concurrent duplicates get distinct slugs and both succeed - collections: collection_items append/idempotency, atomic reorder, the per-user slug-clash seal (clean Validation error, not a raw 23505), and concurrent adds of distinct items each landing exactly once Make db::collections pub for the test crate, matching the ssh_keys/issues/ passkeys/synckit convention. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-02 14:56 UTC
Commit: 0337663033807e20f5244de45407197b6cad08c9
Parent: f37e74c
4 files changed, +441 insertions, -1 deletion
@@ -51,7 +51,7 @@ pub mod git_access_tokens;
51 51 pub mod issues;
52 52 pub(crate) mod reports;
53 53 pub(crate) mod fan_plus;
54 - pub(crate) mod collections;
54 + pub mod collections; // pub so the integration test crate can exercise the layer directly
55 55 pub(crate) mod ota;
56 56 pub(crate) mod builds;
57 57 pub mod creator_tiers;
@@ -0,0 +1,177 @@
1 + //! DB-layer contract tests for `db::collections` — user-curated item lists.
2 + //!
3 + //! Audit Run 16 flagged `collections` as a Concurrency/Testing cold spot (B+).
4 + //! These pin the append/idempotency/reorder contract of `collection_items` at
5 + //! the DB layer: `add_item_to_collection` appends at `MAX(position)+1` and is
6 + //! idempotent via `ON CONFLICT (collection_id, item_id) DO NOTHING`; reorder
7 + //! reassigns positions atomically; the slug-clash "seal" rejects a duplicate as
8 + //! a clean validation error rather than a raw 23505; and concurrent adds of
9 + //! distinct items each land exactly once (no lost writes).
10 + //!
11 + //! NOTE: `add_item_to_collection` computes `MAX(position)+1` outside a lock, so
12 + //! two concurrent adds of *different* items can tie on a position (ordering
13 + //! ambiguity, not data loss). The concurrency test asserts the guaranteed
14 + //! invariant — every item is present exactly once — not position distinctness.
15 +
16 + use crate::harness::db::TestDb;
17 + use makenotwork::db::{collections, ItemId, ProjectId, Slug, UserId};
18 +
19 + async fn seed_user(pool: &sqlx::PgPool, username: &str) -> UserId {
20 + let hash = makenotwork::auth::hash_password("password123").expect("hash");
21 + sqlx::query_scalar::<_, UserId>(
22 + "INSERT INTO users (username, email, password_hash, email_verified)
23 + VALUES ($1, $2, $3, true) RETURNING id",
24 + )
25 + .bind(username)
26 + .bind(format!("{username}@test.com"))
27 + .bind(&hash)
28 + .fetch_one(pool)
29 + .await
30 + .expect("seed user")
31 + }
32 +
33 + async fn seed_project(pool: &sqlx::PgPool, user: UserId, slug: &str) -> ProjectId {
34 + sqlx::query_scalar::<_, ProjectId>(
35 + "INSERT INTO projects (user_id, slug, title) VALUES ($1, $2, 'P') RETURNING id",
36 + )
37 + .bind(user)
38 + .bind(slug)
39 + .fetch_one(pool)
40 + .await
41 + .expect("seed project")
42 + }
43 +
44 + async fn seed_item(pool: &sqlx::PgPool, project: ProjectId, slug: &str) -> ItemId {
45 + sqlx::query_scalar::<_, ItemId>(
46 + "INSERT INTO items (project_id, title, item_type, price_cents, slug)
47 + VALUES ($1, $2, 'digital', 1000, $3) RETURNING id",
48 + )
49 + .bind(project)
50 + .bind(format!("Item {slug}"))
51 + .bind(slug)
52 + .fetch_one(pool)
53 + .await
54 + .expect("seed item")
55 + }
56 +
57 + fn slug(s: &str) -> Slug {
58 + Slug::from_trusted(s.to_string())
59 + }
60 +
61 + #[tokio::test]
62 + async fn add_item_appends_positions_and_is_idempotent() {
63 + let db = TestDb::new().await;
64 + let user = seed_user(&db.pool, "col_add").await;
65 + let project = seed_project(&db.pool, user, "col-add").await;
66 + let col = collections::create_collection(&db.pool, user, &slug("faves"), "Faves", None, true)
67 + .await
68 + .unwrap();
69 + let a = seed_item(&db.pool, project, "a").await;
70 + let b = seed_item(&db.pool, project, "b").await;
71 +
72 + collections::add_item_to_collection(&db.pool, col.id, a).await.unwrap();
73 + collections::add_item_to_collection(&db.pool, col.id, b).await.unwrap();
74 + // Re-adding an existing item is a no-op (ON CONFLICT), not a second row.
75 + collections::add_item_to_collection(&db.pool, col.id, a).await.unwrap();
76 +
77 + assert_eq!(collections::count_collection_items(&db.pool, col.id).await.unwrap(), 2);
78 + let items = collections::get_collection_items(&db.pool, col.id).await.unwrap();
79 + // Serial appends take sequential positions starting at 0.
80 + assert_eq!(items.iter().map(|r| r.position).collect::<Vec<_>>(), vec![0, 1]);
81 + assert_eq!(items[0].item_id, a);
82 + assert_eq!(items[1].item_id, b);
83 + }
84 +
85 + #[tokio::test]
86 + async fn remove_item_reports_whether_a_row_was_deleted() {
87 + let db = TestDb::new().await;
88 + let user = seed_user(&db.pool, "col_rm").await;
89 + let project = seed_project(&db.pool, user, "col-rm").await;
90 + let col = collections::create_collection(&db.pool, user, &slug("rm"), "Rm", None, false).await.unwrap();
91 + let item = seed_item(&db.pool, project, "r").await;
92 + collections::add_item_to_collection(&db.pool, col.id, item).await.unwrap();
93 +
94 + assert!(collections::remove_item_from_collection(&db.pool, col.id, item).await.unwrap());
95 + // Removing again finds nothing.
96 + assert!(!collections::remove_item_from_collection(&db.pool, col.id, item).await.unwrap());
97 + assert_eq!(collections::count_collection_items(&db.pool, col.id).await.unwrap(), 0);
98 + }
99 +
100 + #[tokio::test]
101 + async fn reorder_reassigns_positions_by_the_given_sequence() {
102 + let db = TestDb::new().await;
103 + let user = seed_user(&db.pool, "col_reorder").await;
104 + let project = seed_project(&db.pool, user, "col-reorder").await;
105 + let col = collections::create_collection(&db.pool, user, &slug("ord"), "Ord", None, true).await.unwrap();
106 + let a = seed_item(&db.pool, project, "oa").await;
107 + let b = seed_item(&db.pool, project, "ob").await;
108 + let c = seed_item(&db.pool, project, "oc").await;
109 + for it in [a, b, c] {
110 + collections::add_item_to_collection(&db.pool, col.id, it).await.unwrap();
111 + }
112 +
113 + // Reverse the order: c, b, a -> positions 0, 1, 2.
114 + collections::reorder_collection_items(&db.pool, col.id, &[c, b, a]).await.unwrap();
115 +
116 + let items = collections::get_collection_items(&db.pool, col.id).await.unwrap();
117 + assert_eq!(
118 + items.iter().map(|r| r.item_id).collect::<Vec<_>>(),
119 + vec![c, b, a],
120 + "get_collection_items returns items in the reordered sequence"
121 + );
122 + assert_eq!(items.iter().map(|r| r.position).collect::<Vec<_>>(), vec![0, 1, 2]);
123 + }
124 +
125 + /// The slug "seal": a per-user duplicate slug is mapped to a clean validation
126 + /// error, never a raw 23505 bubbling to a 500.
127 + #[tokio::test]
128 + async fn duplicate_slug_is_a_clean_validation_error() {
129 + let db = TestDb::new().await;
130 + let user = seed_user(&db.pool, "col_slug").await;
131 + collections::create_collection(&db.pool, user, &slug("dup"), "First", None, true).await.unwrap();
132 +
133 + let clash = collections::create_collection(&db.pool, user, &slug("dup"), "Second", None, true).await;
134 + assert!(
135 + matches!(clash, Err(makenotwork::error::AppError::BadRequest(_) | makenotwork::error::AppError::Validation(_))),
136 + "a duplicate slug must surface as a validation error, got {clash:?}"
137 + );
138 + }
139 +
140 + /// Concurrent adds of distinct items must not lose a write: every item lands in
141 + /// the collection exactly once. (Positions may tie under the race — see the
142 + /// module note — so this asserts membership, the invariant that actually holds.)
143 + #[tokio::test]
144 + async fn concurrent_adds_of_distinct_items_all_land_once() {
145 + let db = TestDb::new().await;
146 + let user = seed_user(&db.pool, "col_race").await;
147 + let project = seed_project(&db.pool, user, "col-race").await;
148 + let col = collections::create_collection(&db.pool, user, &slug("race"), "Race", None, true).await.unwrap();
149 +
150 + let mut item_ids = Vec::new();
151 + for i in 0..6 {
152 + item_ids.push(seed_item(&db.pool, project, &format!("rc{i}")).await);
153 + }
154 +
155 + let mut handles = Vec::new();
156 + for item in item_ids.iter().copied() {
157 + let pool = db.pool.clone();
158 + let cid = col.id;
159 + handles.push(tokio::spawn(async move {
160 + collections::add_item_to_collection(&pool, cid, item).await
161 + }));
162 + }
163 + for h in handles {
164 + h.await.expect("task panicked").expect("concurrent add must not error");
165 + }
166 +
167 + assert_eq!(collections::count_collection_items(&db.pool, col.id).await.unwrap(), 6);
168 + let present: std::collections::HashSet<_> = collections::get_collection_items(&db.pool, col.id)
169 + .await
170 + .unwrap()
171 + .into_iter()
172 + .map(|r| r.item_id)
173 + .collect();
174 + for item in item_ids {
175 + assert!(present.contains(&item), "every concurrently-added item is present exactly once");
176 + }
177 + }
@@ -0,0 +1,261 @@
1 + //! DB-layer contract tests for `db::items::bulk` — batch item operations.
2 + //!
3 + //! Audit Run 16 flagged `bulk` as a Testing cold spot. Every bulk mutation is
4 + //! ownership-scoped via `project_id IN (SELECT id FROM projects WHERE user_id =
5 + //! $n)` — an IDOR seal that was only exercised through the HTTP surface. These
6 + //! pin that seal at the DB layer (a non-owner's bulk op touches zero rows), plus
7 + //! the soft-delete idempotency, the tag ON CONFLICT dedup, `move_item`'s
8 + //! sort-order swap, and `duplicate_item`'s savepoint slug-collision retry (the
9 + //! Phase 3 TOCTOU fix): two concurrent duplicates of one source get distinct
10 + //! slugs and both succeed.
11 +
12 + use crate::harness::db::TestDb;
13 + use makenotwork::db::items;
14 + use makenotwork::db::{ItemId, PriceCents, ProjectId, TagId, UserId};
15 +
16 + async fn seed_user(pool: &sqlx::PgPool, username: &str) -> UserId {
17 + let hash = makenotwork::auth::hash_password("password123").expect("hash");
18 + sqlx::query_scalar::<_, UserId>(
19 + "INSERT INTO users (username, email, password_hash, email_verified)
20 + VALUES ($1, $2, $3, true) RETURNING id",
21 + )
22 + .bind(username)
23 + .bind(format!("{username}@test.com"))
24 + .bind(&hash)
25 + .fetch_one(pool)
26 + .await
27 + .expect("seed user")
28 + }
29 +
30 + async fn seed_project(pool: &sqlx::PgPool, user: UserId, slug: &str) -> ProjectId {
31 + sqlx::query_scalar::<_, ProjectId>(
32 + "INSERT INTO projects (user_id, slug, title) VALUES ($1, $2, 'P') RETURNING id",
33 + )
34 + .bind(user)
35 + .bind(slug)
36 + .fetch_one(pool)
37 + .await
38 + .expect("seed project")
39 + }
40 +
41 + /// Seed an item with an explicit sort_order and a unique slug.
42 + async fn seed_item(pool: &sqlx::PgPool, project: ProjectId, slug: &str, sort: i32) -> ItemId {
43 + // Explicit is_public = false: the column defaults to true, but these tests
44 + // assert publish/unpublish transitions, so items start unpublished.
45 + sqlx::query_scalar::<_, ItemId>(
46 + "INSERT INTO items (project_id, title, item_type, price_cents, slug, sort_order, is_public)
47 + VALUES ($1, $2, 'digital', 1000, $3, $4, false) RETURNING id",
48 + )
49 + .bind(project)
50 + .bind(format!("Item {slug}"))
51 + .bind(slug)
52 + .bind(sort)
53 + .fetch_one(pool)
54 + .await
55 + .expect("seed item")
56 + }
57 +
58 + async fn seed_tag(pool: &sqlx::PgPool, slug: &str) -> TagId {
59 + sqlx::query_scalar::<_, TagId>(
60 + "INSERT INTO tags (name, slug, path) VALUES ($1, $2, $2) RETURNING id",
61 + )
62 + .bind(slug)
63 + .bind(slug)
64 + .fetch_one(pool)
65 + .await
66 + .expect("seed tag")
67 + }
68 +
69 + async fn is_public(pool: &sqlx::PgPool, item: ItemId) -> bool {
70 + sqlx::query_scalar::<_, bool>("SELECT is_public FROM items WHERE id = $1")
71 + .bind(item)
72 + .fetch_one(pool)
73 + .await
74 + .unwrap()
75 + }
76 +
77 + async fn sort_order(pool: &sqlx::PgPool, item: ItemId) -> i32 {
78 + sqlx::query_scalar::<_, i32>("SELECT sort_order FROM items WHERE id = $1")
79 + .bind(item)
80 + .fetch_one(pool)
81 + .await
82 + .unwrap()
83 + }
84 +
85 + async fn price_of(pool: &sqlx::PgPool, item: ItemId) -> i32 {
86 + sqlx::query_scalar::<_, i32>("SELECT price_cents FROM items WHERE id = $1")
87 + .bind(item)
88 + .fetch_one(pool)
89 + .await
90 + .unwrap()
91 + }
92 +
93 + // ── ownership scoping (IDOR seal) ────────────────────────────────────────────
94 +
95 + #[tokio::test]
96 + async fn bulk_publish_affects_only_owned_items_and_clears_schedule() {
97 + let db = TestDb::new().await;
98 + let owner = seed_user(&db.pool, "blk_pub_owner").await;
99 + let attacker = seed_user(&db.pool, "blk_pub_attacker").await;
100 + let project = seed_project(&db.pool, owner, "blk-pub").await;
101 + let a = seed_item(&db.pool, project, "pa", 0).await;
102 + let b = seed_item(&db.pool, project, "pb", 1).await;
103 + // Give one a scheduled publish_at to prove bulk_publish clears it.
104 + sqlx::query("UPDATE items SET publish_at = NOW() + INTERVAL '1 day' WHERE id = $1")
105 + .bind(a).execute(&db.pool).await.unwrap();
106 +
107 + // A non-owner cannot publish the items — zero rows, nothing changes.
108 + let hijack = items::bulk_publish(&db.pool, &[a, b], project, attacker).await.unwrap();
109 + assert_eq!(hijack, 0, "a non-owner bulk_publish must touch no rows");
110 + assert!(!is_public(&db.pool, a).await);
111 +
112 + // The owner publishes both and the schedule is cleared.
113 + let n = items::bulk_publish(&db.pool, &[a, b], project, owner).await.unwrap();
114 + assert_eq!(n, 2);
115 + assert!(is_public(&db.pool, a).await && is_public(&db.pool, b).await);
116 + let sched: Option<chrono::DateTime<chrono::Utc>> =
117 + sqlx::query_scalar("SELECT publish_at FROM items WHERE id = $1").bind(a).fetch_one(&db.pool).await.unwrap();
118 + assert!(sched.is_none(), "bulk_publish clears any scheduled publish_at");
119 + }
120 +
121 + #[tokio::test]
122 + async fn bulk_unpublish_is_owner_scoped() {
123 + let db = TestDb::new().await;
124 + let owner = seed_user(&db.pool, "blk_unpub_owner").await;
125 + let attacker = seed_user(&db.pool, "blk_unpub_attacker").await;
126 + let project = seed_project(&db.pool, owner, "blk-unpub").await;
127 + let item = seed_item(&db.pool, project, "ua", 0).await;
128 + items::bulk_publish(&db.pool, &[item], project, owner).await.unwrap();
129 +
130 + assert_eq!(items::bulk_unpublish(&db.pool, &[item], project, attacker).await.unwrap(), 0);
131 + assert!(is_public(&db.pool, item).await, "a non-owner cannot unpublish");
132 + assert_eq!(items::bulk_unpublish(&db.pool, &[item], project, owner).await.unwrap(), 1);
133 + assert!(!is_public(&db.pool, item).await);
134 + }
135 +
136 + #[tokio::test]
137 + async fn bulk_delete_soft_deletes_once_and_is_owner_scoped() {
138 + let db = TestDb::new().await;
139 + let owner = seed_user(&db.pool, "blk_del_owner").await;
140 + let attacker = seed_user(&db.pool, "blk_del_attacker").await;
141 + let project = seed_project(&db.pool, owner, "blk-del").await;
142 + let item = seed_item(&db.pool, project, "da", 0).await;
143 +
144 + assert_eq!(items::bulk_delete(&db.pool, &[item], project, attacker).await.unwrap(), 0,
145 + "a non-owner cannot delete");
146 +
147 + // First delete soft-deletes; the second is a no-op (deleted_at IS NULL guard).
148 + assert_eq!(items::bulk_delete(&db.pool, &[item], project, owner).await.unwrap(), 1);
149 + assert_eq!(items::bulk_delete(&db.pool, &[item], project, owner).await.unwrap(), 0,
150 + "re-deleting an already-deleted item is a no-op");
151 + let (deleted, public): (bool, bool) =
152 + sqlx::query_as("SELECT deleted_at IS NOT NULL, is_public FROM items WHERE id = $1")
153 + .bind(item).fetch_one(&db.pool).await.unwrap();
154 + assert!(deleted && !public, "soft-delete stamps deleted_at and unpublishes");
155 + }
156 +
157 + #[tokio::test]
158 + async fn bulk_update_price_is_owner_scoped() {
159 + let db = TestDb::new().await;
160 + let owner = seed_user(&db.pool, "blk_price_owner").await;
161 + let attacker = seed_user(&db.pool, "blk_price_attacker").await;
162 + let project = seed_project(&db.pool, owner, "blk-price").await;
163 + let item = seed_item(&db.pool, project, "pra", 0).await;
164 +
165 + let new_price = PriceCents::new(4200).expect("valid price");
166 + assert_eq!(items::bulk_update_price(&db.pool, &[item], project, attacker, new_price).await.unwrap(), 0);
167 + assert_eq!(price_of(&db.pool, item).await, 1000, "a non-owner cannot reprice");
168 +
169 + assert_eq!(items::bulk_update_price(&db.pool, &[item], project, owner, new_price).await.unwrap(), 1);
170 + assert_eq!(price_of(&db.pool, item).await, 4200);
171 + }
172 +
173 + #[tokio::test]
174 + async fn bulk_add_tag_dedups_and_is_owner_scoped() {
175 + let db = TestDb::new().await;
176 + let owner = seed_user(&db.pool, "blk_tag_owner").await;
177 + let attacker = seed_user(&db.pool, "blk_tag_attacker").await;
178 + let project = seed_project(&db.pool, owner, "blk-tag").await;
179 + let a = seed_item(&db.pool, project, "ta", 0).await;
180 + let b = seed_item(&db.pool, project, "tb", 1).await;
181 + let tag = seed_tag(&db.pool, "genre").await;
182 +
183 + assert_eq!(items::bulk_add_tag(&db.pool, &[a, b], project, attacker, tag).await.unwrap(), 0,
184 + "a non-owner cannot tag");
185 +
186 + // First tag both; a repeat is a no-op via ON CONFLICT (item_id, tag_id).
187 + assert_eq!(items::bulk_add_tag(&db.pool, &[a, b], project, owner, tag).await.unwrap(), 2);
188 + assert_eq!(items::bulk_add_tag(&db.pool, &[a, b], project, owner, tag).await.unwrap(), 0,
189 + "re-tagging is idempotent");
190 + }
191 +
192 + // ── move_item reorder ────────────────────────────────────────────────────────
193 +
194 + #[tokio::test]
195 + async fn move_item_swaps_sort_order_within_the_project() {
196 + let db = TestDb::new().await;
197 + let owner = seed_user(&db.pool, "blk_move_owner").await;
198 + let project = seed_project(&db.pool, owner, "blk-move").await;
199 + let first = seed_item(&db.pool, project, "m0", 0).await;
200 + let middle = seed_item(&db.pool, project, "m1", 1).await;
201 + let last = seed_item(&db.pool, project, "m2", 2).await;
202 +
203 + // Move the middle item up: it swaps with the first.
204 + items::move_item(&db.pool, project, owner, middle, "up").await.unwrap();
205 + assert_eq!(sort_order(&db.pool, middle).await, 0);
206 + assert_eq!(sort_order(&db.pool, first).await, 1);
207 + assert_eq!(sort_order(&db.pool, last).await, 2, "the untouched item keeps its slot");
208 +
209 + // Moving the top item up again is a no-op (already first).
210 + items::move_item(&db.pool, project, owner, middle, "up").await.unwrap();
211 + assert_eq!(sort_order(&db.pool, middle).await, 0);
212 + }
213 +
214 + // ── duplicate_item — metadata copy + slug-collision retry ────────────────────
215 +
216 + #[tokio::test]
217 + async fn duplicate_item_makes_a_draft_copy_with_metadata() {
218 + let db = TestDb::new().await;
219 + let owner = seed_user(&db.pool, "blk_dup_owner").await;
220 + let attacker = seed_user(&db.pool, "blk_dup_attacker").await;
221 + let project = seed_project(&db.pool, owner, "blk-dup").await;
222 + let source = seed_item(&db.pool, project, "orig", 0).await;
223 + let tag = seed_tag(&db.pool, "dup-tag").await;
224 + sqlx::query("INSERT INTO item_tags (item_id, tag_id) VALUES ($1, $2)").bind(source).bind(tag).execute(&db.pool).await.unwrap();
225 + sqlx::query("UPDATE items SET is_public = true WHERE id = $1").bind(source).execute(&db.pool).await.unwrap();
226 +
227 + // A non-owner cannot duplicate (ownership verified via project subquery).
228 + assert!(items::duplicate_item(&db.pool, source, attacker).await.is_err());
229 +
230 + let copy = items::duplicate_item(&db.pool, source, owner).await.unwrap();
231 + assert!(copy.title.starts_with("Copy of "), "the copy is titled 'Copy of ...'");
232 + assert!(!copy.is_public, "the copy is a private draft regardless of the source");
233 + let tag_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM item_tags WHERE item_id = $1")
234 + .bind(copy.id).fetch_one(&db.pool).await.unwrap();
235 + assert_eq!(tag_count, 1, "tags are carried onto the copy");
236 + }
237 +
238 + /// The Phase 3 slug-TOCTOU fix: two duplicates of the same source race for the
239 + /// base slug. The savepoint retry makes the unique index the arbiter, so both
240 + /// succeed with distinct slugs rather than one 500-ing on a raw 23505.
241 + #[tokio::test]
242 + async fn concurrent_duplicates_get_distinct_slugs() {
243 + let db = TestDb::new().await;
244 + let owner = seed_user(&db.pool, "blk_dupr_owner").await;
245 + let project = seed_project(&db.pool, owner, "blk-dupr").await;
246 + let source = seed_item(&db.pool, project, "racey", 0).await;
247 +
248 + let p1 = db.pool.clone();
249 + let p2 = db.pool.clone();
250 + let (a, b) = tokio::join!(
251 + tokio::spawn(async move { items::duplicate_item(&p1, source, owner).await }),
252 + tokio::spawn(async move { items::duplicate_item(&p2, source, owner).await }),
253 + );
254 + let a = a.unwrap().expect("first duplicate succeeds");
255 + let b = b.unwrap().expect("second duplicate succeeds under contention");
256 +
257 + let slug_a: String = sqlx::query_scalar("SELECT slug FROM items WHERE id = $1").bind(a.id).fetch_one(&db.pool).await.unwrap();
258 + let slug_b: String = sqlx::query_scalar("SELECT slug FROM items WHERE id = $1").bind(b.id).fetch_one(&db.pool).await.unwrap();
259 + assert_ne!(slug_a, slug_b, "concurrent duplicates must land on distinct slugs");
260 + assert_ne!(a.id, b.id);
261 + }
@@ -98,8 +98,10 @@ mod cart;
98 98 mod bundles;
99 99 mod idempotency;
100 100 mod db_payments_layer;
101 + mod db_collections_layer;
101 102 mod db_creator_tiers_layer;
102 103 mod db_issues_layer;
104 + mod db_items_bulk_layer;
103 105 mod db_passkeys_layer;
104 106 mod db_ssh_keys_layer;
105 107 mod db_synckit_rotation;