75 lines
3.2 KiB
Swift
75 lines
3.2 KiB
Swift
import ConjureKit
|
|
import Foundation
|
|
import Testing
|
|
|
|
@MainActor
|
|
private final class RateRequests {
|
|
var pending: [CheckedContinuation<CurrencyRates, any Error>] = []
|
|
func fetch() async throws -> CurrencyRates {
|
|
try await withCheckedThrowingContinuation { pending.append($0) }
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private func waitForRates(_ condition: () -> Bool) async throws {
|
|
// Scheduling deadline only; calculation freshness uses the injected clock.
|
|
for _ in 0..<1_000 {
|
|
if condition() { return }
|
|
try await Task.sleep(for: .milliseconds(1))
|
|
}
|
|
try #require(condition(), "Currency calculation did not reach the expected state")
|
|
}
|
|
|
|
@Test @MainActor
|
|
func currencyLifetimeSharesRefreshAndPreservesStaleCopyUntilFailure() async throws {
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
|
|
var now = calendar.date(from: DateComponents(year: 2026, month: 1, day: 2, hour: 12))!
|
|
let requests = RateRequests()
|
|
let calculation = CurrencyCalculation(now: { now }, calendar: calendar, fetch: { try await requests.fetch() })
|
|
var completions = 0
|
|
calculation.changed = { completions += 1 }
|
|
let snapshot = CurrencyRates(date: "2026-01-02", rates: ["eur": 1, "usd": 2, "dkk": 8])
|
|
|
|
calculation.update("100 USD to DKK")
|
|
#expect(calculation.isCurrency)
|
|
#expect(calculation.result?.copyValue == nil)
|
|
try await waitForRates { requests.pending.count == 1 }
|
|
calculation.update("50 USD to DKK")
|
|
requests.pending[0].resume(returning: snapshot)
|
|
try await waitForRates { completions == 1 }
|
|
#expect(calculation.result?.copyValue == "200 DKK")
|
|
calculation.update("100 USD to DKK")
|
|
#expect(calculation.result?.copyValue == "400 DKK")
|
|
#expect(requests.pending.count == 1)
|
|
|
|
now = calendar.date(byAdding: .day, value: 1, to: now)!
|
|
calculation.update("100 USD to DKK")
|
|
#expect(calculation.result?.copyValue == "400 DKK")
|
|
#expect(calculation.result?.detail.contains("Stale cache; refreshing") == true)
|
|
try await waitForRates { requests.pending.count == 2 }
|
|
calculation.update("50 USD to DKK")
|
|
requests.pending[1].resume(throwing: URLError(.notConnectedToInternet))
|
|
try await waitForRates { completions == 2 }
|
|
#expect(calculation.result?.copyValue == nil)
|
|
#expect(calculation.result?.displayValue == "Currency rates unavailable")
|
|
calculation.update("50 USD to DKK")
|
|
#expect(calculation.result?.copyValue == nil)
|
|
#expect(requests.pending.count == 2)
|
|
|
|
calculation.update("25 USD to DKK")
|
|
try await waitForRates { requests.pending.count == 3 }
|
|
calculation.update("Safari") // Leaving currency does not cancel shared work.
|
|
#expect(!calculation.isCurrency)
|
|
#expect(calculation.result == nil)
|
|
requests.pending[2].resume(returning: snapshot)
|
|
try await waitForRates { completions == 3 }
|
|
#expect(calculation.result == nil)
|
|
calculation.update("25 USD to DKK")
|
|
#expect(calculation.result?.copyValue == "100 DKK")
|
|
#expect(requests.pending.count == 3) // Fetch day, not snapshot metadata, controls freshness.
|
|
calculation.update("1 km to mi")
|
|
#expect(!calculation.isCurrency)
|
|
#expect(calculation.result?.copyValue == "0.621371192237 mi")
|
|
}
|