376 lines
12 KiB
Swift
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 = TestClock.calendar
|
|
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 = TestClock.calendar
|
|
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: TestClock.now)
|
|
#expect(query.sport == .nba)
|
|
}
|
|
|
|
@Test("init: stores team abbreviations")
|
|
func init_storesTeams() {
|
|
let query = HistoricalGameQuery(
|
|
sport: .mlb,
|
|
date: TestClock.now,
|
|
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: TestClock.now)
|
|
|
|
#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: TestClock.now,
|
|
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: TestClock.now,
|
|
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)
|
|
}
|
|
}
|
|
}
|