Add Sun Country (SY) load integration
Sun Country runs Navitaire (same PSS as JSX) but exposes their public availability search endpoint that returns BETTER load data than AA: per-flight `capacity` AND `sold` (booked passenger count), so we can compute exact load factor. Implementation: - AirlineLoadService.fetchSunCountryLoad: POSTs to syprod-api.suncountry.com/api/nsk/v4/availability/search/simple. Parses results→trips→journeysAvailableByMarket, matches by flight number, pulls capacity + sold + equipmentType from legInfo. - Returns a single Economy CabinLoad with capacity/booked = sold. No standby program — SY is single-cabin Y. - Auth: Azure APIM subscription key + a long-lived dotREZ JWT (both static, captured from suncountry.com network traffic, neither is a user session token). - Anti-bot: Imperva WAF in front of syprod-api.suncountry.com is gated on User-Agent + Referer + Origin headers. applySunCountryBrowserHeaders mirrors the pattern we use for UA / AA. NO WebView needed. - Explicit ⚠️ log when 403 Incapsula response detected, pointing at the header helper. Test infrastructure: - knownDailyFlights now carries a dayOffset (today vs tomorrow) per carrier — different upstreams have different snapshot windows: AM is T-1d..T+0 (today); SY's Navitaire only returns future flights (tomorrow); others default to tomorrow as a safer choice. - Added test_SY_sunCountry with hubs MSP/LAS/MCO/DEN. Fallback is SY104 LAS-MSP tomorrow. Docs: - AIRLINE_INTEGRATION_GUIDE: SY status row + full section 5c covering endpoint, auth, headers, response shape, failure modes, and how to re-capture tokens when they rotate. Reverse-engineering notes: - SY app is Flutter (Dart AOT) — bridge smali is minimal. Strings extracted from libapp.so revealed isNonRevTrip/isStandby/ inventoryControl keywords + the syprod-api hostname. - Token endpoint is PUT (not POST). Returns {"data":null} — token is the existing Authorization JWT, not a session refresh. - Confirmed working from plain curl with browser headers (no Imperva TLS-fingerprint gate beyond UA/Referer/Origin). Test run 2026-05-26 (xcodebuild test): ✅ AA, AM, AS, B6, EK, KE, SY (capacity=186 sold=184 load=99%), UA ⏭️ XE 8 passing, 1 skipped, 0 failures, 11s total. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,10 +45,16 @@ final class AirlineLoadIntegrationTests: XCTestCase {
|
||||
/// route-explorer's schedule feed). Each entry is a well-known daily
|
||||
/// operation that's been stable over time; if any of these stop
|
||||
/// operating, update the entry.
|
||||
private static let knownDailyFlights: [String: (flightNumber: String, origin: String, destination: String)] = [
|
||||
"EK": ("201", "JFK", "DXB"), // Emirates JFK → Dubai, daily flagship
|
||||
"KE": ("82", "JFK", "ICN"), // Korean Air JFK → Incheon, daily
|
||||
"AM": ("58", "MEX", "MTY"), // Aeromexico MEX → Monterrey, multiple daily
|
||||
///
|
||||
/// `dayOffset` controls which day's flight to probe:
|
||||
/// - `0` (today) for carriers whose snapshot window is T-1d to T+0 (AM)
|
||||
/// - `1` (tomorrow) for carriers whose API only returns future flights
|
||||
/// (SY's Navitaire availability/search drops already-departed legs)
|
||||
private static let knownDailyFlights: [String: (flightNumber: String, origin: String, destination: String, dayOffset: Int)] = [
|
||||
"EK": ("201", "JFK", "DXB", 1), // Emirates JFK → Dubai, daily flagship
|
||||
"KE": ("82", "JFK", "ICN", 1), // Korean Air JFK → Incheon, daily
|
||||
"AM": ("58", "MEX", "MTY", 0), // Aeromexico — snapshot only T-1d/T+0
|
||||
"SY": ("104", "LAS", "MSP", 1), // Sun Country — Navitaire shows future only
|
||||
]
|
||||
|
||||
// MARK: - Per-airline tests
|
||||
@@ -104,6 +110,16 @@ final class AirlineLoadIntegrationTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func test_SY_sunCountry() async throws {
|
||||
// Sun Country runs on Navitaire; the load endpoint takes flight
|
||||
// number + route + date. Route-explorer doesn't cover SY well, so
|
||||
// most runs hit the known-daily fallback (SY104 LAS-MSP).
|
||||
try await runAirlineLoadTest(
|
||||
carrier: "SY",
|
||||
hubs: ["MSP", "LAS", "MCO", "DEN"]
|
||||
)
|
||||
}
|
||||
|
||||
func test_XE_jsx() async throws {
|
||||
// JSX uses a WKWebView path that needs a host scene / main thread.
|
||||
// Skipped here; manual verification via the app remains.
|
||||
@@ -159,12 +175,15 @@ final class AirlineLoadIntegrationTests: XCTestCase {
|
||||
// Fallback: known-good daily flight. Triggers when route-explorer
|
||||
// found nothing OR when the discovered flight returned nil (e.g. a
|
||||
// regional carrier op that isn't in the upstream load system).
|
||||
// dayOffset in the table controls today-vs-tomorrow based on each
|
||||
// carrier's snapshot window quirks.
|
||||
if let known = Self.knownDailyFlights[carrier] {
|
||||
NSLog("[\(carrier)Test] Using known daily \(carrier)\(known.flightNumber) \(known.origin)→\(known.destination)")
|
||||
let probeDate = Date().addingTimeInterval(TimeInterval(known.dayOffset * 86400))
|
||||
NSLog("[\(carrier)Test] Using known daily \(carrier)\(known.flightNumber) \(known.origin)→\(known.destination) +\(known.dayOffset)d")
|
||||
let load = await loadService.fetchLoad(
|
||||
airlineCode: carrier,
|
||||
flightNumber: known.flightNumber,
|
||||
date: Date(),
|
||||
date: probeDate,
|
||||
origin: known.origin,
|
||||
destination: known.destination,
|
||||
departureTime: nil
|
||||
|
||||
Reference in New Issue
Block a user