road distance: pin-fingerprint guard so embedded/live/cached never cross-pollute

The road-distance cache was keyed purely by station ID with no coordinate, so
a value routed to one pin could be served for a same-ID station whose stored
coordinate came from a different source (live fetch vs bundled offline dump
vs a corrected pin), reproducing Maps mismatches.
- RoadDistanceCache now stores each routed pin (CachedRoadDistance{meters,
  lat, lng}) and roadDistanceMeters() only serves a value when the displayed
  station's coordinate matches the pinned one (within ~11 m).
- Kept the existing origin-distance staleness guard.
- Verified embedded sample data (ids y1/se1/...) never collides with real
  relay IDs, and the widget STRICT radius filter drops far-offline samples, so
  no actual leak existed in practice — this closes the theoretical stale-pin
  channel and future-proofs against coordinate fixes.
This commit is contained in:
FuelBoard Contributor
2026-08-20 17:26:33 +01:00
parent 01517ba687
commit 0b27d3d4f6
3 changed files with 61 additions and 14 deletions
@@ -647,7 +647,8 @@ final class RoadDistanceCacheTests: XCTestCase {
func testRoadDistanceUsedWhenCachedNear() {
let s = station("a", 51.5074, -0.1278)
// Cache a road distance of 3.2 km for this station from the user's fix.
FuelStore.saveRoadDistances(sourceLat: 51.6, sourceLng: -0.1, entries: ["a": 3200])
FuelStore.saveRoadDistances(sourceLat: 51.6, sourceLng: -0.1,
entries: ["a": .init(meters: 3200, lat: 51.5074, lng: -0.1278)])
let km = FuelStore.displayDistanceKM(station: s, userLat: 51.6, userLng: -0.1)
XCTAssertEqual(km, 3.2, accuracy: 0.0001)
}
@@ -655,8 +656,9 @@ final class RoadDistanceCacheTests: XCTestCase {
func testRoadDistanceNilWhenOriginFar() {
let s = station("a", 51.5074, -0.1278)
// Cache built in London, but the user is now ~200 km away -> stale.
FuelStore.saveRoadDistances(sourceLat: 51.5074, sourceLng: -0.1278, entries: ["a": 3200])
let meters = FuelStore.roadDistanceMeters(for: "a", userLat: 53.4808, userLng: -2.2426)
FuelStore.saveRoadDistances(sourceLat: 51.5074, sourceLng: -0.1278,
entries: ["a": .init(meters: 3200, lat: 51.5074, lng: -0.1278)])
let meters = FuelStore.roadDistanceMeters(for: s, userLat: 53.4808, userLng: -2.2426)
XCTAssertNil(meters)
// And display falls back to straight-line.
let km = FuelStore.displayDistanceKM(station: s, userLat: 53.4808, userLng: -2.2426)
@@ -664,12 +666,26 @@ final class RoadDistanceCacheTests: XCTestCase {
}
func testRoadDistanceUsedForOtherStationNotFound() {
FuelStore.saveRoadDistances(sourceLat: 51.6, sourceLng: -0.1, entries: ["a": 3200])
FuelStore.saveRoadDistances(sourceLat: 51.6, sourceLng: -0.1,
entries: ["a": .init(meters: 3200, lat: 51.5074, lng: -0.1278)])
// A station that isn't in the cache falls back to straight-line.
let s = station("z", 51.51, -0.13)
let km = FuelStore.displayDistanceKM(station: s, userLat: 51.6, userLng: -0.1)
XCTAssertEqual(km, s.distanceKM(to: 51.6, lng2: -0.1), accuracy: 0.0001)
}
func testRoadDistanceNotServedWhenStationPinDiffers() {
// Route a road distance to station "a" at pin P1.
FuelStore.saveRoadDistances(sourceLat: 51.6, sourceLng: -0.1,
entries: ["a": .init(meters: 3200, lat: 51.5074, lng: -0.1278)])
// The SAME station id appears with a moved pin (corrected coordinate /
// different embedded vs live source): the cached route to P1 must NOT
// be served it belongs to a different location.
let moved = station("a", 51.5400, -0.1600)
let km = FuelStore.displayDistanceKM(station: moved, userLat: 51.6, userLng: -0.1)
XCTAssertEqual(km, moved.distanceKM(to: 51.6, lng2: -0.1), accuracy: 0.0001,
"road value routed to the old pin leaked onto a different coordinate")
}
}
// MARK: - Install identity