Dated pricing had crowded More info and the station rows, and most of the extra copy described a distinction the data cannot make: every station carries a single station-wide timestamp, so "grade-specific vs station-wide" never varied in practice. - More info shows one row, "Price updated 28h ago" (or "Not provided"), replacing the separate station-report and fuel-update-time rows and the blanket "price freshness is not confirmed" label. - Rows and the detail sheet stay quiet when a saving is shown; only an ineligible comparison explains itself. Provisional savings already read as estimates from the capsule, so the fuel-named warning lines are gone. - Freshness/eligibility logic is unchanged: it still gates the baseline and still supports grade-specific timestamps for when the relay emits them. - Tests updated; localisation keys for the removed copy dropped.
252 lines
12 KiB
Swift
252 lines
12 KiB
Swift
import XCTest
|
|
#if SWIFT_PACKAGE
|
|
@testable import FuelBoardShared
|
|
#endif
|
|
|
|
final class FuelPriceFreshnessTests: XCTestCase {
|
|
private let now = Date(timeIntervalSince1970: 1_792_022_400)
|
|
|
|
private func station(
|
|
priceUpdated: TimeInterval? = nil,
|
|
priceUpdatedByFuel: [FuelType: TimeInterval]? = nil
|
|
) -> FuelStation {
|
|
FuelStation(
|
|
id: "freshness-station",
|
|
name: "Freshness Station",
|
|
brand: "Test",
|
|
address: "1 High Street",
|
|
postcode: "BD1 1AA",
|
|
lat: 53.79,
|
|
lng: -1.75,
|
|
prices: [.diesel: 129.9, .e10: 169.9],
|
|
priceUpdated: priceUpdated,
|
|
priceUpdatedByFuel: priceUpdatedByFuel
|
|
)
|
|
}
|
|
|
|
func testPerGradeTimestampIsDecodedAndEligibleWhenFresh() throws {
|
|
let json = """
|
|
{
|
|
"stations": [{
|
|
"id": "s1",
|
|
"name": "LISTER ARMS",
|
|
"brand": "Shell",
|
|
"address": "MANCHESTER ROAD, BRADFORD",
|
|
"postcode": "BD5 7JX",
|
|
"lat": 53.78,
|
|
"lng": -1.75,
|
|
"prices": {"E10": 169.9, "DIESEL": 129.9},
|
|
"price_updated": "2026-09-25T07:45:52Z",
|
|
"price_updated_by_grade": {
|
|
"E10": "2026-10-14T07:45:52Z",
|
|
"DIESEL": "2026-10-14T07:45:52Z"
|
|
}
|
|
}]
|
|
}
|
|
"""
|
|
let stations = try FuelPriceProvider.decodeStations(from: Data(json.utf8))
|
|
let decoded = try XCTUnwrap(stations.first)
|
|
|
|
XCTAssertEqual(decoded.priceUpdatedByFuel?[.diesel],
|
|
ISO8601DateFormatter.fuelBoardDate("2026-10-14T07:45:52Z")?.timeIntervalSince1970)
|
|
let assessment = FuelPriceFreshness.assessment(for: decoded, fuel: .diesel, now: now)
|
|
XCTAssertEqual(assessment.source, .gradeSpecific)
|
|
XCTAssertEqual(assessment.status, .fresh)
|
|
XCTAssertTrue(assessment.isEligibleForBestValue)
|
|
}
|
|
|
|
func testStationWideTimestampIsNotPresentedAsGradeSpecificOrEligible() {
|
|
let updated = now.addingTimeInterval(-FuelPriceFreshness.warningAge / 2).timeIntervalSince1970
|
|
let assessment = FuelPriceFreshness.assessment(
|
|
for: station(priceUpdated: updated), fuel: .diesel, now: now
|
|
)
|
|
|
|
XCTAssertEqual(assessment.source, .stationWide)
|
|
XCTAssertEqual(assessment.status, .fresh)
|
|
XCTAssertEqual(assessment.confidence, .provisional)
|
|
XCTAssertTrue(assessment.recommendationEligibility == .eligible)
|
|
XCTAssertTrue(assessment.isEligibleForBestValue)
|
|
XCTAssertTrue(assessment.warning)
|
|
XCTAssertTrue(assessment.displayLabel.contains("station-wide"))
|
|
}
|
|
|
|
func testMoreInfoReportsOneStationPriceAge() {
|
|
let updated = now.addingTimeInterval(-28 * 60 * 60).timeIntervalSince1970
|
|
let assessment = FuelPriceFreshness.assessment(for: station(priceUpdated: updated), fuel: .e10, now: now)
|
|
XCTAssertEqual(assessment.updateAgeLabel, "28h ago")
|
|
XCTAssertEqual(assessment.source, .stationWide)
|
|
XCTAssertEqual(assessment.status, .fresh)
|
|
}
|
|
|
|
func testOlderFuelSpecificTimestampStillReportsItsOwnAgeOnce() {
|
|
let updated = now.addingTimeInterval(-10 * 86_400).timeIntervalSince1970
|
|
let priced = station(priceUpdatedByFuel: [.diesel: updated])
|
|
let assessment = FuelPriceFreshness.assessment(for: priced, fuel: .diesel, now: now)
|
|
XCTAssertEqual(assessment.updateAgeLabel, "10d ago")
|
|
XCTAssertEqual(assessment.source, .gradeSpecific)
|
|
}
|
|
|
|
func testMissingUpdateTimeReadsNotProvided() {
|
|
let assessment = FuelPriceFreshness.assessment(for: station(), fuel: .e10, now: now)
|
|
XCTAssertEqual(assessment.updateAgeLabel, "Not provided")
|
|
}
|
|
|
|
func testProvisionalSavingAddsNoSecondWarningLine() {
|
|
let recent = now.addingTimeInterval(-28 * 60 * 60).timeIntervalSince1970
|
|
let stationWide = station(priceUpdated: recent)
|
|
let gradeSpecific = station(priceUpdatedByFuel: [.e10: recent])
|
|
XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(
|
|
station: gradeSpecific, fuel: .e10, baseline: stationWide, now: now), .provisional)
|
|
XCTAssertNil(FuelPriceFreshness.comparisonWarning(
|
|
station: stationWide, fuel: .e10, baseline: stationWide, now: now))
|
|
XCTAssertNil(FuelPriceFreshness.comparisonWarning(
|
|
station: gradeSpecific, fuel: .e10, baseline: stationWide, now: now))
|
|
}
|
|
|
|
func testIneligibleComparisonStillExplainsItself() {
|
|
let old = now.addingTimeInterval(-FuelPriceFreshness.staleAge - 1).timeIntervalSince1970
|
|
let stale = station(priceUpdatedByFuel: [.diesel: old])
|
|
XCTAssertEqual(FuelPriceFreshness.comparisonWarning(
|
|
station: stale, fuel: .diesel, baseline: stale, now: now),
|
|
"Price age unavailable or too old — not a Best Value recommendation.")
|
|
}
|
|
|
|
func testStationWideTimestampAtWarningBoundaryChangesRecommendationEligibility() {
|
|
let updated = now.addingTimeInterval(-FuelPriceFreshness.warningAge).timeIntervalSince1970
|
|
let onBoundary = FuelPriceFreshness.assessment(for: station(priceUpdated: updated), fuel: .diesel, now: now)
|
|
XCTAssertEqual(onBoundary.status, .fresh)
|
|
XCTAssertTrue(onBoundary.isEligibleForBestValue)
|
|
let pastBoundary = FuelPriceFreshness.assessment(
|
|
for: station(priceUpdated: updated - 1), fuel: .diesel, now: now)
|
|
XCTAssertEqual(pastBoundary.status, .warning)
|
|
XCTAssertFalse(pastBoundary.isEligibleForBestValue)
|
|
}
|
|
|
|
func testStationWideTimestampOlderThanWarningThresholdCannotRecommendSuspiciousDiesel() {
|
|
let now = Date(timeIntervalSince1970: 1_800_000_000)
|
|
let shell = station(priceUpdated: now.addingTimeInterval(-20 * 86_400).timeIntervalSince1970)
|
|
let age = FuelPriceFreshness.assessment(for: shell, fuel: .diesel, now: now)
|
|
XCTAssertEqual(age.status, .warning)
|
|
XCTAssertFalse(age.isEligibleForBestValue)
|
|
XCTAssertEqual(shell.prices[.diesel], 129.9)
|
|
}
|
|
|
|
func testComparisonConfidenceRejectsMissingSelectedFuelPriceEvenWithFreshTimestamp() {
|
|
let now = Date(timeIntervalSince1970: 1_800_000_000)
|
|
let updated = now.addingTimeInterval(-86_400).timeIntervalSince1970
|
|
let dieselOnly = FuelStation(id: "diesel", name: "Diesel", brand: "", address: "", postcode: "",
|
|
lat: 53, lng: -1, prices: [.diesel: 129.9], priceUpdated: nil,
|
|
priceUpdatedByFuel: [.e10: updated])
|
|
let e10 = FuelStation(id: "petrol", name: "Petrol", brand: "", address: "", postcode: "",
|
|
lat: 53, lng: -1, prices: [.e10: 169.9], priceUpdated: nil,
|
|
priceUpdatedByFuel: [.e10: updated])
|
|
XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: dieselOnly, fuel: .e10,
|
|
baseline: e10, now: now), .ineligible)
|
|
XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: e10, fuel: .e10,
|
|
baseline: dieselOnly, now: now), .ineligible)
|
|
}
|
|
|
|
func testComparisonConfidenceRequiresBothSelectedGradeTimestamps() {
|
|
let now = Date(timeIntervalSince1970: 1_800_000_000)
|
|
let recent = now.addingTimeInterval(-86_400).timeIntervalSince1970
|
|
let old = now.addingTimeInterval(-20 * 86_400).timeIntervalSince1970
|
|
let verified = station(priceUpdatedByFuel: [.diesel: recent])
|
|
let provisional = station(priceUpdated: recent)
|
|
let oldStationWide = station(priceUpdated: old)
|
|
let oldGrade = station(priceUpdatedByFuel: [.diesel: old])
|
|
XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: verified, fuel: .diesel,
|
|
baseline: verified, now: now), .verified)
|
|
XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: verified, fuel: .diesel,
|
|
baseline: provisional, now: now), .provisional)
|
|
XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: oldGrade, fuel: .diesel,
|
|
baseline: verified, now: now), .provisional)
|
|
XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: verified, fuel: .diesel,
|
|
baseline: oldStationWide, now: now), .ineligible)
|
|
XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: provisional, fuel: .diesel,
|
|
baseline: verified, now: now), .provisional)
|
|
XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: verified, fuel: .e10,
|
|
baseline: verified, now: now), .ineligible)
|
|
}
|
|
|
|
func testNonFiniteAndExtremeEpochsCannotBecomeTrusted() {
|
|
let now = Date(timeIntervalSince1970: 1_800_000_000)
|
|
for value in [Double.nan, .infinity, -.infinity, Double.greatestFiniteMagnitude, -Double.greatestFiniteMagnitude] {
|
|
let assessment = FuelPriceFreshness.assessment(
|
|
priceUpdated: value, priceUpdatedByFuel: nil, fuel: .diesel, now: now)
|
|
XCTAssertFalse(assessment.isEligibleForBestValue)
|
|
XCTAssertTrue(assessment.warning)
|
|
}
|
|
}
|
|
|
|
func testMissingAndMalformedTimestampsAreUnknownAndIneligible() {
|
|
let missing = FuelPriceFreshness.assessment(for: station(), fuel: .diesel, now: now)
|
|
XCTAssertEqual(missing.status, .unknown)
|
|
XCTAssertEqual(missing.source, .unknown)
|
|
XCTAssertFalse(missing.isEligibleForBestValue)
|
|
|
|
let malformed = FuelPriceFreshness.assessment(
|
|
priceUpdated: "not-a-date", priceUpdatedByGrade: nil, fuel: .diesel, now: now
|
|
)
|
|
XCTAssertEqual(malformed.status, .invalid)
|
|
XCTAssertFalse(malformed.isEligibleForBestValue)
|
|
}
|
|
|
|
func testStaleDisplayLabelDoesNotCallIneligiblePriceProvisional() {
|
|
let stale = station(priceUpdatedByFuel: [
|
|
.diesel: now.addingTimeInterval(-FuelPriceFreshness.staleAge - 1).timeIntervalSince1970
|
|
])
|
|
let assessment = FuelPriceFreshness.assessment(for: stale, fuel: .diesel, now: now)
|
|
XCTAssertFalse(assessment.isEligibleForBestValue)
|
|
XCTAssertFalse(assessment.displayLabel.contains("provisional"))
|
|
XCTAssertTrue(assessment.displayLabel.contains("outdated"))
|
|
}
|
|
|
|
func testStaleAndFutureGradeTimestampsAreRejected() {
|
|
let stale = station(priceUpdatedByFuel: [
|
|
.diesel: now.addingTimeInterval(-FuelPriceFreshness.staleAge - 1).timeIntervalSince1970
|
|
])
|
|
let staleAssessment = FuelPriceFreshness.assessment(for: stale, fuel: .diesel, now: now)
|
|
XCTAssertEqual(staleAssessment.status, .stale)
|
|
XCTAssertFalse(staleAssessment.isEligibleForBestValue)
|
|
XCTAssertTrue(staleAssessment.warning)
|
|
|
|
let future = station(priceUpdatedByFuel: [
|
|
.diesel: now.addingTimeInterval(FuelPriceFreshness.futureTolerance + 1).timeIntervalSince1970
|
|
])
|
|
let futureAssessment = FuelPriceFreshness.assessment(for: future, fuel: .diesel, now: now)
|
|
XCTAssertEqual(futureAssessment.status, .invalid)
|
|
XCTAssertFalse(futureAssessment.isEligibleForBestValue)
|
|
}
|
|
|
|
func testMalformedAndFutureSourceValuesDoNotCrashOrProduceNegativeAge() {
|
|
let malformed = FuelPriceFreshness.assessment(
|
|
priceUpdated: "2026-99-99T99:99:99Z",
|
|
priceUpdatedByGrade: ["DIESEL": "2026-99-99T99:99:99Z"],
|
|
fuel: .diesel,
|
|
now: now
|
|
)
|
|
XCTAssertNil(malformed.age)
|
|
XCTAssertFalse(malformed.isEligibleForBestValue)
|
|
|
|
let future = FuelPriceFreshness.assessment(
|
|
priceUpdated: now.addingTimeInterval(86_400).timeIntervalSince1970,
|
|
priceUpdatedByFuel: nil,
|
|
fuel: .diesel,
|
|
now: now
|
|
)
|
|
XCTAssertNil(future.age)
|
|
XCTAssertFalse(future.isEligibleForBestValue)
|
|
}
|
|
}
|
|
|
|
private extension ISO8601DateFormatter {
|
|
static func fuelBoardDate(_ string: String) -> Date? {
|
|
let formatter = ISO8601DateFormatter()
|
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
return formatter.date(from: string) ?? {
|
|
formatter.formatOptions = [.withInternetDateTime]
|
|
return formatter.date(from: string)
|
|
}()
|
|
}
|
|
}
|