From 0b00f97e0a12efa5253de33c9d9020ef208ff82b Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Wed, 6 Nov 2024 16:07:56 +0100 Subject: [PATCH 1/4] test: apply on null test --- Tests/ConfidenceTests/ConfidenceTest.swift | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/Tests/ConfidenceTests/ConfidenceTest.swift b/Tests/ConfidenceTests/ConfidenceTest.swift index 4d8c3f96..e99988c4 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 From 1cd12a75ada33e10ac734c3ad74a17b553a9b0fb Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Wed, 6 Nov 2024 16:20:03 +0100 Subject: [PATCH 2/4] fix: TypeMismatch doesnt trigger apply --- Sources/Confidence/FlagEvaluation.swift | 18 +++++++++++++----- Tests/ConfidenceTests/ConfidenceTest.swift | 1 + 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Sources/Confidence/FlagEvaluation.swift b/Sources/Confidence/FlagEvaluation.swift index 393fbc2a..c04d6c61 100644 --- a/Sources/Confidence/FlagEvaluation.swift +++ b/Sources/Confidence/FlagEvaluation.swift @@ -44,11 +44,7 @@ extension FlagResolution { ) } - if resolvedFlag.resolveReason != .targetingKeyError { - Task { - await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) - } - } else { + if resolvedFlag.resolveReason == .targetingKeyError { return Evaluation( value: defaultValue, variant: nil, @@ -59,6 +55,9 @@ extension FlagResolution { } guard let value = resolvedFlag.value else { + Task { + await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) + } return Evaluation( value: defaultValue, variant: resolvedFlag.variant, @@ -77,6 +76,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 +89,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 +110,9 @@ extension FlagResolution { } } } else { + Task { + await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) + } return Evaluation( value: defaultValue, variant: resolvedFlag.variant, diff --git a/Tests/ConfidenceTests/ConfidenceTest.swift b/Tests/ConfidenceTests/ConfidenceTest.swift index e99988c4..51dc65d5 100644 --- a/Tests/ConfidenceTests/ConfidenceTest.swift +++ b/Tests/ConfidenceTests/ConfidenceTest.swift @@ -694,6 +694,7 @@ class ConfidenceTest: XCTestCase { XCTAssertNil(evaluation.errorMessage, "") XCTAssertEqual(evaluation.reason, .error) XCTAssertEqual(evaluation.variant, nil) + XCTAssertEqual(flagApplier.applyCallCount, 0) } func testConcurrentActivate() async { From 632c3bf040f46b3df47a13fa1ffef4041df0d5ae Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Thu, 7 Nov 2024 11:19:43 +0100 Subject: [PATCH 3/4] refactor: Clearer code paths --- Sources/Confidence/FlagEvaluation.swift | 35 +++++++++++++++++++------ 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/Sources/Confidence/FlagEvaluation.swift b/Sources/Confidence/FlagEvaluation.swift index c04d6c61..775867db 100644 --- a/Sources/Confidence/FlagEvaluation.swift +++ b/Sources/Confidence/FlagEvaluation.swift @@ -44,17 +44,12 @@ extension FlagResolution { ) } - if resolvedFlag.resolveReason == .targetingKeyError { - return Evaluation( - value: defaultValue, - variant: nil, - reason: .targetingKeyError, - errorCode: .invalidContext, - errorMessage: "Invalid targeting key" - ) + if let evaluation = checkBackendErrrs(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) } @@ -133,6 +128,30 @@ extension FlagResolution { } // swiftlint:enable function_body_length + private func checkBackendErrrs(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 { From bfd55044e3aea40b2e6dd4be99d64d8e17648940 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Thu, 7 Nov 2024 18:18:35 +0100 Subject: [PATCH 4/4] fixup! refactor: Clearer code paths --- Sources/Confidence/FlagEvaluation.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Confidence/FlagEvaluation.swift b/Sources/Confidence/FlagEvaluation.swift index 775867db..d64e43ca 100644 --- a/Sources/Confidence/FlagEvaluation.swift +++ b/Sources/Confidence/FlagEvaluation.swift @@ -44,7 +44,7 @@ extension FlagResolution { ) } - if let evaluation = checkBackendErrrs(resolvedFlag: resolvedFlag, defaultValue: defaultValue) { + if let evaluation = checkBackendErrors(resolvedFlag: resolvedFlag, defaultValue: defaultValue) { return evaluation } @@ -128,7 +128,7 @@ extension FlagResolution { } // swiftlint:enable function_body_length - private func checkBackendErrrs(resolvedFlag: ResolvedValue, defaultValue: T) -> Evaluation? { + private func checkBackendErrors(resolvedFlag: ResolvedValue, defaultValue: T) -> Evaluation? { if resolvedFlag.resolveReason == .targetingKeyError { return Evaluation( value: defaultValue,