//! Integration tests for pickleball-elo v3.0 #[cfg(test)] mod integration_tests { use sqlx::sqlite::SqlitePoolOptions; async fn setup_test_db() -> sqlx::SqlitePool { let pool = SqlitePoolOptions::new() .max_connections(2) .connect("sqlite::memory:") .await .expect("Failed to create test database"); // Initialize schema let statements = vec![ "CREATE TABLE players ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, email TEXT, rating REAL NOT NULL DEFAULT 1500.0, created_at TEXT NOT NULL DEFAULT (datetime('now')), last_played TEXT NOT NULL DEFAULT (datetime('now')) )", "CREATE TABLE sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, start_time TEXT NOT NULL DEFAULT (datetime('now')), end_time TEXT, summary_sent BOOLEAN NOT NULL DEFAULT 0, notes TEXT )", "CREATE TABLE matches ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id INTEGER NOT NULL, match_type TEXT NOT NULL CHECK(match_type IN ('singles', 'doubles')), timestamp TEXT NOT NULL DEFAULT (datetime('now')), team1_score INTEGER NOT NULL, team2_score INTEGER NOT NULL, FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE )", "CREATE TABLE match_participants ( id INTEGER PRIMARY KEY AUTOINCREMENT, match_id INTEGER NOT NULL, player_id INTEGER NOT NULL, team INTEGER NOT NULL CHECK(team IN (1, 2)), rating_before REAL NOT NULL, rating_after REAL NOT NULL, rating_change REAL NOT NULL, FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE, FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE )", "CREATE INDEX idx_players_rating ON players(rating DESC)", ]; for stmt in statements { sqlx::query(stmt) .execute(&pool) .await .expect("Failed to create schema"); } pool } #[tokio::test] async fn test_create_players_record_match() { let pool = setup_test_db().await; // Create two players sqlx::query("INSERT INTO players (name, rating) VALUES (?, ?)") .bind("Alice") .bind(1500.0) .execute(&pool) .await .unwrap(); sqlx::query("INSERT INTO players (name, rating) VALUES (?, ?)") .bind("Bob") .bind(1500.0) .execute(&pool) .await .unwrap(); // Create session sqlx::query("INSERT INTO sessions DEFAULT VALUES") .execute(&pool) .await .unwrap(); // Record match sqlx::query( "INSERT INTO matches (session_id, match_type, team1_score, team2_score) VALUES (?, ?, ?, ?)" ) .bind(1i64) .bind("singles") .bind(11) .bind(9) .execute(&pool) .await .unwrap(); // Record rating changes sqlx::query( "INSERT INTO match_participants (match_id, player_id, team, rating_before, rating_after, rating_change) \ VALUES (?, ?, ?, ?, ?, ?)" ) .bind(1i64) // match_id .bind(1i64) // alice .bind(1) .bind(1500.0) .bind(1516.0) .bind(16.0) .execute(&pool) .await .unwrap(); sqlx::query( "INSERT INTO match_participants (match_id, player_id, team, rating_before, rating_after, rating_change) \ VALUES (?, ?, ?, ?, ?, ?)" ) .bind(1i64) // match_id .bind(2i64) // bob .bind(2) .bind(1500.0) .bind(1484.0) .bind(-16.0) .execute(&pool) .await .unwrap(); // Verify match was recorded let match_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM matches") .fetch_one(&pool) .await .unwrap(); assert_eq!(match_count, 1); // Verify rating changes let alice_change: f64 = sqlx::query_scalar( "SELECT rating_change FROM match_participants WHERE player_id = 1" ) .fetch_one(&pool) .await .unwrap(); let bob_change: f64 = sqlx::query_scalar( "SELECT rating_change FROM match_participants WHERE player_id = 2" ) .fetch_one(&pool) .await .unwrap(); assert_eq!(alice_change, 16.0); assert_eq!(bob_change, -16.0); } #[tokio::test] async fn test_multiple_matches_leaderboard() { let pool = setup_test_db().await; // Create 3 players for (name, rating) in &[("Alice", 1600.0), ("Bob", 1500.0), ("Charlie", 1400.0)] { sqlx::query("INSERT INTO players (name, rating) VALUES (?, ?)") .bind(name) .bind(rating) .execute(&pool) .await .unwrap(); } // Create session sqlx::query("INSERT INTO sessions DEFAULT VALUES") .execute(&pool) .await .unwrap(); // Record 3 matches for match_num in 0..3 { sqlx::query( "INSERT INTO matches (session_id, match_type, team1_score, team2_score) VALUES (?, ?, ?, ?)" ) .bind(1i64) .bind("singles") .bind(11) .bind(5 + match_num) .execute(&pool) .await .unwrap(); } // Verify all matches recorded let match_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM matches") .fetch_one(&pool) .await .unwrap(); assert_eq!(match_count, 3); // Verify leaderboard can be retrieved let leaderboard: Vec<(String, f64)> = sqlx::query_as( "SELECT name, rating FROM players ORDER BY rating DESC" ) .fetch_all(&pool) .await .unwrap(); assert_eq!(leaderboard.len(), 3); assert_eq!(leaderboard[0].0, "Alice"); assert_eq!(leaderboard[1].0, "Bob"); assert_eq!(leaderboard[2].0, "Charlie"); } #[tokio::test] async fn test_session_with_many_matches() { let pool = setup_test_db().await; // Create 2 players sqlx::query("INSERT INTO players (name, rating) VALUES (?, ?)") .bind("Player1") .bind(1500.0) .execute(&pool) .await .unwrap(); sqlx::query("INSERT INTO players (name, rating) VALUES (?, ?)") .bind("Player2") .bind(1500.0) .execute(&pool) .await .unwrap(); // Create session sqlx::query("INSERT INTO sessions DEFAULT VALUES") .execute(&pool) .await .unwrap(); // Record 10 matches in a session for i in 0..10 { sqlx::query( "INSERT INTO matches (session_id, match_type, team1_score, team2_score) VALUES (?, ?, ?, ?)" ) .bind(1i64) .bind("singles") .bind(11) .bind(5 + (i % 7)) .execute(&pool) .await .unwrap(); } // Verify all matches in session let session_matches: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM matches WHERE session_id = 1" ) .fetch_one(&pool) .await .unwrap(); assert_eq!(session_matches, 10); } #[tokio::test] async fn test_rating_history() { let pool = setup_test_db().await; // Create player sqlx::query("INSERT INTO players (name, rating) VALUES (?, ?)") .bind("Tracker") .bind(1500.0) .execute(&pool) .await .unwrap(); // Create session sqlx::query("INSERT INTO sessions DEFAULT VALUES") .execute(&pool) .await .unwrap(); // Record 5 matches with escalating ratings let mut rating = 1500.0; for i in 1..=5 { sqlx::query( "INSERT INTO matches (session_id, match_type, team1_score, team2_score) VALUES (?, ?, ?, ?)" ) .bind(1i64) .bind("singles") .bind(11) .bind(5) .execute(&pool) .await .unwrap(); let new_rating = rating + 16.0; sqlx::query( "INSERT INTO match_participants (match_id, player_id, team, rating_before, rating_after, rating_change) \ VALUES (?, ?, ?, ?, ?, ?)" ) .bind(i as i64) .bind(1i64) .bind(1) .bind(rating) .bind(new_rating) .bind(16.0) .execute(&pool) .await .unwrap(); rating = new_rating; } // Retrieve final rating let final_rating: f64 = sqlx::query_scalar( "SELECT MAX(rating_after) FROM match_participants WHERE player_id = 1" ) .fetch_one(&pool) .await .unwrap(); assert_eq!(final_rating, 1580.0, "Final rating should be 1580 after 5 wins"); } #[tokio::test] async fn test_match_deletion_cascade() { let pool = setup_test_db().await; // Setup: player, session, match, participant sqlx::query("INSERT INTO players (name, rating) VALUES (?, ?)") .bind("Test") .bind(1500.0) .execute(&pool) .await .unwrap(); sqlx::query("INSERT INTO sessions DEFAULT VALUES") .execute(&pool) .await .unwrap(); sqlx::query( "INSERT INTO matches (session_id, match_type, team1_score, team2_score) VALUES (?, ?, ?, ?)" ) .bind(1i64) .bind("singles") .bind(11) .bind(5) .execute(&pool) .await .unwrap(); sqlx::query( "INSERT INTO match_participants (match_id, player_id, team, rating_before, rating_after, rating_change) \ VALUES (?, ?, ?, ?, ?, ?)" ) .bind(1i64) .bind(1i64) .bind(1) .bind(1500.0) .bind(1520.0) .bind(20.0) .execute(&pool) .await .unwrap(); // Delete the match sqlx::query("DELETE FROM matches WHERE id = ?") .bind(1i64) .execute(&pool) .await .unwrap(); // Verify participants were deleted let participant_count: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM match_participants WHERE match_id = 1" ) .fetch_one(&pool) .await .unwrap(); assert_eq!(participant_count, 0, "Participants should cascade delete"); } #[tokio::test] async fn test_doubles_match_recording() { let pool = setup_test_db().await; // Create 4 players for i in 1..=4 { sqlx::query("INSERT INTO players (name, rating) VALUES (?, ?)") .bind(format!("Player{}", i)) .bind(1500.0) .execute(&pool) .await .unwrap(); } // Create session sqlx::query("INSERT INTO sessions DEFAULT VALUES") .execute(&pool) .await .unwrap(); // Record doubles match sqlx::query( "INSERT INTO matches (session_id, match_type, team1_score, team2_score) VALUES (?, ?, ?, ?)" ) .bind(1i64) .bind("doubles") .bind(11) .bind(9) .execute(&pool) .await .unwrap(); // Record all 4 participants for player_id in 1..=4 { let team = if player_id <= 2 { 1 } else { 2 }; sqlx::query( "INSERT INTO match_participants (match_id, player_id, team, rating_before, rating_after, rating_change) \ VALUES (?, ?, ?, ?, ?, ?)" ) .bind(1i64) .bind(player_id as i64) .bind(team) .bind(1500.0) .bind(1516.0) .bind(16.0) .execute(&pool) .await .unwrap(); } // Verify all participants recorded let participant_count: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM match_participants WHERE match_id = 1" ) .fetch_one(&pool) .await .unwrap(); assert_eq!(participant_count, 4, "Doubles match should have 4 participants"); } }