- Add allRichGames method to DataProvider - Update TripCreationViewModel.loadGamesForBrowsing to use allGames (removes 90-day limit) - Update MockCloudKitService sync methods to use new delta sync signatures - Update MockAppDataProvider with renamed methods and new allGames/allRichGames - Fix all callers: ScheduleViewModel, TripCreationViewModel, SuggestedTripsGenerator, GameMatcher - Update CLAUDE.md documentation with new method names This completes the delta sync implementation: - CloudKit sync now uses modificationDate for proper delta sync - First sync fetches ALL data, subsequent syncs only fetch modified records - "By Games" mode now shows all available games (not just 90 days) - All data types (stadiums, teams, games) use consistent delta sync pattern Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
291 lines
7.7 KiB
Swift
291 lines
7.7 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: [String: Team] = [:]
|
|
private var stadiumsById: [String: Stadium] = [:]
|
|
private var games: [Game] = []
|
|
private var gamesById: [String: 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 filterGamesCallCount = 0
|
|
private(set) var filterRichGamesCallCount = 0
|
|
private(set) var allGamesCallCount = 0
|
|
private(set) var allRichGamesCallCount = 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
|
|
filterGamesCallCount = 0
|
|
filterRichGamesCallCount = 0
|
|
allGamesCallCount = 0
|
|
allRichGamesCallCount = 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: String) -> Team? {
|
|
teamsById[id]
|
|
}
|
|
|
|
func stadium(for id: String) -> Stadium? {
|
|
stadiumsById[id]
|
|
}
|
|
|
|
func teams(for sport: Sport) -> [Team] {
|
|
teams.filter { $0.sport == sport }
|
|
}
|
|
|
|
// MARK: - Game Filtering (Local Queries)
|
|
|
|
func filterGames(sports: Set<Sport>, startDate: Date, endDate: Date) async throws -> [Game] {
|
|
filterGamesCallCount += 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 allGames(for sports: Set<Sport>) async throws -> [Game] {
|
|
allGamesCallCount += 1
|
|
await simulateLatency()
|
|
|
|
if config.shouldFailOnFetch {
|
|
throw DataProviderError.contextNotConfigured
|
|
}
|
|
|
|
return games.filter { game in
|
|
sports.contains(game.sport)
|
|
}.sorted { $0.dateTime < $1.dateTime }
|
|
}
|
|
|
|
func fetchGame(by id: String) async throws -> Game? {
|
|
await simulateLatency()
|
|
|
|
if config.shouldFailOnFetch {
|
|
throw DataProviderError.contextNotConfigured
|
|
}
|
|
|
|
return gamesById[id]
|
|
}
|
|
|
|
func filterRichGames(sports: Set<Sport>, startDate: Date, endDate: Date) async throws -> [RichGame] {
|
|
filterRichGamesCallCount += 1
|
|
let filteredGames = try await filterGames(sports: sports, startDate: startDate, endDate: endDate)
|
|
|
|
return filteredGames.compactMap { game in
|
|
richGame(from: game)
|
|
}
|
|
}
|
|
|
|
func allRichGames(for sports: Set<Sport>) async throws -> [RichGame] {
|
|
allRichGamesCallCount += 1
|
|
let allFilteredGames = try await allGames(for: sports)
|
|
|
|
return allFilteredGames.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 stored games (for test verification)
|
|
func getAllStoredGames() -> [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 }
|
|
}
|