Files
Sportstime/SportsTimeTests/Mocks/MockCloudKitService.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

295 lines
8.5 KiB
Swift

//
// 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)
}
}