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:
296
SportsTimeTests/Mocks/MockLocationService.swift
Normal file
296
SportsTimeTests/Mocks/MockLocationService.swift
Normal file
@@ -0,0 +1,296 @@
|
||||
//
|
||||
// MockLocationService.swift
|
||||
// SportsTimeTests
|
||||
//
|
||||
// Mock implementation of LocationService for testing without MapKit dependencies.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
import MapKit
|
||||
@testable import SportsTime
|
||||
|
||||
// MARK: - Mock Location Service
|
||||
|
||||
actor MockLocationService {
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
struct Configuration {
|
||||
var simulatedLatency: TimeInterval = 0
|
||||
var shouldFailGeocode: Bool = false
|
||||
var shouldFailRoute: Bool = false
|
||||
var defaultDrivingSpeedMPH: Double = 60.0
|
||||
var useHaversineForDistance: Bool = true
|
||||
|
||||
static var `default`: Configuration { Configuration() }
|
||||
static var slow: Configuration { Configuration(simulatedLatency: 1.0) }
|
||||
static var failingGeocode: Configuration { Configuration(shouldFailGeocode: true) }
|
||||
static var failingRoute: Configuration { Configuration(shouldFailRoute: true) }
|
||||
}
|
||||
|
||||
// MARK: - Pre-configured Responses
|
||||
|
||||
private var geocodeResponses: [String: CLLocationCoordinate2D] = [:]
|
||||
private var routeResponses: [String: RouteInfo] = [:]
|
||||
|
||||
// MARK: - Call Tracking
|
||||
|
||||
private(set) var geocodeCallCount = 0
|
||||
private(set) var reverseGeocodeCallCount = 0
|
||||
private(set) var calculateRouteCallCount = 0
|
||||
private(set) var searchLocationsCallCount = 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 setGeocodeResponse(for address: String, coordinate: CLLocationCoordinate2D) {
|
||||
geocodeResponses[address.lowercased()] = coordinate
|
||||
}
|
||||
|
||||
func setRouteResponse(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D, route: RouteInfo) {
|
||||
let key = routeKey(from: from, to: to)
|
||||
routeResponses[key] = route
|
||||
}
|
||||
|
||||
func reset() {
|
||||
geocodeResponses = [:]
|
||||
routeResponses = [:]
|
||||
geocodeCallCount = 0
|
||||
reverseGeocodeCallCount = 0
|
||||
calculateRouteCallCount = 0
|
||||
searchLocationsCallCount = 0
|
||||
config = .default
|
||||
}
|
||||
|
||||
// MARK: - Simulated Network
|
||||
|
||||
private func simulateNetwork() async throws {
|
||||
if config.simulatedLatency > 0 {
|
||||
try await Task.sleep(nanoseconds: UInt64(config.simulatedLatency * 1_000_000_000))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Geocoding
|
||||
|
||||
func geocode(_ address: String) async throws -> CLLocationCoordinate2D? {
|
||||
geocodeCallCount += 1
|
||||
try await simulateNetwork()
|
||||
|
||||
if config.shouldFailGeocode {
|
||||
throw LocationError.geocodingFailed
|
||||
}
|
||||
|
||||
// Check pre-configured responses
|
||||
if let coordinate = geocodeResponses[address.lowercased()] {
|
||||
return coordinate
|
||||
}
|
||||
|
||||
// Return nil for unknown addresses (simulating "not found")
|
||||
return nil
|
||||
}
|
||||
|
||||
func reverseGeocode(_ coordinate: CLLocationCoordinate2D) async throws -> String? {
|
||||
reverseGeocodeCallCount += 1
|
||||
try await simulateNetwork()
|
||||
|
||||
if config.shouldFailGeocode {
|
||||
throw LocationError.geocodingFailed
|
||||
}
|
||||
|
||||
// Return a simple formatted string based on coordinates
|
||||
return "Location at \(String(format: "%.2f", coordinate.latitude)), \(String(format: "%.2f", coordinate.longitude))"
|
||||
}
|
||||
|
||||
func resolveLocation(_ input: LocationInput) async throws -> LocationInput {
|
||||
if input.isResolved { return input }
|
||||
|
||||
let searchText = input.address ?? input.name
|
||||
guard let coordinate = try await geocode(searchText) else {
|
||||
throw LocationError.geocodingFailed
|
||||
}
|
||||
|
||||
return LocationInput(
|
||||
name: input.name,
|
||||
coordinate: coordinate,
|
||||
address: input.address
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Location Search
|
||||
|
||||
func searchLocations(_ query: String) async throws -> [LocationSearchResult] {
|
||||
searchLocationsCallCount += 1
|
||||
try await simulateNetwork()
|
||||
|
||||
if config.shouldFailGeocode {
|
||||
return []
|
||||
}
|
||||
|
||||
// Check if we have a pre-configured response for this query
|
||||
if let coordinate = geocodeResponses[query.lowercased()] {
|
||||
return [
|
||||
LocationSearchResult(
|
||||
name: query,
|
||||
address: "Mocked Address",
|
||||
coordinate: coordinate
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
// MARK: - Distance Calculations
|
||||
|
||||
func calculateDistance(
|
||||
from: CLLocationCoordinate2D,
|
||||
to: CLLocationCoordinate2D
|
||||
) -> CLLocationDistance {
|
||||
if config.useHaversineForDistance {
|
||||
return haversineDistance(from: from, to: to)
|
||||
}
|
||||
|
||||
// Simple Euclidean approximation (less accurate but faster)
|
||||
let fromLocation = CLLocation(latitude: from.latitude, longitude: from.longitude)
|
||||
let toLocation = CLLocation(latitude: to.latitude, longitude: to.longitude)
|
||||
return fromLocation.distance(from: toLocation)
|
||||
}
|
||||
|
||||
func calculateDrivingRoute(
|
||||
from: CLLocationCoordinate2D,
|
||||
to: CLLocationCoordinate2D
|
||||
) async throws -> RouteInfo {
|
||||
calculateRouteCallCount += 1
|
||||
try await simulateNetwork()
|
||||
|
||||
if config.shouldFailRoute {
|
||||
throw LocationError.routeNotFound
|
||||
}
|
||||
|
||||
// Check pre-configured routes
|
||||
let key = routeKey(from: from, to: to)
|
||||
if let route = routeResponses[key] {
|
||||
return route
|
||||
}
|
||||
|
||||
// Generate estimated route based on haversine distance
|
||||
let distanceMeters = haversineDistance(from: from, to: to)
|
||||
let distanceMiles = distanceMeters * 0.000621371
|
||||
|
||||
// Estimate driving time (add 20% for real-world conditions)
|
||||
let drivingHours = (distanceMiles / config.defaultDrivingSpeedMPH) * 1.2
|
||||
let travelTimeSeconds = drivingHours * 3600
|
||||
|
||||
return RouteInfo(
|
||||
distance: distanceMeters,
|
||||
expectedTravelTime: travelTimeSeconds,
|
||||
polyline: nil
|
||||
)
|
||||
}
|
||||
|
||||
func calculateDrivingMatrix(
|
||||
origins: [CLLocationCoordinate2D],
|
||||
destinations: [CLLocationCoordinate2D]
|
||||
) async throws -> [[RouteInfo?]] {
|
||||
var matrix: [[RouteInfo?]] = []
|
||||
|
||||
for origin in origins {
|
||||
var row: [RouteInfo?] = []
|
||||
for destination in destinations {
|
||||
do {
|
||||
let route = try await calculateDrivingRoute(from: origin, to: destination)
|
||||
row.append(route)
|
||||
} catch {
|
||||
row.append(nil)
|
||||
}
|
||||
}
|
||||
matrix.append(row)
|
||||
}
|
||||
|
||||
return matrix
|
||||
}
|
||||
|
||||
// MARK: - Haversine Distance
|
||||
|
||||
/// Calculate haversine distance between two coordinates in meters
|
||||
private func haversineDistance(
|
||||
from: CLLocationCoordinate2D,
|
||||
to: CLLocationCoordinate2D
|
||||
) -> CLLocationDistance {
|
||||
let earthRadiusMeters: Double = 6371000.0
|
||||
|
||||
let lat1 = from.latitude * .pi / 180
|
||||
let lat2 = to.latitude * .pi / 180
|
||||
let deltaLat = (to.latitude - from.latitude) * .pi / 180
|
||||
let deltaLon = (to.longitude - from.longitude) * .pi / 180
|
||||
|
||||
let a = sin(deltaLat / 2) * sin(deltaLat / 2) +
|
||||
cos(lat1) * cos(lat2) *
|
||||
sin(deltaLon / 2) * sin(deltaLon / 2)
|
||||
let c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||
|
||||
return earthRadiusMeters * c
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func routeKey(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D) -> String {
|
||||
"\(from.latitude),\(from.longitude)->\(to.latitude),\(to.longitude)"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience Extensions
|
||||
|
||||
extension MockLocationService {
|
||||
/// Pre-configure common city geocoding responses
|
||||
func loadCommonCities() async {
|
||||
await setGeocodeResponse(for: "New York, NY", coordinate: FixtureGenerator.KnownLocations.nyc)
|
||||
await setGeocodeResponse(for: "Los Angeles, CA", coordinate: FixtureGenerator.KnownLocations.la)
|
||||
await setGeocodeResponse(for: "Chicago, IL", coordinate: FixtureGenerator.KnownLocations.chicago)
|
||||
await setGeocodeResponse(for: "Boston, MA", coordinate: FixtureGenerator.KnownLocations.boston)
|
||||
await setGeocodeResponse(for: "Miami, FL", coordinate: FixtureGenerator.KnownLocations.miami)
|
||||
await setGeocodeResponse(for: "Seattle, WA", coordinate: FixtureGenerator.KnownLocations.seattle)
|
||||
await setGeocodeResponse(for: "Denver, CO", coordinate: FixtureGenerator.KnownLocations.denver)
|
||||
}
|
||||
|
||||
/// Create a mock service with common cities pre-loaded
|
||||
static func withCommonCities() async -> MockLocationService {
|
||||
let mock = MockLocationService()
|
||||
await mock.loadCommonCities()
|
||||
return mock
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test Helpers
|
||||
|
||||
extension MockLocationService {
|
||||
/// Calculate expected travel time in hours for a given distance
|
||||
func expectedTravelHours(distanceMiles: Double) -> Double {
|
||||
(distanceMiles / config.defaultDrivingSpeedMPH) * 1.2
|
||||
}
|
||||
|
||||
/// Check if a coordinate is within radius of another
|
||||
func isWithinRadius(
|
||||
_ coordinate: CLLocationCoordinate2D,
|
||||
of center: CLLocationCoordinate2D,
|
||||
radiusMiles: Double
|
||||
) -> Bool {
|
||||
let distanceMeters = haversineDistance(from: center, to: coordinate)
|
||||
let distanceMiles = distanceMeters * 0.000621371
|
||||
return distanceMiles <= radiusMiles
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user