Add FlightsTests target + fix AA load fetcher (Android UA version bump)

AA was silently returning nil because the server now rejects User-Agent
"Android/2025.31" with HTTP 403 ("Please update your version of the
American Airlines app"). Bumped to "2026.14" (matches the APK in
airlines/) and centralized to a constant so the next bump is one line.
Added comprehensive logging to fetchAmericanLoad (was zero) so the next
breakage won't be silent — including an explicit ⚠️ when the server
returns the "update your version" payload.

New FlightsTests target with AirlineLoadIntegrationTests — hits live
airline APIs to verify each fetcher still returns data. Per-airline
strategy:
- Try route-explorer /departures from carrier hubs for a flight in the
  next 24h (works for AA/UA/AS/B6).
- Fall back to a known-good daily flight when route-explorer doesn't
  have the carrier in its data (NK/EK/KE — ULCC + some intl carriers).
- B6/EK/NK are status-only by design (no standby data without a PNR);
  asserted as non-nil only.
- XE (JSX) skipped: needs WKWebView host.

Retries on route-explorer 429 by parsing the `retryAfter` field and
sleeping the indicated number of seconds. Static-shared client+services
across tests so the token cache survives.

Results 2026-05-26 (xcodebuild test -scheme Flights):
   AA, AS, B6, EK, KE, UA   NK  ⏭️ XE
NK (Spirit) is now broken: GetFlightInfoBI returns HTTP 403 with
{"getFlightInfoBIResult":null}. APIM key still accepted (401 without
it), but the call itself is rejected. Documented in
AIRLINE_INTEGRATION_GUIDE.md as a known regression to fix; likely
needs reverse-engineering against the current Spirit APK in airlines/.

Also: enable shared schemes in .gitignore so `xcodebuild test` works
out of the box for anyone cloning the repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-05-26 14:12:19 -05:00
parent 0c4777216e
commit 62729213d7
6 changed files with 514 additions and 15 deletions
+111
View File
@@ -45,8 +45,19 @@
RE6600006666000066660001 /* ConnectionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = RE6600006666000066660002 /* ConnectionRow.swift */; };
RE7700007777000077770001 /* ConnectionLoadDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = RE7700007777000077770002 /* ConnectionLoadDetailView.swift */; };
RE8800008888000088880001 /* SearchRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = RE8800008888000088880002 /* SearchRoute.swift */; };
T1000000000000000000001A /* AirlineLoadIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = T1000000000000000000001B /* AirlineLoadIntegrationTests.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
T1000000000000000000002A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 5418BEEAEFF644ADA7240CEA /* Project object */;
proxyType = 1;
remoteGlobalIDString = E373C48C497D48D388BF7657;
remoteInfo = Flights;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
04AC23D8748D42C9A7115FAC /* Airline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Airline.swift; sourceTree = "<group>"; };
0CD303E3EDCC4BF2BCF31722 /* AirportSearchField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirportSearchField.swift; sourceTree = "<group>"; };
@@ -87,6 +98,8 @@
RE6600006666000066660002 /* ConnectionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionRow.swift; sourceTree = "<group>"; };
RE7700007777000077770002 /* ConnectionLoadDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionLoadDetailView.swift; sourceTree = "<group>"; };
RE8800008888000088880002 /* SearchRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRoute.swift; sourceTree = "<group>"; };
T1000000000000000000001B /* AirlineLoadIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirlineLoadIntegrationTests.swift; sourceTree = "<group>"; };
T1000000000000000000003A /* FlightsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FlightsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -97,6 +110,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
T1000000000000000000004A /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -155,10 +175,19 @@
isa = PBXGroup;
children = (
8A3CB0CCC2524542AFB0D1D2 /* Flights.app */,
T1000000000000000000003A /* FlightsTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
T1000000000000000000005A /* FlightsTests */ = {
isa = PBXGroup;
children = (
T1000000000000000000001B /* AirlineLoadIntegrationTests.swift */,
);
path = FlightsTests;
sourceTree = "<group>";
};
6E94DB5F9EB345948E2D5E2A /* ViewModels */ = {
isa = PBXGroup;
children = (
@@ -187,6 +216,7 @@
isa = PBXGroup;
children = (
1D5A2C06B99046F3934D2E59 /* Flights */,
T1000000000000000000005A /* FlightsTests */,
517CC07B82D949359C6CD4F5 /* Products */,
);
sourceTree = "<group>";
@@ -229,6 +259,23 @@
productReference = 8A3CB0CCC2524542AFB0D1D2 /* Flights.app */;
productType = "com.apple.product-type.application";
};
T1000000000000000000006A /* FlightsTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = T1000000000000000000007A /* Build configuration list for PBXNativeTarget "FlightsTests" */;
buildPhases = (
T1000000000000000000008A /* Sources */,
T1000000000000000000004A /* Frameworks */,
);
buildRules = (
);
dependencies = (
T1000000000000000000009A /* PBXTargetDependency */,
);
name = FlightsTests;
productName = FlightsTests;
productReference = T1000000000000000000003A /* FlightsTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -253,10 +300,19 @@
projectRoot = "";
targets = (
E373C48C497D48D388BF7657 /* Flights */,
T1000000000000000000006A /* FlightsTests */,
);
};
/* End PBXProject section */
/* Begin PBXTargetDependency section */
T1000000000000000000009A /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = E373C48C497D48D388BF7657 /* Flights */;
targetProxy = T1000000000000000000002A /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXResourcesBuildPhase section */
6B9FCA84AAAA44529A95D7AC /* Resources */ = {
isa = PBXResourcesBuildPhase;
@@ -313,6 +369,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
T1000000000000000000008A /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
T1000000000000000000001A /* AirlineLoadIntegrationTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
@@ -423,6 +487,44 @@
};
name = Debug;
};
T100000000000000000000BA /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = V3PF3M6B6U;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.flights.app.FlightsTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Flights.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Flights";
};
name = Debug;
};
T100000000000000000000CA /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = V3PF3M6B6U;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.flights.app.FlightsTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Flights.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Flights";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -444,6 +546,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
T1000000000000000000007A /* Build configuration list for PBXNativeTarget "FlightsTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
T100000000000000000000BA /* Debug */,
T100000000000000000000CA /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 5418BEEAEFF644ADA7240CEA /* Project object */;
@@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E373C48C497D48D388BF7657"
BuildableName = "Flights.app"
BlueprintName = "Flights"
ReferencedContainer = "container:Flights.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "T1000000000000000000006A"
BuildableName = "FlightsTests.xctest"
BlueprintName = "FlightsTests"
ReferencedContainer = "container:Flights.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "T1000000000000000000006A"
BuildableName = "FlightsTests.xctest"
BlueprintName = "FlightsTests"
ReferencedContainer = "container:Flights.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E373C48C497D48D388BF7657"
BuildableName = "Flights.app"
BlueprintName = "Flights"
ReferencedContainer = "container:Flights.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E373C48C497D48D388BF7657"
BuildableName = "Flights.app"
BlueprintName = "Flights"
ReferencedContainer = "container:Flights.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>