Files
Sportstime/SportsTimeTests/ItineraryConstraintsTests.swift
Trey t c94e373e33 fix: comprehensive codebase hardening — crashes, silent failures, performance, and security
Fixes ~95 issues from deep audit across 12 categories in 82 files:

- Crash prevention: double-resume in PhotoMetadataExtractor, force unwraps in
  DateRangePicker, array bounds checks in polls/achievements, ProGate hit-test
  bypass, Dictionary(uniqueKeysWithValues:) → uniquingKeysWith in 4 files
- Silent failure elimination: all 34 try? sites replaced with do/try/catch +
  logging (SavedTrip, TripDetailView, CanonicalSyncService, BootstrapService,
  CanonicalModels, CKModels, SportsTimeApp, and more)
- Performance: cached DateFormatters (7 files), O(1) team lookups via
  AppDataProvider, achievement definition dictionary, AnimatedBackground
  consolidated from 19 Tasks to 1, task cancellation in SharePreviewView
- Concurrency: UIKit drawing → MainActor.run, background fetch timeout guard,
  @MainActor on ThemeManager/AppearanceManager, SyncLogger read/write race fix
- Planning engine: game end time in travel feasibility, state-aware city
  normalization, exact city matching, DrivingConstraints parameter propagation
- IAP: unknown subscription states → expired, unverified transaction logging,
  entitlements updated before paywall dismiss, restore visible to all users
- Security: API key to Info.plist lookup, filename sanitization in PDF export,
  honest User-Agent, removed stale "Feels" analytics super properties
- Navigation: consolidated competing navigationDestination, boolean → value-based
- Testing: 8 sleep() → waitForExistence, duplicates extracted, Swift 6 compat
- Service bugs: infinite retry cap, duplicate achievement prevention, TOCTOU vote
  fix, PollVote.odg → voterId rename, deterministic placeholder IDs, parallel
  MKDirections, Sendable-safe POI struct

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 17:03:09 -06:00

250 lines
10 KiB
Swift

