Skip to main content

max / makenotwork

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