diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index c0100d77..b20960cc 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -92,31 +92,31 @@ public class ConfidenceFeatureProvider: FeatureProvider { public func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws -> OpenFeature.ProviderEvaluation { - confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation() + try confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation() } public func getStringEvaluation(key: String, defaultValue: String, context: EvaluationContext?) throws -> OpenFeature.ProviderEvaluation { - confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation() + try confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation() } public func getIntegerEvaluation(key: String, defaultValue: Int64, context: EvaluationContext?) throws -> OpenFeature.ProviderEvaluation { - confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation() + try confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation() } public func getDoubleEvaluation(key: String, defaultValue: Double, context: EvaluationContext?) throws -> OpenFeature.ProviderEvaluation { - confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation() + try confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation() } public func getObjectEvaluation(key: String, defaultValue: OpenFeature.Value, context: EvaluationContext?) throws -> OpenFeature.ProviderEvaluation { - confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation() + try confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation() } public func observe() -> AnyPublisher { @@ -134,8 +134,20 @@ public class ConfidenceFeatureProvider: FeatureProvider { } extension Evaluation { - func toProviderEvaluation() -> ProviderEvaluation { - ProviderEvaluation( + func toProviderEvaluation() throws -> ProviderEvaluation { + if let errorCode = self.errorCode { + switch errorCode { + case .providerNotReady: + throw OpenFeatureError.providerNotReadyError + case .invalidContext: + throw OpenFeatureError.invalidContextError + case .flagNotFound: + throw OpenFeatureError.flagNotFoundError(key: self.errorMessage ?? "unknown key") + case .evaluationError: + throw OpenFeatureError.generalError(message: self.errorMessage ?? "unknown error") + } + } + return ProviderEvaluation( value: self.value, variant: self.variant, reason: self.reason.rawValue, diff --git a/Tests/ConfidenceProviderTests/ConfidenceProviderTest.swift b/Tests/ConfidenceProviderTests/ConfidenceProviderTest.swift index edf4945a..b2c630ee 100644 --- a/Tests/ConfidenceProviderTests/ConfidenceProviderTest.swift +++ b/Tests/ConfidenceProviderTests/ConfidenceProviderTest.swift @@ -7,10 +7,8 @@ import XCTest @testable import Confidence class ConfidenceProviderTest: XCTestCase { - private var readyExpectation = XCTestExpectation(description: "Ready") - private var errorExpectation = XCTestExpectation(description: "Error") - func testErrorFetchOnInit() async throws { + let readyExpectation = XCTestExpectation(description: "Ready") class FakeClient: ConfidenceResolveClient { func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { throw ConfidenceError.internalError(message: "test") @@ -28,7 +26,7 @@ class ConfidenceProviderTest: XCTestCase { let cancellable = OpenFeatureAPI.shared.observe().sink { event in if event == .ready { - self.readyExpectation.fulfill() + readyExpectation.fulfill() } else { print(event) } @@ -38,6 +36,7 @@ class ConfidenceProviderTest: XCTestCase { } func testErrorStorageOnInit() async throws { + let errorExpectation = XCTestExpectation(description: "Error") class FakeStorage: Storage { func save(data: Encodable) throws { // no-op @@ -66,7 +65,7 @@ class ConfidenceProviderTest: XCTestCase { let cancellable = OpenFeatureAPI.shared.observe().sink { event in if event == .error { - self.errorExpectation.fulfill() + errorExpectation.fulfill() } else { print(event) } @@ -74,4 +73,95 @@ class ConfidenceProviderTest: XCTestCase { await fulfillment(of: [errorExpectation], timeout: 5.0) cancellable.cancel() } + + func testProviderThrowsOpenFeatureErrors() async throws { + let context = MutableContext(targetingKey: "t") + let readyExpectation = XCTestExpectation(description: "Ready") + let storage = StorageMock() + class FakeClient: ConfidenceResolveClient { + var resolvedValues: [ResolvedValue] = [ + ResolvedValue( + variant: "variant1", + value: .init(structure: ["int": .init(integer: 42)]), + flag: "flagName", + resolveReason: .match) + ] + func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { + return .init(resolvedValues: resolvedValues, resolveToken: "token") + } + } + + let confidence = Confidence.Builder(clientSecret: "test") + .withContext(initialContext: ["targeting_key": .init(string: "t")]) + .withFlagResolverClient(flagResolver: FakeClient()) + .withStorage(storage: storage) + .build() + + let provider = ConfidenceFeatureProvider(confidence: confidence, initializationStrategy: .fetchAndActivate) + OpenFeatureAPI.shared.setProvider(provider: provider) + let cancellable = OpenFeatureAPI.shared.observe().sink { event in + if event == .ready { + readyExpectation.fulfill() + } else { + print(event) + } + } + await fulfillment(of: [readyExpectation], timeout: 1.0) + cancellable.cancel() + let evaluation = try provider.getIntegerEvaluation(key: "flagName.int", defaultValue: -1, context: context) + XCTAssertEqual(evaluation.value, 42) + + XCTAssertThrowsError(try provider.getIntegerEvaluation( + key: "flagNotFound.something", + defaultValue: -1, + context: context)) + { error in + if let specificError = error as? OpenFeatureError { + XCTAssertEqual(specificError.errorCode(), ErrorCode.flagNotFound) + } else { + XCTFail() + } + } + } +} + +private class StorageMock: Storage { + var data = "" + var saveExpectation: XCTestExpectation? + private let storageQueue = DispatchQueue(label: "com.confidence.storagemock") + + convenience init(data: Encodable) throws { + self.init() + try self.save(data: data) + } + + func save(data: Encodable) throws { + try storageQueue.sync { + let dataB = try JSONEncoder().encode(data) + self.data = String(decoding: dataB, as: UTF8.self) + + saveExpectation?.fulfill() + } + } + + func load(defaultValue: T) throws -> T where T: Decodable { + try storageQueue.sync { + if data.isEmpty { + return defaultValue + } + return try JSONDecoder().decode(T.self, from: try XCTUnwrap(data.data(using: .utf8))) + } + } + + func clear() throws { + storageQueue.sync { + data = "" + } + } + + func isEmpty() -> Bool { + storageQueue.sync { + return data.isEmpty + } + } }