This refactor fixes the achievement system by using stable canonical string IDs (e.g., "stadium_mlb_fenway_park") instead of random UUIDs. This ensures stadium mappings for achievements are consistent across app launches and CloudKit sync operations. Changes: - Stadium, Team, Game: id property changed from UUID to String - Trip, TripStop, TripPreferences: updated to use String IDs for games/stadiums - CKModels: removed UUID parsing, use canonical IDs directly - AchievementEngine: now matches against canonical stadium IDs - All test files updated to use String IDs instead of UUID() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
295 lines
8.4 KiB
Swift
295 lines
8.4 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: String) 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
|
|
)
|
|
}
|
|
}
|
|
|
|
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,
|
|
stadiumCanonicalId: team.stadiumId
|
|
)
|
|
}
|
|
}
|
|
|
|
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,
|
|
homeTeamCanonicalId: game.homeTeamId,
|
|
awayTeamCanonicalId: game.awayTeamId,
|
|
stadiumCanonicalId: game.stadiumId
|
|
)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|