Skip to main content

max / makenotwork

4.3 KB · 128 lines History Blame Raw
1 //! DB-layer contract tests for `db::issues`, repo-scoped issue numbering.
2 //!
3 //! Audit Run 16 flagged `issues` as a Concurrency/Testing cold spot (B+). The
4 //! interesting invariant is the sequential per-repo `number` assignment:
5 //! `create_issue` computes `MAX(number)+1` and retries once on the
6 //! `(repo_id, number)` unique violation that a concurrent insert can cause.
7 //! These pin monotonic numbering, per-repo independence, and that two
8 //! simultaneous creates resolve to two distinct numbers (the retry path).
9 //!
10 //! NOTE: the retry is single-shot, so it reliably resolves up to two colliding
11 //! writers; a burst of many simultaneous creates on one repo can still exhaust
12 //! it. The concurrency test stays within that guaranteed envelope.
13
14 use crate::harness::db::TestDb;
15 use crate::harness::seed_user;
16 use makenotwork::db::{GitRepoId, UserId, issues};
17
18 async fn seed_repo(pool: &sqlx::PgPool, user: UserId, name: &str) -> GitRepoId {
19 sqlx::query_scalar::<_, GitRepoId>(
20 "INSERT INTO git_repos (user_id, name) VALUES ($1, $2) RETURNING id",
21 )
22 .bind(user)
23 .bind(name)
24 .fetch_one(pool)
25 .await
26 .expect("seed repo")
27 }
28
29 #[tokio::test]
30 async fn issue_numbers_are_sequential_within_a_repo() {
31 let db = TestDb::new().await;
32 let user = seed_user(&db.pool, "iss_seq").await;
33 let repo = seed_repo(&db.pool, user, "repo").await;
34
35 let mut numbers = Vec::new();
36 for i in 0..4 {
37 let issue = issues::create_issue(&db.pool, repo, user, &format!("Issue {i}"), "md", "html")
38 .await
39 .unwrap();
40 numbers.push(issue.number);
41 }
42 assert_eq!(
43 numbers,
44 [1, 2, 3, 4],
45 "numbers increment monotonically from 1"
46 );
47 }
48
49 #[tokio::test]
50 async fn issue_numbering_is_independent_per_repo() {
51 let db = TestDb::new().await;
52 let user = seed_user(&db.pool, "iss_scope").await;
53 let repo_a = seed_repo(&db.pool, user, "repo-a").await;
54 let repo_b = seed_repo(&db.pool, user, "repo-b").await;
55
56 let a1 = issues::create_issue(&db.pool, repo_a, user, "A1", "m", "h")
57 .await
58 .unwrap();
59 let b1 = issues::create_issue(&db.pool, repo_b, user, "B1", "m", "h")
60 .await
61 .unwrap();
62 let a2 = issues::create_issue(&db.pool, repo_a, user, "A2", "m", "h")
63 .await
64 .unwrap();
65
66 // Each repo has its own sequence starting at 1.
67 assert_eq!(a1.number, 1);
68 assert_eq!(
69 b1.number, 1,
70 "a second repo's numbering is not affected by the first"
71 );
72 assert_eq!(a2.number, 2);
73 }
74
75 #[tokio::test]
76 async fn get_issue_by_number_resolves_within_the_repo() {
77 let db = TestDb::new().await;
78 let user = seed_user(&db.pool, "iss_get").await;
79 let repo = seed_repo(&db.pool, user, "repo").await;
80 let created = issues::create_issue(&db.pool, repo, user, "Findable", "m", "h")
81 .await
82 .unwrap();
83
84 let fetched = issues::get_issue_by_number(&db.pool, repo, created.number)
85 .await
86 .unwrap()
87 .expect("the issue resolves by its repo-scoped number");
88 assert_eq!(fetched.id, created.id);
89 assert_eq!(fetched.title, "Findable");
90
91 // A number that doesn't exist in this repo resolves to None.
92 assert!(
93 issues::get_issue_by_number(&db.pool, repo, 999)
94 .await
95 .unwrap()
96 .is_none()
97 );
98 }
99
100 /// Two simultaneous creates on the same repo must not collide on a number: the
101 /// `MAX(number)+1` race is resolved by the single-shot retry, so both succeed
102 /// with distinct sequential numbers {1, 2}.
103 #[tokio::test]
104 async fn concurrent_creates_get_distinct_numbers() {
105 let db = TestDb::new().await;
106 let user = seed_user(&db.pool, "iss_race").await;
107 let repo = seed_repo(&db.pool, user, "repo").await;
108
109 let p1 = db.pool.clone();
110 let p2 = db.pool.clone();
111 let (a, b) = tokio::join!(
112 tokio::spawn(async move { issues::create_issue(&p1, repo, user, "A", "m", "h").await }),
113 tokio::spawn(async move { issues::create_issue(&p2, repo, user, "B", "m", "h").await }),
114 );
115 let a = a.unwrap().expect("first create must succeed");
116 let b = b
117 .unwrap()
118 .expect("second create must succeed under contention");
119
120 let mut nums = [a.number, b.number];
121 nums.sort_unstable();
122 assert_eq!(
123 nums,
124 [1, 2],
125 "concurrent creates take two distinct sequential numbers"
126 );
127 }
128