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:
Trey T
2026-06-06 01:09:59 -05:00
parent d122c95342
commit ba0688a412
70 changed files with 89096 additions and 209 deletions
@@ -0,0 +1,142 @@
import XCTest
@testable import Flights
/// Tests for ``FlightAwareScheduleClient``. The two pure-parser entry
/// points (``parseIdents`` and ``extractTrackpollBlob``) are exercised
/// directly against fixture HTML captured from a live request this
/// catches FlightAware schema drift the moment it happens (route.rvt or
/// trackpoll layout changes) instead of finding out via empty search
/// results in production.
///
/// Fixtures live next to this file under `Fixtures/`. They're real
/// HTML pages saved verbatim from FlightAware, not synthetic markup,
/// so the tests assert against the actual shapes the parser sees.
final class FlightAwareScheduleClientTests: XCTestCase {
// MARK: - Fixture loading
/// Reads a file from the `Fixtures/` directory sibling to this test
/// source file. Avoids needing the test target's pbxproj to declare
/// a Resources phase `#filePath` resolves to the real source path
/// at test-run time.
private func loadFixture(_ name: String, file: StaticString = #filePath) throws -> String {
let here = URL(fileURLWithPath: String(describing: file))
let url = here.deletingLastPathComponent()
.appendingPathComponent("Fixtures")
.appendingPathComponent(name)
return try String(contentsOf: url, encoding: .utf8)
}
// MARK: - parseIdents
func test_parseIdents_extractsFlightIdent_fromRouteAnalysisPage() throws {
let html = try loadFixture("DFW_EHAM_route.html")
let idents = FlightAwareScheduleClient.parseIdents(routeHTML: html)
XCTAssertFalse(idents.isEmpty,
"Should find at least one operating ident on DFW->AMS route page.")
XCTAssertTrue(idents.contains("AAL220"),
"AAL220 (AA daily 777-200 DFW->AMS) must surface; got \(idents)")
}
func test_parseIdents_dedupesRepeatedIdents() throws {
let html = try loadFixture("DFW_EHAM_route.html")
let idents = FlightAwareScheduleClient.parseIdents(routeHTML: html)
XCTAssertEqual(idents.count, Set(idents).count,
"Returned idents should be deduped; got \(idents)")
}
func test_parseIdents_returnsEmpty_whenNoRoutesPresent() {
let empty = """
<html><body><table>
<tr><th>Filed Time</th><th>Ident</th></tr>
<tr><td>No data</td></tr>
</table></body></html>
"""
XCTAssertEqual(
FlightAwareScheduleClient.parseIdents(routeHTML: empty),
[],
"Page with no flight rows should produce an empty list, not crash."
)
}
// MARK: - extractTrackpollBlob
func test_extractTrackpollBlob_returnsParseableJSON() throws {
let html = try loadFixture("AAL220_trackpoll.html")
guard let blob = FlightAwareScheduleClient.extractTrackpollBlob(from: html) else {
XCTFail("Should extract trackpollBootstrap from AAL220 page")
return
}
XCTAssertTrue(blob.hasPrefix("{") && blob.hasSuffix("}"),
"Extracted blob should be a JSON object literal")
// Round-trip through JSONDecoder to confirm shape.
XCTAssertNoThrow(
try JSONDecoder().decode(TrackpollBootstrap.self, from: Data(blob.utf8)),
"Extracted JSON should decode against TrackpollBootstrap schema"
)
}
func test_extractTrackpollBlob_returnsNil_whenMarkerMissing() {
let html = "<html><body>no script here</body></html>"
XCTAssertNil(FlightAwareScheduleClient.extractTrackpollBlob(from: html))
}
func test_extractTrackpollBlob_isStringContentAware() {
// A closing brace inside a string literal must NOT terminate the scan.
let html = #"""
<script>var trackpollBootstrap = {"a":"} not the end","b":1};</script>
"""#
let blob = FlightAwareScheduleClient.extractTrackpollBlob(from: html)
XCTAssertEqual(
blob,
#"{"a":"} not the end","b":1}"#,
"Braces inside JSON strings must not break the brace-balance scan."
)
}
// MARK: - ident decomposition
func test_identCarrierICAO_stripsTrailingDigits() {
XCTAssertEqual(FlightAwareScheduleClient.identCarrierICAO("AAL220"), "AAL")
XCTAssertEqual(FlightAwareScheduleClient.identCarrierICAO("BAW296"), "BAW")
XCTAssertEqual(FlightAwareScheduleClient.identCarrierICAO("SWA1"), "SWA")
}
func test_identFlightNumber_extractsTrailingDigits() {
XCTAssertEqual(FlightAwareScheduleClient.identFlightNumber("AAL220"), 220)
XCTAssertEqual(FlightAwareScheduleClient.identFlightNumber("BAW296"), 296)
XCTAssertEqual(FlightAwareScheduleClient.identFlightNumber("SWA1"), 1)
}
func test_airlineIATA_mapsKnownAndReturnsNilForUnknown() {
XCTAssertEqual(FlightAwareScheduleClient.airlineIATA(forICAO: "AAL"), "AA")
XCTAssertEqual(FlightAwareScheduleClient.airlineIATA(forICAO: "KLM"), "KL")
XCTAssertEqual(FlightAwareScheduleClient.airlineIATA(forICAO: "BAW"), "BA")
XCTAssertNil(FlightAwareScheduleClient.airlineIATA(forICAO: "ZZZ"),
"Unknown ICAO should return nil so caller can fall back to the raw prefix.")
}
// MARK: - End-to-end against fixture
func test_endToEnd_AAL220_trackpoll_decodesToScheduledLeg() throws {
let html = try loadFixture("AAL220_trackpoll.html")
guard let blob = FlightAwareScheduleClient.extractTrackpollBlob(from: html) else {
XCTFail("missing trackpoll blob")
return
}
let decoded = try JSONDecoder().decode(TrackpollBootstrap.self, from: Data(blob.utf8))
// The fixture was captured on 2026-06-05; it should contain a DFW->AMS
// leg with a B772 aircraft. We don't assert exact timestamps because
// future updates to the fixture (re-capture) will rotate the dates.
let dfwAmsLegs = decoded.flights.values
.flatMap { $0.activityLog.flights }
.filter { $0.origin.iata == "DFW" && $0.destination.iata == "AMS" }
XCTAssertFalse(dfwAmsLegs.isEmpty,
"AAL220 fixture should contain at least one DFW->AMS leg")
XCTAssertTrue(
dfwAmsLegs.contains { $0.aircraftType == "B772" },
"AAL220 DFW->AMS legs should be operated by B772 per the captured fixture"
)
}
}