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 = """
"""
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 = "no script here"
XCTAssertNil(FlightAwareScheduleClient.extractTrackpollBlob(from: html))
}
func test_extractTrackpollBlob_isStringContentAware() {
// A closing brace inside a string literal must NOT terminate the scan.
let html = #"""
"""#
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"
)
}
}