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>
This commit is contained in:
294
SportsTimeTests/Mocks/MockCloudKitService.swift
Normal file
294
SportsTimeTests/Mocks/MockCloudKitService.swift
Normal file
@@ -0,0 +1,294 @@
|
||||
//
|
||||
// MockCloudKitService.swift
|
||||
// SportsTimeTests
|
||||
//
|
||||
// Mock implementation of CloudKitService for testing without network dependencies.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@testable import SportsTime
|
||||
|
||||
// MARK: - Mock CloudKit Service
|
||||
|
||||
actor MockCloudKitService {
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
struct Configuration {
|
||||
var isAvailable: Bool = true
|
||||
var simulatedLatency: TimeInterval = 0
|
||||
var shouldFailWithError: CloudKitError? = nil
|
||||
var errorAfterNCalls: Int? = nil
|
||||
|
||||
static var `default`: Configuration { Configuration() }
|
||||
static var offline: Configuration { Configuration(isAvailable: false) }
|
||||
static var slow: Configuration { Configuration(simulatedLatency: 2.0) }
|
||||
}
|
||||
|
||||
// MARK: - Stored Data
|
||||
|
||||
private var stadiums: [Stadium] = []
|
||||
private var teams: [Team] = []
|
||||
private var games: [Game] = []
|
||||
private var leagueStructure: [LeagueStructureModel] = []
|
||||
private var teamAliases: [TeamAlias] = []
|
||||
private var stadiumAliases: [StadiumAlias] = []
|
||||
|
||||
// MARK: - Call Tracking
|
||||
|
||||
private(set) var fetchStadiumsCallCount = 0
|
||||
private(set) var fetchTeamsCallCount = 0
|
||||
private(set) var fetchGamesCallCount = 0
|
||||
private(set) var isAvailableCallCount = 0
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
private var config: Configuration
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(config: Configuration = .default) {
|
||||
self.config = config
|
||||
}
|
||||
|
||||
// MARK: - Configuration Methods
|
||||
|
||||
func configure(_ newConfig: Configuration) {
|
||||
self.config = newConfig
|
||||
}
|
||||
|
||||
func setStadiums(_ stadiums: [Stadium]) {
|
||||
self.stadiums = stadiums
|
||||
}
|
||||
|
||||
func setTeams(_ teams: [Team]) {
|
||||
self.teams = teams
|
||||
}
|
||||
|
||||
func setGames(_ games: [Game]) {
|
||||
self.games = games
|
||||
}
|
||||
|
||||
func setLeagueStructure(_ structure: [LeagueStructureModel]) {
|
||||
self.leagueStructure = structure
|
||||
}
|
||||
|
||||
func reset() {
|
||||
stadiums = []
|
||||
teams = []
|
||||
games = []
|
||||
leagueStructure = []
|
||||
teamAliases = []
|
||||
stadiumAliases = []
|
||||
fetchStadiumsCallCount = 0
|
||||
fetchTeamsCallCount = 0
|
||||
fetchGamesCallCount = 0
|
||||
isAvailableCallCount = 0
|
||||
config = .default
|
||||
}
|
||||
|
||||
// MARK: - Simulated Network
|
||||
|
||||
private func simulateNetwork() async throws {
|
||||
// Simulate latency
|
||||
if config.simulatedLatency > 0 {
|
||||
try await Task.sleep(nanoseconds: UInt64(config.simulatedLatency * 1_000_000_000))
|
||||
}
|
||||
|
||||
// Check for configured error
|
||||
if let error = config.shouldFailWithError {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private func checkErrorAfterNCalls(_ callCount: Int) throws {
|
||||
if let errorAfterN = config.errorAfterNCalls, callCount >= errorAfterN {
|
||||
throw config.shouldFailWithError ?? CloudKitError.networkUnavailable
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Availability
|
||||
|
||||
func isAvailable() async -> Bool {
|
||||
isAvailableCallCount += 1
|
||||
return config.isAvailable
|
||||
}
|
||||
|
||||
func checkAvailabilityWithError() async throws {
|
||||
if !config.isAvailable {
|
||||
throw CloudKitError.networkUnavailable
|
||||
}
|
||||
if let error = config.shouldFailWithError {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fetch Operations
|
||||
|
||||
func fetchStadiums() async throws -> [Stadium] {
|
||||
fetchStadiumsCallCount += 1
|
||||
try checkErrorAfterNCalls(fetchStadiumsCallCount)
|
||||
try await simulateNetwork()
|
||||
return stadiums
|
||||
}
|
||||
|
||||
func fetchTeams(for sport: Sport) async throws -> [Team] {
|
||||
fetchTeamsCallCount += 1
|
||||
try checkErrorAfterNCalls(fetchTeamsCallCount)
|
||||
try await simulateNetwork()
|
||||
return teams.filter { $0.sport == sport }
|
||||
}
|
||||
|
||||
func fetchGames(
|
||||
sports: Set<Sport>,
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
) async throws -> [Game] {
|
||||
fetchGamesCallCount += 1
|
||||
try checkErrorAfterNCalls(fetchGamesCallCount)
|
||||
try await simulateNetwork()
|
||||
|
||||
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? {
|
||||
try await simulateNetwork()
|
||||
return games.first { $0.id == id }
|
||||
}
|
||||
|
||||
// MARK: - Sync Fetch Methods
|
||||
|
||||
func fetchStadiumsForSync() async throws -> [CloudKitService.SyncStadium] {
|
||||
try await simulateNetwork()
|
||||
return stadiums.map { stadium in
|
||||
CloudKitService.SyncStadium(
|
||||
stadium: stadium,
|
||||
canonicalId: stadium.id.uuidString
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchTeamsForSync(for sport: Sport) async throws -> [CloudKitService.SyncTeam] {
|
||||
try await simulateNetwork()
|
||||
return teams.filter { $0.sport == sport }.map { team in
|
||||
CloudKitService.SyncTeam(
|
||||
team: team,
|
||||
canonicalId: team.id.uuidString,
|
||||
stadiumCanonicalId: team.stadiumId.uuidString
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchGamesForSync(
|
||||
sports: Set<Sport>,
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
) async throws -> [CloudKitService.SyncGame] {
|
||||
try await simulateNetwork()
|
||||
|
||||
return games.filter { game in
|
||||
sports.contains(game.sport) &&
|
||||
game.dateTime >= startDate &&
|
||||
game.dateTime <= endDate
|
||||
}.map { game in
|
||||
CloudKitService.SyncGame(
|
||||
game: game,
|
||||
canonicalId: game.id.uuidString,
|
||||
homeTeamCanonicalId: game.homeTeamId.uuidString,
|
||||
awayTeamCanonicalId: game.awayTeamId.uuidString,
|
||||
stadiumCanonicalId: game.stadiumId.uuidString
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - League Structure & Aliases
|
||||
|
||||
func fetchLeagueStructure(for sport: Sport? = nil) async throws -> [LeagueStructureModel] {
|
||||
try await simulateNetwork()
|
||||
if let sport = sport {
|
||||
return leagueStructure.filter { $0.sport == sport.rawValue }
|
||||
}
|
||||
return leagueStructure
|
||||
}
|
||||
|
||||
func fetchTeamAliases(for teamCanonicalId: String? = nil) async throws -> [TeamAlias] {
|
||||
try await simulateNetwork()
|
||||
if let teamId = teamCanonicalId {
|
||||
return teamAliases.filter { $0.teamCanonicalId == teamId }
|
||||
}
|
||||
return teamAliases
|
||||
}
|
||||
|
||||
func fetchStadiumAliases(for stadiumCanonicalId: String? = nil) async throws -> [StadiumAlias] {
|
||||
try await simulateNetwork()
|
||||
if let stadiumId = stadiumCanonicalId {
|
||||
return stadiumAliases.filter { $0.stadiumCanonicalId == stadiumId }
|
||||
}
|
||||
return stadiumAliases
|
||||
}
|
||||
|
||||
// MARK: - Delta Sync
|
||||
|
||||
func fetchLeagueStructureChanges(since lastSync: Date?) async throws -> [LeagueStructureModel] {
|
||||
try await simulateNetwork()
|
||||
guard let lastSync = lastSync else {
|
||||
return leagueStructure
|
||||
}
|
||||
return leagueStructure.filter { $0.lastModified > lastSync }
|
||||
}
|
||||
|
||||
func fetchTeamAliasChanges(since lastSync: Date?) async throws -> [TeamAlias] {
|
||||
try await simulateNetwork()
|
||||
guard let lastSync = lastSync else {
|
||||
return teamAliases
|
||||
}
|
||||
return teamAliases.filter { $0.lastModified > lastSync }
|
||||
}
|
||||
|
||||
func fetchStadiumAliasChanges(since lastSync: Date?) async throws -> [StadiumAlias] {
|
||||
try await simulateNetwork()
|
||||
guard let lastSync = lastSync else {
|
||||
return stadiumAliases
|
||||
}
|
||||
return stadiumAliases.filter { $0.lastModified > lastSync }
|
||||
}
|
||||
|
||||
// MARK: - Subscriptions (No-ops for testing)
|
||||
|
||||
func subscribeToScheduleUpdates() async throws {}
|
||||
func subscribeToLeagueStructureUpdates() async throws {}
|
||||
func subscribeToTeamAliasUpdates() async throws {}
|
||||
func subscribeToStadiumAliasUpdates() async throws {}
|
||||
func subscribeToAllUpdates() async throws {}
|
||||
}
|
||||
|
||||
// MARK: - Convenience Extensions
|
||||
|
||||
extension MockCloudKitService {
|
||||
/// Load fixture data from FixtureGenerator
|
||||
func loadFixtures(_ data: FixtureGenerator.GeneratedData) {
|
||||
Task {
|
||||
await setStadiums(data.stadiums)
|
||||
await setTeams(data.teams)
|
||||
await setGames(data.games)
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure to simulate specific error scenarios
|
||||
static func withError(_ error: CloudKitError) -> MockCloudKitService {
|
||||
let mock = MockCloudKitService()
|
||||
Task {
|
||||
await mock.configure(Configuration(shouldFailWithError: error))
|
||||
}
|
||||
return mock
|
||||
}
|
||||
|
||||
/// Configure to be offline
|
||||
static var offline: MockCloudKitService {
|
||||
MockCloudKitService(config: .offline)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user