PickleBALLER/tests/integration_tests.rs
Split d4c0bb889b docs: comprehensive documentation for all modules and handlers
- README.md: full project docs with features, API endpoints, Glicko-2 explanation
- main.rs: doc comments for all 18 HTTP handlers
- db/mod.rs: schema and migration documentation
- models/mod.rs: Player struct and Glicko-2 parameter docs
- Fixed route syntax (:id instead of {id}) for Axum 0.7 compatibility
2026-02-07 19:19:50 -05:00

251 lines
8.0 KiB
Rust

//! Integration tests for Pickleball ELO Tracker
use pickleball_elo::glicko::{GlickoRating, calculate_new_ratings};
use pickleball_elo::db;
/// Test Glicko-2 rating calculations
#[test]
fn test_glicko_rating_creation() {
let rating = GlickoRating::new_player();
assert_eq!(rating.rating, 1500.0);
assert_eq!(rating.rd, 350.0);
assert!((rating.volatility - 0.06).abs() < 0.001);
}
#[test]
fn test_glicko_winner_gains_rating() {
let winner = GlickoRating::new_player();
let loser = GlickoRating::new_player();
let (new_winner, new_loser) = calculate_new_ratings(&winner, &loser, 1.0, 1.0);
// Winner should gain rating
assert!(new_winner.rating > winner.rating,
"Winner rating {} should be greater than {}", new_winner.rating, winner.rating);
// Loser should lose rating
assert!(new_loser.rating < loser.rating,
"Loser rating {} should be less than {}", new_loser.rating, loser.rating);
}
#[test]
fn test_glicko_rating_changes_are_symmetric() {
let player1 = GlickoRating::new_player();
let player2 = GlickoRating::new_player();
let (new_p1, new_p2) = calculate_new_ratings(&player1, &player2, 1.0, 1.0);
let p1_change = new_p1.rating - player1.rating;
let p2_change = new_p2.rating - player2.rating;
// Changes should be roughly symmetric (opposite signs)
assert!((p1_change + p2_change).abs() < 1.0,
"Rating changes should be symmetric: {} + {} = {}", p1_change, p2_change, p1_change + p2_change);
}
#[test]
fn test_glicko_bigger_upset_bigger_change() {
let favorite = GlickoRating { rating: 1800.0, rd: 100.0, volatility: 0.06 };
let underdog = GlickoRating { rating: 1400.0, rd: 100.0, volatility: 0.06 };
// Underdog wins (upset)
let (new_underdog, new_favorite) = calculate_new_ratings(&underdog, &favorite, 1.0, 1.0);
// Underdog should gain a lot
let underdog_gain = new_underdog.rating - underdog.rating;
assert!(underdog_gain > 20.0,
"Underdog upset gain {} should be significant", underdog_gain);
}
#[test]
fn test_glicko_rd_decreases_after_match() {
let player1 = GlickoRating { rating: 1500.0, rd: 200.0, volatility: 0.06 };
let player2 = GlickoRating { rating: 1500.0, rd: 200.0, volatility: 0.06 };
let (new_p1, _) = calculate_new_ratings(&player1, &player2, 1.0, 1.0);
// RD should decrease after playing (more certainty)
assert!(new_p1.rd < player1.rd,
"RD {} should decrease from {}", new_p1.rd, player1.rd);
}
#[test]
fn test_score_weighting_blowout_vs_close() {
let player1 = GlickoRating::new_player();
let player2 = GlickoRating::new_player();
// Blowout win (11-0)
let (blowout_winner, _) = calculate_new_ratings(&player1, &player2, 1.0, 1.5);
// Close win (11-9)
let (close_winner, _) = calculate_new_ratings(&player1, &player2, 1.0, 1.05);
// Blowout should give more rating
assert!(blowout_winner.rating > close_winner.rating,
"Blowout {} should give more than close {}", blowout_winner.rating, close_winner.rating);
}
/// Test database operations
#[tokio::test]
async fn test_database_creation() {
let temp_dir = std::env::temp_dir();
let db_path = temp_dir.join("test_pickleball.db");
let db_str = db_path.to_str().unwrap();
// Clean up from previous runs
let _ = std::fs::remove_file(&db_path);
let pool = db::create_pool(db_str).await.expect("Failed to create pool");
// Verify tables exist
let result: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM players")
.fetch_one(&pool)
.await
.expect("Players table should exist");
assert_eq!(result.0, 0, "Players table should be empty");
// Clean up
drop(pool);
let _ = std::fs::remove_file(&db_path);
}
#[tokio::test]
async fn test_player_crud() {
let temp_dir = std::env::temp_dir();
let db_path = temp_dir.join("test_crud.db");
let db_str = db_path.to_str().unwrap();
let _ = std::fs::remove_file(&db_path);
let pool = db::create_pool(db_str).await.unwrap();
// Create player
sqlx::query("INSERT INTO players (name, email) VALUES ('Test Player', 'test@example.com')")
.execute(&pool)
.await
.expect("Should insert player");
// Read player
let player: (i64, String, Option<String>, f64) = sqlx::query_as(
"SELECT id, name, email, singles_rating FROM players WHERE name = 'Test Player'"
)
.fetch_one(&pool)
.await
.expect("Should find player");
assert_eq!(player.1, "Test Player");
assert_eq!(player.2, Some("test@example.com".to_string()));
assert_eq!(player.3, 1500.0); // Default rating
// Update player
sqlx::query("UPDATE players SET singles_rating = 1600.0 WHERE id = ?")
.bind(player.0)
.execute(&pool)
.await
.expect("Should update player");
let updated: (f64,) = sqlx::query_as("SELECT singles_rating FROM players WHERE id = ?")
.bind(player.0)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(updated.0, 1600.0);
// Delete player
sqlx::query("DELETE FROM players WHERE id = ?")
.bind(player.0)
.execute(&pool)
.await
.expect("Should delete player");
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM players")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(count.0, 0);
drop(pool);
let _ = std::fs::remove_file(&db_path);
}
#[tokio::test]
async fn test_match_recording() {
let temp_dir = std::env::temp_dir();
let db_path = temp_dir.join("test_matches.db");
let db_str = db_path.to_str().unwrap();
let _ = std::fs::remove_file(&db_path);
let pool = db::create_pool(db_str).await.unwrap();
// Create two players
sqlx::query("INSERT INTO players (name) VALUES ('Player A')")
.execute(&pool).await.unwrap();
sqlx::query("INSERT INTO players (name) VALUES ('Player B')")
.execute(&pool).await.unwrap();
// Create a session
let session_id: i64 = sqlx::query_scalar(
"INSERT INTO sessions (notes) VALUES ('Test Session') RETURNING id"
)
.fetch_one(&pool)
.await
.unwrap();
// Create a match
let match_id: i64 = sqlx::query_scalar(
"INSERT INTO matches (session_id, match_type, team1_score, team2_score) VALUES (?, 'singles', 11, 5) RETURNING id"
)
.bind(session_id)
.fetch_one(&pool)
.await
.unwrap();
assert!(match_id > 0, "Match should be created with valid ID");
// Verify match
let match_data: (String, i32, i32) = sqlx::query_as(
"SELECT match_type, team1_score, team2_score FROM matches WHERE id = ?"
)
.bind(match_id)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(match_data.0, "singles");
assert_eq!(match_data.1, 11);
assert_eq!(match_data.2, 5);
drop(pool);
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_rating_bounds() {
// Test that ratings don't go below 0 or above unreasonable values
let very_low = GlickoRating { rating: 100.0, rd: 50.0, volatility: 0.06 };
let very_high = GlickoRating { rating: 2500.0, rd: 50.0, volatility: 0.06 };
let (new_low, _) = calculate_new_ratings(&very_low, &very_high, 0.0, 1.0);
assert!(new_low.rating > 0.0, "Rating should stay positive");
assert!(new_low.rd > 0.0, "RD should stay positive");
}
#[test]
fn test_draw_handling() {
let player1 = GlickoRating::new_player();
let player2 = GlickoRating::new_player();
// Score of 0.5 = draw
let (new_p1, new_p2) = calculate_new_ratings(&player1, &player2, 0.5, 1.0);
// In a draw between equal players, ratings shouldn't change much
let p1_change = (new_p1.rating - player1.rating).abs();
let p2_change = (new_p2.rating - player2.rating).abs();
assert!(p1_change < 1.0, "Draw should not change rating much: {}", p1_change);
assert!(p2_change < 1.0, "Draw should not change rating much: {}", p2_change);
}