Search: FlightAware backbone, blob catalog, diagnostic infra
route-explorer's /api/token sits behind invisible Cloudflare Turnstile
that requires Apple's Private Access Token attestation. Third-party
iOS apps don't qualify for PAT issuance, and Linux Docker containers
can't pass it either (cross-OS fingerprint, even with patchright /
Camoufox). Migrates direct-flight search to FlightAware; multi-stop
and where-can-I-go remain via embedded SFSafariViewController.
- FlightAwareScheduleClient — scrapes route.rvt + trackpoll JSON for
real schedules without auth. T+0..2 day window. Tests against
captured HTML fixtures.
- BlobRouteClient — pulls the public Vercel blob route catalog
route-explorer's frontend reads (no auth, no Turnstile).
- DiagnosticLogger + LoggingURLSessionDelegate + DiagnosticsView —
device-shareable forensic trace. Boot header captures device, OS,
locale, UA; share-sheet export of session logs.
- TurnstileDebugView — live WKWebView gate inspector. Used to prove
the PAT-entitlement gap on a real device.
- RouteExplorerBrowserView — SFSafariViewController wrapper. Real
Safari clears Turnstile naturally; the in-app browser opens at
pre-filled search URLs. Surfaced from Search ("Open in
route-explorer") and Settings → Tools.
- RouteExplorerTokenStore + RouteExplorerSetupView — bookmarklet
capture flow (token round-tripped via flights://routeexplorer-token
URL scheme). Kept dormant for future use.
backend/ — Docker proxy attempts (Playwright, patchright, Camoufox).
All fail on Linux because Cloudflare auto-denies before the Turnstile
widget renders. Documented; kept as scaffolding for a future paid-
solver integration.
scripts/probe_flightaware.py — reference algorithm for the FA path.
scripts/probe_nodriver.py — local-Mac sanity check confirming the
gate clears with real macOS Chrome (proves the blocker is
fingerprint-level, not network-level).
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import XCTest
|
||||
import SwiftData
|
||||
@testable import Flights
|
||||
|
||||
/// Unit tests for `AirframeHistoryStore`.
|
||||
///
|
||||
/// We exercise the store against an in-memory `ModelContainer` seeded
|
||||
/// with `LoggedFlight` rows that vary by tail number, route, and date.
|
||||
/// All assertions reference the documented `AirframeStats` contract.
|
||||
@MainActor
|
||||
final class AirframeHistoryStoreTests: XCTestCase {
|
||||
|
||||
private var container: ModelContainer!
|
||||
private var context: ModelContext!
|
||||
private var store: AirframeHistoryStore!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
let schema = Schema([LoggedFlight.self])
|
||||
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
|
||||
container = try ModelContainer(for: schema, configurations: config)
|
||||
context = ModelContext(container)
|
||||
store = AirframeHistoryStore()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
store = nil
|
||||
context = nil
|
||||
container = nil
|
||||
try super.tearDownWithError()
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private static let epoch = Date(timeIntervalSince1970: 1_700_000_000)
|
||||
|
||||
private func date(_ dayOffset: Int) -> Date {
|
||||
Self.epoch.addingTimeInterval(TimeInterval(dayOffset) * 86_400)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func insert(
|
||||
registration: String?,
|
||||
origin: String,
|
||||
dest: String,
|
||||
flightDate: Date
|
||||
) -> LoggedFlight {
|
||||
let flight = LoggedFlight(
|
||||
flightDate: flightDate,
|
||||
departureIATA: origin,
|
||||
arrivalIATA: dest,
|
||||
registration: registration
|
||||
)
|
||||
context.insert(flight)
|
||||
return flight
|
||||
}
|
||||
|
||||
// MARK: - Tests
|
||||
|
||||
/// Empty store → empty stats sentinel.
|
||||
func test_stats_emptyContext_returnsEmpty() {
|
||||
let stats = store.stats(forTail: "N281WN", context: context)
|
||||
|
||||
XCTAssertEqual(stats.totalFlights, 0)
|
||||
XCTAssertTrue(stats.routes.isEmpty)
|
||||
XCTAssertNil(stats.firstSeen)
|
||||
XCTAssertNil(stats.lastSeen)
|
||||
XCTAssertNil(stats.mostCommonRoute)
|
||||
}
|
||||
|
||||
/// 3 flights on the same tail across 2 distinct routes — verify the
|
||||
/// aggregate counts and the "DAL→HOU (2 of 3)" most-common-route
|
||||
/// formatting.
|
||||
func test_stats_threeFlightsTwoRoutes_aggregatesCorrectly() {
|
||||
insert(registration: "N281WN", origin: "DAL", dest: "HOU", flightDate: date(0))
|
||||
insert(registration: "N281WN", origin: "DAL", dest: "HOU", flightDate: date(5))
|
||||
insert(registration: "N281WN", origin: "DAL", dest: "LAS", flightDate: date(10))
|
||||
// Other-tail noise — must not be counted.
|
||||
insert(registration: "N999AA", origin: "DAL", dest: "HOU", flightDate: date(2))
|
||||
|
||||
let stats = store.stats(forTail: "N281WN", context: context)
|
||||
|
||||
XCTAssertEqual(stats.totalFlights, 3)
|
||||
XCTAssertEqual(Set(stats.routes), Set(["DAL→HOU", "DAL→LAS"]))
|
||||
XCTAssertEqual(stats.routes.count, 2)
|
||||
XCTAssertEqual(stats.firstSeen, date(0))
|
||||
XCTAssertEqual(stats.lastSeen, date(10))
|
||||
XCTAssertEqual(stats.mostCommonRoute, "DAL→HOU (2 of 3)")
|
||||
}
|
||||
|
||||
/// Lookup tail must be normalized to uppercase — passing "n281wn"
|
||||
/// matches a stored "N281WN".
|
||||
func test_stats_lookupIsCaseInsensitive() {
|
||||
insert(registration: "N281WN", origin: "DAL", dest: "HOU", flightDate: date(0))
|
||||
|
||||
let stats = store.stats(forTail: "n281wn", context: context)
|
||||
|
||||
XCTAssertEqual(stats.totalFlights, 1)
|
||||
XCTAssertEqual(stats.mostCommonRoute, "DAL→HOU (1 of 1)")
|
||||
}
|
||||
|
||||
/// The store should still report stats for a single-flight tail. The
|
||||
/// History UI hides the section in that case, but the underlying
|
||||
/// store contract returns the real count.
|
||||
func test_stats_singleFlight_returnsTotalOne() {
|
||||
insert(registration: "N281WN", origin: "DAL", dest: "HOU", flightDate: date(0))
|
||||
|
||||
let stats = store.stats(forTail: "N281WN", context: context)
|
||||
|
||||
XCTAssertEqual(stats.totalFlights, 1)
|
||||
XCTAssertEqual(stats.routes, ["DAL→HOU"])
|
||||
XCTAssertEqual(stats.firstSeen, date(0))
|
||||
XCTAssertEqual(stats.lastSeen, date(0))
|
||||
XCTAssertEqual(stats.mostCommonRoute, "DAL→HOU (1 of 1)")
|
||||
}
|
||||
|
||||
/// Mixed-case stored registration: a record persisted with lowercase
|
||||
/// "n281wn" must still be discoverable when callers ask for
|
||||
/// "N281WN". Today the fast-path #Predicate misses (it compares
|
||||
/// exact bytes against the uppercased query) and the fallback
|
||||
/// table-scan recovers it. After Phase 3 fixes registration
|
||||
/// normalisation at write-time (or switches to a case-insensitive
|
||||
/// predicate), the fast path will hit — but this test should still
|
||||
/// pass either way.
|
||||
func test_stats_lowercaseStoredRegistration_isFoundViaFallback() {
|
||||
insert(registration: "n281wn", origin: "DAL", dest: "HOU", flightDate: date(0))
|
||||
|
||||
let stats = store.stats(forTail: "N281WN", context: context)
|
||||
|
||||
XCTAssertEqual(stats.totalFlights, 1)
|
||||
XCTAssertEqual(stats.routes, ["DAL→HOU"])
|
||||
XCTAssertEqual(stats.mostCommonRoute, "DAL→HOU (1 of 1)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user