[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:
@@ -0,0 +1,93 @@
|
||||
import XCTest
|
||||
@testable import LCFeatureControl
|
||||
@testable import LCEssentials
|
||||
|
||||
final class FeatureControlNotificationsClientTests: 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 makeClient() -> FeatureControlNotificationsClient {
|
||||
let config = FeatureControlConfiguration(baseURL: "https://api.example.com",
|
||||
environment: "production",
|
||||
auth: FeatureControlBearerAuth { "jwt-123" })
|
||||
return FeatureControlNotificationsClient(configuration: config, api: api)
|
||||
}
|
||||
|
||||
func testListDecodesItemsAndNextCursor() async throws {
|
||||
let body = """
|
||||
{"error": false, "result": {"items": [
|
||||
{"id": "n1", "title": "T", "body": "B", "severity": "info",
|
||||
"createdAt": "2026-04-16T12:00:00.000Z", "read": false}
|
||||
], "nextCursor": "cursor-2"}}
|
||||
"""
|
||||
StubURLProtocol.setStub(.init(statusCode: 200, body: Data(body.utf8)))
|
||||
let client = makeClient()
|
||||
|
||||
let (items, nextCursor) = try await client.list(status: .unread, limit: 20)
|
||||
|
||||
XCTAssertEqual(items.count, 1)
|
||||
XCTAssertEqual(items.first?.id, "n1")
|
||||
XCTAssertEqual(nextCursor, "cursor-2")
|
||||
}
|
||||
|
||||
func testListPassesStatusAndLimitAsQueryParams() async throws {
|
||||
let body = #"{"error": false, "result": {"items": [], "nextCursor": null}}"#
|
||||
StubURLProtocol.setStub(.init(statusCode: 200, body: Data(body.utf8)))
|
||||
let client = makeClient()
|
||||
|
||||
_ = try await client.list(status: .unread, limit: 5)
|
||||
|
||||
let url = StubURLProtocol.capturedRequests.first?.url?.absoluteString
|
||||
XCTAssertEqual(url, "https://api.example.com/api/feature-control/notifications?status=unread&limit=5")
|
||||
XCTAssertEqual(StubURLProtocol.capturedRequests.first?.value(forHTTPHeaderField: "Authorization"),
|
||||
"Bearer jwt-123")
|
||||
}
|
||||
|
||||
func testMarkReadPostsToCorrectPathAndSucceedsOn200() async throws {
|
||||
StubURLProtocol.setStub(.init(statusCode: 200, headers: ["Content-Type": "text/plain"], body: Data("ok".utf8)))
|
||||
let client = makeClient()
|
||||
|
||||
try await client.markRead(id: "n1")
|
||||
|
||||
let sent = StubURLProtocol.capturedRequests.first
|
||||
XCTAssertEqual(sent?.httpMethod, "POST")
|
||||
XCTAssertEqual(sent?.url?.absoluteString, "https://api.example.com/api/feature-control/notifications/n1/read")
|
||||
}
|
||||
|
||||
func testMarkAllReadSucceedsOn200() async throws {
|
||||
StubURLProtocol.setStub(.init(statusCode: 200, headers: ["Content-Type": "text/plain"], body: Data("ok".utf8)))
|
||||
let client = makeClient()
|
||||
|
||||
try await client.markAllRead()
|
||||
|
||||
XCTAssertEqual(StubURLProtocol.capturedRequests.first?.url?.absoluteString,
|
||||
"https://api.example.com/api/feature-control/notifications/read-all")
|
||||
}
|
||||
|
||||
func testUnauthorizedMapsToFeatureControlErrorNotCrash() async {
|
||||
StubURLProtocol.setStub(.init(statusCode: 401, body: Data(#"{"code":"FEATURE_CONTROL_MISSING_AUTH"}"#.utf8)))
|
||||
let client = makeClient()
|
||||
|
||||
do {
|
||||
try await client.markAllRead()
|
||||
XCTFail("expected throw")
|
||||
} catch let error as FeatureControlError {
|
||||
XCTAssertEqual(error, .unauthorized(code: "FEATURE_CONTROL_MISSING_AUTH"))
|
||||
} catch {
|
||||
XCTFail("expected FeatureControlError, got \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user