[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:
84
Tests/LCFeatureControlTests/FeatureControlErrorTests.swift
Normal file
84
Tests/LCFeatureControlTests/FeatureControlErrorTests.swift
Normal file
@@ -0,0 +1,84 @@
|
||||
import XCTest
|
||||
@testable import LCFeatureControl
|
||||
@testable import LCEssentials
|
||||
|
||||
final class FeatureControlErrorTests: XCTestCase {
|
||||
|
||||
private var api: API!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
let cfg = URLSessionConfiguration.ephemeral
|
||||
cfg.protocolClasses = [StubURLProtocol.self]
|
||||
api = API(testConfiguration: cfg)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
StubURLProtocol.reset()
|
||||
api = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
private func makeManager() -> FeatureControlManager {
|
||||
let config = FeatureControlConfiguration(baseURL: "https://api.example.com",
|
||||
environment: "production",
|
||||
auth: FeatureControlHeaderAuth(headers: [:]))
|
||||
return FeatureControlManager(configuration: config, api: api)
|
||||
}
|
||||
|
||||
private func context() -> FeatureControlContext {
|
||||
FeatureControlContext(subjectType: .customer, subjectId: "cust_1")
|
||||
}
|
||||
|
||||
private func expectMappedError(status: Int, bodyCode: String) async throws -> FeatureControlError {
|
||||
StubURLProtocol.setStub(.init(statusCode: status, body: Data(#"{"code":"\#(bodyCode)"}"#.utf8)))
|
||||
let manager = makeManager()
|
||||
do {
|
||||
_ = try await manager.evaluateOrThrow(keys: ["x"], context: context())
|
||||
XCTFail("expected FeatureControlError")
|
||||
throw FeatureControlError.transport(message: "unreachable")
|
||||
} catch let error as FeatureControlError {
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
func testMaps400ToInvalidContext() async throws {
|
||||
let error = try await expectMappedError(status: 400, bodyCode: "FEATURE_CONTROL_CONTEXT_INVALID")
|
||||
XCTAssertEqual(error, .invalidContext(code: "FEATURE_CONTROL_CONTEXT_INVALID"))
|
||||
}
|
||||
|
||||
func testMaps401ToUnauthorized() async throws {
|
||||
let error = try await expectMappedError(status: 401, bodyCode: "FEATURE_CONTROL_MISSING_AUTH")
|
||||
XCTAssertEqual(error, .unauthorized(code: "FEATURE_CONTROL_MISSING_AUTH"))
|
||||
}
|
||||
|
||||
func testMaps403ToForbidden() async throws {
|
||||
let error = try await expectMappedError(status: 403, bodyCode: "FEATURE_CONTROL_PANEL_ROLE_REQUIRED")
|
||||
XCTAssertEqual(error, .forbidden(code: "FEATURE_CONTROL_PANEL_ROLE_REQUIRED"))
|
||||
}
|
||||
|
||||
func testMaps429ToRateLimited() async throws {
|
||||
let error = try await expectMappedError(status: 429, bodyCode: "FEATURE_CONTROL_RATE_LIMIT")
|
||||
XCTAssertEqual(error, .rateLimited(code: "FEATURE_CONTROL_RATE_LIMIT"))
|
||||
}
|
||||
|
||||
func testMaps500ToServerWithStatusCode() async throws {
|
||||
let error = try await expectMappedError(status: 500, bodyCode: "FEATURE_CONTROL_ERROR")
|
||||
XCTAssertEqual(error, .server(code: "FEATURE_CONTROL_ERROR", status: 500))
|
||||
}
|
||||
|
||||
func testDecodingFailureMapsToTransport() async {
|
||||
StubURLProtocol.setStub(.init(statusCode: 200, body: Data("not json at all".utf8)))
|
||||
let manager = makeManager()
|
||||
do {
|
||||
_ = try await manager.evaluateOrThrow(keys: ["x"], context: context())
|
||||
XCTFail("expected throw")
|
||||
} catch let error as FeatureControlError {
|
||||
guard case .transport = error else {
|
||||
return XCTFail("expected .transport, got \(error)")
|
||||
}
|
||||
} catch {
|
||||
XCTFail("expected FeatureControlError, got \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user