diff --git a/Sources/Confidence/FlagEvaluation.swift b/Sources/Confidence/FlagEvaluation.swift index 393fbc2a..d64e43ca 100644 --- a/Sources/Confidence/FlagEvaluation.swift +++ b/Sources/Confidence/FlagEvaluation.swift @@ -44,21 +44,15 @@ extension FlagResolution { ) } - if resolvedFlag.resolveReason != .targetingKeyError { - Task { - await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) - } - } else { - return Evaluation( - value: defaultValue, - variant: nil, - reason: .targetingKeyError, - errorCode: .invalidContext, - errorMessage: "Invalid targeting key" - ) + if let evaluation = checkBackendErrors(resolvedFlag: resolvedFlag, defaultValue: defaultValue) { + return evaluation } guard let value = resolvedFlag.value else { + // No backend error, but nil value returned. This can happend with "noSegmentMatch" or "archived", for example + Task { + await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) + } return Evaluation( value: defaultValue, variant: resolvedFlag.variant, @@ -77,6 +71,9 @@ extension FlagResolution { resolveReason = .stale } if let typedValue = typedValue { + Task { + await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) + } return Evaluation( value: typedValue, variant: resolvedFlag.variant, @@ -87,6 +84,9 @@ extension FlagResolution { } else { // `null` type from backend instructs to use client-side default value if parsedValue == .init(null: ()) { + Task { + await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) + } return Evaluation( value: defaultValue, variant: resolvedFlag.variant, @@ -105,6 +105,9 @@ extension FlagResolution { } } } else { + Task { + await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) + } return Evaluation( value: defaultValue, variant: resolvedFlag.variant, @@ -125,6 +128,30 @@ extension FlagResolution { } // swiftlint:enable function_body_length + private func checkBackendErrors(resolvedFlag: ResolvedValue, defaultValue: T) -> Evaluation? { + if resolvedFlag.resolveReason == .targetingKeyError { + return Evaluation( + value: defaultValue, + variant: nil, + reason: .targetingKeyError, + errorCode: .invalidContext, + errorMessage: "Invalid targeting key" + ) + } else if resolvedFlag.resolveReason == .error || + resolvedFlag.resolveReason == .unknown || + resolvedFlag.resolveReason == .unspecified { + return Evaluation( + value: defaultValue, + variant: nil, + reason: .error, + errorCode: .evaluationError, + errorMessage: "Unknown error from backend" + ) + } else { + return nil + } + } + // swiftlint:disable:next cyclomatic_complexity private func getTyped(value: ConfidenceValue) -> T? { if let value = self as? T { diff --git a/Tests/ConfidenceTests/ConfidenceTest.swift b/Tests/ConfidenceTests/ConfidenceTest.swift index 4d8c3f96..51dc65d5 100644 --- a/Tests/ConfidenceTests/ConfidenceTest.swift +++ b/Tests/ConfidenceTests/ConfidenceTest.swift @@ -285,6 +285,46 @@ class ConfidenceTest: XCTestCase { XCTAssertEqual(flagApplier.applyCallCount, 1) } + func testResolveAndApplyIntegerFlagNullValue() async throws { + class FakeClient: ConfidenceResolveClient { + var resolveStats: Int = 0 + var resolvedValues: [ResolvedValue] = [] + func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { + self.resolveStats += 1 + return .init(resolvedValues: resolvedValues, resolveToken: "token") + } + } + + let client = FakeClient() + client.resolvedValues = [ + ResolvedValue( + value: .init(structure: ["size": .init(null: ())]), + flag: "flag", + resolveReason: .match) + ] + + let confidence = Confidence.Builder(clientSecret: "test") + .withContext(initialContext: ["targeting_key": .init(string: "user2")]) + .withFlagResolverClient(flagResolver: client) + .withFlagApplier(flagApplier: flagApplier) + .build() + + try await confidence.fetchAndActivate() + let evaluation = confidence.getEvaluation( + key: "flag.size", + defaultValue: 4) + + XCTAssertEqual(client.resolveStats, 1) + XCTAssertEqual(evaluation.value, 4) + XCTAssertNil(evaluation.errorCode) + XCTAssertNil(evaluation.errorMessage) + XCTAssertEqual(evaluation.reason, .match) + XCTAssertNil(evaluation.variant) + XCTAssertEqual(client.resolveStats, 1) + await fulfillment(of: [flagApplier.applyExpectation], timeout: 1) + XCTAssertEqual(flagApplier.applyCallCount, 1) + } + func testResolveAndApplyIntegerFlagTwice() async throws { class FakeClient: ConfidenceResolveClient { var resolveStats: Int = 0 @@ -654,6 +694,7 @@ class ConfidenceTest: XCTestCase { XCTAssertNil(evaluation.errorMessage, "") XCTAssertEqual(evaluation.reason, .error) XCTAssertEqual(evaluation.variant, nil) + XCTAssertEqual(flagApplier.applyCallCount, 0) } func testConcurrentActivate() async {