446 lines
13 KiB
Rust
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");
|
|
}
|
|
}
|