612 lines
17 KiB
Rust
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());
|
|
}
|