[sub-spm-optional-products] Add LCECryptoKit and LCFeatureControl as optional sub-SPM products
LCEssentials installs standalone; each sub target-depends on it so linking a sub's product always pulls LCEssentials in too, without the consumer having to declare it separately. - LCECryptoKit: internalized from the remote LCECryptoKitBinary git dependency (embedded token URL removed) into a local binaryTarget vendoring Frameworks/LCECryptoKit.xcframework. LCECryptoKitManager moved out of LCEssentials core into its own LCECryptoKitManager target/product; the no-op fallback for when the binary wasn't linked is gone (breaking change for existing consumers, see decisions/2026-09-15-sub-spm-optional-products.md). - LCFeatureControl: new product wrapping Atomenta's Feature Control API (flag evaluation with TTL cache + safe-degrade fallback to defaults, notifications inbox, batched exposure telemetry). 45 new tests.
This commit is contained in:
70
Tests/LCFeatureControlTests/FeatureControlCacheTests.swift
Normal file
70
Tests/LCFeatureControlTests/FeatureControlCacheTests.swift
Normal file
@@ -0,0 +1,70 @@
|
||||
import XCTest
|
||||
@testable import LCFeatureControl
|
||||
|
||||
/// Test-only mutable clock box — a `var` captured directly by the cache's
|
||||
/// escaping `@Sendable` closure would trip strict-concurrency capture checks;
|
||||
/// this mirrors the `@unchecked Sendable` pattern `StubURLProtocol` already uses.
|
||||
private final class MutableClock: @unchecked Sendable {
|
||||
var value: Date
|
||||
init(_ value: Date) { self.value = value }
|
||||
}
|
||||
|
||||
final class FeatureControlCacheTests: XCTestCase {
|
||||
|
||||
private func snapshot(configVersion: Int) -> FeatureControlSnapshot {
|
||||
FeatureControlSnapshot(evaluatedAt: Date(), configVersion: configVersion, flags: [:])
|
||||
}
|
||||
|
||||
private func key(_ keys: [String] = ["a", "b"]) -> FeatureControlCacheKey {
|
||||
FeatureControlCacheKey(environment: "production", subjectId: "cust_1",
|
||||
sortedKeys: keys.sorted(), platform: "ios", appVersion: "1.0")
|
||||
}
|
||||
|
||||
func testHitWithinTTLReturnsSameSnapshot() async {
|
||||
let cache = FeatureControlCache(now: { Date() })
|
||||
await cache.set(key(), snapshot: snapshot(configVersion: 1), ttl: 45)
|
||||
let hit = await cache.get(key(), allowStale: false)
|
||||
XCTAssertEqual(hit?.configVersion, 1)
|
||||
}
|
||||
|
||||
func testMissAfterTTLExpiryReturnsNilWhenStaleNotAllowed() async {
|
||||
let clock = MutableClock(Date())
|
||||
let cache = FeatureControlCache(now: { clock.value })
|
||||
await cache.set(key(), snapshot: snapshot(configVersion: 1), ttl: 1)
|
||||
clock.value = clock.value.addingTimeInterval(2)
|
||||
let hit = await cache.get(key(), allowStale: false)
|
||||
XCTAssertNil(hit)
|
||||
}
|
||||
|
||||
func testStaleAllowedReturnsExpiredEntry() async {
|
||||
let clock = MutableClock(Date())
|
||||
let cache = FeatureControlCache(now: { clock.value })
|
||||
await cache.set(key(), snapshot: snapshot(configVersion: 1), ttl: 1)
|
||||
clock.value = clock.value.addingTimeInterval(2)
|
||||
let hit = await cache.get(key(), allowStale: true)
|
||||
XCTAssertEqual(hit?.configVersion, 1)
|
||||
}
|
||||
|
||||
func testKeyDiffersBySortedKeysNotInputOrder() async {
|
||||
let cache = FeatureControlCache(now: { Date() })
|
||||
await cache.set(key(["a", "b"]), snapshot: snapshot(configVersion: 1), ttl: 45)
|
||||
let hit = await cache.get(key(["b", "a"]), allowStale: false)
|
||||
XCTAssertEqual(hit?.configVersion, 1)
|
||||
}
|
||||
|
||||
func testSetOverwritesPriorConfigVersion() async {
|
||||
let cache = FeatureControlCache(now: { Date() })
|
||||
await cache.set(key(), snapshot: snapshot(configVersion: 7), ttl: 45)
|
||||
await cache.set(key(), snapshot: snapshot(configVersion: 8), ttl: 45)
|
||||
let hit = await cache.get(key(), allowStale: false)
|
||||
XCTAssertEqual(hit?.configVersion, 8)
|
||||
}
|
||||
|
||||
func testInvalidateAllClearsEntries() async {
|
||||
let cache = FeatureControlCache(now: { Date() })
|
||||
await cache.set(key(), snapshot: snapshot(configVersion: 1), ttl: 45)
|
||||
await cache.invalidateAll()
|
||||
let hit = await cache.get(key(), allowStale: true)
|
||||
XCTAssertNil(hit)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user