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 = """
Filed TimeIdent
No data
""" 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" ) } }