PickleBALLER/tests/integration_tests.rs

446 lines
13 KiB
Rust

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