diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index db67fedf..e58a3625 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -2046,6 +2046,8 @@ 989428BC2DBFA431008BA1C8 /* MockBucketer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989428B22DBFA431008BA1C8 /* MockBucketer.swift */; }; 989428BD2DBFA431008BA1C8 /* MockBucketer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989428B22DBFA431008BA1C8 /* MockBucketer.swift */; }; 989428BE2DBFA431008BA1C8 /* MockBucketer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989428B22DBFA431008BA1C8 /* MockBucketer.swift */; }; + 989428C02DBFDCE5008BA1C8 /* DecisionListenerTest_Holdouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989428BF2DBFDCE5008BA1C8 /* DecisionListenerTest_Holdouts.swift */; }; + 989428C12DBFDCE5008BA1C8 /* DecisionListenerTest_Holdouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989428BF2DBFDCE5008BA1C8 /* DecisionListenerTest_Holdouts.swift */; }; 98AC97E22DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; }; 98AC97E32DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; }; 98AC97E42DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; }; @@ -2516,6 +2518,7 @@ 984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileTracker.swift; sourceTree = ""; }; 987F11D92AF3F56F0083D3F9 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 989428B22DBFA431008BA1C8 /* MockBucketer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBucketer.swift; sourceTree = ""; }; + 989428BF2DBFDCE5008BA1C8 /* DecisionListenerTest_Holdouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecisionListenerTest_Holdouts.swift; sourceTree = ""; }; 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoldoutConfig.swift; sourceTree = ""; }; 98AC97F22DAE9685001405DD /* HoldoutConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoldoutConfigTests.swift; sourceTree = ""; }; 98AC98452DB7B762001405DD /* BucketTests_HoldoutToVariation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BucketTests_HoldoutToVariation.swift; sourceTree = ""; }; @@ -3033,6 +3036,7 @@ 6E75199622C5211100B2B157 /* DatafileHandlerTests.swift */, 6E75199822C5211100B2B157 /* DataStoreTests.swift */, 6E75199022C5211100B2B157 /* DecisionListenerTests.swift */, + 989428BF2DBFDCE5008BA1C8 /* DecisionListenerTest_Holdouts.swift */, 6E981FC1232C363300FADDD6 /* DecisionListenerTests_Datafile.swift */, 6E27ECBD266FD78600B4A6D4 /* DecisionReasonsTests.swift */, 6E75198022C5211100B2B157 /* DecisionServiceTests_Experiments.swift */, @@ -4910,6 +4914,7 @@ 6EC6DD4C24ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, 8464087D28130D3200CCF97D /* Integration.swift in Sources */, 6E7517E922C520D400B2B157 /* DefaultDecisionService.swift in Sources */, + 989428C02DBFDCE5008BA1C8 /* DecisionListenerTest_Holdouts.swift in Sources */, 6E5D12282638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E9B116A22C5487100C22D81 /* BucketTests_Base.swift in Sources */, 6E9B115F22C5487100C22D81 /* MurmurTests.swift in Sources */, @@ -5192,6 +5197,7 @@ 6E7517CB22C520D400B2B157 /* DefaultBucketer.swift in Sources */, 845945C1287758A000D13E11 /* OdpConfig.swift in Sources */, 8464087528130D3200CCF97D /* Integration.swift in Sources */, + 989428C12DBFDCE5008BA1C8 /* DecisionListenerTest_Holdouts.swift in Sources */, 6E5D12202638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E9B115022C5486E00C22D81 /* BucketTests_Base.swift in Sources */, 6E9B114522C5486E00C22D81 /* MurmurTests.swift in Sources */, diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 15905a5c..7c7179a4 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -801,7 +801,11 @@ extension OptimizelyClient { func shouldSendDecisionEvent(source: String, decision: FeatureDecision?) -> Bool { guard let config = self.config else { return false } - return (source == Constants.DecisionSource.featureTest.rawValue && decision?.variation != nil) || config.sendFlagDecisions + let allowedSources: [String] = [ + Constants.DecisionSource.featureTest.rawValue, + Constants.DecisionSource.holdout.rawValue + ] + return (allowedSources.contains(source) && decision?.variation != nil) || config.sendFlagDecisions } func sendImpressionEvent(experiment: ExperimentCore?, diff --git a/Tests/OptimizelyTests-Common/BatchEventBuilderTests_Events.swift b/Tests/OptimizelyTests-Common/BatchEventBuilderTests_Events.swift index f0d6f345..516f9ea3 100644 --- a/Tests/OptimizelyTests-Common/BatchEventBuilderTests_Events.swift +++ b/Tests/OptimizelyTests-Common/BatchEventBuilderTests_Events.swift @@ -27,6 +27,26 @@ class BatchEventBuilderTests_Events: XCTestCase { var project: Project! let datafile = OTUtils.loadJSONDatafile("api_datafile")! + var sampleHoldout: [String: Any] { + return [ + "status": "Running", + "id": "holdout_4444444", + "key": "holdout_key", + "layerId": "10420273888", + "trafficAllocation": [ + ["entityId": "holdout_variation_a11", "endOfRange": 10000] // 100% traffic allocation + ], + "audienceIds": [], + "variations": [ + [ + "variables": [], + "id": "holdout_variation_a11", + "key": "holdout_a" + ] + ] + ] + } + override func setUp() { eventDispatcher = MockEventDispatcher() optimizely = OTUtils.createOptimizely(datafileName: "audience_targeting", @@ -38,6 +58,10 @@ class BatchEventBuilderTests_Events: XCTestCase { override func tearDown() { Utils.sdkVersion = OPTIMIZELYSDKVERSION Utils.swiftSdkClientName = "swift-sdk" + optimizely?.close() + optimizely = nil + optimizely?.eventDispatcher = nil + super.tearDown() } func testCreateImpressionEvent() { @@ -461,6 +485,164 @@ extension BatchEventBuilderTests_Events { } } +// MARK:- Holdouts + +extension BatchEventBuilderTests_Events { + func testImpressionEvent_UserInHoldout() { + let eventDispatcher2 = MockEventDispatcher() + var optimizely: OptimizelyClient! = OptimizelyClient(sdkKey: "12345", eventDispatcher: eventDispatcher2) + + try! optimizely.start(datafile: datafile) + + let holdout: Holdout = try! OTUtils.model(from: sampleHoldout) + optimizely.config?.project.holdouts = [holdout] + + let exp = expectation(description: "Wait for event to dispatch") + let user = optimizely.createUserContext(userId: userId) + _ = user.decide(key: featureKey) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + exp.fulfill() + } + + let result = XCTWaiter.wait(for: [exp], timeout: 0.2) + if result == XCTWaiter.Result.completed { + let event = getFirstEventJSON(client: optimizely)! + let visitor = (event["visitors"] as! Array>)[0] + let snapshot = (visitor["snapshots"] as! Array>)[0] + let decision = (snapshot["decisions"] as! Array>)[0] + + let metaData = decision["metadata"] as! Dictionary + XCTAssertEqual(metaData["rule_type"] as! String, Constants.DecisionSource.holdout.rawValue) + XCTAssertEqual(metaData["rule_key"] as! String, "holdout_key") + XCTAssertEqual(metaData["flag_key"] as! String, "feature_1") + XCTAssertEqual(metaData["variation_key"] as! String, "holdout_a") + XCTAssertFalse(metaData["enabled"] as! Bool) + } else { + XCTFail("No event found") + } + + } + + func testImpressionEvent_UserInHoldout_IncludedFlags() { + let eventDispatcher2 = MockEventDispatcher() + var optimizely: OptimizelyClient! = OptimizelyClient(sdkKey: "12345", eventDispatcher: eventDispatcher2) + + try! optimizely.start(datafile: datafile) + + var holdout: Holdout = try! OTUtils.model(from: sampleHoldout) + holdout.includedFlags = ["4482920077"] + optimizely.config?.project.holdouts = [holdout] + + let exp = expectation(description: "Wait for event to dispatch") + + let user = optimizely.createUserContext(userId: userId) + _ = user.decide(key: featureKey) + + + // Add a delay before evaluating getFirstEventJSON + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + exp.fulfill() // Fulfill the expectation after the delay + } + + let result = XCTWaiter.wait(for: [exp], timeout: 0.2) + if result == XCTWaiter.Result.completed { + let event = getFirstEventJSON(client: optimizely)! + let visitor = (event["visitors"] as! Array>)[0] + let snapshot = (visitor["snapshots"] as! Array>)[0] + let decision = (snapshot["decisions"] as! Array>)[0] + + let metaData = decision["metadata"] as! Dictionary + XCTAssertEqual(metaData["rule_type"] as! String, Constants.DecisionSource.holdout.rawValue) + XCTAssertEqual(metaData["rule_key"] as! String, "holdout_key") + XCTAssertEqual(metaData["flag_key"] as! String, "feature_1") + XCTAssertEqual(metaData["variation_key"] as! String, "holdout_a") + XCTAssertFalse(metaData["enabled"] as! Bool) + } else { + XCTFail("No event found") + } + optimizely = nil + + } + + func testImpressionEvent_UserNotInHoldout_ExcludedFlags() { + let eventDispatcher2 = MockEventDispatcher() + var optimizely: OptimizelyClient! = OptimizelyClient(sdkKey: "123456", eventDispatcher: eventDispatcher2) + + try! optimizely.start(datafile: datafile) + + var holdout: Holdout = try! OTUtils.model(from: sampleHoldout) + holdout.excludedFlags = ["4482920077"] + optimizely.config?.project.holdouts = [holdout] + + let exp = expectation(description: "Wait for event to dispatch") + + let user = optimizely.createUserContext(userId: userId) + _ = user.decide(key: featureKey) + + // Add a delay before evaluating getFirstEventJSON + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + exp.fulfill() // Fulfill the expectation after the delay + } + + let result = XCTWaiter.wait(for: [exp], timeout: 0.2) + if result == XCTWaiter.Result.completed { + let event = getFirstEventJSON(client: optimizely)! + let visitor = (event["visitors"] as! Array>)[0] + let snapshot = (visitor["snapshots"] as! Array>)[0] + let decision = (snapshot["decisions"] as! Array>)[0] + + let metaData = decision["metadata"] as! Dictionary + XCTAssertEqual(metaData["rule_type"] as! String, Constants.DecisionSource.featureTest.rawValue) + XCTAssertEqual(metaData["rule_key"] as! String, "exp_with_audience") + XCTAssertEqual(metaData["flag_key"] as! String, "feature_1") + XCTAssertEqual(metaData["variation_key"] as! String, "a") + XCTAssertTrue(metaData["enabled"] as! Bool) + } else { + XCTFail("No event found") + } + } + + func testImpressionEvent_UserNotInHoldout_MissesTrafficAllocation() { + let eventDispatcher2 = MockEventDispatcher() + var optimizely: OptimizelyClient! = OptimizelyClient(sdkKey: "123457", eventDispatcher: eventDispatcher2) + + try! optimizely.start(datafile: datafile) + + var holdout: Holdout = try! OTUtils.model(from: sampleHoldout) + /// Set traffic allocation to gero + holdout.trafficAllocation[0].endOfRange = 0 + holdout.includedFlags = ["4482920077"] + optimizely.config?.project.holdouts = [holdout] + + let exp = expectation(description: "Wait for event to dispatch") + + let user = optimizely.createUserContext(userId: userId) + _ = user.decide(key: featureKey) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + exp.fulfill() // Fulfill the expectation after the delay + } + + let result = XCTWaiter.wait(for: [exp], timeout: 0.2) + if result == XCTWaiter.Result.completed { + let event = getFirstEventJSON(client: optimizely)! + let visitor = (event["visitors"] as! Array>)[0] + let snapshot = (visitor["snapshots"] as! Array>)[0] + let decision = (snapshot["decisions"] as! Array>)[0] + + let metaData = decision["metadata"] as! Dictionary + XCTAssertEqual(metaData["rule_type"] as! String, Constants.DecisionSource.featureTest.rawValue) + XCTAssertEqual(metaData["rule_key"] as! String, "exp_with_audience") + XCTAssertEqual(metaData["flag_key"] as! String, "feature_1") + XCTAssertEqual(metaData["variation_key"] as! String, "a") + XCTAssertTrue(metaData["enabled"] as! Bool) + } else { + XCTFail("No event found") + } + } +} + // MARK: - Utils extension BatchEventBuilderTests_Events { @@ -477,7 +659,14 @@ extension BatchEventBuilderTests_Events { return json } - func getEventJSON(data: Data) -> [String: Any]? { + func getFirstEventJSON(client: OptimizelyClient) -> [String: Any]? { + guard let event = getFirstEvent(dispatcher: client.eventDispatcher as! MockEventDispatcher) else { return nil } + + let json = try! JSONSerialization.jsonObject(with: event.body, options: .allowFragments) as! [String: Any] + return json + } + + func getEventJSON(data: Data) -> [String: Any]? { let json = try! JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any] return json } diff --git a/Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift b/Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift new file mode 100644 index 00000000..e62905fb --- /dev/null +++ b/Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift @@ -0,0 +1,290 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class DecisionListenerTests_Holdouts: XCTestCase { + let kUserId = "11111" + var optimizely: OptimizelyClient! + var notificationCenter: OPTNotificationCenter! + var eventDispatcher = MockEventDispatcher() + + var kAttributesCountryMatch: [String: Any] = ["country": "US"] + var kAttributesCountryNotMatch: [String: Any] = ["country": "ca"] + + let kFeatureKey = "feature_1" + let kFeatureId = "4482920077" + + let kVariableKeyString = "s_foo" + let kVariableKeyInt = "i_42" + let kVariableKeyDouble = "d_4_2" + let kVariableKeyBool = "b_true" + let kVariableKeyJSON = "j_1" + + let kVariableValueString = "foo" + let kVariableValueInt = 42 + let kVariableValueDouble = 4.2 + let kVariableValueBool = true + + var sampleHoldout: [String: Any] { + return [ + "status": "Running", + "id": "id_holdout", + "key": "key_holdout", + "layerId": "10420273888", + "trafficAllocation": [ + ["entityId": "id_holdout_variation", "endOfRange": 500] + ], + "audienceIds": [], + "variations": [ + [ + "variables": [], + "id": "id_holdout_variation", + "key": "key_holdout_variation" + ] + ], + "includedFlags": [], + "excludedFlags": [] + ] + } + + override func setUp() { + super.setUp() + + optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, + eventDispatcher: eventDispatcher, + userProfileService: OTUtils.createClearUserProfileService()) + + try! optimizely.start(datafile: OTUtils.loadJSONDatafile("decide_datafile")!) + + var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout + // Audience "13389130056" requires "country" = "US" + holdout.audienceIds = ["13389130056"] + + let mockDecisionService = DefaultDecisionService(userProfileService: OTUtils.createClearUserProfileService(), bucketer: MockBucketer(mockBucketValue: 400)) + optimizely.decisionService = mockDecisionService + optimizely.config!.project.holdouts = [holdout] + + self.notificationCenter = self.optimizely.notificationCenter! + } + + func testDecisionListenerDecideWithUserInHoldout() { + let exp = expectation(description: "x") + + let user = optimizely.createUserContext(userId: kUserId, attributes: kAttributesCountryMatch) + + notificationCenter.clearAllNotificationListeners() + _ = notificationCenter.addDecisionNotificationListener { (type, userId, attributes, decisionInfo) in + XCTAssertEqual(type, Constants.DecisionType.flag.rawValue) + XCTAssertEqual(userId, user.userId) + XCTAssertEqual(attributes!["country"] as! String, "US") + + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.flagKey] as! String, self.kFeatureKey) + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.enabled] as! Bool, false) + + XCTAssertNotNil(decisionInfo[Constants.DecisionInfoKeys.variables]) + let variableValues = decisionInfo[Constants.DecisionInfoKeys.variables] as! [String: Any] + + XCTAssertEqual(variableValues[self.kVariableKeyString] as! String, "foo") + XCTAssertEqual(variableValues[self.kVariableKeyInt] as! Int, self.kVariableValueInt) + XCTAssertEqual(variableValues[self.kVariableKeyDouble] as! Double, self.kVariableValueDouble) + XCTAssertEqual(variableValues[self.kVariableKeyBool] as! Bool, self.kVariableValueBool) + let jsonMap = (variableValues[self.kVariableKeyJSON] as! [String: Any]) + XCTAssertEqual(jsonMap["value"] as! Int, 1) + + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.variationKey] as! String, "key_holdout_variation") + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.ruleKey] as! String, "key_holdout") + XCTAssertNotNil(decisionInfo[Constants.DecisionInfoKeys.reasons]) + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.decisionEventDispatched] as! Bool, true) + exp.fulfill() + } + + _ = user.decide(key: self.kFeatureKey) + wait(for: [exp], timeout: 1) + } + + func testDecisionListenerDecideWithIncludedFlags() { + var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout + holdout.includedFlags = [kFeatureId] + optimizely.config!.project.holdouts = [holdout] + + let exp = expectation(description: "x") + let user = optimizely.createUserContext(userId: kUserId, attributes: kAttributesCountryMatch) + + notificationCenter.clearAllNotificationListeners() + _ = notificationCenter.addDecisionNotificationListener { (type, userId, attributes, decisionInfo) in + XCTAssertEqual(type, Constants.DecisionType.flag.rawValue) + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.flagKey] as! String, self.kFeatureKey) + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.enabled] as! Bool, false) + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.variationKey] as! String, "key_holdout_variation") + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.ruleKey] as! String, "key_holdout") + exp.fulfill() + } + + _ = user.decide(key: kFeatureKey) + wait(for: [exp], timeout: 1) + } + + func testDecisionListenerDecideWithExcludedFlags() { + var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout + holdout.excludedFlags = [kFeatureId] + optimizely.config!.project.holdouts = [holdout] + + let exp = expectation(description: "x") + let user = optimizely.createUserContext(userId: kUserId, attributes: kAttributesCountryMatch) + + notificationCenter.clearAllNotificationListeners() + _ = notificationCenter.addDecisionNotificationListener { (type, userId, attributes, decisionInfo) in + XCTAssertEqual(type, Constants.DecisionType.flag.rawValue) + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.flagKey] as! String, self.kFeatureKey) + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.enabled] as! Bool, true) + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.variationKey] as! String, "3324490633") + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.ruleKey] as! String, "3332020515") + exp.fulfill() + } + + _ = user.decide(key: kFeatureKey) + wait(for: [exp], timeout: 1) + } + + func testDecisionListenerDecideWithMultipleHoldouts() { + var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout + holdout.excludedFlags = [kFeatureId] + + var holdout_2 = holdout + holdout_2.key = "holdout_key_2" + holdout_2.id = "holdout_id_2" + holdout_2.includedFlags = [kFeatureId] + + optimizely.config!.project.holdouts = [holdout, holdout_2] + + let exp = expectation(description: "x") + let user = optimizely.createUserContext(userId: kUserId, attributes: kAttributesCountryMatch) + + notificationCenter.clearAllNotificationListeners() + _ = notificationCenter.addDecisionNotificationListener { (type, userId, attributes, decisionInfo) in + XCTAssertEqual(type, Constants.DecisionType.flag.rawValue) + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.flagKey] as! String, self.kFeatureKey) + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.enabled] as! Bool, false) + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.variationKey] as! String, "key_holdout_variation") + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.ruleKey] as! String, "holdout_key_2") + exp.fulfill() + } + + _ = user.decide(key: kFeatureKey) + wait(for: [exp], timeout: 1) + } + + func testDecisionListener_DecisionEventDispatched_withSendFlagDecisions() { + let user = optimizely.createUserContext(userId: kUserId, attributes: kAttributesCountryMatch) + + // (1) sendFlagDecision = false. feature-test. + + optimizely.config?.project.sendFlagDecisions = false + + var exp = expectation(description: "x") + notificationCenter.clearAllNotificationListeners() + _ = notificationCenter.addDecisionNotificationListener { (type, userId, attributes, decisionInfo) in + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.decisionEventDispatched] as! Bool, true) + exp.fulfill() + } + _ = user.decide(key: kFeatureKey) + wait(for: [exp], timeout: 1) + + // (2) sendFlagDecision = true + + optimizely.config?.project.sendFlagDecisions = true + + exp = expectation(description: "x") + notificationCenter.clearAllNotificationListeners() + _ = notificationCenter.addDecisionNotificationListener { (type, userId, attributes, decisionInfo) in + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.decisionEventDispatched] as! Bool, true) + exp.fulfill() + } + _ = user.decide(key: kFeatureKey) + wait(for: [exp], timeout: 1) + } + + func testDecisionListenerDecide_disableDecisionEvent() { + let user = optimizely.createUserContext(userId: kUserId, attributes:["country": "US"]) + + // (1) default (send-decision-event) + + var exp = expectation(description: "x") + + notificationCenter.clearAllNotificationListeners() + _ = notificationCenter.addDecisionNotificationListener { (type, userId, attributes, decisionInfo) in + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.decisionEventDispatched] as! Bool, true) + exp.fulfill() + } + _ = user.decide(key: kFeatureKey) + wait(for: [exp], timeout: 1) + + // (2) disable-decision-event) + + exp = expectation(description: "x") + + notificationCenter.clearAllNotificationListeners() + _ = notificationCenter.addDecisionNotificationListener { (type, userId, attributes, decisionInfo) in + XCTAssertEqual(decisionInfo[Constants.DecisionInfoKeys.decisionEventDispatched] as! Bool, false) + exp.fulfill() + } + _ = user.decide(key: kFeatureKey, options: [.disableDecisionEvent]) + wait(for: [exp], timeout: 1) + } + + func testDecisionListenerDecideForKeys() { + let user = optimizely.createUserContext(userId: kUserId, attributes:["country": "US"]) + + var count = 0 + notificationCenter.clearAllNotificationListeners() + _ = notificationCenter.addDecisionNotificationListener { (type, userId, attributes, decisionInfo) in + XCTAssertEqual(type, Constants.DecisionType.flag.rawValue) + XCTAssertEqual(userId, user.userId) + XCTAssertEqual(attributes!["country"] as! String, "US") + + XCTAssertNotNil(decisionInfo[Constants.DecisionInfoKeys.flagKey]) + XCTAssertNotNil(decisionInfo[Constants.DecisionInfoKeys.enabled]) + count += 1 + } + + _ = user.decide(keys: [kFeatureKey, kFeatureKey, kFeatureKey, kFeatureKey]) + sleep(1) + + XCTAssertEqual(count, 4) + } + + func testDecisionListenerDecideAll() { + let user = optimizely.createUserContext(userId: kUserId, attributes:["country": "US"]) + + var count = 0 + notificationCenter.clearAllNotificationListeners() + _ = notificationCenter.addDecisionNotificationListener { (type, userId, attributes, decisionInfo) in + XCTAssertEqual(type, Constants.DecisionType.flag.rawValue) + XCTAssertEqual(userId, user.userId) + XCTAssertEqual(attributes!["country"] as! String, "US") + + XCTAssertNotNil(decisionInfo[Constants.DecisionInfoKeys.flagKey]) + XCTAssertNotNil(decisionInfo[Constants.DecisionInfoKeys.enabled]) + count += 1 + } + + _ = user.decideAll() + sleep(1) + + XCTAssertEqual(count, 3) + } +} diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift index 9820f6cd..7f2228e8 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift @@ -559,6 +559,6 @@ extension OptimizelyUserContextTests_Decide_Holdouts { XCTAssertEqual(decision.variationKey, "key_holdout_variation") XCTAssertFalse(decision.enabled) optimizely.eventLock.sync{} - XCTAssert(eventDispatcher.events.isEmpty) + XCTAssertFalse(eventDispatcher.events.isEmpty) } } diff --git a/Tests/OptimizelyTests-DataModel/HoldoutTests.swift b/Tests/OptimizelyTests-DataModel/HoldoutTests.swift index aba7150d..df815ce1 100644 --- a/Tests/OptimizelyTests-DataModel/HoldoutTests.swift +++ b/Tests/OptimizelyTests-DataModel/HoldoutTests.swift @@ -22,7 +22,7 @@ import XCTest class HoldoutTests: XCTestCase { static var variationData: [String: Any] = ["id": "553339214", "key": "house", - "featureEnabled": true] + "featureEnabled": false] static var trafficAllocationData: [String: Any] = ["entityId": "553339214", "endOfRange": 5000]