mirror of
https://github.com/Xevion/banner.git
synced 2026-01-31 06:23:37 -06:00
Replace is_creating mutex with atomic flag and RAII guard to ensure proper cleanup when acquire() futures are cancelled mid-creation, preventing permanent deadlock for subsequent callers.
327 lines
8.8 KiB
Rust
327 lines
8.8 KiB
Rust
#[allow(dead_code)]
|
|
mod helpers;
|
|
|
|
use banner::data::batch::batch_upsert_courses;
|
|
use sqlx::PgPool;
|
|
|
|
#[sqlx::test]
|
|
async fn test_batch_upsert_empty_slice(pool: PgPool) {
|
|
batch_upsert_courses(&[], &pool).await.unwrap();
|
|
|
|
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM courses")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(count.0, 0);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
async fn test_batch_upsert_inserts_new_courses(pool: PgPool) {
|
|
let courses = vec![
|
|
helpers::make_course("10001", "202510", "CS", "1083", "Intro to CS", 25, 30, 0, 5),
|
|
helpers::make_course(
|
|
"10002",
|
|
"202510",
|
|
"MAT",
|
|
"1214",
|
|
"Calculus I",
|
|
40,
|
|
45,
|
|
3,
|
|
10,
|
|
),
|
|
];
|
|
|
|
batch_upsert_courses(&courses, &pool).await.unwrap();
|
|
|
|
let rows: Vec<(String, String, String, String, i32, i32, i32, i32)> = sqlx::query_as(
|
|
"SELECT crn, subject, course_number, title, enrollment, max_enrollment, wait_count, wait_capacity
|
|
FROM courses ORDER BY crn",
|
|
)
|
|
.fetch_all(&pool)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(rows.len(), 2);
|
|
|
|
let (crn, subject, course_number, title, enrollment, max_enrollment, wait_count, wait_capacity) =
|
|
&rows[0];
|
|
assert_eq!(crn, "10001");
|
|
assert_eq!(subject, "CS");
|
|
assert_eq!(course_number, "1083");
|
|
assert_eq!(title, "Intro to CS");
|
|
assert_eq!(*enrollment, 25);
|
|
assert_eq!(*max_enrollment, 30);
|
|
assert_eq!(*wait_count, 0);
|
|
assert_eq!(*wait_capacity, 5);
|
|
|
|
let (crn, subject, ..) = &rows[1];
|
|
assert_eq!(crn, "10002");
|
|
assert_eq!(subject, "MAT");
|
|
}
|
|
|
|
#[sqlx::test]
|
|
async fn test_batch_upsert_updates_existing(pool: PgPool) {
|
|
let initial = vec![helpers::make_course(
|
|
"20001",
|
|
"202510",
|
|
"CS",
|
|
"3443",
|
|
"App Programming",
|
|
10,
|
|
35,
|
|
0,
|
|
5,
|
|
)];
|
|
batch_upsert_courses(&initial, &pool).await.unwrap();
|
|
|
|
// Upsert the same CRN+term with updated enrollment
|
|
let updated = vec![helpers::make_course(
|
|
"20001",
|
|
"202510",
|
|
"CS",
|
|
"3443",
|
|
"App Programming",
|
|
30,
|
|
35,
|
|
2,
|
|
5,
|
|
)];
|
|
batch_upsert_courses(&updated, &pool).await.unwrap();
|
|
|
|
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM courses")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(count.0, 1, "upsert should not create a duplicate row");
|
|
|
|
let (enrollment, wait_count): (i32, i32) =
|
|
sqlx::query_as("SELECT enrollment, wait_count FROM courses WHERE crn = '20001'")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(enrollment, 30);
|
|
assert_eq!(wait_count, 2);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
async fn test_batch_upsert_mixed_insert_and_update(pool: PgPool) {
|
|
let initial = vec![
|
|
helpers::make_course("30001", "202510", "CS", "1083", "Intro to CS", 10, 30, 0, 5),
|
|
helpers::make_course(
|
|
"30002",
|
|
"202510",
|
|
"CS",
|
|
"2073",
|
|
"Computer Architecture",
|
|
20,
|
|
30,
|
|
0,
|
|
5,
|
|
),
|
|
];
|
|
batch_upsert_courses(&initial, &pool).await.unwrap();
|
|
|
|
// Update both existing courses and add a new one
|
|
let mixed = vec![
|
|
helpers::make_course("30001", "202510", "CS", "1083", "Intro to CS", 15, 30, 1, 5),
|
|
helpers::make_course(
|
|
"30002",
|
|
"202510",
|
|
"CS",
|
|
"2073",
|
|
"Computer Architecture",
|
|
25,
|
|
30,
|
|
0,
|
|
5,
|
|
),
|
|
helpers::make_course(
|
|
"30003",
|
|
"202510",
|
|
"MAT",
|
|
"1214",
|
|
"Calculus I",
|
|
40,
|
|
45,
|
|
3,
|
|
10,
|
|
),
|
|
];
|
|
batch_upsert_courses(&mixed, &pool).await.unwrap();
|
|
|
|
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM courses")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(count.0, 3, "should have 2 updated + 1 new = 3 total rows");
|
|
|
|
// Verify updated values
|
|
let (enrollment,): (i32,) =
|
|
sqlx::query_as("SELECT enrollment FROM courses WHERE crn = '30001'")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(enrollment, 15);
|
|
|
|
let (enrollment,): (i32,) =
|
|
sqlx::query_as("SELECT enrollment FROM courses WHERE crn = '30002'")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(enrollment, 25);
|
|
|
|
// Verify new row
|
|
let (subject,): (String,) = sqlx::query_as("SELECT subject FROM courses WHERE crn = '30003'")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(subject, "MAT");
|
|
}
|
|
|
|
#[sqlx::test]
|
|
async fn test_batch_upsert_unique_constraint_crn_term(pool: PgPool) {
|
|
// Same CRN, different term codes → should produce two separate rows
|
|
let courses = vec![
|
|
helpers::make_course("40001", "202510", "CS", "1083", "Intro to CS", 25, 30, 0, 5),
|
|
helpers::make_course("40001", "202520", "CS", "1083", "Intro to CS", 10, 30, 0, 5),
|
|
];
|
|
|
|
batch_upsert_courses(&courses, &pool).await.unwrap();
|
|
|
|
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM courses WHERE crn = '40001'")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
count.0, 2,
|
|
"same CRN with different term codes should be separate rows"
|
|
);
|
|
|
|
let rows: Vec<(String, i32)> = sqlx::query_as(
|
|
"SELECT term_code, enrollment FROM courses WHERE crn = '40001' ORDER BY term_code",
|
|
)
|
|
.fetch_all(&pool)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(rows[0].0, "202510");
|
|
assert_eq!(rows[0].1, 25);
|
|
assert_eq!(rows[1].0, "202520");
|
|
assert_eq!(rows[1].1, 10);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
async fn test_batch_upsert_creates_audit_and_metric_entries(pool: PgPool) {
|
|
// Insert initial data — should NOT create audits/metrics (it's a fresh insert)
|
|
let initial = vec![helpers::make_course(
|
|
"50001",
|
|
"202510",
|
|
"CS",
|
|
"3443",
|
|
"App Programming",
|
|
10,
|
|
35,
|
|
0,
|
|
5,
|
|
)];
|
|
batch_upsert_courses(&initial, &pool).await.unwrap();
|
|
|
|
let (audit_count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM course_audits")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
audit_count, 0,
|
|
"initial insert should not create audit entries"
|
|
);
|
|
|
|
let (metric_count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM course_metrics")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
metric_count, 0,
|
|
"initial insert should not create metric entries"
|
|
);
|
|
|
|
// Update enrollment and wait_count
|
|
let updated = vec![helpers::make_course(
|
|
"50001",
|
|
"202510",
|
|
"CS",
|
|
"3443",
|
|
"App Programming",
|
|
20,
|
|
35,
|
|
2,
|
|
5,
|
|
)];
|
|
batch_upsert_courses(&updated, &pool).await.unwrap();
|
|
|
|
// Should have audit entries for enrollment and wait_count changes
|
|
let (audit_count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM course_audits")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert!(
|
|
audit_count >= 2,
|
|
"should have audit entries for enrollment and wait_count changes, got {audit_count}"
|
|
);
|
|
|
|
// Should have exactly 1 metric entry
|
|
let (metric_count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM course_metrics")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(metric_count, 1, "should have 1 metric snapshot");
|
|
|
|
// Verify metric values
|
|
let (enrollment, wait_count, seats): (i32, i32, i32) = sqlx::query_as(
|
|
"SELECT enrollment, wait_count, seats_available FROM course_metrics LIMIT 1",
|
|
)
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(enrollment, 20);
|
|
assert_eq!(wait_count, 2);
|
|
assert_eq!(seats, 15); // 35 - 20
|
|
}
|
|
|
|
#[sqlx::test]
|
|
async fn test_batch_upsert_no_change_no_audit(pool: PgPool) {
|
|
// Insert then re-insert identical data — should produce zero audits/metrics
|
|
let course = vec![helpers::make_course(
|
|
"60001",
|
|
"202510",
|
|
"CS",
|
|
"1083",
|
|
"Intro to CS",
|
|
25,
|
|
30,
|
|
0,
|
|
5,
|
|
)];
|
|
batch_upsert_courses(&course, &pool).await.unwrap();
|
|
batch_upsert_courses(&course, &pool).await.unwrap();
|
|
|
|
let (audit_count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM course_audits")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
audit_count, 0,
|
|
"identical re-upsert should not create audit entries"
|
|
);
|
|
|
|
let (metric_count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM course_metrics")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
metric_count, 0,
|
|
"identical re-upsert should not create metric entries"
|
|
);
|
|
}
|