import XCTest
@testable import SportsTime
@MainActor
final class ItineraryConstraintsTests: XCTestCase {
// MARK: - Custom Item Tests (No Constraints)
func test_customItem_canGoOnAnyDay() {
// Given: A 5-day trip with games on days 1 and 5
let constraints = makeConstraints(tripDays: 5, gameDays: [1, 5])
let customItem = makeCustomItem(day: 1, sortOrder: 50)
// When/Then: Custom item can go on any day
XCTAssertTrue(constraints.isValidPosition(for: customItem, day: 1, sortOrder: 50))
XCTAssertTrue(constraints.isValidPosition(for: customItem, day: 2, sortOrder: 50))
XCTAssertTrue(constraints.isValidPosition(for: customItem, day: 3, sortOrder: 50))
XCTAssertTrue(constraints.isValidPosition(for: customItem, day: 4, sortOrder: 50))
XCTAssertTrue(constraints.isValidPosition(for: customItem, day: 5, sortOrder: 50))
}
func test_customItem_canGoBeforeOrAfterGames() {
// Given: A day with a game at sortOrder 100
let constraints = makeConstraints(tripDays: 3, gameDays: [2])
let customItem = makeCustomItem(day: 2, sortOrder: 50)
// When/Then: Custom item can go before or after game
XCTAssertTrue(constraints.isValidPosition(for: customItem, day: 2, sortOrder: 50)) // Before
XCTAssertTrue(constraints.isValidPosition(for: customItem, day: 2, sortOrder: 150)) // After
}
// MARK: - Travel Constraint Tests
func test_travel_validDayRange_simpleCase() {
// Given: Chicago game Day 1, Detroit game Day 3
let constraints = makeConstraints(
tripDays: 5,
games: [
makeGameItem(city: "Chicago", day: 1),
makeGameItem(city: "Detroit", day: 3)
]
)
let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 1, sortOrder: 200)
// When
let range = constraints.validDayRange(for: travel)
// Then: Travel can be on days 1 (after game), 2, or 3 (before game)
XCTAssertEqual(range, 1...3)
}
func test_travel_mustBeAfterDepartureGames() {
// Given: Chicago game on Day 1
// Visual boundary is 0: sortOrder < 0 = before games, sortOrder >= 0 = after games
let constraints = makeConstraints(
tripDays: 3,
games: [makeGameItem(city: "Chicago", day: 1)]
)
let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 1, sortOrder: -1)
// When/Then: Travel before game (negative sortOrder) is invalid on departure day
XCTAssertFalse(constraints.isValidPosition(for: travel, day: 1, sortOrder: -1))
// Travel after game (non-negative sortOrder) is valid
XCTAssertTrue(constraints.isValidPosition(for: travel, day: 1, sortOrder: 1))
}
func test_travel_mustBeBeforeArrivalGames() {
// Given: Detroit game on Day 3
// Visual boundary is 0: sortOrder < 0 = before games, sortOrder >= 0 = after games
let constraints = makeConstraints(
tripDays: 3,
games: [makeGameItem(city: "Detroit", day: 3)]
)
let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 3, sortOrder: 1)
// When/Then: Travel after arrival game (non-negative sortOrder) is invalid on arrival day
XCTAssertFalse(constraints.isValidPosition(for: travel, day: 3, sortOrder: 1))
// Travel before game (negative sortOrder) is valid
XCTAssertTrue(constraints.isValidPosition(for: travel, day: 3, sortOrder: -1))
}
func test_travel_canBeAnywhereOnRestDays() {
// Given: Chicago game Day 1, Detroit game Day 4, rest days 2-3
let constraints = makeConstraints(
tripDays: 4,
games: [
makeGameItem(city: "Chicago", day: 1),
makeGameItem(city: "Detroit", day: 4)
]
)
let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 2, sortOrder: 50)
// When/Then: Any position on rest days is valid
XCTAssertTrue(constraints.isValidPosition(for: travel, day: 2, sortOrder: 1))
XCTAssertTrue(constraints.isValidPosition(for: travel, day: 2, sortOrder: 100))
XCTAssertTrue(constraints.isValidPosition(for: travel, day: 2, sortOrder: 500))
XCTAssertTrue(constraints.isValidPosition(for: travel, day: 3, sortOrder: 50))
}
func test_travel_mustBeAfterAllDepartureGamesOnSameDay() {
// Given: Two games in Chicago on Day 1 (1pm and 7pm)
let constraints = makeConstraints(
tripDays: 3,
games: [
makeGameItem(city: "Chicago", day: 1, sortOrder: 100), // 1pm
makeGameItem(city: "Chicago", day: 1, sortOrder: 101) // 7pm
]
)
let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 1, sortOrder: 100.5)
// When/Then: Between games is invalid
XCTAssertFalse(constraints.isValidPosition(for: travel, day: 1, sortOrder: 100.5))
// After all games is valid
XCTAssertTrue(constraints.isValidPosition(for: travel, day: 1, sortOrder: 150))
}
func test_travel_mustBeBeforeAllArrivalGamesOnSameDay() {
// Given: Two games in Detroit on Day 3 (1pm and 7pm)
let constraints = makeConstraints(
tripDays: 3,
games: [
makeGameItem(city: "Detroit", day: 3, sortOrder: 100), // 1pm
makeGameItem(city: "Detroit", day: 3, sortOrder: 101) // 7pm
]
)
let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 3, sortOrder: 100.5)
// When/Then: Between games is invalid
XCTAssertFalse(constraints.isValidPosition(for: travel, day: 3, sortOrder: 100.5))
// Before all games is valid
XCTAssertTrue(constraints.isValidPosition(for: travel, day: 3, sortOrder: 50))
}
func test_travel_cannotGoOutsideValidDayRange() {
// Given: Chicago game Day 2, Detroit game Day 4
let constraints = makeConstraints(
tripDays: 5,
games: [
makeGameItem(city: "Chicago", day: 2),
makeGameItem(city: "Detroit", day: 4)
]
)
let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 1, sortOrder: 50)
// When/Then: Day 1 (before departure game) is invalid
XCTAssertFalse(constraints.isValidPosition(for: travel, day: 1, sortOrder: 50))
// Day 5 (after arrival game) is invalid
XCTAssertFalse(constraints.isValidPosition(for: travel, day: 5, sortOrder: 50))
}
// MARK: - Game Immutability Tests
func test_gameItem_cannotBeMoved() {
// Given: A game item
let gameItem = makeGameItem(city: "Chicago", day: 2)
let constraints = makeConstraints(tripDays: 5, games: [gameItem])
// When/Then: Game items should never be valid for any position
XCTAssertFalse(constraints.isValidPosition(for: gameItem, day: 2, sortOrder: 100))
XCTAssertFalse(constraints.isValidPosition(for: gameItem, day: 3, sortOrder: 100))
XCTAssertFalse(constraints.isValidPosition(for: gameItem, day: 1, sortOrder: 50))
}
// MARK: - Edge Case Tests
func test_travel_validDayRange_returnsNil_whenConstraintsImpossible() {
// Given: Departure game on Day 3, Arrival game on Day 1 (reversed order)
let constraints = makeConstraints(
tripDays: 3,
games: [
makeGameItem(city: "Chicago", day: 3), // Must depart AFTER day 3
makeGameItem(city: "Detroit", day: 1) // Must arrive BEFORE day 1
]
)
let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 2, sortOrder: 50)
// When/Then: No valid range exists (impossible constraints)
XCTAssertNil(constraints.validDayRange(for: travel))
}
// MARK: - Barrier Games Tests
func test_barrierGames_returnsDepartureAndArrivalGames() {
// Given: Chicago games Days 1-2, Detroit games Days 4-5
let chicagoGame1 = makeGameItem(city: "Chicago", day: 1, sortOrder: 100)
let chicagoGame2 = makeGameItem(city: "Chicago", day: 2, sortOrder: 100)
let detroitGame1 = makeGameItem(city: "Detroit", day: 4, sortOrder: 100)
let detroitGame2 = makeGameItem(city: "Detroit", day: 5, sortOrder: 100)
let constraints = makeConstraints(
tripDays: 5,
games: [chicagoGame1, chicagoGame2, detroitGame1, detroitGame2]
)
let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 3, sortOrder: 50)
// When
let barriers = constraints.barrierGames(for: travel)
// Then: Returns the last Chicago game and first Detroit game
XCTAssertEqual(barriers.count, 2)
XCTAssertTrue(barriers.contains { $0.id == chicagoGame2.id })
XCTAssertTrue(barriers.contains { $0.id == detroitGame1.id })
}
// MARK: - Helpers
private let testTripId = UUID()
private func makeConstraints(tripDays: Int, gameDays: [Int] = []) -> ItineraryConstraints {
let games = gameDays.map { makeGameItem(city: "TestCity", day: $0) }
return makeConstraints(tripDays: tripDays, games: games)
}
private func makeConstraints(tripDays: Int, games: [ItineraryItem]) -> ItineraryConstraints {
return ItineraryConstraints(tripDayCount: tripDays, items: games)
}
private func makeGameItem(city: String, day: Int, sortOrder: Double = 0) -> ItineraryItem {
return ItineraryItem(
tripId: testTripId,
day: day,
sortOrder: sortOrder,
kind: .game(gameId: "game-\(city)-\(UUID().uuidString.prefix(4))", city: city)
)
}
private func makeTravelItem(from: String, to: String, day: Int, sortOrder: Double) -> ItineraryItem {
return ItineraryItem(
tripId: testTripId,
day: day,
sortOrder: sortOrder,
kind: .travel(TravelInfo(fromCity: from, toCity: to))
)
}
private func makeCustomItem(day: Int, sortOrder: Double) -> ItineraryItem {
return ItineraryItem(
tripId: testTripId,
day: day,
sortOrder: sortOrder,
kind: .custom(CustomInfo(title: "Test Item", icon: "star"))
)
}
}