import XCTest @testable import LCFeatureControl @testable import LCEssentials final class FeatureControlNotificationsClientTests: XCTestCase { private var api = API.lce_featureControlTestInstance() override func tearDown() { StubURLProtocol.reset() 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 testListIncludesEncodedCursorWhenProvided() 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: .all, limit: 20, cursor: "cursor abc") let url = StubURLProtocol.capturedRequests.first?.url?.absoluteString XCTAssertEqual(url, "https://api.example.com/api/feature-control/notifications?status=all&limit=20&cursor=cursor%20abc") } func testListOmitsCursorWhenNil() 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: .all, limit: 20) let url = StubURLProtocol.capturedRequests.first?.url?.absoluteString XCTAssertEqual(url, "https://api.example.com/api/feature-control/notifications?status=all&limit=20") } func testListThrowsWhenEnvelopeErrorIsTrueDespite200() async { let body = #"{"error": true, "result": {"items": [], "nextCursor": null}}"# StubURLProtocol.setStub(.init(statusCode: 200, body: Data(body.utf8))) let client = makeClient() do { _ = try await client.list(status: .all, limit: 20) XCTFail("expected throw") } catch let error as FeatureControlError { XCTAssertEqual(error, .server(code: "UNKNOWN", status: 200)) } catch { XCTFail("expected FeatureControlError, got \(error)") } } 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)") } } }