import XCTest @testable import LCFeatureControl /// Exact evaluate-response shape from Atomenta's `docs/feature-control/EXTERNAL_CLIENTS.md` §3. private let evaluateResponseFixture = """ { "error": false, "code": "FEATURE_CONTROL_EVALUATED", "result": { "evaluatedAt": "2026-04-16T12:00:00.000Z", "configVersion": 7, "flags": { "fc.checkout_v2": { "enabled": true, "variant": "on", "payload": null, "reason": "rollout" }, "fc.search_ranking_v3": { "enabled": false, "variant": "off", "payload": null, "reason": "default" } } } } """ final class FeatureControlModelsTests: XCTestCase { func testFlagDecodesFromEvaluateResponseFixture() throws { let data = Data(evaluateResponseFixture.utf8) let envelope = try JSONDecoder().decode(FeatureControlEvaluateEnvelope.self, from: data) XCTAssertFalse(envelope.error) XCTAssertEqual(envelope.result.configVersion, 7) let checkout = try XCTUnwrap(envelope.result.flags["fc.checkout_v2"]) XCTAssertTrue(checkout.enabled) XCTAssertEqual(checkout.variant, "on") XCTAssertNil(checkout.payload) XCTAssertEqual(checkout.reason, "rollout") let ranking = try XCTUnwrap(envelope.result.flags["fc.search_ranking_v3"]) XCTAssertFalse(ranking.enabled) } func testEvaluatedAtParsesFractionalSecondsISO8601() throws { let data = Data(evaluateResponseFixture.utf8) let envelope = try JSONDecoder().decode(FeatureControlEvaluateEnvelope.self, from: data) var calendar = Calendar(identifier: .gregorian) calendar.timeZone = TimeZone(identifier: "UTC")! let components = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: envelope.result.evaluatedAt) XCTAssertEqual(components.year, 2026) XCTAssertEqual(components.month, 4) XCTAssertEqual(components.day, 16) XCTAssertEqual(components.hour, 12) } func testContextEncodesExactOpenAPIShape() throws { let context = FeatureControlContext(subjectType: .customer, subjectId: "cust_123", storeId: "store_001", platform: "ios", appVersion: "2.3.1", attributes: ["city": "Belo Horizonte"]) let data = try JSONEncoder().encode(context) let object = try XCTUnwrap(try JSONSerialization.jsonObject(with: data) as? [String: Any]) XCTAssertEqual(object["subjectType"] as? String, "customer") XCTAssertEqual(object["subjectId"] as? String, "cust_123") XCTAssertEqual(object["storeId"] as? String, "store_001") XCTAssertEqual(object["platform"] as? String, "ios") XCTAssertEqual(object["appVersion"] as? String, "2.3.1") let attributes = try XCTUnwrap(object["attributes"] as? [String: String]) XCTAssertEqual(attributes["city"], "Belo Horizonte") } func testNotificationDecodesFromOpenAPIShape() throws { let json = """ { "id": "n1", "title": "Manutenção", "body": "Janela de manutenção às 22h", "severity": "warning", "createdAt": "2026-04-16T12:00:00.000Z", "read": false, "ctaLabel": "Ver detalhes", "ctaUrl": "https://example.com" } """ let notification = try JSONDecoder().decode(FeatureControlNotification.self, from: Data(json.utf8)) XCTAssertEqual(notification.id, "n1") XCTAssertEqual(notification.severity, "warning") XCTAssertFalse(notification.read) XCTAssertEqual(notification.ctaLabel, "Ver detalhes") } func testNotificationDecodesWithoutOptionalCTAFields() throws { let json = """ { "id": "n2", "title": "Info", "body": "Just FYI", "severity": "info", "createdAt": "2026-04-16T12:00:00Z", "read": true } """ let notification = try JSONDecoder().decode(FeatureControlNotification.self, from: Data(json.utf8)) XCTAssertNil(notification.ctaLabel) XCTAssertNil(notification.ctaUrl) } func testExposureEventEncodesExpectedShape() throws { let event = FeatureControlExposureEvent(featureKey: "fc.checkout_v2", variant: "on", subjectType: .customer, storeId: "store_001") let data = try JSONEncoder().encode(event) let object = try XCTUnwrap(try JSONSerialization.jsonObject(with: data) as? [String: Any]) XCTAssertEqual(object["featureKey"] as? String, "fc.checkout_v2") XCTAssertEqual(object["subjectType"] as? String, "customer") } }