- 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)
119 lines
5.0 KiB
Swift
119 lines
5.0 KiB
Swift
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")))
|
|
}
|
|
}
|