//! Database integration tests //! //! Tests database operations: player CRUD, match recording, rating updates use sqlx::SqlitePool; use sqlx::sqlite::SqlitePoolOptions; /// Create an in-memory SQLite database for testing async fn setup_test_db() -> SqlitePool { let pool = SqlitePoolOptions::new() .max_connections(1) .connect("sqlite::memory:") .await .expect("Failed to create test database"); // Create tables sqlx::query(r#" CREATE TABLE IF NOT EXISTS players ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, email TEXT DEFAULT '', singles_rating REAL DEFAULT 1500.0, singles_rd REAL DEFAULT 350.0, doubles_rating REAL DEFAULT 1500.0, doubles_rd REAL DEFAULT 350.0, created_at TEXT DEFAULT (datetime('now')) ) "#) .execute(&pool) .await .expect("Failed to create players table"); sqlx::query(r#" CREATE TABLE IF NOT EXISTS matches ( id INTEGER PRIMARY KEY AUTOINCREMENT, match_type TEXT NOT NULL, team1_score INTEGER NOT NULL, team2_score INTEGER NOT NULL, timestamp TEXT DEFAULT (datetime('now')), session_id INTEGER ) "#) .execute(&pool) .await .expect("Failed to create matches table"); sqlx::query(r#" CREATE TABLE IF NOT EXISTS match_participants ( id INTEGER PRIMARY KEY AUTOINCREMENT, match_id INTEGER NOT NULL, player_id INTEGER NOT NULL, team INTEGER NOT NULL, rating_before REAL, rating_after REAL, rating_change REAL, FOREIGN KEY (match_id) REFERENCES matches(id), FOREIGN KEY (player_id) REFERENCES players(id) ) "#) .execute(&pool) .await .expect("Failed to create match_participants table"); pool } #[tokio::test] async fn test_create_player() { let pool = setup_test_db().await; let result = sqlx::query( "INSERT INTO players (name, email) VALUES (?, ?)" ) .bind("Test Player") .bind("test@example.com") .execute(&pool) .await; assert!(result.is_ok()); let player: (i64, String, String, f64) = sqlx::query_as( "SELECT id, name, email, singles_rating FROM players WHERE name = ?" ) .bind("Test Player") .fetch_one(&pool) .await .expect("Failed to fetch player"); assert_eq!(player.1, "Test Player"); assert_eq!(player.2, "test@example.com"); assert!((player.3 - 1500.0).abs() < 0.01); // Default rating } #[tokio::test] async fn test_update_player_rating() { let pool = setup_test_db().await; // Create player sqlx::query("INSERT INTO players (name) VALUES (?)") .bind("Rating Test") .execute(&pool) .await .unwrap(); // Update rating sqlx::query("UPDATE players SET singles_rating = ? WHERE name = ?") .bind(1550.0) .bind("Rating Test") .execute(&pool) .await .unwrap(); let new_rating: (f64,) = sqlx::query_as( "SELECT singles_rating FROM players WHERE name = ?" ) .bind("Rating Test") .fetch_one(&pool) .await .unwrap(); assert!((new_rating.0 - 1550.0).abs() < 0.01); } #[tokio::test] async fn test_record_singles_match() { let pool = setup_test_db().await; // Create two players sqlx::query("INSERT INTO players (name, singles_rating) VALUES (?, ?)") .bind("Player A") .bind(1500.0) .execute(&pool) .await .unwrap(); sqlx::query("INSERT INTO players (name, singles_rating) VALUES (?, ?)") .bind("Player B") .bind(1500.0) .execute(&pool) .await .unwrap(); // Record match let match_result = sqlx::query( "INSERT INTO matches (match_type, team1_score, team2_score) VALUES (?, ?, ?)" ) .bind("singles") .bind(11) .bind(7) .execute(&pool) .await .unwrap(); let match_id = match_result.last_insert_rowid(); // Get player IDs let player_a_id: (i64,) = sqlx::query_as("SELECT id FROM players WHERE name = ?") .bind("Player A") .fetch_one(&pool) .await .unwrap(); let player_b_id: (i64,) = sqlx::query_as("SELECT id FROM players WHERE name = ?") .bind("Player B") .fetch_one(&pool) .await .unwrap(); // Record participants sqlx::query( "INSERT INTO match_participants (match_id, player_id, team, rating_before, rating_after, rating_change) VALUES (?, ?, ?, ?, ?, ?)" ) .bind(match_id) .bind(player_a_id.0) .bind(1) // Team 1 .bind(1500.0) .bind(1505.0) .bind(5.0) .execute(&pool) .await .unwrap(); sqlx::query( "INSERT INTO match_participants (match_id, player_id, team, rating_before, rating_after, rating_change) VALUES (?, ?, ?, ?, ?, ?)" ) .bind(match_id) .bind(player_b_id.0) .bind(2) // Team 2 .bind(1500.0) .bind(1495.0) .bind(-5.0) .execute(&pool) .await .unwrap(); // Verify match recorded let participants: Vec<(String, i32, f64)> = sqlx::query_as( r#"SELECT p.name, mp.team, mp.rating_change FROM match_participants mp JOIN players p ON mp.player_id = p.id WHERE mp.match_id = ?"# ) .bind(match_id) .fetch_all(&pool) .await .unwrap(); assert_eq!(participants.len(), 2); // Player A (winner) should have positive change let player_a = participants.iter().find(|(n, _, _)| n == "Player A").unwrap(); assert!(player_a.2 > 0.0); // Player B (loser) should have negative change let player_b = participants.iter().find(|(n, _, _)| n == "Player B").unwrap(); assert!(player_b.2 < 0.0); // Changes should sum to zero (approximately) let total_change: f64 = participants.iter().map(|(_, _, c)| c).sum(); assert!(total_change.abs() < 0.01); } #[tokio::test] async fn test_record_doubles_match() { let pool = setup_test_db().await; // Create four players for (name, rating) in [("P1", 1550.0), ("P2", 1450.0), ("P3", 1520.0), ("P4", 1480.0)] { sqlx::query("INSERT INTO players (name, singles_rating) VALUES (?, ?)") .bind(name) .bind(rating) .execute(&pool) .await .unwrap(); } // Record doubles match let match_result = sqlx::query( "INSERT INTO matches (match_type, team1_score, team2_score) VALUES (?, ?, ?)" ) .bind("doubles") .bind(11) .bind(8) .execute(&pool) .await .unwrap(); let match_id = match_result.last_insert_rowid(); // Get player IDs let players: Vec<(i64, String)> = sqlx::query_as("SELECT id, name FROM players") .fetch_all(&pool) .await .unwrap(); let get_id = |name: &str| players.iter().find(|(_, n)| n == name).unwrap().0; // Team 1: P1 + P2 (winners) // Team 2: P3 + P4 (losers) let team_assignments = [ (get_id("P1"), 1, 5.0), // Team 1, gains (get_id("P2"), 1, 8.0), // Team 1, gains more (weaker player carried) (get_id("P3"), 2, -6.0), // Team 2, loses (get_id("P4"), 2, -7.0), // Team 2, loses more ]; for (player_id, team, change) in team_assignments { sqlx::query( "INSERT INTO match_participants (match_id, player_id, team, rating_change) VALUES (?, ?, ?, ?)" ) .bind(match_id) .bind(player_id) .bind(team) .bind(change) .execute(&pool) .await .unwrap(); } // Verify all four participants recorded let count: (i64,) = sqlx::query_as( "SELECT COUNT(*) FROM match_participants WHERE match_id = ?" ) .bind(match_id) .fetch_one(&pool) .await .unwrap(); assert_eq!(count.0, 4); } #[tokio::test] async fn test_leaderboard_query() { let pool = setup_test_db().await; // Create players with different ratings let players = [ ("Elite", 1800.0), ("Strong", 1650.0), ("Average", 1500.0), ("Beginner", 1350.0), ]; for (name, rating) in players { sqlx::query("INSERT INTO players (name, singles_rating) VALUES (?, ?)") .bind(name) .bind(rating) .execute(&pool) .await .unwrap(); } // Query leaderboard let leaderboard: Vec<(String, f64)> = sqlx::query_as( "SELECT name, singles_rating FROM players ORDER BY singles_rating DESC LIMIT 10" ) .fetch_all(&pool) .await .unwrap(); assert_eq!(leaderboard.len(), 4); assert_eq!(leaderboard[0].0, "Elite"); assert_eq!(leaderboard[1].0, "Strong"); assert_eq!(leaderboard[2].0, "Average"); assert_eq!(leaderboard[3].0, "Beginner"); } #[tokio::test] async fn test_player_match_history() { let pool = setup_test_db().await; // Create players sqlx::query("INSERT INTO players (name) VALUES (?)") .bind("History Test") .execute(&pool) .await .unwrap(); sqlx::query("INSERT INTO players (name) VALUES (?)") .bind("Opponent") .execute(&pool) .await .unwrap(); let player_id: (i64,) = sqlx::query_as("SELECT id FROM players WHERE name = ?") .bind("History Test") .fetch_one(&pool) .await .unwrap(); let opp_id: (i64,) = sqlx::query_as("SELECT id FROM players WHERE name = ?") .bind("Opponent") .fetch_one(&pool) .await .unwrap(); // Record multiple matches for i in 0..5 { let match_result = sqlx::query( "INSERT INTO matches (match_type, team1_score, team2_score) VALUES (?, ?, ?)" ) .bind("singles") .bind(11) .bind(5 + i) // Different scores .execute(&pool) .await .unwrap(); let match_id = match_result.last_insert_rowid(); sqlx::query( "INSERT INTO match_participants (match_id, player_id, team, rating_change) VALUES (?, ?, ?, ?)" ) .bind(match_id) .bind(player_id.0) .bind(1) .bind(5.0 - i as f64) // Decreasing gains .execute(&pool) .await .unwrap(); sqlx::query( "INSERT INTO match_participants (match_id, player_id, team, rating_change) VALUES (?, ?, ?, ?)" ) .bind(match_id) .bind(opp_id.0) .bind(2) .bind(-(5.0 - i as f64)) .execute(&pool) .await .unwrap(); } // Query player's match history let history: Vec<(i64, f64)> = sqlx::query_as( r#"SELECT m.id, mp.rating_change FROM match_participants mp JOIN matches m ON mp.match_id = m.id WHERE mp.player_id = ? ORDER BY m.timestamp DESC"# ) .bind(player_id.0) .fetch_all(&pool) .await .unwrap(); assert_eq!(history.len(), 5); } #[tokio::test] async fn test_head_to_head_stats() { let pool = setup_test_db().await; // Create two players sqlx::query("INSERT INTO players (name) VALUES (?)") .bind("Player X") .execute(&pool) .await .unwrap(); sqlx::query("INSERT INTO players (name) VALUES (?)") .bind("Player Y") .execute(&pool) .await .unwrap(); let px_id: (i64,) = sqlx::query_as("SELECT id FROM players WHERE name = ?") .bind("Player X") .fetch_one(&pool) .await .unwrap(); let py_id: (i64,) = sqlx::query_as("SELECT id FROM players WHERE name = ?") .bind("Player Y") .fetch_one(&pool) .await .unwrap(); // X wins 3 matches, Y wins 2 matches let results = [(11, 7, 1), (11, 9, 1), (7, 11, 2), (11, 5, 1), (8, 11, 2)]; for (t1_score, t2_score, winner_team) in results { let match_result = sqlx::query( "INSERT INTO matches (match_type, team1_score, team2_score) VALUES (?, ?, ?)" ) .bind("singles") .bind(t1_score) .bind(t2_score) .execute(&pool) .await .unwrap(); let match_id = match_result.last_insert_rowid(); // X is always team 1, Y is always team 2 sqlx::query( "INSERT INTO match_participants (match_id, player_id, team) VALUES (?, ?, ?)" ) .bind(match_id) .bind(px_id.0) .bind(1) .execute(&pool) .await .unwrap(); sqlx::query( "INSERT INTO match_participants (match_id, player_id, team) VALUES (?, ?, ?)" ) .bind(match_id) .bind(py_id.0) .bind(2) .execute(&pool) .await .unwrap(); } // Calculate head-to-head let h2h: (i64, i64) = sqlx::query_as( r#"SELECT SUM(CASE WHEN m.team1_score > m.team2_score THEN 1 ELSE 0 END) as x_wins, SUM(CASE WHEN m.team2_score > m.team1_score THEN 1 ELSE 0 END) as y_wins FROM matches m JOIN match_participants mp1 ON m.id = mp1.match_id AND mp1.player_id = ? JOIN match_participants mp2 ON m.id = mp2.match_id AND mp2.player_id = ?"# ) .bind(px_id.0) .bind(py_id.0) .fetch_one(&pool) .await .unwrap(); assert_eq!(h2h.0, 3); // X wins assert_eq!(h2h.1, 2); // Y wins } #[tokio::test] async fn test_daily_summary_query() { let pool = setup_test_db().await; // Create players for name in ["Daily1", "Daily2"] { sqlx::query("INSERT INTO players (name) VALUES (?)") .bind(name) .execute(&pool) .await .unwrap(); } // Record match with specific date let today = chrono::Local::now().format("%Y-%m-%d").to_string(); let match_result = sqlx::query( "INSERT INTO matches (match_type, team1_score, team2_score, timestamp) VALUES (?, ?, ?, ?)" ) .bind("singles") .bind(11) .bind(7) .bind(format!("{} 14:30:00", today)) .execute(&pool) .await .unwrap(); let match_id = match_result.last_insert_rowid(); let p1_id: (i64,) = sqlx::query_as("SELECT id FROM players WHERE name = ?") .bind("Daily1") .fetch_one(&pool) .await .unwrap(); let p2_id: (i64,) = sqlx::query_as("SELECT id FROM players WHERE name = ?") .bind("Daily2") .fetch_one(&pool) .await .unwrap(); sqlx::query( "INSERT INTO match_participants (match_id, player_id, team, rating_change) VALUES (?, ?, ?, ?)" ) .bind(match_id) .bind(p1_id.0) .bind(1) .bind(5.0) .execute(&pool) .await .unwrap(); sqlx::query( "INSERT INTO match_participants (match_id, player_id, team, rating_change) VALUES (?, ?, ?, ?)" ) .bind(match_id) .bind(p2_id.0) .bind(2) .bind(-5.0) .execute(&pool) .await .unwrap(); // Query daily summary let daily_matches: Vec<(i64, String, i32, i32)> = sqlx::query_as( "SELECT id, match_type, team1_score, team2_score FROM matches WHERE date(timestamp) = ?" ) .bind(&today) .fetch_all(&pool) .await .unwrap(); assert_eq!(daily_matches.len(), 1); assert_eq!(daily_matches[0].1, "singles"); assert_eq!(daily_matches[0].2, 11); assert_eq!(daily_matches[0].3, 7); // Query daily rating changes let daily_changes: Vec<(String, f64)> = sqlx::query_as( r#"SELECT p.name, SUM(mp.rating_change) as total_change FROM match_participants mp JOIN matches m ON mp.match_id = m.id JOIN players p ON mp.player_id = p.id WHERE date(m.timestamp) = ? GROUP BY p.name ORDER BY total_change DESC"# ) .bind(&today) .fetch_all(&pool) .await .unwrap(); assert_eq!(daily_changes.len(), 2); assert_eq!(daily_changes[0].0, "Daily1"); // Winner first assert!(daily_changes[0].1 > 0.0); } #[tokio::test] async fn test_unique_player_name_constraint() { let pool = setup_test_db().await; // Create player sqlx::query("INSERT INTO players (name) VALUES (?)") .bind("Unique Name") .execute(&pool) .await .unwrap(); // Try to create duplicate - should fail let result = sqlx::query("INSERT INTO players (name) VALUES (?)") .bind("Unique Name") .execute(&pool) .await; assert!(result.is_err()); }