Christopher Hotchkiss Christopher Hotchkiss
Login

Code

remindwall / RWPureSwift / Tests / TransitAPITests / DecodingTests.swift

Back to remindwall — Current RemindWall

Download this file

import Foundation
import Testing

@testable import TransitAPI

@Suite("OBA decoder tests")
struct DecodingTests {

    private func loadFixture(_ name: String) throws -> Data {
        guard let url = Bundle.module.url(forResource: name, withExtension: "json") else {
            Issue.record("Missing fixture: \(name).json")
            return Data()
        }
        return try Data(contentsOf: url)
    }

    @Test("arrivals.json decodes to three predictions with correct stopId")
    func decodeArrivals() throws {
        let data = try loadFixture("arrivals")
        let envelope = try OBAEnvelope<OBAArrivalsEntry>.decode(from: data)
        let preds = envelope.data.entry?.toPredictions() ?? []

        #expect(preds.count == 3)
        #expect(preds.allSatisfy { $0.stopId == "1_75403" })
    }

    @Test("predictedArrivalTime == 0 maps to nil predictedArrival and isLive == false")
    func zeroPredictedTimeIsNil() throws {
        let data = try loadFixture("arrivals")
        let envelope = try OBAEnvelope<OBAArrivalsEntry>.decode(from: data)
        let preds = envelope.data.entry?.toPredictions() ?? []

        let scheduledOnly = preds.first { $0.tripId == "1_45872999" }
        #expect(scheduledOnly != nil)
        #expect(scheduledOnly?.predictedArrival == nil)
        #expect(scheduledOnly?.scheduledArrival != nil)
        #expect(scheduledOnly?.isLive == false)
        // effectiveArrival falls back to scheduledArrival
        #expect(scheduledOnly?.effectiveArrival == scheduledOnly?.scheduledArrival)
    }

    @Test("predicted == true with non-zero time gives isLive == true")
    func livePrediction() throws {
        let data = try loadFixture("arrivals")
        let envelope = try OBAEnvelope<OBAArrivalsEntry>.decode(from: data)
        let preds = envelope.data.entry?.toPredictions() ?? []

        let live = preds.first { $0.tripId == "1_45872551" }
        #expect(live != nil)
        #expect(live?.isLive == true)
        #expect(live?.predictedArrival != nil)
        #expect(live?.scheduledArrival != nil)
    }

    @Test("lateness is positive when predicted > scheduled")
    func latenessPositive() throws {
        let data = try loadFixture("arrivals")
        let envelope = try OBAEnvelope<OBAArrivalsEntry>.decode(from: data)
        let preds = envelope.data.entry?.toPredictions() ?? []

        // First trip: scheduled 1746118900000, predicted 1746118960000 → 60s late
        let first = preds.first { $0.tripId == "1_45872551" }
        #expect(first?.lateness == 60.0)

        // Third trip: scheduled 1746120100000, predicted 1746120400000 → 300s late
        let third = preds.first { $0.tripId == "1_45873222" }
        #expect(third?.lateness == 300.0)
    }

    @Test("lateness is nil when only scheduled time is available")
    func latenessNilWhenScheduleOnly() throws {
        let data = try loadFixture("arrivals")
        let envelope = try OBAEnvelope<OBAArrivalsEntry>.decode(from: data)
        let preds = envelope.data.entry?.toPredictions() ?? []
        let scheduledOnly = preds.first { $0.tripId == "1_45872999" }
        #expect(scheduledOnly?.lateness == nil)
    }

    @Test("client-side route filter selects only requested routeId")
    func clientSideRouteFilter() throws {
        let data = try loadFixture("arrivals")
        let envelope = try OBAEnvelope<OBAArrivalsEntry>.decode(from: data)
        let preds = envelope.data.entry?.toPredictions() ?? []

        let route12 = preds.filter { $0.routeId == "1_100224" }
        let route40 = preds.filter { $0.routeId == "1_100479" }
        #expect(route12.count == 2)
        #expect(route40.count == 1)
    }

    @Test("stop.json decodes to StopInfo with routeIds")
    func decodeStop() throws {
        let data = try loadFixture("stop")
        let envelope = try OBAEnvelope<OBAStop>.decode(from: data)
        let stop = envelope.data.entry?.toModel()

        #expect(stop?.stopId == "1_75403")
        #expect(stop?.code == "75403")
        #expect(stop?.name == "3rd Ave & Pike St")
        #expect(stop?.routeIds == ["1_100224", "1_100479"])
    }

    @Test("route.json decodes to RouteInfo with shortName + agencyId")
    func decodeRoute() throws {
        let data = try loadFixture("route")
        let envelope = try OBAEnvelope<OBARoute>.decode(from: data)
        let route = envelope.data.entry?.toModel()

        #expect(route?.routeId == "1_100224")
        #expect(route?.shortName == "12")
        #expect(route?.longName == "Capitol Hill - Downtown")
        #expect(route?.agencyId == "1")
    }

    @Test("unauthorized envelope throws TransitAPIError.unauthorized")
    func unauthorizedEnvelope() throws {
        let data = try loadFixture("unauthorized")
        #expect(throws: TransitAPIError.unauthorized) {
            _ = try OBAEnvelope<OBAEmptyEntry>.decode(from: data)
        }
    }

    @Test("rate gate spaces requests at least 110ms apart")
    func rateGateSpacing() async {
        let gate = OBARateGate()
        let start = Date()
        await gate.acquire()
        await gate.acquire()
        await gate.acquire()
        let elapsed = Date().timeIntervalSince(start)
        // Two waits of ≥110ms each (the first call passes through immediately).
        #expect(elapsed >= 0.220, "expected ≥220ms total, got \(elapsed)")
    }
}