P0: live provider chain — GitHub mirror primary, relay fallback, bundled dump last resort + app telemetry beacon
- MirrorFuelProvider: fetches CURRENT prices from raw.githubusercontent (latest.json pointer → history/<day>.json full dump), app-group day-cache so the ~2.8 MB dump is re-downloaded only when the mirror pushes a new day - LiveChainProvider: full path (app refresh) GitHub → relay; focused path (background alert checks) relay-first so alerts never pull the full dump over mobile data; records which leg served for About + telemetry - FuelBeacon: fire-and-forget X-Client app ping to the relay's existing widget-diag route (zero relay changes) — attribution + cadence on-LAN, silent skip off-LAN is the reachability datum; /stats app-hit spike = GitHub path failing - Bundled dump: FuelBoardDump dataset (real 2026-08-15 snapshot, 8,022 stations) + BundledDumpProvider + scripts/refresh_bundled_dump.sh — no-network last resort replaces the demo sample set - ContentView: About meta from the chain; catch falls back cached → bundled → sample - 89 tests (6 new: chain order x4, cache decision, beacon URL); Release build green; beacon route verified against the live relay (204 + logged)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
../../../Shared/MirrorFuelProvider.swift
|
||||
@@ -0,0 +1,118 @@
|
||||
import XCTest
|
||||
@testable import FuelBoardShared
|
||||
|
||||
/// P0 live chain: GitHub mirror → LAN relay → bundled dump.
|
||||
final class LiveChainTests: XCTestCase {
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
FuelBeacon.isEnabled = false // chain tests must not fire real pings
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
FuelBeacon.isEnabled = true
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: Fixtures
|
||||
|
||||
private final class StubProvider: FuelPriceProviding {
|
||||
var result: [FuelStation]
|
||||
var error: Error?
|
||||
private(set) var callCount = 0
|
||||
|
||||
init(result: [FuelStation]? = nil, error: Error? = nil) {
|
||||
self.result = result ?? []
|
||||
self.error = error
|
||||
}
|
||||
|
||||
func fetchStations(near lat: Double?, lng: Double?, fuel: FuelType, radiusKM: Double?) async throws -> [FuelStation] {
|
||||
callCount += 1
|
||||
if let error { throw error }
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private func fixtureStation(_ id: String = "s1") -> FuelStation {
|
||||
FuelStation(id: id, name: "Station \(id)", brand: "Test", address: "1 High St",
|
||||
postcode: "SW1A 1AA", lat: 53.7, lng: -1.8,
|
||||
prices: [.e10: 137.9, .e5: 144.9, .diesel: 144.9], priceUpdated: nil)
|
||||
}
|
||||
|
||||
// MARK: Full path (app refresh): GitHub first
|
||||
|
||||
func testFullPathPrefersMirror() async throws {
|
||||
let mirror = StubProvider(result: [fixtureStation()])
|
||||
let relay = StubProvider(error: FuelProviderError.relayUnavailable)
|
||||
let chain = LiveChainProvider(mirror: mirror, relay: relay)
|
||||
|
||||
let stations = try await chain.fetchStations(near: nil, lng: nil, fuel: .e10, radiusKM: nil)
|
||||
|
||||
XCTAssertEqual(stations.count, 1)
|
||||
XCTAssertEqual(mirror.callCount, 1, "full path must try the GitHub mirror first")
|
||||
XCTAssertEqual(relay.callCount, 0, "relay must not run when the mirror serves")
|
||||
XCTAssertEqual(LiveChainProvider.lastSource, "github")
|
||||
}
|
||||
|
||||
func testFullPathFallsBackToRelay() async throws {
|
||||
let mirror = StubProvider(error: FuelProviderError.mirrorUnavailable)
|
||||
let relay = StubProvider(result: [fixtureStation("relay")])
|
||||
let chain = LiveChainProvider(mirror: mirror, relay: relay)
|
||||
|
||||
let stations = try await chain.fetchStations(near: nil, lng: nil, fuel: .e10, radiusKM: nil)
|
||||
|
||||
XCTAssertEqual(stations.first?.id, "relay")
|
||||
XCTAssertEqual(relay.callCount, 1, "relay must serve when the mirror is down")
|
||||
XCTAssertEqual(LiveChainProvider.lastSource, "relay")
|
||||
}
|
||||
|
||||
// MARK: Focused path (alert checks): relay first — never pull the full
|
||||
// ~2.8 MB dump over mobile data for a background alert when the relay is up.
|
||||
|
||||
func testFocusedPathPrefersRelay() async throws {
|
||||
let mirror = StubProvider(result: [fixtureStation("mirror")])
|
||||
let relay = StubProvider(result: [fixtureStation("relay")])
|
||||
let chain = LiveChainProvider(mirror: mirror, relay: relay)
|
||||
|
||||
_ = try await chain.fetchStations(near: 53.7, lng: -1.8, fuel: .e10, radiusKM: 5)
|
||||
|
||||
XCTAssertEqual(relay.callCount, 1, "focused alert fetch must prefer the light relay call")
|
||||
XCTAssertEqual(mirror.callCount, 0, "mirror full dump must NOT be pulled when the relay is up")
|
||||
XCTAssertEqual(LiveChainProvider.lastSource, "relay")
|
||||
}
|
||||
|
||||
func testFocusedPathFallsBackToMirror() async throws {
|
||||
let mirror = StubProvider(result: [fixtureStation("mirror")])
|
||||
let relay = StubProvider(error: FuelProviderError.relayUnavailable)
|
||||
let chain = LiveChainProvider(mirror: mirror, relay: relay)
|
||||
|
||||
let stations = try await chain.fetchStations(near: 53.7, lng: -1.8, fuel: .e10, radiusKM: 5)
|
||||
|
||||
XCTAssertEqual(stations.first?.id, "mirror")
|
||||
XCTAssertEqual(mirror.callCount, 1)
|
||||
XCTAssertEqual(LiveChainProvider.lastSource, "github")
|
||||
}
|
||||
|
||||
// MARK: Mirror day-cache decision
|
||||
|
||||
func testDumpCacheReuseDecision() {
|
||||
XCTAssertTrue(MirrorFuelProvider.canReuseCache(cachedDay: "2026-08-15", latestDay: "2026-08-15"),
|
||||
"same day → reuse the cached dump, no ~2.8 MB re-download")
|
||||
XCTAssertFalse(MirrorFuelProvider.canReuseCache(cachedDay: "2026-08-14", latestDay: "2026-08-15"))
|
||||
XCTAssertFalse(MirrorFuelProvider.canReuseCache(cachedDay: nil, latestDay: "2026-08-15"))
|
||||
XCTAssertFalse(MirrorFuelProvider.canReuseCache(cachedDay: "2026-08-15", latestDay: nil))
|
||||
}
|
||||
|
||||
// MARK: Beacon URL
|
||||
|
||||
func testBeaconURLCarriesAppAttribution() {
|
||||
guard let url = FuelBeacon.beaconURL(source: "github", n: 8022) else {
|
||||
return XCTFail("beacon URL must build")
|
||||
}
|
||||
XCTAssertTrue(url.absoluteString.contains("api/v1/widget-diag"))
|
||||
let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? []
|
||||
XCTAssertTrue(items.contains(URLQueryItem(name: "intent", value: "app-live")))
|
||||
XCTAssertTrue(items.contains(URLQueryItem(name: "source", value: "github")))
|
||||
XCTAssertTrue(items.contains(URLQueryItem(name: "n", value: "8022")))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user