Files
Sportstime/SportsTimeTests/Services/FreeScoreAPITests.swift
Trey t 8162b4a029 refactor(tests): TDD rewrite of all unit tests with spec documentation
Complete rewrite of unit test suite using TDD methodology:

Planning Engine Tests:
- GameDAGRouterTests: Beam search, anchor games, transitions
- ItineraryBuilderTests: Stop connection, validators, EV enrichment
- RouteFiltersTests: Region, time window, scoring filters
- ScenarioA/B/C/D PlannerTests: All planning scenarios
- TravelEstimatorTests: Distance, duration, travel days
- TripPlanningEngineTests: Orchestration, caching, preferences

Domain Model Tests:
- AchievementDefinitionsTests, AnySportTests, DivisionTests
- GameTests, ProgressTests, RegionTests, StadiumTests
- TeamTests, TravelSegmentTests, TripTests, TripPollTests
- TripPreferencesTests, TripStopTests, SportTests

Service Tests:
- FreeScoreAPITests, RouteDescriptionGeneratorTests
- SuggestedTripsGeneratorTests

Export Tests:
- ShareableContentTests (card types, themes, dimensions)

Bug fixes discovered through TDD:
- ShareCardDimensions: mapSnapshotSize exceeded available width (960x480)
- ScenarioBPlanner: Added anchor game validation filter

All tests include:
- Specification tests (expected behavior)
- Invariant tests (properties that must always hold)
- Edge case tests (boundary conditions)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 14:07:41 -06:00

376 lines
12 KiB
Swift

