Files
Sportstime/SportsTimeTests/Mocks/MockAppDataProvider.swift
Trey t 1bd248c255 test(planning): complete test suite with Phase 11 edge cases
Implement comprehensive test infrastructure and all 124 tests across 11 phases:

- Phase 0: Test infrastructure (fixtures, mocks, helpers)
- Phases 1-10: Core planning engine tests (previously implemented)
- Phase 11: Edge case omnibus (11 new tests)
  - Data edge cases: nil stadiums, malformed dates, invalid coordinates
  - Boundary conditions: driving limits, radius boundaries
  - Time zone cases: cross-timezone games, DST transitions

Reorganize test structure under Planning/ directory with proper organization.

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

265 lines
6.8 KiB
Swift

//
// MockAppDataProvider.swift
// SportsTimeTests
//
// Mock implementation of AppDataProvider for testing without SwiftData dependencies.
//
import Foundation
import Combine
@testable import SportsTime
// MARK: - Mock App Data Provider
@MainActor
final class MockAppDataProvider: ObservableObject {
// MARK: - Published State
@Published private(set) var teams: [Team] = []
@Published private(set) var stadiums: [Stadium] = []
@Published private(set) var isLoading = false
@Published private(set) var error: Error?
@Published private(set) var errorMessage: String?
// MARK: - Internal Storage
private var teamsById: [UUID: Team] = [:]
private var stadiumsById: [UUID: Stadium] = [:]
private var games: [Game] = []
private var gamesById: [UUID: Game] = [:]
// MARK: - Configuration
struct Configuration {
var simulatedLatency: TimeInterval = 0
var shouldFailOnLoad: Bool = false
var shouldFailOnFetch: Bool = false
var isEmpty: Bool = false
static var `default`: Configuration { Configuration() }
static var empty: Configuration { Configuration(isEmpty: true) }
static var failing: Configuration { Configuration(shouldFailOnLoad: true) }
static var slow: Configuration { Configuration(simulatedLatency: 1.0) }
}
private var config: Configuration
// MARK: - Call Tracking
private(set) var loadInitialDataCallCount = 0
private(set) var fetchGamesCallCount = 0
private(set) var fetchRichGamesCallCount = 0
// MARK: - Initialization
init(config: Configuration = .default) {
self.config = config
}
// MARK: - Configuration Methods
func configure(_ newConfig: Configuration) {
self.config = newConfig
}
func setTeams(_ newTeams: [Team]) {
self.teams = newTeams
self.teamsById = Dictionary(uniqueKeysWithValues: newTeams.map { ($0.id, $0) })
}
func setStadiums(_ newStadiums: [Stadium]) {
self.stadiums = newStadiums
self.stadiumsById = Dictionary(uniqueKeysWithValues: newStadiums.map { ($0.id, $0) })
}
func setGames(_ newGames: [Game]) {
self.games = newGames
self.gamesById = Dictionary(uniqueKeysWithValues: newGames.map { ($0.id, $0) })
}
func reset() {
teams = []
stadiums = []
games = []
teamsById = [:]
stadiumsById = [:]
gamesById = [:]
isLoading = false
error = nil
errorMessage = nil
loadInitialDataCallCount = 0
fetchGamesCallCount = 0
fetchRichGamesCallCount = 0
config = .default
}
// MARK: - Simulated Network
private func simulateLatency() async {
if config.simulatedLatency > 0 {
try? await Task.sleep(nanoseconds: UInt64(config.simulatedLatency * 1_000_000_000))
}
}
// MARK: - Data Loading
func loadInitialData() async {
loadInitialDataCallCount += 1
if config.isEmpty {
teams = []
stadiums = []
return
}
isLoading = true
error = nil
errorMessage = nil
await simulateLatency()
if config.shouldFailOnLoad {
error = DataProviderError.contextNotConfigured
errorMessage = "Mock load failure"
isLoading = false
return
}
isLoading = false
}
func clearError() {
error = nil
errorMessage = nil
}
func retry() async {
await loadInitialData()
}
// MARK: - Data Access
func team(for id: UUID) -> Team? {
teamsById[id]
}
func stadium(for id: UUID) -> Stadium? {
stadiumsById[id]
}
func teams(for sport: Sport) -> [Team] {
teams.filter { $0.sport == sport }
}
// MARK: - Game Fetching
func fetchGames(sports: Set<Sport>, startDate: Date, endDate: Date) async throws -> [Game] {
fetchGamesCallCount += 1
await simulateLatency()
if config.shouldFailOnFetch {
throw DataProviderError.contextNotConfigured
}
return games.filter { game in
sports.contains(game.sport) &&
game.dateTime >= startDate &&
game.dateTime <= endDate
}.sorted { $0.dateTime < $1.dateTime }
}
func fetchGame(by id: UUID) async throws -> Game? {
await simulateLatency()
if config.shouldFailOnFetch {
throw DataProviderError.contextNotConfigured
}
return gamesById[id]
}
func fetchRichGames(sports: Set<Sport>, startDate: Date, endDate: Date) async throws -> [RichGame] {
fetchRichGamesCallCount += 1
let filteredGames = try await fetchGames(sports: sports, startDate: startDate, endDate: endDate)
return filteredGames.compactMap { game in
richGame(from: game)
}
}
func richGame(from game: Game) -> RichGame? {
guard let homeTeam = teamsById[game.homeTeamId],
let awayTeam = teamsById[game.awayTeamId],
let stadium = stadiumsById[game.stadiumId] else {
return nil
}
return RichGame(game: game, homeTeam: homeTeam, awayTeam: awayTeam, stadium: stadium)
}
}
// MARK: - Convenience Extensions
extension MockAppDataProvider {
/// Load fixture data from FixtureGenerator
func loadFixtures(_ data: FixtureGenerator.GeneratedData) {
setTeams(data.teams)
setStadiums(data.stadiums)
setGames(data.games)
}
/// Create a mock provider with fixture data pre-loaded
static func withFixtures(_ config: FixtureGenerator.Configuration = .default) -> MockAppDataProvider {
let mock = MockAppDataProvider()
let data = FixtureGenerator.generate(with: config)
mock.loadFixtures(data)
return mock
}
/// Create a mock provider configured as empty
static var empty: MockAppDataProvider {
MockAppDataProvider(config: .empty)
}
/// Create a mock provider configured to fail
static var failing: MockAppDataProvider {
MockAppDataProvider(config: .failing)
}
}
// MARK: - Test Helpers
extension MockAppDataProvider {
/// Add a single game
func addGame(_ game: Game) {
games.append(game)
gamesById[game.id] = game
}
/// Add a single team
func addTeam(_ team: Team) {
teams.append(team)
teamsById[team.id] = team
}
/// Add a single stadium
func addStadium(_ stadium: Stadium) {
stadiums.append(stadium)
stadiumsById[stadium.id] = stadium
}
/// Get all games (for test verification)
func allGames() -> [Game] {
games
}
/// Get games count
var gamesCount: Int { games.count }
/// Get teams count
var teamsCount: Int { teams.count }
/// Get stadiums count
var stadiumsCount: Int { stadiums.count }
}