PickleBALLER/tests/database_tests.rs

612 lines
17 KiB
Rust

//! 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());
}