From a7cbb195dd06d64e9ff12686d17994820ce6d90e Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Wed, 24 Apr 2024 16:01:53 +0200 Subject: [PATCH] feat: Add resolving against confidence context (#94) * add resolving against confidence context * send context hash to the provider cash * evaluate against flatten of context * fixup! evaluate against flatten of context * use datecomponents for date * fix lint * hash only exists for confidence struct * Update Sources/Confidence/ConfidenceValue.swift Co-authored-by: Fabrizio Demaria * write open feature keys flatten and remove the flattening logi * update test to remove the flattening logic * fixup! update test to remove the flattening logic * fixup! Merge branch 'main' into of-resolve-conf-context * fixup! fixup! Merge branch 'main' into of-resolve-conf-context * internal constructor --------- Co-authored-by: Fabrizio Demaria --- Sources/Confidence/ConfidenceValueHash.swift | 91 +++++++++++++++++++ .../Cache/InMemoryProviderCache.swift | 4 +- .../Cache/ProviderCache.swift | 2 +- .../ConfidenceClient/ConfidenceClient.swift | 3 +- .../LocalStorageResolver.swift | 4 +- .../RemoteResolveConfidenceClient.swift | 16 +--- .../ConfidenceClient/Resolver.swift | 2 +- .../ConfidenceFeatureProvider.swift | 91 +++++++++++-------- .../Utils/EvaluationContextHash.swift | 70 -------------- .../ConfidenceProvider/Utils/Extensions.swift | 4 +- .../ConfidenceFeatureProviderTest.swift | 72 +++++++++++++-- .../EvaluationContextHashTest.swift | 31 ------- .../Helpers/AlwaysFailCache.swift | 2 +- .../Helpers/ClientMock.swift | 2 +- .../MockedResolveClientURLProtocol.swift | 3 + .../Helpers/StorageMock.swift | 2 +- .../LocalStorageResolverTest.swift | 6 +- .../PersistentProviderCacheTest.swift | 22 +++-- .../RemoteResolveConfidenceClientTest.swift | 5 +- .../ConfidenceValueHashTest.swift | 44 +++++++++ 20 files changed, 295 insertions(+), 181 deletions(-) create mode 100644 Sources/Confidence/ConfidenceValueHash.swift delete mode 100644 Sources/ConfidenceProvider/Utils/EvaluationContextHash.swift delete mode 100644 Tests/ConfidenceProviderTests/EvaluationContextHashTest.swift create mode 100644 Tests/ConfidenceTests/ConfidenceValueHashTest.swift diff --git a/Sources/Confidence/ConfidenceValueHash.swift b/Sources/Confidence/ConfidenceValueHash.swift new file mode 100644 index 00000000..214e260b --- /dev/null +++ b/Sources/Confidence/ConfidenceValueHash.swift @@ -0,0 +1,91 @@ +import CryptoKit +import Foundation + +public extension ConfidenceStruct { + func hash() -> String { + hashConfidenceValue(context: self) + } +} + +func hashConfidenceValue(context: ConfidenceStruct) -> String { + var hasher = SHA256() + + context.sorted { $0.key < $1.key }.forEach { key, value in + hasher.update(data: key.data) + hashValue(value: value, hasher: &hasher) + } + + let digest = hasher.finalize() + + return digest.map { String(format: "%02hhx", $0) }.joined() +} + +// swiftlint:disable:next cyclomatic_complexity +func hashValue(value: ConfidenceValue, hasher: inout some HashFunction) { + switch value.type() { + case .boolean: + if let booleanData = value.asBoolean()?.data { + hasher.update(data: booleanData) + } + + case .string: + if let stringData = value.asString()?.data { + hasher.update(data: stringData) + } + + case .integer: + if let integerData = value.asInteger()?.data { + hasher.update(data: integerData) + } + + case .double: + if let doubleData = value.asDouble()?.data { + hasher.update(data: doubleData) + } + + case .date: + if let dateData = value.asDateComponents()?.date?.data { + hasher.update(data: dateData) + } + + case .list: + value.asList()?.forEach { listValue in + hashValue(value: listValue, hasher: &hasher) + } + + case .timestamp: + if let timestampData = value.asDate()?.data { + hasher.update(data: timestampData) + } + + case .structure: + value.asStructure()?.sorted { $0.key < $1.key }.forEach { key, structureValue in + hasher.update(data: key.data) + hashValue(value: structureValue, hasher: &hasher) + } + + case .null: + hasher.update(data: UInt8(0).data) + } +} + +extension StringProtocol { + var data: Data { .init(utf8) } +} + +extension Numeric { + var data: Data { + var source = self + return .init(bytes: &source, count: MemoryLayout.size) + } +} + +extension Bool { + var data: Data { UInt8(self ? 1 : 0).data } +} + +extension Date { + var data: Data { + self.timeIntervalSince1970.data + } +} diff --git a/Sources/ConfidenceProvider/Cache/InMemoryProviderCache.swift b/Sources/ConfidenceProvider/Cache/InMemoryProviderCache.swift index 1573e5f3..78be56bd 100644 --- a/Sources/ConfidenceProvider/Cache/InMemoryProviderCache.swift +++ b/Sources/ConfidenceProvider/Cache/InMemoryProviderCache.swift @@ -21,13 +21,13 @@ public class InMemoryProviderCache: ProviderCache { self.curEvalContextHash = curEvalContextHash } - public func getValue(flag: String, ctx: EvaluationContext) throws -> CacheGetValueResult? { + public func getValue(flag: String, contextHash: String) throws -> CacheGetValueResult? { if let value = self.cache[flag] { guard let curResolveToken = curResolveToken else { throw ConfidenceError.noResolveTokenFromCache } return .init( - resolvedValue: value, needsUpdate: curEvalContextHash != ctx.hash(), resolveToken: curResolveToken) + resolvedValue: value, needsUpdate: curEvalContextHash != contextHash, resolveToken: curResolveToken) } else { return nil } diff --git a/Sources/ConfidenceProvider/Cache/ProviderCache.swift b/Sources/ConfidenceProvider/Cache/ProviderCache.swift index da623458..c9d1feca 100644 --- a/Sources/ConfidenceProvider/Cache/ProviderCache.swift +++ b/Sources/ConfidenceProvider/Cache/ProviderCache.swift @@ -2,7 +2,7 @@ import Foundation import OpenFeature public protocol ProviderCache { - func getValue(flag: String, ctx: EvaluationContext) throws -> CacheGetValueResult? + func getValue(flag: String, contextHash: String) throws -> CacheGetValueResult? } public struct CacheGetValueResult { diff --git a/Sources/ConfidenceProvider/ConfidenceClient/ConfidenceClient.swift b/Sources/ConfidenceProvider/ConfidenceClient/ConfidenceClient.swift index a000dd5d..6a9fe729 100644 --- a/Sources/ConfidenceProvider/ConfidenceClient/ConfidenceClient.swift +++ b/Sources/ConfidenceProvider/ConfidenceClient/ConfidenceClient.swift @@ -1,9 +1,10 @@ import Foundation +import Confidence import OpenFeature public protocol ConfidenceResolveClient { // Async - func resolve(ctx: EvaluationContext) async throws -> ResolvesResult + func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult } public struct ResolvedValue: Codable, Equatable { diff --git a/Sources/ConfidenceProvider/ConfidenceClient/LocalStorageResolver.swift b/Sources/ConfidenceProvider/ConfidenceClient/LocalStorageResolver.swift index bd24535c..6870f9f2 100644 --- a/Sources/ConfidenceProvider/ConfidenceClient/LocalStorageResolver.swift +++ b/Sources/ConfidenceProvider/ConfidenceClient/LocalStorageResolver.swift @@ -10,8 +10,8 @@ public class LocalStorageResolver: Resolver { self.cache = cache } - public func resolve(flag: String, ctx: EvaluationContext) throws -> ResolveResult { - let getResult = try self.cache.getValue(flag: flag, ctx: ctx) + public func resolve(flag: String, contextHash: String) throws -> ResolveResult { + let getResult = try self.cache.getValue(flag: flag, contextHash: contextHash) guard let getResult = getResult else { throw OpenFeatureError.flagNotFoundError(key: flag) } diff --git a/Sources/ConfidenceProvider/ConfidenceClient/RemoteResolveConfidenceClient.swift b/Sources/ConfidenceProvider/ConfidenceClient/RemoteResolveConfidenceClient.swift index d80bc5e5..ab22f8ce 100644 --- a/Sources/ConfidenceProvider/ConfidenceClient/RemoteResolveConfidenceClient.swift +++ b/Sources/ConfidenceProvider/ConfidenceClient/RemoteResolveConfidenceClient.swift @@ -30,10 +30,10 @@ public class RemoteConfidenceResolveClient: ConfidenceResolveClient { // MARK: Resolver - public func resolve(flags: [String], ctx: EvaluationContext) async throws -> ResolvesResult { + public func resolve(flags: [String], ctx: ConfidenceStruct) async throws -> ResolvesResult { let request = ResolveFlagsRequest( flags: flags.map { "flags/\($0)" }, - evaluationContext: try getEvaluationContextStruct(ctx: ctx), + evaluationContext: try NetworkTypeMapper.from(value: ctx), clientSecret: options.credentials.getSecret(), apply: applyOnResolve, sdk: Sdk(id: metadata.name, version: metadata.version) @@ -51,7 +51,7 @@ public class RemoteConfidenceResolveClient: ConfidenceResolveClient { throw OpenFeatureError.parseError(message: "Unable to parse request response") } let resolvedValues = try response.resolvedFlags.map { resolvedFlag in - try convert(resolvedFlag: resolvedFlag, ctx: ctx) + try convert(resolvedFlag: resolvedFlag) } return ResolvesResult(resolvedValues: resolvedValues, resolveToken: response.resolveToken) case .failure(let errorData): @@ -60,13 +60,13 @@ public class RemoteConfidenceResolveClient: ConfidenceResolveClient { } } - public func resolve(ctx: EvaluationContext) async throws -> ResolvesResult { + public func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { return try await resolve(flags: [], ctx: ctx) } // MARK: Private - private func convert(resolvedFlag: ResolvedFlag, ctx: EvaluationContext) throws -> ResolvedValue { + private func convert(resolvedFlag: ResolvedFlag) throws -> ResolvedValue { guard let responseFlagSchema = resolvedFlag.flagSchema, let responseValue = resolvedFlag.value, !responseValue.fields.isEmpty @@ -87,12 +87,6 @@ public class RemoteConfidenceResolveClient: ConfidenceResolveClient { resolveReason: convert(resolveReason: resolvedFlag.reason)) } - private func getEvaluationContextStruct(ctx: EvaluationContext) throws -> NetworkStruct { - var evaluationContext = TypeMapper.from(value: ctx) - evaluationContext.fields[targetingKey] = .string(ctx.getTargetingKey()) - return evaluationContext - } - private func handleError(error: Error) -> Error { if error is ConfidenceError || error is OpenFeatureError { return error diff --git a/Sources/ConfidenceProvider/ConfidenceClient/Resolver.swift b/Sources/ConfidenceProvider/ConfidenceClient/Resolver.swift index 7cf3741f..cbd6ef52 100644 --- a/Sources/ConfidenceProvider/ConfidenceClient/Resolver.swift +++ b/Sources/ConfidenceProvider/ConfidenceClient/Resolver.swift @@ -2,7 +2,7 @@ import OpenFeature public protocol Resolver { // This throws if the requested flag is not found - func resolve(flag: String, ctx: EvaluationContext) throws -> ResolveResult + func resolve(flag: String, contextHash: String) throws -> ResolveResult } public struct ResolveResult { diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index 1a1c47d1..c719eb87 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -47,8 +47,12 @@ public class ConfidenceFeatureProvider: FeatureProvider { self.resolver = LocalStorageResolver(cache: cache) } + public convenience init(confidence: Confidence) { + self.init(confidence: confidence, session: nil) + } + /// Initialize the Provider via a `Confidence` object. - public init(confidence: Confidence) { + internal init(confidence: Confidence, session: URLSession? = nil) { let metadata = ConfidenceMetadata(version: "0.1.4") // x-release-please-version let options = ConfidenceClientOptions( credentials: ConfidenceClientCredentials.clientSecret(secret: confidence.clientSecret), @@ -65,6 +69,7 @@ public class ConfidenceFeatureProvider: FeatureProvider { metadata: metadata) self.client = RemoteConfidenceResolveClient( options: options, + session: session, applyOnResolve: false, flagApplier: flagApplier, metadata: metadata) @@ -85,11 +90,12 @@ public class ConfidenceFeatureProvider: FeatureProvider { Task { do { - let resolveResult = try await resolve(context: initialContext) + let context = confidence?.getContext() ?? ConfidenceTypeMapper.from(ctx: initialContext) + let resolveResult = try await resolve(context: context) // update cache with stored values try await store( - with: initialContext, + with: context, resolveResult: resolveResult, refreshCache: self.initializationStrategy == .fetchAndActivate ) @@ -106,7 +112,7 @@ public class ConfidenceFeatureProvider: FeatureProvider { } private func store( - with context: OpenFeature.EvaluationContext, + with context: ConfidenceStruct, resolveResult result: ResolvesResult, refreshCache: Bool ) async throws { @@ -126,17 +132,22 @@ public class ConfidenceFeatureProvider: FeatureProvider { oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext ) { - guard oldContext?.hash() != newContext.hash() else { + var oldConfidenceContext: ConfidenceStruct = [:] + if let context = oldContext { + oldConfidenceContext = ConfidenceTypeMapper.from(ctx: context) + } + guard oldConfidenceContext.hash() != ConfidenceTypeMapper.from(ctx: newContext).hash() else { return } self.updateConfidenceContext(context: newContext) Task { do { - let resolveResult = try await resolve(context: newContext) + let context = confidence?.getContext() ?? ConfidenceTypeMapper.from(ctx: newContext) + let resolveResult = try await resolve(context: context) // update the storage - try await store(with: newContext, resolveResult: resolveResult, refreshCache: true) + try await store(with: context, resolveResult: resolveResult, refreshCache: true) eventHandler.send(ProviderEvent.ready) } catch { @@ -147,9 +158,12 @@ public class ConfidenceFeatureProvider: FeatureProvider { } private func updateConfidenceContext(context: EvaluationContext) { - confidence?.updateContextEntry( - key: "open_feature", - value: ConfidenceValue(structure: ConfidenceTypeMapper.from(ctx: context))) + for entry in ConfidenceTypeMapper.from(ctx: context) { + confidence?.updateContextEntry( + key: entry.key, + value: entry.value + ) + } } public func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws @@ -220,7 +234,7 @@ public class ConfidenceFeatureProvider: FeatureProvider { } } - private func resolve(context: OpenFeature.EvaluationContext) async throws -> ResolvesResult { + private func resolve(context: ConfidenceStruct) async throws -> ResolvesResult { do { let resolveResult = try await client.resolve(ctx: context) return resolveResult @@ -260,44 +274,48 @@ public class ConfidenceFeatureProvider: FeatureProvider { throw OpenFeatureError.invalidContextError } - let resolverResult = try resolver.resolve(flag: path.flag, ctx: ctx) + let context = confidence?.getContext() ?? ConfidenceTypeMapper.from(ctx: ctx) - guard let value = resolverResult.resolvedValue.value else { - return resolveFlagNoValue( - defaultValue: defaultValue, - resolverResult: resolverResult, - ctx: ctx + do { + let resolverResult = try resolver.resolve(flag: path.flag, contextHash: context.hash()) + + guard let value = resolverResult.resolvedValue.value else { + return resolveFlagNoValue( + defaultValue: defaultValue, + resolverResult: resolverResult, + ctx: context + ) + } + + let pathValue: Value = try getValue(path: path.path, value: value) + guard let typedValue: T = pathValue == .null ? defaultValue : pathValue.getTyped() else { + throw OpenFeatureError.parseError(message: "Unable to parse flag value: \(pathValue)") + } + + let isStale = resolverResult.resolvedValue.resolveReason == .stale + let evaluationResult = ProviderEvaluation( + value: typedValue, + variant: resolverResult.resolvedValue.variant, + reason: isStale ? Reason.stale.rawValue : Reason.targetingMatch.rawValue ) - } - let pathValue: Value = try getValue(path: path.path, value: value) - guard let typedValue: T = pathValue == .null ? defaultValue : pathValue.getTyped() else { - throw OpenFeatureError.parseError(message: "Unable to parse flag value: \(pathValue)") - } + processResultForApply( + resolverResult: resolverResult, + applyTime: Date.backport.now + ) - let isStale = resolverResult.resolvedValue.resolveReason == .stale - let evaluationResult = ProviderEvaluation( - value: typedValue, - variant: resolverResult.resolvedValue.variant, - reason: isStale ? Reason.stale.rawValue : Reason.targetingMatch.rawValue - ) - processResultForApply( - resolverResult: resolverResult, - ctx: ctx, - applyTime: Date.backport.now - ) - return evaluationResult + return evaluationResult + } } - private func resolveFlagNoValue(defaultValue: T, resolverResult: ResolveResult, ctx: EvaluationContext) + private func resolveFlagNoValue(defaultValue: T, resolverResult: ResolveResult, ctx: ConfidenceStruct) -> ProviderEvaluation { switch resolverResult.resolvedValue.resolveReason { case .noMatch: processResultForApply( resolverResult: resolverResult, - ctx: ctx, applyTime: Date.backport.now) return ProviderEvaluation( value: defaultValue, @@ -396,7 +414,6 @@ public class ConfidenceFeatureProvider: FeatureProvider { private func processResultForApply( resolverResult: ResolveResult?, - ctx: OpenFeature.EvaluationContext?, applyTime: Date ) { guard let resolverResult = resolverResult, let resolveToken = resolverResult.resolveToken else { diff --git a/Sources/ConfidenceProvider/Utils/EvaluationContextHash.swift b/Sources/ConfidenceProvider/Utils/EvaluationContextHash.swift deleted file mode 100644 index 52579a79..00000000 --- a/Sources/ConfidenceProvider/Utils/EvaluationContextHash.swift +++ /dev/null @@ -1,70 +0,0 @@ -import CryptoKit -import Foundation -import OpenFeature - -extension EvaluationContext { - func hash() -> String { - hashEvaluationContext(context: self) - } -} - -func hashEvaluationContext(context: EvaluationContext) -> String { - var hasher = SHA256() - - hasher.update(data: context.getTargetingKey().data) - context.asMap().sorted { $0.key < $1.key }.forEach { key, value in - hasher.update(data: key.data) - hashValue(value: value, hasher: &hasher) - } - - let digest = hasher.finalize() - - return digest.map { String(format: "%02hhx", $0) }.joined() -} - -func hashValue(value: Value, hasher: inout some HashFunction) { - switch value { - case .boolean(let bool): - hasher.update(data: bool.data) - case .string(let string): - hasher.update(data: string.data) - case .integer(let int64): - hasher.update(data: int64.data) - case .double(let double): - hasher.update(data: double.data) - case .date(let date): - hasher.update(data: date.data) - case .list(let list): - list.forEach { listValue in - hashValue(value: listValue, hasher: &hasher) - } - case .structure(let structure): - structure.sorted { $0.key < $1.key }.forEach { key, structureValue in - hasher.update(data: key.data) - hashValue(value: structureValue, hasher: &hasher) - } - case .null: - hasher.update(data: UInt8(0).data) - } -} - -extension StringProtocol { - var data: Data { .init(utf8) } -} - -extension Numeric { - var data: Data { - var source = self - return .init(bytes: &source, count: MemoryLayout.size) - } -} - -extension Bool { - var data: Data { UInt8(self ? 1 : 0).data } -} - -extension Date { - var data: Data { - self.timeIntervalSince1970.data - } -} diff --git a/Sources/ConfidenceProvider/Utils/Extensions.swift b/Sources/ConfidenceProvider/Utils/Extensions.swift index 6a120375..3165b0af 100644 --- a/Sources/ConfidenceProvider/Utils/Extensions.swift +++ b/Sources/ConfidenceProvider/Utils/Extensions.swift @@ -1,8 +1,8 @@ import Foundation -import OpenFeature +import Confidence extension [ResolvedValue] { - func toCacheData(context: EvaluationContext, resolveToken: String) -> StoredCacheData { + func toCacheData(context: ConfidenceStruct, resolveToken: String) -> StoredCacheData { var cacheValues: [String: ResolvedValue] = [:] forEach { value in diff --git a/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift b/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift index 448bbdb3..338834df 100644 --- a/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift +++ b/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift @@ -310,6 +310,66 @@ class ConfidenceFeatureProviderTest: XCTestCase { } } + func testCreateProviderUsingConfidenceContextResolvesCorrectly() throws { + let resolve: [String: MockedResolveClientURLProtocol.ResolvedTestFlag] = [ + "user1": .init(variant: "control", value: .structure(["size": .integer(3)])) + ] + + let flags: [String: MockedResolveClientURLProtocol.TestFlag] = [ + "flags/flag": .init(resolve: resolve) + ] + + let session = MockedResolveClientURLProtocol.mockedSession(flags: flags) + + let confidence = Confidence + .Builder(clientSecret: "") + .build() + .withContext(["my_string": ConfidenceValue(string: "my_value")]) + + let provider = ConfidenceFeatureProvider(confidence: confidence, session: session) + + try withExtendedLifetime( + provider.observe().sink { event in + if event == .ready { + self.readyExpectation.fulfill() + } + }) + { + provider.initialize(initialContext: MutableContext(targetingKey: "user1")) + wait(for: [readyExpectation], timeout: 5) + + XCTAssertEqual(MockedResolveClientURLProtocol.resolveStats, 1) + XCTAssertTrue(MockedResolveClientURLProtocol + .resolveRequestFields.fields.contains { $0.key == "my_string" && $0.value == .string("my_value") } + ) + + XCTAssertTrue(MockedResolveClientURLProtocol + .resolveRequestFields.fields.contains { $0.key == "targeting_key" } + ) + + let requestTargetingKey = MockedResolveClientURLProtocol + .resolveRequestFields + .fields["targeting_key"] + + if case .string(let targetingKey) = requestTargetingKey { + XCTAssertTrue(!targetingKey.isEmpty) + } else { + XCTFail("targeting key could not be found") + } + + let evaluation = try provider.getIntegerEvaluation( + key: "flag.size", + defaultValue: 0, + context: MutableContext(targetingKey: "user1")) + + XCTAssertEqual(evaluation.value, 3) + XCTAssertNil(evaluation.errorCode) + XCTAssertNil(evaluation.errorMessage) + XCTAssertEqual(evaluation.reason, Reason.targetingMatch.rawValue) + XCTAssertEqual(evaluation.variant, "control") + } + } + func testStaleEvaluationContextInCache() throws { let resolve: [String: MockedResolveClientURLProtocol.ResolvedTestFlag] = [ "user0": .init(variant: "control", value: .structure(["size": .integer(3)])) @@ -906,9 +966,7 @@ class ConfidenceFeatureProviderTest: XCTestCase { provider.initialize(initialContext: MutableContext(targetingKey: "user1")) wait(for: [readyExpectation], timeout: 5) let context = confidence.getContext() - let expected = [ - "open_feature": ConfidenceValue(structure: ["targeting_key": ConfidenceValue(string: "user1")]) - ] + let expected = ["targeting_key": ConfidenceValue(string: "user1")] XCTAssertEqual(context, expected) } } @@ -935,11 +993,9 @@ class ConfidenceFeatureProviderTest: XCTestCase { provider.onContextSet(oldContext: ctx1, newContext: ctx2) wait(for: [readyExpectation], timeout: 5) let context = confidence.getContext() - let expected = [ - "open_feature": ConfidenceValue(structure: [ - "targeting_key": ConfidenceValue(string: "user1"), - "active": ConfidenceValue(boolean: true) - ]) + let expected: ConfidenceStruct = [ + "targeting_key": ConfidenceValue(string: "user1"), + "active": ConfidenceValue(boolean: true) ] XCTAssertEqual(context, expected) } diff --git a/Tests/ConfidenceProviderTests/EvaluationContextHashTest.swift b/Tests/ConfidenceProviderTests/EvaluationContextHashTest.swift deleted file mode 100644 index c60be68a..00000000 --- a/Tests/ConfidenceProviderTests/EvaluationContextHashTest.swift +++ /dev/null @@ -1,31 +0,0 @@ -import OpenFeature -import XCTest - -@testable import ConfidenceProvider - -final class MutableContextTests: XCTestCase { - func testHashRespectsTargetingKey() throws { - let ctx1 = MutableContext(targetingKey: "user1", structure: MutableStructure()) - let ctx2 = MutableContext(targetingKey: "user2", structure: MutableStructure()) - - XCTAssertNotEqual(ctx1.hash(), ctx2.hash()) - } - - func testHashRespectsStructure() throws { - let ctx1 = MutableContext( - targetingKey: "", structure: MutableStructure(attributes: ["field": .list([.integer(3)])])) - let ctx2 = MutableContext( - targetingKey: "", structure: MutableStructure(attributes: ["field": .list([.integer(4)])])) - - XCTAssertNotEqual(ctx1.hash(), ctx2.hash()) - } - - func testHashIsEqualForEqualContext() throws { - let ctx1 = MutableContext( - targetingKey: "user1", structure: MutableStructure(attributes: ["field": .list([.integer(3)])])) - let ctx2 = MutableContext( - targetingKey: "user1", structure: MutableStructure(attributes: ["field": .list([.integer(3)])])) - - XCTAssertEqual(ctx1.hash(), ctx2.hash()) - } -} diff --git a/Tests/ConfidenceProviderTests/Helpers/AlwaysFailCache.swift b/Tests/ConfidenceProviderTests/Helpers/AlwaysFailCache.swift index ac51f6e6..6e67ec97 100644 --- a/Tests/ConfidenceProviderTests/Helpers/AlwaysFailCache.swift +++ b/Tests/ConfidenceProviderTests/Helpers/AlwaysFailCache.swift @@ -7,7 +7,7 @@ import OpenFeature public class AlwaysFailCache: ProviderCache { public func getValue( - flag: String, ctx: EvaluationContext + flag: String, contextHash: String ) throws -> CacheGetValueResult? { throw ConfidenceError.cacheError(message: "Always Fails (getValue)") } diff --git a/Tests/ConfidenceProviderTests/Helpers/ClientMock.swift b/Tests/ConfidenceProviderTests/Helpers/ClientMock.swift index ad05fc33..14416c54 100644 --- a/Tests/ConfidenceProviderTests/Helpers/ClientMock.swift +++ b/Tests/ConfidenceProviderTests/Helpers/ClientMock.swift @@ -20,7 +20,7 @@ class ClientMock: ConfidenceResolveClient { self.testMode = testMode } - func resolve(ctx: EvaluationContext) throws -> ResolvesResult { + func resolve(ctx: ConfidenceStruct) throws -> ResolvesResult { return ResolvesResult(resolvedValues: [], resolveToken: "") } diff --git a/Tests/ConfidenceProviderTests/Helpers/MockedResolveClientURLProtocol.swift b/Tests/ConfidenceProviderTests/Helpers/MockedResolveClientURLProtocol.swift index a9f4b412..1fd87b1e 100644 --- a/Tests/ConfidenceProviderTests/Helpers/MockedResolveClientURLProtocol.swift +++ b/Tests/ConfidenceProviderTests/Helpers/MockedResolveClientURLProtocol.swift @@ -8,6 +8,7 @@ import OpenFeature class MockedResolveClientURLProtocol: URLProtocol { public static var callStats = 0 public static var resolveStats = 0 + public static var resolveRequestFields = NetworkStruct(fields: [:]) public static var flags: [String: TestFlag] = [:] public static var failFirstApply = false @@ -66,6 +67,8 @@ class MockedResolveClientURLProtocol: URLProtocol { return } + MockedResolveClientURLProtocol.resolveRequestFields = request.evaluationContext + guard case .string(let targetingKey) = request.evaluationContext.fields["targeting_key"] else { respondWithError( statusCode: 400, diff --git a/Tests/ConfidenceProviderTests/Helpers/StorageMock.swift b/Tests/ConfidenceProviderTests/Helpers/StorageMock.swift index bc98ec1f..a6276bc1 100644 --- a/Tests/ConfidenceProviderTests/Helpers/StorageMock.swift +++ b/Tests/ConfidenceProviderTests/Helpers/StorageMock.swift @@ -28,7 +28,7 @@ class StorageMock: Storage { if data.isEmpty { return defaultValue } - return try JSONDecoder().decode(T.self, from: data.data) + return try JSONDecoder().decode(T.self, from: try XCTUnwrap(data.data(using: .utf8))) } } diff --git a/Tests/ConfidenceProviderTests/LocalStorageResolverTest.swift b/Tests/ConfidenceProviderTests/LocalStorageResolverTest.swift index 95866066..7bc970c1 100644 --- a/Tests/ConfidenceProviderTests/LocalStorageResolverTest.swift +++ b/Tests/ConfidenceProviderTests/LocalStorageResolverTest.swift @@ -13,7 +13,7 @@ class LocalStorageResolverTest: XCTestCase { let ctx = MutableContext(targetingKey: "key", structure: MutableStructure()) XCTAssertNoThrow( - try resolver.resolve(flag: "test", ctx: ctx) + try resolver.resolve(flag: "test", contextHash: ConfidenceTypeMapper.from(ctx: ctx).hash()) ) } @@ -23,7 +23,7 @@ class LocalStorageResolverTest: XCTestCase { let ctx = MutableContext(targetingKey: "key", structure: MutableStructure()) XCTAssertThrowsError( - try resolver.resolve(flag: "test", ctx: ctx) + try resolver.resolve(flag: "test", contextHash: ConfidenceTypeMapper.from(ctx: ctx).hash()) ) { error in XCTAssertEqual( error as? OpenFeatureError, OpenFeatureError.flagNotFoundError(key: "test")) @@ -39,7 +39,7 @@ class TestCache: ProviderCache { self.returnType = returnType } - func getValue(flag: String, ctx: EvaluationContext) -> ConfidenceProvider.CacheGetValueResult? { + func getValue(flag: String, contextHash: String) -> ConfidenceProvider.CacheGetValueResult? { switch returnType { case .noValue: return nil diff --git a/Tests/ConfidenceProviderTests/PersistentProviderCacheTest.swift b/Tests/ConfidenceProviderTests/PersistentProviderCacheTest.swift index bd74baef..1ca77bbe 100644 --- a/Tests/ConfidenceProviderTests/PersistentProviderCacheTest.swift +++ b/Tests/ConfidenceProviderTests/PersistentProviderCacheTest.swift @@ -23,10 +23,11 @@ class PersistentProviderCacheTest: XCTestCase { flag: flag, resolveReason: .match) - try storage.save(data: [value].toCacheData(context: ctx, resolveToken: resolveToken)) + let context = ConfidenceTypeMapper.from(ctx: ctx) + try storage.save(data: [value].toCacheData(context: context, resolveToken: resolveToken)) cache = InMemoryProviderCache.from(storage: storage) - let cachedValue = try cache.getValue(flag: flag, ctx: ctx) + let cachedValue = try cache.getValue(flag: flag, contextHash: ConfidenceTypeMapper.from(ctx: ctx).hash()) XCTAssertEqual(cachedValue?.resolvedValue, value) XCTAssertFalse(cachedValue?.needsUpdate ?? true) XCTAssertFalse(cachedValue?.needsUpdate ?? true) @@ -48,7 +49,8 @@ class PersistentProviderCacheTest: XCTestCase { resolveReason: .match) XCTAssertFalse(try FileManager.default.fileExists(atPath: storage.getConfigUrl().backport.path)) - try storage.save(data: [value1, value2].toCacheData(context: ctx, resolveToken: resolveToken)) + let context = ConfidenceTypeMapper.from(ctx: ctx) + try storage.save(data: [value1, value2].toCacheData(context: context, resolveToken: resolveToken)) cache = InMemoryProviderCache.from(storage: storage) expectToEventually( @@ -56,8 +58,9 @@ class PersistentProviderCacheTest: XCTestCase { let newCache = InMemoryProviderCache.from( storage: DefaultStorage(filePath: "resolver.flags.cache")) - let cachedValue1 = try newCache.getValue(flag: flag1, ctx: ctx) - let cachedValue2 = try newCache.getValue(flag: flag2, ctx: ctx) + let contextHash = ConfidenceTypeMapper.from(ctx: ctx).hash() + let cachedValue1 = try newCache.getValue(flag: flag1, contextHash: contextHash) + let cachedValue2 = try newCache.getValue(flag: flag2, contextHash: contextHash) XCTAssertEqual(cachedValue1?.resolvedValue, value1) XCTAssertEqual(cachedValue2?.resolvedValue, value2) XCTAssertEqual(cachedValue1?.needsUpdate, false) @@ -71,7 +74,8 @@ class PersistentProviderCacheTest: XCTestCase { try storage.clear() - let cachedValue = try cache.getValue(flag: "flag", ctx: ctx) + let contextHash = ConfidenceTypeMapper.from(ctx: ctx).hash() + let cachedValue = try cache.getValue(flag: "flag", contextHash: contextHash) XCTAssertNil(cachedValue?.resolvedValue.value) } @@ -85,10 +89,12 @@ class PersistentProviderCacheTest: XCTestCase { value: Value.double(3.14), flag: flag, resolveReason: .match) - try storage.save(data: [value].toCacheData(context: ctx1, resolveToken: resolveToken)) + let context = ConfidenceTypeMapper.from(ctx: ctx1) + try storage.save(data: [value].toCacheData(context: context, resolveToken: resolveToken)) cache = InMemoryProviderCache.from(storage: storage) - let cachedValue = try cache.getValue(flag: flag, ctx: ctx2) + let contextHash = ConfidenceTypeMapper.from(ctx: ctx2).hash() + let cachedValue = try cache.getValue(flag: flag, contextHash: contextHash) XCTAssertEqual(cachedValue?.resolvedValue, value) XCTAssertTrue(cachedValue?.needsUpdate ?? false) } diff --git a/Tests/ConfidenceProviderTests/RemoteResolveConfidenceClientTest.swift b/Tests/ConfidenceProviderTests/RemoteResolveConfidenceClientTest.swift index 19193e76..0fdfc1c0 100644 --- a/Tests/ConfidenceProviderTests/RemoteResolveConfidenceClientTest.swift +++ b/Tests/ConfidenceProviderTests/RemoteResolveConfidenceClientTest.swift @@ -1,5 +1,6 @@ import Foundation import OpenFeature +import Confidence import XCTest @testable import ConfidenceProvider @@ -34,7 +35,9 @@ class RemoteResolveConfidenceClientTest: XCTestCase { metadata: ConfidenceMetadata() ) - let result = try await client.resolve(ctx: MutableContext(targetingKey: "user1")) + let context = ConfidenceTypeMapper.from(ctx: MutableContext(targetingKey: "user1")) + + let result = try await client.resolve(ctx: context) XCTAssertEqual(result.resolvedValues.count, 2) let sortedResultValues = result.resolvedValues.sorted { resolvedValue1, resolvedValue2 in resolvedValue1.flag < resolvedValue2.flag diff --git a/Tests/ConfidenceTests/ConfidenceValueHashTest.swift b/Tests/ConfidenceTests/ConfidenceValueHashTest.swift new file mode 100644 index 00000000..76184373 --- /dev/null +++ b/Tests/ConfidenceTests/ConfidenceValueHashTest.swift @@ -0,0 +1,44 @@ +import XCTest + +@testable import Confidence + +final class MutableContextTests: XCTestCase { + func testHashRespectsTargetingKey() throws { + let ctx1: ConfidenceStruct = + ["targetingKey": ConfidenceValue(string: "user1"), "structure": ConfidenceValue(structure: [:])] + let ctx2: ConfidenceStruct = + ["targetingKey": ConfidenceValue(string: "user2"), "structure": ConfidenceValue(structure: [:])] + + XCTAssertNotEqual(ctx1.hash(), ctx2.hash()) + } + + func testHashRespectsStructure() throws { + let ctx1: ConfidenceStruct = + [ + "targetingKey": ConfidenceValue(string: "user1"), + "structure": ConfidenceValue(structure: ["integer": ConfidenceValue(integer: 3)]) + ] + let ctx2: ConfidenceStruct = + [ + "targetingKey": ConfidenceValue(string: "user2"), + "structure": ConfidenceValue(structure: ["integer": ConfidenceValue(integer: 4)]) + ] + + XCTAssertNotEqual(ctx1.hash(), ctx2.hash()) + } + + func testHashIsEqualForEqualContext() throws { + let ctx1: ConfidenceStruct = + [ + "targetingKey": ConfidenceValue(string: "user1"), + "structure": ConfidenceValue(structure: ["integer": ConfidenceValue(integer: 3)]) + ] + let ctx2: ConfidenceStruct = + [ + "targetingKey": ConfidenceValue(string: "user2"), + "structure": ConfidenceValue(structure: ["integer": ConfidenceValue(integer: 3)]) + ] + + XCTAssertNotEqual(ctx1.hash(), ctx2.hash()) + } +}