Files
Sportstime/SportsTimeTests/Services/ScoreResolutionCacheTests.swift
Trey T a6f538dfed Audit and fix 52 test correctness issues across 22 files
Systematic audit of 1,191 tests found tests written to pass rather than
verify correctness. Key fixes:

Infrastructure:
- TestClock: fixed timezone from .current to America/New_York (deterministic)
- TestFixtures: added 1.3x road routing factor to match production
- ItineraryTestHelpers: real per-city coordinates instead of hardcoded (40,-80)

Planning tests:
- Added missing Scenario E factory dispatch tests
- Tightened 12 loose assertions (>= 1 → == 8.0, > 0 → range checks)
- Fixed 4 no-op tests that accepted both success and failure
- Fixed wrong repeat-city invariant (was checking same-day, not different-day)
- Fixed tautological assertion in missing-stadium edge case

Services/Domain/Export tests:
- Replaced 4 placeholder tests (#expect(true)) with real assertions
- Fixed tautological assertions in POISearchServiceTests
- Fixed Chicago coordinate in RegionMapSelectorTests (-89 → -87.6553)
- Added sort order verification to ItineraryRowFlatteningTests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 23:00:46 -05:00

187 lines
6.6 KiB
Swift

//
// ScoreResolutionCacheTests.swift
// SportsTimeTests
//
// TDD specification tests for ScoreResolutionCache types.
//
import Testing
import Foundation
@testable import SportsTime
// MARK: - CacheStats Tests
@Suite("CacheStats")
struct CacheStatsTests {
// MARK: - Test Data
private func makeStats(
totalEntries: Int = 100,
entriesWithScores: Int = 80,
entriesWithoutScores: Int = 20,
expiredEntries: Int = 5,
entriesBySport: [Sport: Int] = [.mlb: 50, .nba: 30, .nhl: 20]
) -> CacheStats {
CacheStats(
totalEntries: totalEntries,
entriesWithScores: entriesWithScores,
entriesWithoutScores: entriesWithoutScores,
expiredEntries: expiredEntries,
entriesBySport: entriesBySport
)
}
// MARK: - Specification Tests: Properties
@Test("CacheStats: stores totalEntries")
func cacheStats_totalEntries() {
let stats = makeStats(totalEntries: 150)
#expect(stats.totalEntries == 150)
}
@Test("CacheStats: stores entriesWithScores")
func cacheStats_entriesWithScores() {
let stats = makeStats(entriesWithScores: 75)
#expect(stats.entriesWithScores == 75)
}
@Test("CacheStats: stores entriesWithoutScores")
func cacheStats_entriesWithoutScores() {
let stats = makeStats(entriesWithoutScores: 25)
#expect(stats.entriesWithoutScores == 25)
}
@Test("CacheStats: stores expiredEntries")
func cacheStats_expiredEntries() {
let stats = makeStats(expiredEntries: 10)
#expect(stats.expiredEntries == 10)
}
@Test("CacheStats: stores entriesBySport")
func cacheStats_entriesBySport() {
let bySport: [Sport: Int] = [.mlb: 40, .nba: 60]
let stats = makeStats(entriesBySport: bySport)
#expect(stats.entriesBySport[.mlb] == 40)
#expect(stats.entriesBySport[.nba] == 60)
}
// MARK: - Edge Cases
@Test("CacheStats: handles empty cache")
func cacheStats_emptyCache() {
let stats = makeStats(
totalEntries: 0,
entriesWithScores: 0,
entriesWithoutScores: 0,
expiredEntries: 0,
entriesBySport: [:]
)
#expect(stats.totalEntries == 0)
#expect(stats.entriesBySport.isEmpty)
}
@Test("CacheStats: handles all expired")
func cacheStats_allExpired() {
let stats = makeStats(totalEntries: 100, expiredEntries: 100)
#expect(stats.expiredEntries == stats.totalEntries)
}
@Test("CacheStats: handles all with scores")
func cacheStats_allWithScores() {
let stats = makeStats(totalEntries: 100, entriesWithScores: 100, entriesWithoutScores: 0)
#expect(stats.entriesWithScores == stats.totalEntries)
#expect(stats.entriesWithoutScores == 0)
}
@Test("CacheStats: handles single sport")
func cacheStats_singleSport() {
let stats = makeStats(entriesBySport: [.mlb: 100])
#expect(stats.entriesBySport.count == 1)
#expect(stats.entriesBySport[.mlb] == 100)
}
// MARK: - Invariant Tests
/// - Invariant: entriesWithScores + entriesWithoutScores == totalEntries
@Test("Invariant: scores split equals total")
func invariant_scoresSplitEqualsTotal() {
let stats = makeStats(totalEntries: 100, entriesWithScores: 80, entriesWithoutScores: 20)
#expect(stats.entriesWithScores + stats.entriesWithoutScores == stats.totalEntries)
}
/// - Invariant: expiredEntries <= totalEntries
@Test("Invariant: expired entries cannot exceed total")
func invariant_expiredCannotExceedTotal() {
let stats = makeStats(totalEntries: 100, expiredEntries: 50)
#expect(stats.expiredEntries <= stats.totalEntries)
}
/// - Invariant: sum of entriesBySport <= totalEntries
// NOTE: CacheStats is a plain data struct this test documents the expected
// relationship between sport entries and total, not enforcement by the cache.
// The struct does not validate or clamp values; callers are responsible for
// providing consistent data.
@Test("Invariant: sport entries sum does not exceed total")
func invariant_sportEntriesSumDoesNotExceedTotal() {
let bySport: [Sport: Int] = [.mlb: 30, .nba: 40, .nhl: 30]
let stats = makeStats(totalEntries: 100, entriesBySport: bySport)
let sportSum = stats.entriesBySport.values.reduce(0, +)
#expect(sportSum <= stats.totalEntries)
}
}
// MARK: - Cache Expiration Behavior Tests
@Suite("Cache Expiration Behavior")
struct CacheExpirationBehaviorTests {
// These tests document the expected cache expiration behavior
// based on ScoreResolutionCache.calculateExpiration
// MARK: - Specification Tests: Cache Durations
/// - Expected Behavior: Recent games (< 30 days old) expire after 24 hours
@Test("Expiration: recent games expire after 24 hours")
func expiration_recentGames() {
// Games less than 30 days old should expire after 24 hours
let recentGameCacheDuration: TimeInterval = 24 * 60 * 60
#expect(recentGameCacheDuration == 86400) // 24 hours in seconds
}
/// - Expected Behavior: Historical games (> 30 days old) never expire (nil)
@Test("Expiration: historical games never expire")
func expiration_historicalGames() {
// Games older than 30 days should have nil expiration (never expire)
let historicalAgeThreshold: TimeInterval = 30 * 24 * 60 * 60
#expect(historicalAgeThreshold == 2592000) // 30 days in seconds
}
/// - Expected Behavior: Failed lookups expire after 7 days
@Test("Expiration: failed lookups expire after 7 days")
func expiration_failedLookups() {
let failedLookupCacheDuration: TimeInterval = 7 * 24 * 60 * 60
#expect(failedLookupCacheDuration == 604800) // 7 days in seconds
}
// MARK: - Invariant Tests
/// - Invariant: Historical threshold > recent cache duration
@Test("Invariant: historical threshold exceeds recent cache duration")
func invariant_historicalExceedsRecent() {
let recentCacheDuration: TimeInterval = 24 * 60 * 60
let historicalThreshold: TimeInterval = 30 * 24 * 60 * 60
#expect(historicalThreshold > recentCacheDuration)
}
/// - Invariant: Failed lookup duration > recent cache duration
@Test("Invariant: failed lookup duration exceeds recent cache duration")
func invariant_failedLookupExceedsRecent() {
let recentCacheDuration: TimeInterval = 24 * 60 * 60
let failedLookupDuration: TimeInterval = 7 * 24 * 60 * 60
#expect(failedLookupDuration > recentCacheDuration)
}
}