migration
This commit is contained in:
104
PediFoodsTests/FeatureControlServiceTests.swift
Normal file
104
PediFoodsTests/FeatureControlServiceTests.swift
Normal file
@@ -0,0 +1,104 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import PediFoods
|
||||
|
||||
/// All these tests set the shared `URLProtocolStub.handler` static, so they
|
||||
/// must not run concurrently with each other (see the note on
|
||||
/// `SessionStateStorePersistenceTests` for the same reasoning).
|
||||
@Suite(.serialized)
|
||||
struct FeatureControlServiceTests {
|
||||
@MainActor
|
||||
private func makeService(cacheTTL: TimeInterval = 60) -> FeatureControlService {
|
||||
let defaults = UserDefaults(suiteName: UUID().uuidString)!
|
||||
let session = URLProtocolStub.makeSession()
|
||||
return FeatureControlService(session: session, cacheTTL: cacheTTL, userDefaults: defaults)
|
||||
}
|
||||
|
||||
private let bootstrapSuccessBody = Data("""
|
||||
{"ok":true,"configVersion":3,"evaluatedAt":"2026-01-01T00:00:00Z","source":"live",
|
||||
"flags":{"at.promo":true},"raw":{"at.promo":{"enabled":true,"variant":"on"}}}
|
||||
""".utf8)
|
||||
|
||||
@Test("evaluate returns a live snapshot on a successful bootstrap and caches it")
|
||||
@MainActor
|
||||
func evaluateSucceedsAndCaches() async {
|
||||
let service = makeService()
|
||||
URLProtocolStub.handler = { _ in (.stub(statusCode: 200), self.bootstrapSuccessBody) }
|
||||
defer { URLProtocolStub.handler = nil }
|
||||
|
||||
let context = FeatureControlEvaluationContext(subjectType: "customer", subjectId: "u1", storeId: nil, attributes: [:])
|
||||
let snapshot = await service.evaluate(context: context, jwt: nil)
|
||||
|
||||
#expect(snapshot.source == "live")
|
||||
#expect(snapshot.configVersion == 3)
|
||||
#expect(snapshot.isEnabled("at.promo") == true)
|
||||
}
|
||||
|
||||
@Test("evaluate serves from cache on a second call without hitting the network again")
|
||||
@MainActor
|
||||
func evaluateServesFromCacheOnSecondCall() async {
|
||||
let service = makeService()
|
||||
var requestCount = 0
|
||||
URLProtocolStub.handler = { [bootstrapSuccessBody] _ in
|
||||
requestCount += 1
|
||||
return (.stub(statusCode: 200), bootstrapSuccessBody)
|
||||
}
|
||||
defer { URLProtocolStub.handler = nil }
|
||||
|
||||
let context = FeatureControlEvaluationContext(subjectType: "customer", subjectId: "u1", storeId: nil, attributes: [:])
|
||||
_ = await service.evaluate(context: context, jwt: nil)
|
||||
let second = await service.evaluate(context: context, jwt: nil)
|
||||
|
||||
#expect(requestCount == 1)
|
||||
#expect(second.source == "live")
|
||||
}
|
||||
|
||||
@Test("evaluate falls back to a stale cache entry when the network call fails")
|
||||
@MainActor
|
||||
func evaluateFallsBackToCacheOnNetworkError() async {
|
||||
let service = makeService()
|
||||
let context = FeatureControlEvaluationContext(subjectType: "customer", subjectId: "u1", storeId: nil, attributes: [:])
|
||||
|
||||
URLProtocolStub.handler = { _ in (.stub(statusCode: 200), self.bootstrapSuccessBody) }
|
||||
_ = await service.evaluate(context: context, jwt: nil)
|
||||
|
||||
URLProtocolStub.handler = { _ in (.stub(statusCode: 500), Data()) }
|
||||
defer { URLProtocolStub.handler = nil }
|
||||
let refreshed = await service.evaluate(context: context, jwt: nil, forceRefresh: true)
|
||||
|
||||
#expect(refreshed.source == "cache_fallback")
|
||||
#expect(refreshed.isEnabled("at.promo") == true)
|
||||
}
|
||||
|
||||
@Test("evaluate falls back to defaults when the network fails and there's no cache")
|
||||
@MainActor
|
||||
func evaluateFallsBackToDefaultsWithNoCache() async {
|
||||
let service = makeService()
|
||||
URLProtocolStub.handler = { _ in (.stub(statusCode: 500), Data()) }
|
||||
defer { URLProtocolStub.handler = nil }
|
||||
|
||||
let context = FeatureControlEvaluationContext(subjectType: "customer", subjectId: "u1", storeId: nil, attributes: [:])
|
||||
let snapshot = await service.evaluate(context: context, jwt: nil)
|
||||
|
||||
#expect(snapshot.source == "defaults")
|
||||
#expect(snapshot.isEnabled("at.promo") == false)
|
||||
}
|
||||
|
||||
@Test("evaluate re-fetches once a cached entry's TTL has expired")
|
||||
@MainActor
|
||||
func evaluateRefetchesAfterCacheExpires() async {
|
||||
let service = makeService(cacheTTL: -1) // already expired the instant it's written
|
||||
var requestCount = 0
|
||||
URLProtocolStub.handler = { [bootstrapSuccessBody] _ in
|
||||
requestCount += 1
|
||||
return (.stub(statusCode: 200), bootstrapSuccessBody)
|
||||
}
|
||||
defer { URLProtocolStub.handler = nil }
|
||||
|
||||
let context = FeatureControlEvaluationContext(subjectType: "customer", subjectId: "u1", storeId: nil, attributes: [:])
|
||||
_ = await service.evaluate(context: context, jwt: nil)
|
||||
_ = await service.evaluate(context: context, jwt: nil)
|
||||
|
||||
#expect(requestCount == 2)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user