[lcfeaturecontrol-review-fixes] Fix JWT log leak, drop macOS/tvOS, coalesce requests
- FeatureControlManager/NotificationsClient: pass debug: false at every
api.request(...) call site — the API default (debug: true) was printing
Authorization: Bearer <jwt> on every request, release builds included.
- Package.swift: drop macOS/tvOS from platforms — LCEssentials.API (used by
LCFeatureControl) is iOS/watchOS-only and the vendored xcframework has no
macOS/tvOS slice.
- FeatureControlManager: coalesce concurrent evaluate() calls on a cold cache
into a single in-flight request per key instead of firing one per caller.
- FeatureControlManager/NotificationsClient: treat a 200 response carrying
{"error": true} as a failure instead of caching/returning it as success.
- FeatureControlNotificationsClient: thread a cursor param through list() so
the already-decoded nextCursor can actually be used to page.
- Tests: 9 new tests (coalescing, envelope-error-on-200, cursor param, a real
object payload decoded through a full evaluate response, date-decode
failures) and removed the 7 remaining force-unwraps in test scaffolding.
- Documentation/FeatureControl.md: new guide for the LCFeatureControl product,
cross-linked from README.md and Extensions.md.
This commit is contained in:
@@ -51,7 +51,7 @@ final class FeatureControlModelsTests: XCTestCase {
|
||||
let envelope = try JSONDecoder().decode(FeatureControlEvaluateEnvelope.self, from: data)
|
||||
|
||||
var calendar = Calendar(identifier: .gregorian)
|
||||
calendar.timeZone = TimeZone(identifier: "UTC")!
|
||||
calendar.timeZone = try XCTUnwrap(TimeZone(identifier: "UTC"))
|
||||
let components = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second],
|
||||
from: envelope.result.evaluatedAt)
|
||||
XCTAssertEqual(components.year, 2026)
|
||||
@@ -60,6 +60,34 @@ final class FeatureControlModelsTests: XCTestCase {
|
||||
XCTAssertEqual(components.hour, 12)
|
||||
}
|
||||
|
||||
func testEvaluatedAtDecodeFailsOnUnrecognizedDateFormat() {
|
||||
let json = #"{"evaluatedAt": "not-a-date", "configVersion": 1, "flags": {}}"#
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(FeatureControlSnapshot.self, from: Data(json.utf8))) { error in
|
||||
guard case DecodingError.dataCorrupted = error else {
|
||||
return XCTFail("expected .dataCorrupted, got \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testFlagDecodesObjectPayloadThroughEvaluateEnvelope() throws {
|
||||
let json = """
|
||||
{"error": false, "code": "FEATURE_CONTROL_EVALUATED", "result": {
|
||||
"evaluatedAt": "2026-04-16T12:00:00.000Z",
|
||||
"configVersion": 3,
|
||||
"flags": {"fc.promo_banner": {"enabled": true, "variant": "on",
|
||||
"payload": {"title": "Sale", "limit": 5, "tags": ["a", "b"]}, "reason": "rollout"}}
|
||||
}}
|
||||
"""
|
||||
let envelope = try JSONDecoder().decode(FeatureControlEvaluateEnvelope.self, from: Data(json.utf8))
|
||||
let flag = try XCTUnwrap(envelope.result.flags["fc.promo_banner"])
|
||||
guard case let .object(payload) = flag.payload else {
|
||||
return XCTFail("expected .object payload, got \(String(describing: flag.payload))")
|
||||
}
|
||||
XCTAssertEqual(payload["title"], .string("Sale"))
|
||||
XCTAssertEqual(payload["limit"], .number(5))
|
||||
XCTAssertEqual(payload["tags"], .array([.string("a"), .string("b")]))
|
||||
}
|
||||
|
||||
func testContextEncodesExactOpenAPIShape() throws {
|
||||
let context = FeatureControlContext(subjectType: .customer, subjectId: "cust_123",
|
||||
storeId: "store_001", platform: "ios",
|
||||
@@ -113,6 +141,18 @@ final class FeatureControlModelsTests: XCTestCase {
|
||||
XCTAssertNil(notification.ctaUrl)
|
||||
}
|
||||
|
||||
func testNotificationCreatedAtDecodeFailsOnUnrecognizedDateFormat() {
|
||||
let json = """
|
||||
{"id": "n3", "title": "T", "body": "B", "severity": "info",
|
||||
"createdAt": "not-a-date", "read": false}
|
||||
"""
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(FeatureControlNotification.self, from: Data(json.utf8))) { error in
|
||||
guard case DecodingError.dataCorrupted = error else {
|
||||
return XCTFail("expected .dataCorrupted, got \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testExposureEventEncodesExpectedShape() throws {
|
||||
let event = FeatureControlExposureEvent(featureKey: "fc.checkout_v2", variant: "on",
|
||||
subjectType: .customer, storeId: "store_001")
|
||||
|
||||
Reference in New Issue
Block a user