//! DB-layer contract tests for `db::issues`, repo-scoped issue numbering. //! //! Audit Run 16 flagged `issues` as a Concurrency/Testing cold spot (B+). The //! interesting invariant is the sequential per-repo `number` assignment: //! `create_issue` computes `MAX(number)+1` and retries once on the //! `(repo_id, number)` unique violation that a concurrent insert can cause. //! These pin monotonic numbering, per-repo independence, and that two //! simultaneous creates resolve to two distinct numbers (the retry path). //! //! NOTE: the retry is single-shot, so it reliably resolves up to two colliding //! writers; a burst of many simultaneous creates on one repo can still exhaust //! it. The concurrency test stays within that guaranteed envelope. use crate::harness::db::TestDb; use makenotwork::db::{GitRepoId, UserId, issues}; async fn seed_user(pool: &sqlx::PgPool, username: &str) -> UserId { let hash = makenotwork::auth::hash_password("password123").expect("hash"); sqlx::query_scalar::<_, UserId>( "INSERT INTO users (username, email, password_hash, email_verified) VALUES ($1, $2, $3, true) RETURNING id", ) .bind(username) .bind(format!("{username}@test.com")) .bind(&hash) .fetch_one(pool) .await .expect("seed user") } async fn seed_repo(pool: &sqlx::PgPool, user: UserId, name: &str) -> GitRepoId { sqlx::query_scalar::<_, GitRepoId>( "INSERT INTO git_repos (user_id, name) VALUES ($1, $2) RETURNING id", ) .bind(user) .bind(name) .fetch_one(pool) .await .expect("seed repo") } #[tokio::test] async fn issue_numbers_are_sequential_within_a_repo() { let db = TestDb::new().await; let user = seed_user(&db.pool, "iss_seq").await; let repo = seed_repo(&db.pool, user, "repo").await; let mut numbers = Vec::new(); for i in 0..4 { let issue = issues::create_issue(&db.pool, repo, user, &format!("Issue {i}"), "md", "html") .await .unwrap(); numbers.push(issue.number); } assert_eq!( numbers, [1, 2, 3, 4], "numbers increment monotonically from 1" ); } #[tokio::test] async fn issue_numbering_is_independent_per_repo() { let db = TestDb::new().await; let user = seed_user(&db.pool, "iss_scope").await; let repo_a = seed_repo(&db.pool, user, "repo-a").await; let repo_b = seed_repo(&db.pool, user, "repo-b").await; let a1 = issues::create_issue(&db.pool, repo_a, user, "A1", "m", "h") .await .unwrap(); let b1 = issues::create_issue(&db.pool, repo_b, user, "B1", "m", "h") .await .unwrap(); let a2 = issues::create_issue(&db.pool, repo_a, user, "A2", "m", "h") .await .unwrap(); // Each repo has its own sequence starting at 1. assert_eq!(a1.number, 1); assert_eq!( b1.number, 1, "a second repo's numbering is not affected by the first" ); assert_eq!(a2.number, 2); } #[tokio::test] async fn get_issue_by_number_resolves_within_the_repo() { let db = TestDb::new().await; let user = seed_user(&db.pool, "iss_get").await; let repo = seed_repo(&db.pool, user, "repo").await; let created = issues::create_issue(&db.pool, repo, user, "Findable", "m", "h") .await .unwrap(); let fetched = issues::get_issue_by_number(&db.pool, repo, created.number) .await .unwrap() .expect("the issue resolves by its repo-scoped number"); assert_eq!(fetched.id, created.id); assert_eq!(fetched.title, "Findable"); // A number that doesn't exist in this repo resolves to None. assert!( issues::get_issue_by_number(&db.pool, repo, 999) .await .unwrap() .is_none() ); } /// Two simultaneous creates on the same repo must not collide on a number: the /// `MAX(number)+1` race is resolved by the single-shot retry, so both succeed /// with distinct sequential numbers {1, 2}. #[tokio::test] async fn concurrent_creates_get_distinct_numbers() { let db = TestDb::new().await; let user = seed_user(&db.pool, "iss_race").await; let repo = seed_repo(&db.pool, user, "repo").await; let p1 = db.pool.clone(); let p2 = db.pool.clone(); let (a, b) = tokio::join!( tokio::spawn(async move { issues::create_issue(&p1, repo, user, "A", "m", "h").await }), tokio::spawn(async move { issues::create_issue(&p2, repo, user, "B", "m", "h").await }), ); let a = a.unwrap().expect("first create must succeed"); let b = b .unwrap() .expect("second create must succeed under contention"); let mut nums = [a.number, b.number]; nums.sort_unstable(); assert_eq!( nums, [1, 2], "concurrent creates take two distinct sequential numbers" ); }