[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:
Daniel Arantes Loverde
2026-09-16 13:21:10 -03:00
parent 2ba487a6e8
commit c6e9803c75
12 changed files with 392 additions and 69 deletions

View File

@@ -1,4 +1,15 @@
import Foundation
@testable import LCEssentials
/// Shared factory so test classes don't each declare a force-unwrapped `API!`
/// var for the XCTest setUp/tearDown lifecycle.
extension API {
static func lce_featureControlTestInstance() -> API {
let cfg = URLSessionConfiguration.ephemeral
cfg.protocolClasses = [StubURLProtocol.self]
return API(testConfiguration: cfg)
}
}
/// Test double for `URLProtocol`. Intercepts every request on a `URLSession`
/// configured with it, records the outgoing `URLRequest`, and replays a canned
@@ -56,6 +67,13 @@ final class StubURLProtocol: URLProtocol, @unchecked Sendable {
return _capturedRequests.count
}
/// Bodies of every intercepted request, in order for tests asserting batch
/// boundaries across multiple requests (`lastCapturedBody` only sees the last one).
static var capturedBodies: [Data] {
lock.lock(); defer { lock.unlock() }
return _capturedBodies
}
private static func currentStub() -> Stub {
lock.lock(); defer { lock.unlock() }
return _stub
@@ -84,11 +102,14 @@ final class StubURLProtocol: URLProtocol, @unchecked Sendable {
return
}
let url = request.url ?? URL(string: "https://stub.invalid")!
let response = HTTPURLResponse(url: url,
statusCode: stub.statusCode,
httpVersion: "HTTP/1.1",
headerFields: stub.headers)!
guard let url = request.url ?? URL(string: "https://stub.invalid"),
let response = HTTPURLResponse(url: url,
statusCode: stub.statusCode,
httpVersion: "HTTP/1.1",
headerFields: stub.headers) else {
client.urlProtocol(self, didFailWithError: URLError(.badServerResponse))
return
}
client.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client.urlProtocol(self, didLoad: stub.body)
client.urlProtocolDidFinishLoading(self)