//
// FreeScoreAPITests.swift
// SportsTimeTests
//
// TDD specification tests for FreeScoreAPI types.
//
import Testing
import Foundation
@testable import SportsTime
// MARK: - ProviderReliability Tests
@Suite("ProviderReliability")
struct ProviderReliabilityTests {
// MARK: - Specification Tests: Values
@Test("official: raw value is 'official'")
func official_rawValue() {
#expect(ProviderReliability.official.rawValue == "official")
}
@Test("unofficial: raw value is 'unofficial'")
func unofficial_rawValue() {
#expect(ProviderReliability.unofficial.rawValue == "unofficial")
}
@Test("scraped: raw value is 'scraped'")
func scraped_rawValue() {
#expect(ProviderReliability.scraped.rawValue == "scraped")
}
// MARK: - Invariant Tests
/// - Invariant: All reliability levels have distinct raw values
@Test("Invariant: all raw values are distinct")
func invariant_distinctRawValues() {
let all: [ProviderReliability] = [.official, .unofficial, .scraped]
let rawValues = all.map { $0.rawValue }
let uniqueValues = Set(rawValues)
#expect(rawValues.count == uniqueValues.count)
}
}
// MARK: - HistoricalGameQuery Tests
@Suite("HistoricalGameQuery")
struct HistoricalGameQueryTests {
// MARK: - Specification Tests: normalizedDateString
/// - Expected Behavior: Returns date in yyyy-MM-dd format (Eastern time)
@Test("normalizedDateString: formats as yyyy-MM-dd")
func normalizedDateString_format() {
var calendar = Calendar.current
calendar.timeZone = TimeZone(identifier: "America/New_York")!
let date = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
let query = HistoricalGameQuery(sport: .mlb, date: date)
#expect(query.normalizedDateString == "2026-06-15")
}
@Test("normalizedDateString: pads single-digit months")
func normalizedDateString_padMonth() {
var calendar = Calendar.current
calendar.timeZone = TimeZone(identifier: "America/New_York")!
let date = calendar.date(from: DateComponents(year: 2026, month: 3, day: 5))!
let query = HistoricalGameQuery(sport: .mlb, date: date)
#expect(query.normalizedDateString == "2026-03-05")
}
// MARK: - Specification Tests: Initialization
@Test("init: stores sport correctly")
func init_storesSport() {
let query = HistoricalGameQuery(sport: .nba, date: Date())
#expect(query.sport == .nba)
}
@Test("init: stores team abbreviations")
func init_storesTeams() {
let query = HistoricalGameQuery(
sport: .mlb,
date: Date(),
homeTeamAbbrev: "NYY",
awayTeamAbbrev: "BOS"
)
#expect(query.homeTeamAbbrev == "NYY")
#expect(query.awayTeamAbbrev == "BOS")
}
@Test("init: team abbreviations default to nil")
func init_defaultNilTeams() {
let query = HistoricalGameQuery(sport: .mlb, date: Date())
#expect(query.homeTeamAbbrev == nil)
#expect(query.awayTeamAbbrev == nil)
#expect(query.stadiumCanonicalId == nil)
}
}
// MARK: - HistoricalGameResult Tests
@Suite("HistoricalGameResult")
struct HistoricalGameResultTests {
// MARK: - Test Data
private func makeResult(
homeScore: Int? = 5,
awayScore: Int? = 3
) -> HistoricalGameResult {
HistoricalGameResult(
sport: .mlb,
gameDate: Date(),
homeTeamAbbrev: "NYY",
awayTeamAbbrev: "BOS",
homeTeamName: "Yankees",
awayTeamName: "Red Sox",
homeScore: homeScore,
awayScore: awayScore,
source: .api,
providerName: "Test Provider"
)
}
// MARK: - Specification Tests: scoreString
/// - Expected Behavior: Format is "away-home" (e.g., "3-5")
@Test("scoreString: formats as away-home")
func scoreString_format() {
let result = makeResult(homeScore: 5, awayScore: 3)
#expect(result.scoreString == "3-5")
}
@Test("scoreString: nil when homeScore is nil")
func scoreString_nilHomeScore() {
let result = makeResult(homeScore: nil, awayScore: 3)
#expect(result.scoreString == nil)
}
@Test("scoreString: nil when awayScore is nil")
func scoreString_nilAwayScore() {
let result = makeResult(homeScore: 5, awayScore: nil)
#expect(result.scoreString == nil)
}
@Test("scoreString: nil when both scores are nil")
func scoreString_bothNil() {
let result = makeResult(homeScore: nil, awayScore: nil)
#expect(result.scoreString == nil)
}
// MARK: - Specification Tests: hasScore
/// - Expected Behavior: true only when both scores are present
@Test("hasScore: true when both scores present")
func hasScore_bothPresent() {
let result = makeResult(homeScore: 5, awayScore: 3)
#expect(result.hasScore == true)
}
@Test("hasScore: false when homeScore is nil")
func hasScore_nilHomeScore() {
let result = makeResult(homeScore: nil, awayScore: 3)
#expect(result.hasScore == false)
}
@Test("hasScore: false when awayScore is nil")
func hasScore_nilAwayScore() {
let result = makeResult(homeScore: 5, awayScore: nil)
#expect(result.hasScore == false)
}
@Test("hasScore: false when both are nil")
func hasScore_bothNil() {
let result = makeResult(homeScore: nil, awayScore: nil)
#expect(result.hasScore == false)
}
// MARK: - Invariant Tests
/// - Invariant: hasScore == true implies scoreString != nil
@Test("Invariant: hasScore implies scoreString exists")
func invariant_hasScoreImpliesScoreString() {
let withScore = makeResult(homeScore: 5, awayScore: 3)
if withScore.hasScore {
#expect(withScore.scoreString != nil)
}
}
/// - Invariant: scoreString != nil implies hasScore
@Test("Invariant: scoreString exists implies hasScore")
func invariant_scoreStringImpliesHasScore() {
let withScore = makeResult(homeScore: 5, awayScore: 3)
if withScore.scoreString != nil {
#expect(withScore.hasScore)
}
}
}
// MARK: - ScoreResolutionResult Tests
@Suite("ScoreResolutionResult")
struct ScoreResolutionResultTests {
// MARK: - Test Data
private func makeHistoricalResult() -> HistoricalGameResult {
HistoricalGameResult(
sport: .mlb,
gameDate: Date(),
homeTeamAbbrev: "NYY",
awayTeamAbbrev: "BOS",
homeTeamName: "Yankees",
awayTeamName: "Red Sox",
homeScore: 5,
awayScore: 3,
source: .api,
providerName: "Test"
)
}
// MARK: - Specification Tests: isResolved
@Test("isResolved: true for resolved case")
func isResolved_resolved() {
let result = ScoreResolutionResult.resolved(makeHistoricalResult())
#expect(result.isResolved == true)
}
@Test("isResolved: false for pending case")
func isResolved_pending() {
let result = ScoreResolutionResult.pending
#expect(result.isResolved == false)
}
@Test("isResolved: false for requiresUserInput case")
func isResolved_requiresUserInput() {
let result = ScoreResolutionResult.requiresUserInput(reason: "Test reason")
#expect(result.isResolved == false)
}
@Test("isResolved: false for notFound case")
func isResolved_notFound() {
let result = ScoreResolutionResult.notFound(reason: "No game found")
#expect(result.isResolved == false)
}
// MARK: - Specification Tests: result
@Test("result: returns HistoricalGameResult for resolved case")
func result_resolved() {
let historical = makeHistoricalResult()
let result = ScoreResolutionResult.resolved(historical)
#expect(result.result != nil)
#expect(result.result?.homeTeamAbbrev == "NYY")
}
@Test("result: returns nil for pending case")
func result_pending() {
let result = ScoreResolutionResult.pending
#expect(result.result == nil)
}
@Test("result: returns nil for requiresUserInput case")
func result_requiresUserInput() {
let result = ScoreResolutionResult.requiresUserInput(reason: "Test")
#expect(result.result == nil)
}
@Test("result: returns nil for notFound case")
func result_notFound() {
let result = ScoreResolutionResult.notFound(reason: "Not found")
#expect(result.result == nil)
}
// MARK: - Invariant Tests
/// - Invariant: isResolved == true implies result != nil
@Test("Invariant: isResolved implies result exists")
func invariant_isResolvedImpliesResult() {
let resolved = ScoreResolutionResult.resolved(makeHistoricalResult())
if resolved.isResolved {
#expect(resolved.result != nil)
}
}
/// - Invariant: isResolved == false implies result == nil
@Test("Invariant: not resolved implies result nil")
func invariant_notResolvedImpliesNoResult() {
let cases: [ScoreResolutionResult] = [
.pending,
.requiresUserInput(reason: "Test"),
.notFound(reason: "Test")
]
for resolution in cases {
if !resolution.isResolved {
#expect(resolution.result == nil)
}
}
}
}
// MARK: - ScoreProviderError Tests
@Suite("ScoreProviderError")
struct ScoreProviderErrorTests {
// MARK: - Specification Tests: errorDescription
@Test("errorDescription: networkError includes underlying message")
func errorDescription_networkError() {
let error = ScoreProviderError.networkError(underlying: "Connection timeout")
#expect(error.errorDescription?.contains("timeout") == true)
}
@Test("errorDescription: rateLimited has description")
func errorDescription_rateLimited() {
let error = ScoreProviderError.rateLimited
#expect(error.errorDescription != nil)
#expect(!error.errorDescription!.isEmpty)
}
@Test("errorDescription: parseError includes message")
func errorDescription_parseError() {
let error = ScoreProviderError.parseError(message: "Invalid JSON")
#expect(error.errorDescription?.contains("Invalid JSON") == true)
}
@Test("errorDescription: gameNotFound has description")
func errorDescription_gameNotFound() {
let error = ScoreProviderError.gameNotFound
#expect(error.errorDescription != nil)
}
@Test("errorDescription: unsupportedSport includes sport")
func errorDescription_unsupportedSport() {
let error = ScoreProviderError.unsupportedSport(.nfl)
#expect(error.errorDescription?.contains("NFL") == true) // rawValue is uppercase
}
@Test("errorDescription: providerUnavailable includes reason")
func errorDescription_providerUnavailable() {
let error = ScoreProviderError.providerUnavailable(reason: "Maintenance")
#expect(error.errorDescription?.contains("Maintenance") == true)
}
// MARK: - Invariant Tests
/// - Invariant: All errors have non-empty descriptions
@Test("Invariant: all errors have descriptions")
func invariant_allHaveDescriptions() {
let errors: [ScoreProviderError] = [
.networkError(underlying: "test"),
.rateLimited,
.parseError(message: "test"),
.gameNotFound,
.unsupportedSport(.mlb),
.providerUnavailable(reason: "test")
]
for error in errors {
#expect(error.errorDescription != nil)
#expect(!error.errorDescription!.isEmpty)
}
}
}