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>
265 lines
6.8 KiB
Swift
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 }
|
|
}
|