From d0b650e08b7d2f717ce88058eed46ae8f9a788c3 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 27 Apr 2026 03:54:49 +0000 Subject: [PATCH] test(sync): add unit tests for StatsSnapshot::win_rate and AchievementRecord::unlock Both public APIs in solitaire_sync had no test coverage: - win_rate(): None before any game, 100/50/0% cases - AchievementRecord::locked(), unlock(), idempotency preserving earliest date Co-Authored-By: Claude Sonnet 4.6 --- solitaire_sync/src/achievements.rs | 33 ++++++++++++++++++++ solitaire_sync/src/stats.rs | 50 ++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/solitaire_sync/src/achievements.rs b/solitaire_sync/src/achievements.rs index 1c310ce..a610e15 100644 --- a/solitaire_sync/src/achievements.rs +++ b/solitaire_sync/src/achievements.rs @@ -46,3 +46,36 @@ impl AchievementRecord { self.unlock_date = Some(at); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn locked_creates_an_unlocked_record() { + let r = AchievementRecord::locked("first_win"); + assert_eq!(r.id, "first_win"); + assert!(!r.unlocked); + assert!(r.unlock_date.is_none()); + assert!(!r.reward_granted); + } + + #[test] + fn unlock_sets_unlocked_and_stores_timestamp() { + let mut r = AchievementRecord::locked("first_win"); + let ts = Utc::now(); + r.unlock(ts); + assert!(r.unlocked); + assert_eq!(r.unlock_date, Some(ts)); + } + + #[test] + fn unlock_is_idempotent_and_preserves_earliest_date() { + let mut r = AchievementRecord::locked("first_win"); + let early = DateTime::UNIX_EPOCH; + let later = Utc::now(); + r.unlock(early); + r.unlock(later); // should be a no-op + assert_eq!(r.unlock_date, Some(early), "earliest unlock date must be preserved"); + } +} diff --git a/solitaire_sync/src/stats.rs b/solitaire_sync/src/stats.rs index 9d322f3..9f0285e 100644 --- a/solitaire_sync/src/stats.rs +++ b/solitaire_sync/src/stats.rs @@ -74,3 +74,53 @@ impl StatsSnapshot { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn win_rate_is_none_before_any_game() { + let s = StatsSnapshot::default(); + assert!(s.win_rate().is_none()); + } + + #[test] + fn win_rate_100_when_all_games_won() { + let s = StatsSnapshot { + games_played: 5, + games_won: 5, + ..StatsSnapshot::default() + }; + let rate = s.win_rate().expect("should have a rate"); + assert!((rate - 100.0).abs() < 0.01, "expected 100.0, got {rate}"); + } + + #[test] + fn win_rate_50_when_half_won() { + let s = StatsSnapshot { + games_played: 10, + games_won: 5, + ..StatsSnapshot::default() + }; + let rate = s.win_rate().expect("should have a rate"); + assert!((rate - 50.0).abs() < 0.01, "expected 50.0, got {rate}"); + } + + #[test] + fn win_rate_0_when_no_wins() { + let s = StatsSnapshot { + games_played: 3, + games_won: 0, + ..StatsSnapshot::default() + }; + let rate = s.win_rate().expect("should have a rate"); + assert!((rate - 0.0).abs() < 0.01, "expected 0.0, got {rate}"); + } + + #[test] + fn fastest_win_seconds_defaults_to_max() { + let s = StatsSnapshot::default(); + assert_eq!(s.fastest_win_seconds, u64::MAX); + } +}