diff --git a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj index 3b2962e05..66204977c 100644 --- a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj +++ b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj @@ -10,6 +10,84 @@ IOptimizely.cs + + Notifications\NotificationCenterRegistry.cs + + + Odp\Constants.cs + + + Odp\Entity\Audience.cs + + + Odp\Entity\Customer.cs + + + Odp\Entity\Data.cs + + + Odp\Entity\Edge.cs + + + Odp\Entity\Error.cs + + + Odp\Entity\Extension.cs + + + Odp\Entity\Location.cs + + + Odp\Entity\Node.cs + + + Odp\Entity\OdpEvent.cs + + + Odp\Entity\Response.cs + + + Odp\Enums.cs + + + Odp\ICache.cs + + + Odp\IOdpEventApiManager.cs + + + Odp\IOdpEventManager.cs + + + Odp\IOdpManager.cs + + + Odp\IOdpSegmentApiManager.cs + + + Odp\IOdpSegmentManager.cs + + + Odp\LruCache.cs + + + Odp\OdpConfig.cs + + + Odp\OdpEventApiManager.cs + + + Odp\OdpEventManager.cs + + + Odp\OdpManager.cs + + + Odp\OdpSegmentApiManager.cs + + + Odp\OdpSegmentManager.cs + Optimizely.cs @@ -223,6 +301,9 @@ Utils\AttributeMatchTypes.cs + + Utils\CollectionExtensions.cs + Utils\ConditionParser.cs diff --git a/OptimizelySDK.Tests/AudienceConditionsTests/SegmentsTests.cs b/OptimizelySDK.Tests/AudienceConditionsTests/SegmentsTests.cs new file mode 100644 index 000000000..42eccab23 --- /dev/null +++ b/OptimizelySDK.Tests/AudienceConditionsTests/SegmentsTests.cs @@ -0,0 +1,176 @@ +/* + * Copyright 2022-2023 Optimizely + * + * 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 + * + * https://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. + */ + +using Moq; +using NUnit.Framework; +using OptimizelySDK.AudienceConditions; +using OptimizelySDK.Entity; +using OptimizelySDK.Logger; +using System.Collections.Generic; +using System.Linq; + +namespace OptimizelySDK.Tests.AudienceConditionsTests +{ + [TestFixture] + public class SegmentsTests + { + private BaseCondition _firstThirdPartyOdpQualifiedMatchCondition; + private BaseCondition _secondThirdPartyOdpQualifiedMatchCondition; + private ICondition _customExactMatchCondition; + private Mock _mockLogger; + + private const string FIRST_CONDITION_VALUE = "first_condition_value"; + private const string SECOND_CONDITION_VALUE = "second_condition_value"; + + [TestFixtureSetUp] + public void Setup() + { + _firstThirdPartyOdpQualifiedMatchCondition = new BaseCondition + { + Value = FIRST_CONDITION_VALUE, + Type = "third_party_dimension", + Name = "odp.audiences", + Match = "qualified", + }; + + _secondThirdPartyOdpQualifiedMatchCondition = new BaseCondition + { + Value = SECOND_CONDITION_VALUE, + Type = "third_party_dimension", + Name = "odp.audiences", + Match = "qualified", + }; + + _customExactMatchCondition = new BaseCondition() + { + Value = "test_custom_value", + Type = "custom_attribute", + Name = "test_custom_name", + Match = "exact", + }; + + _mockLogger = new Mock(); + _mockLogger.Setup(l => l.Log(It.IsAny(), It.IsAny())); + } + + [Test] + public void ShouldGetSegmentsFromDatafileTypedAudiences() + { + var expectedSegments = new SortedSet + { + "ats_bug_bash_segment_gender", + "ats_bug_bash_segment_has_purchased", + "has_email_opted_out", + "ats_bug_bash_segment_dob", + }; + var optimizelyClient = new Optimizely(TestData.OdpSegmentsDatafile, + new ValidEventDispatcher(), _mockLogger.Object); + + var allSegments = optimizelyClient.ProjectConfigManager.GetConfig().Segments; + + var orderedDistinctSegments = new SortedSet(allSegments); + // check for no duplicates + Assert.AreEqual(allSegments.Length, orderedDistinctSegments.Count); + Assert.AreEqual(expectedSegments, orderedDistinctSegments); + } + + [Test] + public void ShouldFindOdpSegmentFromAndCondition() + { + var conditions = new AndCondition + { + Conditions = new[] + { + _firstThirdPartyOdpQualifiedMatchCondition, _customExactMatchCondition, + }, + }; + + var allSegments = Audience.GetSegments(conditions); + + Assert.AreEqual(_firstThirdPartyOdpQualifiedMatchCondition.Value.ToString(), + allSegments.FirstOrDefault()); + } + + [Test] + public void ShouldFindOdpSegmentFromOrCondition() + { + var conditions = new OrCondition + { + Conditions = new[] + { + _customExactMatchCondition, _firstThirdPartyOdpQualifiedMatchCondition, + }, + }; + + var allSegments = Audience.GetSegments(conditions); + + Assert.AreEqual(_firstThirdPartyOdpQualifiedMatchCondition.Value.ToString(), + allSegments.FirstOrDefault()); + } + + [Test] + public void ShouldNotFindOdpSegmentsFromConditions() + { + var conditions = new AndCondition + { + Conditions = new[] + { + _customExactMatchCondition, _customExactMatchCondition, _customExactMatchCondition, + }, + }; + + var allSegments = Audience.GetSegments(conditions); + + Assert.IsEmpty(allSegments); + } + + [Test] + public void ShouldFindAndDedupeNestedOdpSegments() + { + var qualifiedAndExact = new AndCondition + { + Conditions = new ICondition[] + { + _firstThirdPartyOdpQualifiedMatchCondition, _customExactMatchCondition, + }, + }; + var twoQualified = new AndCondition + { + Conditions = new ICondition[] + { + _secondThirdPartyOdpQualifiedMatchCondition, _firstThirdPartyOdpQualifiedMatchCondition, + }, + }; + var orConditions = new OrCondition + { + Conditions = new ICondition[] + { + qualifiedAndExact, twoQualified, + }, + }; + var notCondition = new NotCondition + { + Condition = orConditions, + }; + + var allSegments = Audience.GetSegments(notCondition).ToList(); + + Assert.AreEqual(2, allSegments.Count); + Assert.Contains(FIRST_CONDITION_VALUE, allSegments); + Assert.Contains(SECOND_CONDITION_VALUE, allSegments); + } + } +} diff --git a/OptimizelySDK.Tests/ConfigTest/PollingProjectConfigManagerTest.cs b/OptimizelySDK.Tests/ConfigTest/PollingProjectConfigManagerTest.cs index e7877ca80..96603d6b8 100644 --- a/OptimizelySDK.Tests/ConfigTest/PollingProjectConfigManagerTest.cs +++ b/OptimizelySDK.Tests/ConfigTest/PollingProjectConfigManagerTest.cs @@ -1,11 +1,11 @@ /* - * Copyright 2019, Optimizely + * Copyright 2019, 2023 Optimizely * * 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 + * https://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, @@ -22,7 +22,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Threading; namespace OptimizelySDK.Tests.DatafileManagement_Tests { @@ -34,17 +33,20 @@ public class PollingProjectConfigManagerTest [SetUp] public void Setup() - { + { LoggerMock = new Mock(); LoggerMock.Setup(l => l.Log(It.IsAny(), It.IsAny())); - ProjectConfig = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, null); + ProjectConfig = + DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, null); } - + [Test] public void TestPollingConfigManagerDoesNotBlockWhenProjectConfigIsAlreadyProvided() { var stopwatch = new Stopwatch(); - var configManager = new TestPollingProjectConfigManager(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3), true, LoggerMock.Object, new int[] { }); + var configManager = new TestPollingProjectConfigManager(TimeSpan.FromSeconds(2), + TimeSpan.FromSeconds(3), true, LoggerMock.Object, new int[] + { }); configManager.SetConfig(ProjectConfig); stopwatch.Start(); @@ -59,14 +61,19 @@ public void TestPollingConfigManagerDoesNotBlockWhenProjectConfigIsAlreadyProvid [Test] public void TestPollingConfigManagerBlocksWhenProjectConfigIsNotProvided() { + const int POLL_EVERY_MILLISECONDS = 500; var stopwatch = new Stopwatch(); - var configManager = new TestPollingProjectConfigManager(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2), true, LoggerMock.Object, new int[] {500 }); + var configManager = new TestPollingProjectConfigManager(TimeSpan.FromSeconds(2), + TimeSpan.FromSeconds(2), true, LoggerMock.Object, new[] + { + POLL_EVERY_MILLISECONDS, + }); stopwatch.Start(); - var config = configManager.GetConfig(); + configManager.GetConfig(); stopwatch.Stop(); - Assert.True(stopwatch.Elapsed.TotalMilliseconds >= 500); + Assert.GreaterOrEqual(stopwatch.Elapsed.TotalMilliseconds, POLL_EVERY_MILLISECONDS); configManager.Dispose(); } @@ -75,7 +82,11 @@ public void TestImmediatelyCalledScheduledRequestIfPreviousRequestDelayedInRespo { // period to call is one second // Giving response in 1200 milliseconds - var configManager = new TestPollingProjectConfigManager(TimeSpan.FromSeconds(1), TimeSpan.FromMilliseconds(1500), true, LoggerMock.Object, new int[] { 1200, 500, 500 }); + var configManager = new TestPollingProjectConfigManager(TimeSpan.FromSeconds(1), + TimeSpan.FromMilliseconds(1500), true, LoggerMock.Object, new int[] + { + 1200, 500, 500 + }); configManager.Start(); System.Threading.Tasks.Task.Delay(50).Wait(); @@ -89,7 +100,6 @@ public void TestImmediatelyCalledScheduledRequestIfPreviousRequestDelayedInRespo //Thread.Sleep(200); Assert.AreEqual(2, configManager.Counter); configManager.Dispose(); - } [Test] @@ -97,11 +107,16 @@ public void TestTimedoutIfTakingMorethanBlockingTimeout() { // period to call is one second // Giving response in 1200 milliseconds - var configManager = new TestPollingProjectConfigManager(TimeSpan.FromSeconds(3), TimeSpan.FromMilliseconds(1000), true, LoggerMock.Object, new int[] { 1300, 500, 500 }); + var configManager = new TestPollingProjectConfigManager(TimeSpan.FromSeconds(3), + TimeSpan.FromMilliseconds(1000), true, LoggerMock.Object, new int[] + { + 1300, 500, 500 + }); configManager.Start(); var config = configManager.GetConfig(); - LoggerMock.Verify(l => l.Log(LogLevel.WARN, "Timeout exceeded waiting for ProjectConfig to be set, returning null.")); + LoggerMock.Verify(l => l.Log(LogLevel.WARN, + "Timeout exceeded waiting for ProjectConfig to be set, returning null.")); configManager.Dispose(); } @@ -110,7 +125,11 @@ public void TestTimedoutOnlyIfSchedulerStarted() { // period to call is 3 second // Giving response in 1200 milliseconds and timedout should be in 1000 miliseconds - var configManager = new TestPollingProjectConfigManager(TimeSpan.FromSeconds(3), TimeSpan.FromMilliseconds(1000), true, LoggerMock.Object, new int[] { 1300, 500, 500 }); + var configManager = new TestPollingProjectConfigManager(TimeSpan.FromSeconds(3), + TimeSpan.FromMilliseconds(1000), true, LoggerMock.Object, new int[] + { + 1300, 500, 500 + }); Stopwatch sw = new Stopwatch(); sw.Start(); var config = configManager.GetConfig(); @@ -124,7 +143,11 @@ public void TestDontTimedoutIfSchedulerNotStarted() { // period to call is 3 second // Giving response in 1200 milliseconds and timedout should be in 1000 miliseconds - var configManager = new TestPollingProjectConfigManager(TimeSpan.FromSeconds(3), TimeSpan.FromMilliseconds(1000), true, LoggerMock.Object, new int[] { 1300, 500, 500 }); + var configManager = new TestPollingProjectConfigManager(TimeSpan.FromSeconds(3), + TimeSpan.FromMilliseconds(1000), true, LoggerMock.Object, new int[] + { + 1300, 500, 500 + }); Stopwatch sw = new Stopwatch(); sw.Start(); var config = configManager.GetConfig(); @@ -136,13 +159,26 @@ public void TestDontTimedoutIfSchedulerNotStarted() [Test] public void TestReturnDatafileImmediatelyOnceGetValidDatafileRemotely() { - var projConfig = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, null); - var data = new List() { - new TestPollingData { PollingTime = 500, ChangeVersion = false, ConfigDatafile = projConfig}, - new TestPollingData { PollingTime = 500, ChangeVersion = false, ConfigDatafile = projConfig} + var projConfig = + DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, null); + var data = new List() + { + new TestPollingData + { + PollingTime = 500, + ChangeVersion = false, + ConfigDatafile = projConfig + }, + new TestPollingData + { + PollingTime = 500, + ChangeVersion = false, + ConfigDatafile = projConfig + } }; - var configManager = new TestPollingProjectConfigManager(TimeSpan.FromSeconds(3), TimeSpan.FromMilliseconds(5000), true, LoggerMock.Object, data.ToArray()); + var configManager = new TestPollingProjectConfigManager(TimeSpan.FromSeconds(3), + TimeSpan.FromMilliseconds(5000), true, LoggerMock.Object, data.ToArray()); var config = configManager.GetConfig(); Assert.NotNull(config); @@ -160,15 +196,33 @@ public void TestWaitUntilValidDatfileIsNotGiven() // then send the right datafile // see it should release blocking. // blocking timeout must be inifinity. - var projConfig = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, null); - var data = new List() { - new TestPollingData { PollingTime = 50, ChangeVersion = false, ConfigDatafile = null}, - new TestPollingData { PollingTime = 50, ChangeVersion = false, ConfigDatafile = null}, - new TestPollingData { PollingTime = 50, ChangeVersion = false, ConfigDatafile = projConfig} + var projConfig = + DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, null); + var data = new List() + { + new TestPollingData + { + PollingTime = 50, + ChangeVersion = false, + ConfigDatafile = null + }, + new TestPollingData + { + PollingTime = 50, + ChangeVersion = false, + ConfigDatafile = null + }, + new TestPollingData + { + PollingTime = 50, + ChangeVersion = false, + ConfigDatafile = projConfig + } }; - var configManager = new TestPollingProjectConfigManager(TimeSpan.FromMilliseconds(500), TimeSpan.FromMilliseconds(10000), true, LoggerMock.Object, data.ToArray()); + var configManager = new TestPollingProjectConfigManager(TimeSpan.FromMilliseconds(500), + TimeSpan.FromMilliseconds(10000), true, LoggerMock.Object, data.ToArray()); configManager.Start(); // after 3rd attempt should get var config = configManager.GetConfig(); @@ -181,13 +235,30 @@ public void TestWaitUntilValidDatfileIsNotGiven() [Test] public void TestWaitUntilValidDatafileIsNotGivenOrTimedout() { - var data = new List() { - new TestPollingData { PollingTime = 50, ChangeVersion = false, ConfigDatafile = null}, - new TestPollingData { PollingTime = 50, ChangeVersion = false, ConfigDatafile = null}, - new TestPollingData { PollingTime = 50, ChangeVersion = false, ConfigDatafile = null} + var data = new List() + { + new TestPollingData + { + PollingTime = 50, + ChangeVersion = false, + ConfigDatafile = null + }, + new TestPollingData + { + PollingTime = 50, + ChangeVersion = false, + ConfigDatafile = null + }, + new TestPollingData + { + PollingTime = 50, + ChangeVersion = false, + ConfigDatafile = null + } }; - var configManager = new TestPollingProjectConfigManager(TimeSpan.FromMilliseconds(1000), TimeSpan.FromMilliseconds(2500), true, LoggerMock.Object, data.ToArray()); + var configManager = new TestPollingProjectConfigManager(TimeSpan.FromMilliseconds(1000), + TimeSpan.FromMilliseconds(2500), true, LoggerMock.Object, data.ToArray()); configManager.Start(); // after 3rd attempt should be released with null. var config = configManager.GetConfig(); diff --git a/OptimizelySDK.Tests/IntegrationOdpDatafile.json b/OptimizelySDK.Tests/IntegrationOdpDatafile.json index 219718e0d..d5c94dc0c 100644 --- a/OptimizelySDK.Tests/IntegrationOdpDatafile.json +++ b/OptimizelySDK.Tests/IntegrationOdpDatafile.json @@ -273,7 +273,7 @@ { "key": "odp", "host": "https://api.zaius.com", - "publicKey": "W4WzcEs-ABgXorzY7h1LCQ" + "publicKey": "fake-public-key" } ], "attributes": [{ diff --git a/OptimizelySDK.Tests/IntegrationOdpWithOtherFieldsDatafile.json b/OptimizelySDK.Tests/IntegrationOdpWithOtherFieldsDatafile.json index 4f6dd8ea6..3fea39e72 100644 --- a/OptimizelySDK.Tests/IntegrationOdpWithOtherFieldsDatafile.json +++ b/OptimizelySDK.Tests/IntegrationOdpWithOtherFieldsDatafile.json @@ -273,7 +273,7 @@ { "key": "odp", "host": "https://api.zaius.com", - "publicKey": "W4WzcEs-ABgXorzY7h1LCQ", + "publicKey": "fake-public-key", "k1": 10, "k2": true, "k3": "any-value" diff --git a/OptimizelySDK.Tests/OdpSegmentsDatafile.json b/OptimizelySDK.Tests/OdpSegmentsDatafile.json new file mode 100644 index 000000000..074ecb48f --- /dev/null +++ b/OptimizelySDK.Tests/OdpSegmentsDatafile.json @@ -0,0 +1,327 @@ +{ + "groups": [ + ], + "environmentKey": "production", + "rollouts": [ + { + "experiments": [ + { + "status": "Running", + "audienceConditions": [ + ], + "audienceIds": [ + ], + "variations": [ + { + "variables": [ + ], + "id": "109682", + "key": "off", + "featureEnabled": false + } + ], + "forcedVariations": { + }, + "key": "default-rollout-36645-22702840437", + "layerId": "rollout-36645-22702840437", + "trafficAllocation": [ + { + "entityId": "109682", + "endOfRange": 10000 + } + ], + "id": "default-rollout-36645-22702840437" + } + ], + "id": "rollout-36645-22702840437" + } + ], + "typedAudiences": [ + { + "id": "22727912729", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": "ats_bug_bash_segment_gender", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + ] + ], + "name": "ODP Gender Audience" + }, + { + "id": "22642853106", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": "has_email_opted_out", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + ] + ], + "name": "Email Opt-Out Audience" + }, + { + "id": "22722060443", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": "kwanzaa", + "type": "custom_attribute", + "name": "current_holiday", + "match": "exact" + } + ], + [ + "or", + { + "value": "ats_bug_bash_segment_has_purchased", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ], + [ + "or", + { + "value": "ats_bug_bash_segment_gender", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + ] + ], + "name": "Current Holiday Purchases" + }, + { + "id": "22713562125", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": "ats_bug_bash_segment_dob", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + ] + ], + "name": "ODP Birthdate Audience" + }, + { + "id": "22734824597", + "conditions": [ + "and", + [ + "or", + [ + "not", + [ + "or", + { + "value": true, + "type": "custom_attribute", + "name": "likes_presents", + "match": "exact" + } + ] + ] + ] + ], + "name": "Hates Presents" + } + ], + "projectId": "22720961022", + "variables": [ + ], + "featureFlags": [ + { + "experimentIds": [ + "9300000149777" + ], + "rolloutId": "rollout-36645-22702840437", + "variables": [ + ], + "id": "36645", + "key": "favorite_holiday" + } + ], + "integrations": [ + { + "publicKey": "ax6Bz223fD-jpOo9u0BMg", + "host": "https://example.com", + "key": "odp" + } + ], + "experiments": [ + { + "status": "Running", + "audienceConditions": [ + "or", + "22722060443", + "22713562125", + "22727912729", + "22642853106", + "22734824597" + ], + "audienceIds": [ + "22722060443", + "22713562125", + "22727912729", + "22642853106", + "22734824597" + ], + "variations": [ + { + "variables": [ + ], + "id": "109684", + "key": "christmas", + "featureEnabled": true + }, + { + "variables": [ + ], + "id": "109685", + "key": "hanukkah", + "featureEnabled": true + }, + { + "variables": [ + ], + "id": "109686", + "key": "kwanzaa", + "featureEnabled": true + } + ], + "forcedVariations": { + }, + "key": "holiday_banner", + "layerId": "9300000114602", + "trafficAllocation": [ + { + "entityId": "109684", + "endOfRange": 1966 + }, + { + "entityId": "", + "endOfRange": 2400 + }, + { + "entityId": "109685", + "endOfRange": 4366 + }, + { + "entityId": "", + "endOfRange": 4800 + }, + { + "entityId": "109686", + "endOfRange": 6767 + }, + { + "entityId": "", + "endOfRange": 7200 + }, + { + "entityId": "109684", + "endOfRange": 7500 + }, + { + "entityId": "109685", + "endOfRange": 7800 + }, + { + "entityId": "109686", + "endOfRange": 8100 + }, + { + "entityId": "109684", + "endOfRange": 8234 + }, + { + "entityId": "109685", + "endOfRange": 8368 + }, + { + "entityId": "109686", + "endOfRange": 8501 + } + ], + "id": "9300000149777" + } + ], + "version": "4", + "audiences": [ + { + "id": "22727912729", + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + "name": "ODP Gender Audience" + }, + { + "id": "22642853106", + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + "name": "Email Opt-Out Audience" + }, + { + "id": "22722060443", + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + "name": "Current Holiday Purchases" + }, + { + "id": "22713562125", + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + "name": "ODP Birthdate Audience" + }, + { + "id": "22734824597", + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + "name": "Hates Presents" + }, + { + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + "id": "$opt_dummy_audience", + "name": "Optimizely-Generated Audience for Backwards Compatibility" + } + ], + "anonymizeIP": true, + "sdkKey": "VZEZsi7xS2Tv3nESLvFui", + "attributes": [ + { + "id": "22696496486", + "key": "likes_presents" + }, + { + "id": "22700670748", + "key": "current_holiday" + } + ], + "botFiltering": false, + "accountId": "21468570738", + "events": [ + ], + "revision": "29" +} diff --git a/OptimizelySDK.Tests/OdpTests/LruCacheTest.cs b/OptimizelySDK.Tests/OdpTests/LruCacheTest.cs index dd5183ab1..27f8ee169 100644 --- a/OptimizelySDK.Tests/OdpTests/LruCacheTest.cs +++ b/OptimizelySDK.Tests/OdpTests/LruCacheTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022, Optimizely + * Copyright 2022-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,7 +67,7 @@ public void ShouldSaveAndLookupMultipleItems() cache.Save("user2", _segments3And4); cache.Save("user3", _segments5And6); - var cacheKeys = cache._readCurrentCacheKeys(); + var cacheKeys = cache.CurrentCacheKeysForTesting(); Assert.AreEqual("user3", cacheKeys[0]); Assert.AreEqual("user2", cacheKeys[1]); @@ -76,7 +76,7 @@ public void ShouldSaveAndLookupMultipleItems() // Lookup should move user1 to top of the list and push down others. Assert.AreEqual(_segments1And2, cache.Lookup("user1")); - cacheKeys = cache._readCurrentCacheKeys(); + cacheKeys = cache.CurrentCacheKeysForTesting(); Assert.AreEqual("user1", cacheKeys[0]); Assert.AreEqual("user3", cacheKeys[1]); Assert.AreEqual("user2", cacheKeys[2]); @@ -84,7 +84,7 @@ public void ShouldSaveAndLookupMultipleItems() // Lookup should move user2 to the beginning of the list and push others. Assert.AreEqual(_segments3And4, cache.Lookup("user2")); - cacheKeys = cache._readCurrentCacheKeys(); + cacheKeys = cache.CurrentCacheKeysForTesting(); Assert.AreEqual("user2", cacheKeys[0]); Assert.AreEqual("user1", cacheKeys[1]); Assert.AreEqual("user3", cacheKeys[2]); @@ -92,7 +92,7 @@ public void ShouldSaveAndLookupMultipleItems() // Lookup moves user3 to top and pushes others down. Assert.AreEqual(_segments5And6, cache.Lookup("user3")); - cacheKeys = cache._readCurrentCacheKeys(); + cacheKeys = cache.CurrentCacheKeysForTesting(); Assert.AreEqual("user3", cacheKeys[0]); Assert.AreEqual("user2", cacheKeys[1]); Assert.AreEqual("user1", cacheKeys[2]); @@ -107,36 +107,36 @@ public void ShouldReorderListOnSave() cache.Save("user2", _segments3And4); cache.Save("user3", _segments5And6); - var cacheKeys = cache._readCurrentCacheKeys(); - + var cacheKeys = cache.CurrentCacheKeysForTesting(); + // last added should be at the top of the list - Assert.AreEqual("user3",cacheKeys[0]); - Assert.AreEqual("user2",cacheKeys[1]); - Assert.AreEqual("user1" ,cacheKeys[2]); + Assert.AreEqual("user3", cacheKeys[0]); + Assert.AreEqual("user2", cacheKeys[1]); + Assert.AreEqual("user1", cacheKeys[2]); // save should move user1 to the top of the list and push down others. cache.Save("user1", _segments1And2); - - cacheKeys = cache._readCurrentCacheKeys(); - Assert.AreEqual("user1",cacheKeys[0]); - Assert.AreEqual("user3",cacheKeys[1]); - Assert.AreEqual("user2",cacheKeys[2]); + + cacheKeys = cache.CurrentCacheKeysForTesting(); + Assert.AreEqual("user1", cacheKeys[0]); + Assert.AreEqual("user3", cacheKeys[1]); + Assert.AreEqual("user2", cacheKeys[2]); // save user2 should bring it to the top and push down others. cache.Save("user2", _segments3And4); - - cacheKeys = cache._readCurrentCacheKeys(); - Assert.AreEqual("user2",cacheKeys[0]); - Assert.AreEqual("user1",cacheKeys[1]); - Assert.AreEqual("user3",cacheKeys[2]); + + cacheKeys = cache.CurrentCacheKeysForTesting(); + Assert.AreEqual("user2", cacheKeys[0]); + Assert.AreEqual("user1", cacheKeys[1]); + Assert.AreEqual("user3", cacheKeys[2]); // saving user3 again should return to the original insertion order. cache.Save("user3", _segments5And6); - - cacheKeys = cache._readCurrentCacheKeys(); - Assert.AreEqual("user3",cacheKeys[0]); - Assert.AreEqual("user2",cacheKeys[1]); - Assert.AreEqual("user1" ,cacheKeys[2]); + + cacheKeys = cache.CurrentCacheKeysForTesting(); + Assert.AreEqual("user3", cacheKeys[0]); + Assert.AreEqual("user2", cacheKeys[1]); + Assert.AreEqual("user1", cacheKeys[2]); } [Test] @@ -161,12 +161,12 @@ public void ShouldHandleWhenItemsExpire() cache.Save("user1", _segments1And2); Assert.AreEqual(_segments1And2, cache.Lookup("user1")); - Assert.AreEqual(1, cache._readCurrentCacheKeys().Length); + Assert.AreEqual(1, cache.CurrentCacheKeysForTesting().Length); Thread.Sleep(1200); Assert.IsNull(cache.Lookup("user1")); - Assert.AreEqual(0, cache._readCurrentCacheKeys().Length); + Assert.AreEqual(0, cache.CurrentCacheKeysForTesting().Length); } [Test] @@ -178,7 +178,7 @@ public void ShouldHandleWhenCacheReachesMaxSize() cache.Save("user2", _segments3And4); cache.Save("user3", _segments5And6); - Assert.AreEqual(2, cache._readCurrentCacheKeys().Length); + Assert.AreEqual(2, cache.CurrentCacheKeysForTesting().Length); Assert.AreEqual(_segments5And6, cache.Lookup("user3")); Assert.AreEqual(_segments3And4, cache.Lookup("user2")); @@ -198,7 +198,7 @@ public void ShouldHandleWhenCacheIsReset() Assert.AreEqual(_segments3And4, cache.Lookup("user2")); Assert.AreEqual(_segments5And6, cache.Lookup("user3")); - Assert.AreEqual(3, cache._readCurrentCacheKeys().Length); + Assert.AreEqual(3, cache.CurrentCacheKeysForTesting().Length); cache.Reset(); @@ -206,7 +206,7 @@ public void ShouldHandleWhenCacheIsReset() Assert.IsNull(cache.Lookup("user2")); Assert.IsNull(cache.Lookup("user3")); - Assert.AreEqual(0, cache._readCurrentCacheKeys().Length); + Assert.AreEqual(0, cache.CurrentCacheKeysForTesting().Length); } } } diff --git a/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs b/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs index 0fea8a829..87ce608da 100644 --- a/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs +++ b/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022, Optimizely + * Copyright 2022-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -155,12 +155,14 @@ public void Setup() [Test] public void ShouldLogAndDiscardEventsWhenEventManagerNotRunning() { - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). - Build(startImmediately: false); + WithAutoStart(false). + Build(); + eventManager.UpdateSettings(_odpConfig); - // since we've not called start() then... + // since we've not called Start() then... eventManager.SendEvent(_testEvents[0]); // ...we should get a notice after trying to send an event @@ -173,11 +175,13 @@ public void ShouldLogAndDiscardEventsWhenEventManagerNotRunning() public void ShouldLogAndDiscardEventsWhenEventManagerConfigNotReady() { var mockOdpConfig = new Mock(API_KEY, API_HOST, _emptySegmentsToCheck); - mockOdpConfig.Setup(o => o.IsReady()).Returns(false); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(mockOdpConfig.Object). + mockOdpConfig.Setup(o => o.IsReady()).Returns(false); // stay not ready + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). - Build(startImmediately: false); // doing it manually in Act next + WithAutoStart(false). // start manually in Act + Build(); + eventManager.UpdateSettings(mockOdpConfig.Object); eventManager.Start(); // Log when Start() called eventManager.SendEvent(_testEvents[0]); // Log when enqueue attempted @@ -191,30 +195,34 @@ public void ShouldLogAndDiscardEventsWhenEventManagerConfigNotReady() public void ShouldLogWhenOdpNotIntegratedAndIdentifyUserCalled() { var mockOdpConfig = new Mock(API_KEY, API_HOST, _emptySegmentsToCheck); - mockOdpConfig.Setup(o => o.IsReady()).Returns(false); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(mockOdpConfig.Object). + mockOdpConfig.Setup(o => o.IsReady()).Returns(false); // never ready + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). - Build(); + Build(); // assumed AutoStart true; Logs 1x here + eventManager.UpdateSettings(mockOdpConfig. + Object); // auto-start after update; Logs 1x here - eventManager.IdentifyUser(FS_USER_ID); + eventManager.IdentifyUser(FS_USER_ID); // Logs 1x here too _mockLogger.Verify( l => l.Log(LogLevel.WARN, Constants.ODP_NOT_INTEGRATED_MESSAGE), - Times.Exactly(2)); // during Start() and SendEvent() + Times.Exactly(3)); // during Start() and SendEvent() } [Test] public void ShouldLogWhenOdpNotIntegratedAndStartCalled() { var mockOdpConfig = new Mock(API_KEY, API_HOST, _emptySegmentsToCheck); - mockOdpConfig.Setup(o => o.IsReady()).Returns(false); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(mockOdpConfig.Object). + mockOdpConfig.Setup(o => o.IsReady()).Returns(false); // since never ready + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). - Build(startImmediately: false); // doing it manually in Act next + WithAutoStart(false). // doing it manually in Act next + Build(); + eventManager.UpdateSettings(mockOdpConfig.Object); - eventManager.Start(); + eventManager.Start(); // Log 1x here too _mockLogger.Verify(l => l.Log(LogLevel.WARN, Constants.ODP_NOT_INTEGRATED_MESSAGE), Times.Once); @@ -250,10 +258,11 @@ public void ShouldDiscardEventsWithInvalidData() "key-3", new DateTime() }, }); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). Build(); + eventManager.UpdateSettings(_odpConfig); eventManager.SendEvent(eventWithAnArray); eventManager.SendEvent(eventWithADate); @@ -271,16 +280,16 @@ public void ShouldAddAdditionalInformationToEachEvent() _mockApiManager.Setup(api => api.SendEvents(It.IsAny(), It.IsAny(), Capture.In(eventsCollector))). Callback(() => cde.Signal()); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). WithEventQueue(new BlockingCollection(10)). // max capacity of 10 - WithBatchSize(10). WithFlushInterval(TimeSpan.FromMilliseconds(100)). Build(); + eventManager.UpdateSettings(_odpConfig); eventManager.SendEvent(_testEvents[0]); - cde.Wait(); + cde.Wait(MAX_COUNT_DOWN_EVENT_WAIT_MS); var eventsSentToApi = eventsCollector.FirstOrDefault(); var actualEvent = eventsSentToApi?.FirstOrDefault(); @@ -302,15 +311,15 @@ public void ShouldAddAdditionalInformationToEachEvent() } [Test] - public void ShouldAttemptToFlushAnEmptyQueueAtFlushInterval() + public void ShouldNotAttemptToFlushAnEmptyQueueAtFlushInterval() { - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). WithEventQueue(new BlockingCollection(10)). - WithBatchSize(10). WithFlushInterval(TimeSpan.FromMilliseconds(100)). Build(); + eventManager.UpdateSettings(_odpConfig); // do not add events to the queue, but allow for // at least 3 flush intervals executions @@ -318,7 +327,7 @@ public void ShouldAttemptToFlushAnEmptyQueueAtFlushInterval() eventManager.Stop(); _mockLogger.Verify(l => l.Log(LogLevel.DEBUG, "Flushing queue."), - Times.AtLeast(3)); + Times.Never); } [Test] @@ -328,13 +337,13 @@ public void ShouldDispatchEventsInCorrectNumberOfBatches() a.SendEvents(It.IsAny(), It.IsAny(), It.IsAny>())). Returns(false); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). WithEventQueue(new BlockingCollection(10)). - WithBatchSize(10). WithFlushInterval(TimeSpan.FromMilliseconds(500)). Build(); + eventManager.UpdateSettings(_odpConfig); for (int i = 0; i < 25; i++) { @@ -362,12 +371,12 @@ public void ShouldDispatchEventsWithCorrectPayload() Capture.In(eventCollector))). Callback(() => cde.Signal()). Returns(false); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). - WithBatchSize(10). WithFlushInterval(TimeSpan.FromSeconds(1)). Build(); + eventManager.UpdateSettings(_odpConfig); _testEvents.ForEach(e => eventManager.SendEvent(e)); cde.Wait(MAX_COUNT_DOWN_EVENT_WAIT_MS); @@ -389,43 +398,43 @@ public void ShouldDispatchEventsWithCorrectPayload() [Test] public void ShouldRetryFailedEvents() { - var cde = new CountdownEvent(6); + var cde = new CountdownEvent(12); _mockApiManager.Setup(a => a.SendEvents(It.IsAny(), It.IsAny(), It.IsAny>())). Callback(() => cde.Signal()). Returns(true); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). WithEventQueue(new BlockingCollection(10)). - WithBatchSize(2). - WithFlushInterval(TimeSpan.FromMilliseconds(100)). + WithFlushInterval(TimeSpan.Zero). // batches of 1 Build(); + eventManager.UpdateSettings(_odpConfig); - for (int i = 0; i < 4; i++) + for (int i = 0; i < 4; i++) // send 4 events in batches of 1 { eventManager.SendEvent(MakeEvent(i)); } cde.Wait(MAX_COUNT_DOWN_EVENT_WAIT_MS); - // retry 3x (default) for 2 batches or 6 calls to attempt to process + // retry 3x (default) 4 events (batches of 1) = 12 calls to attempt to process _mockApiManager.Verify( a => a.SendEvents(It.IsAny(), It.IsAny(), - It.IsAny>()), Times.Exactly(6)); + It.IsAny>()), Times.Exactly(12)); } [Test] public void ShouldFlushAllScheduledEventsBeforeStopping() { - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). WithEventQueue(new BlockingCollection(100)). - WithBatchSize(2). // small batch size WithFlushInterval(TimeSpan.FromSeconds(2)). // long flush interval Build(); + eventManager.UpdateSettings(_odpConfig); for (int i = 0; i < 25; i++) { @@ -453,12 +462,13 @@ public void ShouldPrepareCorrectPayloadForIdentifyUser() _mockApiManager.Setup(api => api.SendEvents(It.IsAny(), It.IsAny(), Capture.In(eventsCollector))). Callback(() => cde.Signal()); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). WithEventQueue(new BlockingCollection(1)). - WithBatchSize(1). + WithFlushInterval(TimeSpan.FromSeconds(1)). Build(); + eventManager.UpdateSettings(_odpConfig); eventManager.IdentifyUser(USER_ID); cde.Wait(MAX_COUNT_DOWN_EVENT_WAIT_MS); @@ -488,14 +498,15 @@ public void ShouldApplyUpdatedOdpConfigurationWhenAvailable() "1-item-cart", }; var differentOdpConfig = new OdpConfig(apiKey, apiHost, segmentsToCheck); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). Build(); + eventManager.UpdateSettings(_odpConfig); eventManager.UpdateSettings(differentOdpConfig); - Assert.IsFalse(_odpConfig.Equals(eventManager._readOdpConfigForTesting())); + Assert.IsFalse(_odpConfig.Equals(eventManager.OdpConfigForTesting)); } private static OdpEvent MakeEvent(int id) => diff --git a/OptimizelySDK.Tests/OdpTests/OdpManagerTest.cs b/OptimizelySDK.Tests/OdpTests/OdpManagerTest.cs index 2f943a0c9..a1adaafcb 100644 --- a/OptimizelySDK.Tests/OdpTests/OdpManagerTest.cs +++ b/OptimizelySDK.Tests/OdpTests/OdpManagerTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 Optimizely + * Copyright 2022-2023 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ using OptimizelySDK.Logger; using OptimizelySDK.Odp; using OptimizelySDK.Odp.Entity; +using System; using System.Collections.Generic; using System.Linq; @@ -67,7 +68,6 @@ public class OdpManagerTest private readonly List _emptySegmentsToCheck = new List(0); - private OdpConfig _odpConfig; private Mock _mockLogger; private Mock _mockOdpEventManager; private Mock _mockSegmentManager; @@ -75,18 +75,36 @@ public class OdpManagerTest [SetUp] public void Setup() { - _odpConfig = new OdpConfig(API_KEY, API_HOST, _emptySegmentsToCheck); _mockLogger = new Mock(); _mockOdpEventManager = new Mock(); _mockSegmentManager = new Mock(); } + [Test] + public void ShouldInitializeWithCorrectDefaults() + { + var manager = new OdpManager.Builder(). + WithLogger(_mockLogger.Object). + Build(); + + var eventManager = (manager.EventManager as OdpEventManager); + var segmentCache = + (manager.SegmentManager as OdpSegmentManager)?.SegmentsCacheForTesting as + LruCache>; + Assert.AreEqual(Constants.DEFAULT_FLUSH_INTERVAL, + eventManager?.FlushIntervalForTesting); + Assert.AreEqual(Constants.DEFAULT_TIMEOUT_INTERVAL, + eventManager.TimeoutIntervalForTesting); + Assert.AreEqual(Constants.DEFAULT_MAX_CACHE_SIZE, segmentCache?.MaxSizeForTesting); + Assert.AreEqual(TimeSpan.FromSeconds(Constants.DEFAULT_CACHE_SECONDS), segmentCache.TimeoutForTesting); + } + [Test] public void ShouldStartEventManagerWhenOdpManagerIsInitialized() { _mockOdpEventManager.Setup(e => e.Start()); - _ = new OdpManager.Builder().WithOdpConfig(_odpConfig). + _ = new OdpManager.Builder(). WithSegmentManager(_mockSegmentManager.Object). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). @@ -99,11 +117,12 @@ public void ShouldStartEventManagerWhenOdpManagerIsInitialized() public void ShouldStopEventManagerWhenCloseIsCalled() { _mockOdpEventManager.Setup(e => e.Stop()); - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + var manager = new OdpManager.Builder(). WithSegmentManager(_mockSegmentManager.Object). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). Build(); + manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); manager.Dispose(); @@ -116,7 +135,7 @@ public void ShouldUseNewSettingsInEventManagerWhenOdpConfigIsUpdated() var eventManagerParameterCollector = new List(); _mockOdpEventManager.Setup(e => e.UpdateSettings(Capture.In(eventManagerParameterCollector))); - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + var manager = new OdpManager.Builder(). WithSegmentManager(_mockSegmentManager.Object). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). @@ -138,7 +157,7 @@ public void ShouldUseNewSettingsInSegmentManagerWhenOdpConfigIsUpdated() var segmentManagerParameterCollector = new List(); _mockSegmentManager.Setup(s => s.UpdateSettings(Capture.In(segmentManagerParameterCollector))); - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + var manager = new OdpManager.Builder(). WithSegmentManager(_mockSegmentManager.Object). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). @@ -159,14 +178,17 @@ public void ShouldHandleOdpConfigSettingsNoChange() { _mockSegmentManager.Setup(s => s.UpdateSettings(It.IsAny())); _mockOdpEventManager.Setup(e => e.UpdateSettings(It.IsAny())); - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + var manager = new OdpManager.Builder(). WithSegmentManager(_mockSegmentManager.Object). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). Build(); + manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); // initial set + _mockOdpEventManager.ResetCalls(); + _mockSegmentManager.ResetCalls(); - var wasUpdated = manager.UpdateSettings(_odpConfig.ApiKey, _odpConfig.ApiHost, - _odpConfig.SegmentsToCheck); + // attempt to set with the same config + var wasUpdated = manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); Assert.IsFalse(wasUpdated); _mockSegmentManager.Verify(s => s.UpdateSettings(It.IsAny()), Times.Never); @@ -176,9 +198,10 @@ public void ShouldHandleOdpConfigSettingsNoChange() [Test] public void ShouldUpdateSettingsWithReset() { - _mockSegmentManager.Setup(s => - s.UpdateSettings(It.IsAny())); - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + _mockOdpEventManager.Setup(e => e.UpdateSettings(It.IsAny())); + _mockSegmentManager.Setup(s => s.ResetCache()); + _mockSegmentManager.Setup(s => s.UpdateSettings(It.IsAny())); + var manager = new OdpManager.Builder(). WithSegmentManager(_mockSegmentManager.Object). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). @@ -188,22 +211,31 @@ public void ShouldUpdateSettingsWithReset() _updatedSegmentsToCheck); Assert.IsTrue(wasUpdated); + _mockOdpEventManager.Verify(e => e.UpdateSettings(It.IsAny()), Times.Once); _mockSegmentManager.Verify(s => s.ResetCache(), Times.Once); + _mockSegmentManager.Verify(s => s.UpdateSettings(It.IsAny()), Times.Once); } [Test] public void ShouldDisableOdpThroughConfiguration() { - _mockOdpEventManager.Setup(e => e.SendEvent(It.IsAny())); + _mockOdpEventManager.Setup(e => e.Start()); _mockOdpEventManager.Setup(e => e.IsStarted).Returns(true); - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + _mockOdpEventManager.Setup(e => e.SendEvent(It.IsAny())); + _mockOdpEventManager.Setup(e => e.UpdateSettings(It.IsAny())); + var manager = new OdpManager.Builder(). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). - Build(); + Build(); // auto-start event manager attempted, but no config + manager.UpdateSettings(API_KEY, API_HOST, + _emptySegmentsToCheck); // event manager config added + auto-start + // should send event manager.SendEvent(TEST_EVENT_TYPE, TEST_EVENT_ACTION, _testEventIdentifiers, _testEventData); + _mockOdpEventManager.Verify(e => e.Start(), Times.Once); + _mockOdpEventManager.Verify(e => e.UpdateSettings(It.IsAny()), Times.Once); _mockOdpEventManager.Verify(e => e.SendEvent(It.IsAny()), Times.Once); _mockLogger.Verify(l => l.Log(LogLevel.ERROR, "ODP event not dispatched (ODP disabled)."), Times.Never); @@ -211,12 +243,13 @@ public void ShouldDisableOdpThroughConfiguration() _mockOdpEventManager.ResetCalls(); _mockLogger.ResetCalls(); + // remove config and try sending again manager.UpdateSettings(string.Empty, string.Empty, _emptySegmentsToCheck); - manager.SendEvent(TEST_EVENT_TYPE, TEST_EVENT_ACTION, _testEventIdentifiers, _testEventData); manager.Dispose(); + // should not try to send and provide a log message _mockOdpEventManager.Verify(e => e.SendEvent(It.IsAny()), Times.Never); _mockLogger.Verify(l => l.Log(LogLevel.ERROR, "ODP event not dispatched (ODP disabled)."), Times.Once); @@ -225,11 +258,12 @@ public void ShouldDisableOdpThroughConfiguration() [Test] public void ShouldGetEventManager() { - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + var manager = new OdpManager.Builder(). WithSegmentManager(_mockSegmentManager.Object). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). Build(); + manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); Assert.IsNotNull(manager.EventManager); } @@ -237,11 +271,12 @@ public void ShouldGetEventManager() [Test] public void ShouldGetSegmentManager() { - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + var manager = new OdpManager.Builder(). WithSegmentManager(_mockSegmentManager.Object). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). Build(); + manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); Assert.IsNotNull(manager.SegmentManager); } @@ -251,10 +286,11 @@ public void ShouldIdentifyUserWhenOdpIsIntegrated() { _mockOdpEventManager.Setup(e => e.IdentifyUser(It.IsAny())); _mockOdpEventManager.Setup(e => e.IsStarted).Returns(true); - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + var manager = new OdpManager.Builder(). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). Build(); + manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); manager.IdentifyUser(VALID_FS_USER_ID); manager.Dispose(); @@ -268,10 +304,11 @@ public void ShouldNotIdentifyUserWhenOdpDisabled() { _mockOdpEventManager.Setup(e => e.IdentifyUser(It.IsAny())); _mockOdpEventManager.Setup(e => e.IsStarted).Returns(true); - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + var manager = new OdpManager.Builder(). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). Build(false); + manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); manager.IdentifyUser(VALID_FS_USER_ID); manager.Dispose(); @@ -286,10 +323,11 @@ public void ShouldSendEventWhenOdpIsIntegrated() { _mockOdpEventManager.Setup(e => e.SendEvent(It.IsAny())); _mockOdpEventManager.Setup(e => e.IsStarted).Returns(true); - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + var manager = new OdpManager.Builder(). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). Build(); + manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); manager.SendEvent(TEST_EVENT_TYPE, TEST_EVENT_ACTION, _testEventIdentifiers, _testEventData); @@ -301,11 +339,12 @@ public void ShouldSendEventWhenOdpIsIntegrated() [Test] public void ShouldNotSendEventOdpNotIntegrated() { - var odpConfig = new OdpConfig(string.Empty, string.Empty, _emptySegmentsToCheck); _mockOdpEventManager.Setup(e => e.SendEvent(It.IsAny())); - var manager = new OdpManager.Builder().WithOdpConfig(odpConfig). + var manager = new OdpManager.Builder(). + WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). Build(false); // do not enable + manager.UpdateSettings(string.Empty, string.Empty, _emptySegmentsToCheck); manager.SendEvent(TEST_EVENT_TYPE, TEST_EVENT_ACTION, _testEventIdentifiers, _testEventData); diff --git a/OptimizelySDK.Tests/OdpTests/OdpSegmentManagerTest.cs b/OptimizelySDK.Tests/OdpTests/OdpSegmentManagerTest.cs index c8abe6509..d1db2d6b3 100644 --- a/OptimizelySDK.Tests/OdpTests/OdpSegmentManagerTest.cs +++ b/OptimizelySDK.Tests/OdpTests/OdpSegmentManagerTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 Optimizely + * Copyright 2022, 2023 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,11 +36,7 @@ public class OdpSegmentManagerTest private static readonly string expectedCacheKey = $"fs_user_id-$-{FS_USER_ID}"; - private static readonly List segmentsToCheck = new List - { - "segment1", - "segment2", - }; + private static readonly List segmentsToCheck = new List { "segment1", "segment2", }; private OdpConfig _odpConfig; private Mock _mockApiManager; @@ -64,13 +60,13 @@ public void Setup() public void ShouldFetchSegmentsOnCacheMiss() { var keyCollector = new List(); - _mockCache.Setup(c => c.Lookup(Capture.In(keyCollector))). - Returns(default(List)); + _mockCache.Setup(c => c.Lookup(Capture.In(keyCollector))).Returns(default(List)); _mockApiManager.Setup(a => a.FetchSegments(It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny>())). - Returns(segmentsToCheck.ToArray()); - var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object, - Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); + It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(segmentsToCheck.ToArray()); + var manager = new OdpSegmentManager(_mockApiManager.Object, _mockCache.Object, + _mockLogger.Object); + manager.UpdateSettings(_odpConfig); var segments = manager.FetchQualifiedSegments(FS_USER_ID); @@ -98,8 +94,9 @@ public void ShouldFetchSegmentsSuccessOnCacheHit() _mockCache.Setup(c => c.Lookup(Capture.In(keyCollector))).Returns(segmentsToCheck); _mockApiManager.Setup(a => a.FetchSegments(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); - var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object, - Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); + var manager = new OdpSegmentManager(_mockApiManager.Object, _mockCache.Object, + _mockLogger.Object); + manager.UpdateSettings(_odpConfig); var segments = manager.FetchQualifiedSegments(FS_USER_ID); @@ -122,10 +119,11 @@ public void ShouldHandleFetchSegmentsWithError() { // OdpSegmentApiManager.FetchSegments() return null on any error _mockApiManager.Setup(a => a.FetchSegments(It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny>())). - Returns(null as string[]); - var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object, - Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); + It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(null as string[]); + var manager = new OdpSegmentManager(_mockApiManager.Object, _mockCache.Object, + _mockLogger.Object); + manager.UpdateSettings(_odpConfig); var segments = manager.FetchQualifiedSegments(FS_USER_ID); @@ -143,8 +141,9 @@ public void ShouldHandleFetchSegmentsWithError() public void ShouldLogAndReturnAnEmptySetWhenNoSegmentsToCheck() { var odpConfig = new OdpConfig(API_KEY, API_HOST, new List(0)); - var manager = new OdpSegmentManager(odpConfig, _mockApiManager.Object, - Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); + var manager = new OdpSegmentManager(_mockApiManager.Object, _mockCache.Object, + _mockLogger.Object); + manager.UpdateSettings(odpConfig); var segments = manager.FetchQualifiedSegments(FS_USER_ID); @@ -160,8 +159,9 @@ public void ShouldLogAndReturnNullWhenOdpConfigNotReady() { var mockOdpConfig = new Mock(API_KEY, API_HOST, new List(0)); mockOdpConfig.Setup(o => o.IsReady()).Returns(false); - var manager = new OdpSegmentManager(mockOdpConfig.Object, _mockApiManager.Object, - Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); + var manager = new OdpSegmentManager(_mockApiManager.Object, _mockCache.Object, + _mockLogger.Object); + manager.UpdateSettings(mockOdpConfig.Object); var segments = manager.FetchQualifiedSegments(FS_USER_ID); @@ -174,13 +174,11 @@ public void ShouldLogAndReturnNullWhenOdpConfigNotReady() [Test] public void ShouldIgnoreCache() { - var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object, - Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); + var manager = new OdpSegmentManager(_mockApiManager.Object, _mockCache.Object, + _mockLogger.Object); + manager.UpdateSettings(_odpConfig); - manager.FetchQualifiedSegments(FS_USER_ID, new List - { - OdpSegmentOption.IgnoreCache, - }); + manager.FetchQualifiedSegments(FS_USER_ID, new List { OdpSegmentOption.IGNORE_CACHE, }); _mockCache.Verify(c => c.Reset(), Times.Never); _mockCache.Verify(c => c.Lookup(It.IsAny()), Times.Never); @@ -194,13 +192,11 @@ public void ShouldIgnoreCache() [Test] public void ShouldResetCache() { - var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object, - Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); + var manager = new OdpSegmentManager(_mockApiManager.Object, _mockCache.Object, + _mockLogger.Object); + manager.UpdateSettings(_odpConfig); - manager.FetchQualifiedSegments(FS_USER_ID, new List - { - OdpSegmentOption.ResetCache, - }); + manager.FetchQualifiedSegments(FS_USER_ID, new List { OdpSegmentOption.RESET_CACHE, }); _mockCache.Verify(c => c.Reset(), Times.Once); _mockCache.Verify(c => c.Lookup(It.IsAny()), Times.Once); @@ -216,8 +212,9 @@ public void ShouldMakeValidCacheKey() { var keyCollector = new List(); _mockCache.Setup(c => c.Lookup(Capture.In(keyCollector))); - var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object, - Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); + var manager = new OdpSegmentManager(_mockApiManager.Object, _mockCache.Object, + _mockLogger.Object); + manager.UpdateSettings(_odpConfig); manager.FetchQualifiedSegments(FS_USER_ID); diff --git a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj index fa50e5755..36a800d7e 100644 --- a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj +++ b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj @@ -73,6 +73,7 @@ + @@ -131,6 +132,7 @@ + @@ -151,6 +153,9 @@ + + keypair.snk + diff --git a/OptimizelySDK.Tests/OptimizelyTest.cs b/OptimizelySDK.Tests/OptimizelyTest.cs index 25414234e..cd75f27ab 100644 --- a/OptimizelySDK.Tests/OptimizelyTest.cs +++ b/OptimizelySDK.Tests/OptimizelyTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022, Optimizely + * Copyright 2017-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,29 +14,28 @@ * limitations under the License. */ -using System; -using System.Collections.Generic; using Moq; -using OptimizelySDK.Logger; -using OptimizelySDK.Event.Builder; -using OptimizelySDK.Event.Dispatcher; -using OptimizelySDK.ErrorHandler; -using OptimizelySDK.Exceptions; -using OptimizelySDK.Event; -using OptimizelySDK.Entity; using NUnit.Framework; -using OptimizelySDK.Tests.UtilsTests; using OptimizelySDK.Bucketing; +using OptimizelySDK.Config; +using OptimizelySDK.Entity; +using OptimizelySDK.ErrorHandler; +using OptimizelySDK.Event; +using OptimizelySDK.Event.Dispatcher; +using OptimizelySDK.Event.Entity; +using OptimizelySDK.Exceptions; +using OptimizelySDK.Logger; using OptimizelySDK.Notifications; +using OptimizelySDK.Odp; +using OptimizelySDK.OptimizelyDecisions; using OptimizelySDK.Tests.NotificationTests; +using OptimizelySDK.Tests.Utils; +using OptimizelySDK.Tests.UtilsTests; using OptimizelySDK.Utils; -using OptimizelySDK.Config; -using OptimizelySDK.Event.Entity; -using OptimizelySDK.OptlyConfig; +using System; +using System.Collections.Generic; using System.Globalization; using System.Threading; -using OptimizelySDK.Tests.Utils; -using OptimizelySDK.OptimizelyDecisions; namespace OptimizelySDK.Tests { @@ -90,8 +89,10 @@ public void Initialize() ConfigManager = new FallbackProjectConfigManager(config); Config = ConfigManager.GetConfig(); EventDispatcherMock = new Mock(); - Optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); - OptimizelyWithTypedAudiences = new Optimizely(TestData.TypedAudienceDatafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + Optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, + LoggerMock.Object, ErrorHandlerMock.Object); + OptimizelyWithTypedAudiences = new Optimizely(TestData.TypedAudienceDatafile, + EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); Helper = new OptimizelyHelper { @@ -102,12 +103,14 @@ public void Initialize() SkipJsonValidation = false, }; - OptimizelyMock = new Mock(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object, null, false, null, null) + OptimizelyMock = new Mock(TestData.Datafile, EventDispatcherMock.Object, + LoggerMock.Object, ErrorHandlerMock.Object, null, false, null, null, null) { CallBase = true }; - DecisionServiceMock = new Mock(new Bucketer(LoggerMock.Object), ErrorHandlerMock.Object, + DecisionServiceMock = new Mock(new Bucketer(LoggerMock.Object), + ErrorHandlerMock.Object, null, LoggerMock.Object); NotificationCenter = new NotificationCenter(LoggerMock.Object); @@ -131,37 +134,55 @@ public void Cleanup() private class OptimizelyHelper { - private static Type[] ParameterTypes = { - typeof(string), - typeof(IEventDispatcher), - typeof(ILogger), - typeof(IErrorHandler), - typeof(bool), - typeof(EventProcessor) - }; - - public static Dictionary SingleParameter = new Dictionary + private static Type[] ParameterTypes = { - { "param1", "val1" } + typeof(string), typeof(IEventDispatcher), typeof(ILogger), typeof(IErrorHandler), + typeof(bool), typeof(EventProcessor) }; + public static Dictionary SingleParameter = + new Dictionary + { + { + "param1", "val1" + } + }; + public static UserAttributes UserAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; // NullUserAttributes extends copy of UserAttributes with key-value // pairs containing null values which should not be sent to OPTIMIZELY.COM . public static UserAttributes NullUserAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" }, - { "null_value", null}, - { "wont_be_sent", null}, - { "bad_food", null} + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + }, + { + "null_value", null + }, + { + "wont_be_sent", null + }, + { + "bad_food", null + } }; public string Datafile { get; set; } @@ -174,19 +195,15 @@ private class OptimizelyHelper public OptimizelyDecideOption[] DefaultDecideOptions { get; set; } + public OdpManager OdpManager { get; set; } + public PrivateObject CreatePrivateOptimizely() { return new PrivateObject(typeof(Optimizely), ParameterTypes, new object[] { - Datafile, - EventDispatcher, - Logger, - ErrorHandler, - UserProfileService, - SkipJsonValidation, - EventProcessor, - DefaultDecideOptions + Datafile, EventDispatcher, Logger, ErrorHandler, UserProfileService, + SkipJsonValidation, EventProcessor, DefaultDecideOptions, OdpManager, }); } } @@ -199,10 +216,14 @@ public PrivateObject CreatePrivateOptimizely() public void TestCreateUserContext() { var attribute = new UserAttributes + { { - { "device_type", "iPhone" }, - { "location", "San Francisco" } - }; + "device_type", "iPhone" + }, + { + "location", "San Francisco" + } + }; var optlyUserContext = Optimizely.CreateUserContext(TestUserId, attribute); Assert.AreEqual(TestUserId, optlyUserContext.GetUserId()); Assert.AreEqual(Optimizely, optlyUserContext.GetOptimizely()); @@ -222,17 +243,25 @@ public void TestCreateUserContextWithoutAttributes() public void TestCreateUserContextMultipleAttribute() { var attribute1 = new UserAttributes + { { - { "device_type", "iPhone" }, - { "location", "San Francisco" } - }; + "device_type", "iPhone" + }, + { + "location", "San Francisco" + } + }; var optlyUserContext1 = Optimizely.CreateUserContext("userId1", attribute1); var attribute2 = new UserAttributes + { { - { "device_type2", "Samsung" }, - { "location2", "California" } - }; + "device_type2", "Samsung" + }, + { + "location2", "California" + } + }; var optlyUserContext2 = Optimizely.CreateUserContext("userId2", attribute2); Assert.AreEqual("userId1", optlyUserContext1.GetUserId()); @@ -251,8 +280,12 @@ public void TestDecisionNotificationSentWhenSendFlagDecisionsFalseAndFeature() var variables = Optimizely.GetAllFeatureVariables(featureKey, TestUserId); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "location", "San Francisco" + } }; Config.SendFlagDecisions = false; var fallbackConfigManager = new FallbackProjectConfigManager(Config); @@ -262,26 +295,49 @@ public void TestDecisionNotificationSentWhenSendFlagDecisionsFalseAndFeature() LoggerMock.Object, ErrorHandlerMock.Object, null, - new ForwardingEventProcessor(EventDispatcherMock.Object, NotificationCenter, LoggerMock.Object, ErrorHandlerMock.Object), + new ForwardingEventProcessor(EventDispatcherMock.Object, NotificationCenter, + LoggerMock.Object, ErrorHandlerMock.Object), null); // Mocking objects. - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny>())); - optimizely.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), + It.IsAny(), It.IsAny>())); + optimizely.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); var optimizelyUserContext = optimizely.CreateUserContext(TestUserId, userAttributes); optimizelyUserContext.Decide(featureKey); - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FLAG, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, new Dictionary { - { "flagKey", featureKey }, - { "enabled", false }, - { "variables", variables.ToDictionary() }, - { "variationKey", "group_exp_2_var_1" }, - { "ruleKey", "group_experiment_2" }, - { "reasons", new OptimizelyDecideOption[0] }, - { "decisionEventDispatched", true } - }))), Times.Once); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback( + DecisionNotificationTypes.FLAG, TestUserId, userAttributes, + It.Is>(info => TestData.CompareObjects(info, + new Dictionary + { + { + "flagKey", featureKey + }, + { + "enabled", false + }, + { + "variables", variables.ToDictionary() + }, + { + "variationKey", "group_exp_2_var_1" + }, + { + "ruleKey", "group_experiment_2" + }, + { + "reasons", new OptimizelyDecideOption[0] + }, + { + "decisionEventDispatched", true + } + }))), Times.Once); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Once); } [Test] @@ -291,8 +347,12 @@ public void TestDecisionNotificationSentWhenSendFlagDecisionsTrueAndFeature() var variables = Optimizely.GetAllFeatureVariables(featureKey, TestUserId); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "location", "San Francisco" + } }; var fallbackConfigManager = new FallbackProjectConfigManager(Config); var optimizely = new Optimizely(fallbackConfigManager, @@ -301,26 +361,49 @@ public void TestDecisionNotificationSentWhenSendFlagDecisionsTrueAndFeature() LoggerMock.Object, ErrorHandlerMock.Object, null, - new ForwardingEventProcessor(EventDispatcherMock.Object, NotificationCenter, LoggerMock.Object, ErrorHandlerMock.Object), + new ForwardingEventProcessor(EventDispatcherMock.Object, NotificationCenter, + LoggerMock.Object, ErrorHandlerMock.Object), null); // Mocking objects. - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny>())); - optimizely.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), + It.IsAny(), It.IsAny>())); + optimizely.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); var optimizelyUserContext = optimizely.CreateUserContext(TestUserId, userAttributes); optimizelyUserContext.Decide(featureKey); - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FLAG, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, new Dictionary { - { "flagKey", featureKey }, - { "enabled", false }, - { "variables", variables.ToDictionary() }, - { "variationKey", "group_exp_2_var_1" }, - { "ruleKey", "group_experiment_2" }, - { "reasons", new OptimizelyDecideOption[0] }, - { "decisionEventDispatched", true } - }))), Times.Once); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback( + DecisionNotificationTypes.FLAG, TestUserId, userAttributes, + It.Is>(info => TestData.CompareObjects(info, + new Dictionary + { + { + "flagKey", featureKey + }, + { + "enabled", false + }, + { + "variables", variables.ToDictionary() + }, + { + "variationKey", "group_exp_2_var_1" + }, + { + "ruleKey", "group_experiment_2" + }, + { + "reasons", new OptimizelyDecideOption[0] + }, + { + "decisionEventDispatched", true + } + }))), Times.Once); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Once); } [Test] @@ -331,8 +414,12 @@ public void TestDecisionNotificationNotSentWhenSendFlagDecisionsFalseAndRollout( var variables = Optimizely.GetAllFeatureVariables(featureKey, TestUserId); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "location", "San Francisco" + } }; var experiment = Config.GetRolloutFromId("166660").Experiments[1]; var ruleKey = experiment.Key; @@ -345,26 +432,49 @@ public void TestDecisionNotificationNotSentWhenSendFlagDecisionsFalseAndRollout( LoggerMock.Object, ErrorHandlerMock.Object, null, - new ForwardingEventProcessor(EventDispatcherMock.Object, NotificationCenter, LoggerMock.Object, ErrorHandlerMock.Object), + new ForwardingEventProcessor(EventDispatcherMock.Object, NotificationCenter, + LoggerMock.Object, ErrorHandlerMock.Object), null); // Mocking objects. - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny>())); - optimizely.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), + It.IsAny(), It.IsAny>())); + optimizely.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); var optimizelyUserContext = optimizely.CreateUserContext(TestUserId, userAttributes); optimizelyUserContext.Decide(featureKey); - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FLAG, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, new Dictionary { - { "flagKey", featureKey }, - { "enabled", true }, - { "variables", variables.ToDictionary() }, - { "variationKey", variation.Key }, - { "ruleKey", ruleKey }, - { "reasons", new OptimizelyDecideOption[0] }, - { "decisionEventDispatched", false } - }))), Times.Once); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Never); + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback( + DecisionNotificationTypes.FLAG, TestUserId, userAttributes, + It.Is>(info => TestData.CompareObjects(info, + new Dictionary + { + { + "flagKey", featureKey + }, + { + "enabled", true + }, + { + "variables", variables.ToDictionary() + }, + { + "variationKey", variation.Key + }, + { + "ruleKey", ruleKey + }, + { + "reasons", new OptimizelyDecideOption[0] + }, + { + "decisionEventDispatched", false + } + }))), Times.Once); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Never); } [Test] @@ -375,8 +485,12 @@ public void TestDecisionNotificationSentWhenSendFlagDecisionsTrueAndRollout() var variables = Optimizely.GetAllFeatureVariables(featureKey, TestUserId); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "location", "San Francisco" + } }; var experiment = Config.GetRolloutFromId("166660").Experiments[1]; var ruleKey = experiment.Key; @@ -389,26 +503,49 @@ public void TestDecisionNotificationSentWhenSendFlagDecisionsTrueAndRollout() LoggerMock.Object, ErrorHandlerMock.Object, null, - new ForwardingEventProcessor(EventDispatcherMock.Object, NotificationCenter, LoggerMock.Object, ErrorHandlerMock.Object), + new ForwardingEventProcessor(EventDispatcherMock.Object, NotificationCenter, + LoggerMock.Object, ErrorHandlerMock.Object), null); // Mocking objects. - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny>())); - optimizely.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), + It.IsAny(), It.IsAny>())); + optimizely.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); var optimizelyUserContext = optimizely.CreateUserContext(TestUserId, userAttributes); optimizelyUserContext.Decide(featureKey); - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FLAG, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, new Dictionary { - { "flagKey", featureKey }, - { "enabled", true }, - { "variables", variables.ToDictionary() }, - { "variationKey", variation.Key }, - { "ruleKey", ruleKey }, - { "reasons", new OptimizelyDecideOption[0] }, - { "decisionEventDispatched", true } - }))), Times.Once); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback( + DecisionNotificationTypes.FLAG, TestUserId, userAttributes, + It.Is>(info => TestData.CompareObjects(info, + new Dictionary + { + { + "flagKey", featureKey + }, + { + "enabled", true + }, + { + "variables", variables.ToDictionary() + }, + { + "variationKey", variation.Key + }, + { + "ruleKey", ruleKey + }, + { + "reasons", new OptimizelyDecideOption[0] + }, + { + "decisionEventDispatched", true + } + }))), Times.Once); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Once); } [Test] @@ -416,21 +553,31 @@ public void TestChangeAttributeDoesNotEffectValues() { var userId = "testUserId"; var attribute = new UserAttributes + { { - { "device_type", "iPhone" }, - { "location", "San Francisco" } - }; + "device_type", "iPhone" + }, + { + "location", "San Francisco" + } + }; var optlyUserContext = Optimizely.CreateUserContext(userId, attribute); Assert.AreEqual(TestUserId, optlyUserContext.GetUserId()); Assert.AreEqual(Optimizely, optlyUserContext.GetOptimizely()); Assert.AreEqual(attribute, optlyUserContext.GetAttributes()); attribute = new UserAttributes + { { - { "device_type", "iPhone" }, - { "level", "low" }, - { "location", "San Francisco" } - }; + "device_type", "iPhone" + }, + { + "level", "low" + }, + { + "location", "San Francisco" + } + }; Assert.AreEqual("testUserId", optlyUserContext.GetUserId()); Assert.AreEqual(Optimizely, optlyUserContext.GetOptimizely()); Assert.AreNotEqual(attribute, optlyUserContext.GetAttributes()); @@ -451,23 +598,54 @@ public void TestInvalidInstanceLogMessages() optimizely.Track(string.Empty, string.Empty); Assert.IsFalse(optimizely.IsFeatureEnabled(string.Empty, string.Empty)); Assert.AreEqual(optimizely.GetEnabledFeatures(string.Empty).Count, 0); - Assert.IsNull(optimizely.GetFeatureVariableBoolean(string.Empty, string.Empty, string.Empty)); - Assert.IsNull(optimizely.GetFeatureVariableString(string.Empty, string.Empty, string.Empty)); - Assert.IsNull(optimizely.GetFeatureVariableDouble(string.Empty, string.Empty, string.Empty)); - Assert.IsNull(optimizely.GetFeatureVariableInteger(string.Empty, string.Empty, string.Empty)); - Assert.IsNull(optimizely.GetFeatureVariableJSON(string.Empty, string.Empty, string.Empty)); - - LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Provided 'datafile' has invalid schema."), Times.Once); - LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetVariation'."), Times.Once); - LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'Activate'."), Times.Once); - LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'Track'."), Times.Once); - LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'IsFeatureEnabled'."), Times.Once); - LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetEnabledFeatures'."), Times.Once); - LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetFeatureVariableBoolean'."), Times.Once); - LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetFeatureVariableString'."), Times.Once); - LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetFeatureVariableDouble'."), Times.Once); - LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetFeatureVariableInteger'."), Times.Once); - LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetFeatureVariableJSON'."), Times.Once); + Assert.IsNull( + optimizely.GetFeatureVariableBoolean(string.Empty, string.Empty, string.Empty)); + Assert.IsNull( + optimizely.GetFeatureVariableString(string.Empty, string.Empty, string.Empty)); + Assert.IsNull( + optimizely.GetFeatureVariableDouble(string.Empty, string.Empty, string.Empty)); + Assert.IsNull( + optimizely.GetFeatureVariableInteger(string.Empty, string.Empty, string.Empty)); + Assert.IsNull( + optimizely.GetFeatureVariableJSON(string.Empty, string.Empty, string.Empty)); + + LoggerMock.Verify( + log => log.Log(LogLevel.ERROR, "Provided 'datafile' has invalid schema."), + Times.Once); + LoggerMock.Verify( + log => log.Log(LogLevel.ERROR, + "Datafile has invalid format. Failing 'GetVariation'."), Times.Once); + LoggerMock.Verify( + log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'Activate'."), + Times.Once); + LoggerMock.Verify( + log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'Track'."), + Times.Once); + LoggerMock.Verify( + log => log.Log(LogLevel.ERROR, + "Datafile has invalid format. Failing 'IsFeatureEnabled'."), Times.Once); + LoggerMock.Verify( + log => log.Log(LogLevel.ERROR, + "Datafile has invalid format. Failing 'GetEnabledFeatures'."), Times.Once); + LoggerMock.Verify( + log => log.Log(LogLevel.ERROR, + "Datafile has invalid format. Failing 'GetFeatureVariableBoolean'."), + Times.Once); + LoggerMock.Verify( + log => log.Log(LogLevel.ERROR, + "Datafile has invalid format. Failing 'GetFeatureVariableString'."), + Times.Once); + LoggerMock.Verify( + log => log.Log(LogLevel.ERROR, + "Datafile has invalid format. Failing 'GetFeatureVariableDouble'."), + Times.Once); + LoggerMock.Verify( + log => log.Log(LogLevel.ERROR, + "Datafile has invalid format. Failing 'GetFeatureVariableInteger'."), + Times.Once); + LoggerMock.Verify( + log => log.Log(LogLevel.ERROR, + "Datafile has invalid format. Failing 'GetFeatureVariableJSON'."), Times.Once); } [Test] @@ -482,32 +660,49 @@ public void TestValidateInputsInvalidFileJsonValidationNotSkipped() public void TestValidateInputsInvalidFileJsonValidationSkipped() { string datafile = "{\"name\":\"optimizely\"}"; - Optimizely optimizely = new Optimizely(datafile, null, null, null, skipJsonValidation: true); + Optimizely optimizely = + new Optimizely(datafile, null, null, null, skipJsonValidation: true); Assert.IsFalse(optimizely.IsValid); } [Test] public void TestErrorHandlingWithNullDatafile() { - var optimizelyNullDatafile = new Optimizely(null, null, LoggerMock.Object, ErrorHandlerMock.Object, null, true); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Unable to parse null datafile."), Times.Once); - ErrorHandlerMock.Verify(e => e.HandleError(It.Is(ex => ex.Message == "Unable to parse null datafile.")), Times.Once); + var optimizelyNullDatafile = new Optimizely(null, null, LoggerMock.Object, + ErrorHandlerMock.Object, null, true); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Unable to parse null datafile."), + Times.Once); + ErrorHandlerMock.Verify( + e => e.HandleError(It.Is(ex => + ex.Message == "Unable to parse null datafile.")), Times.Once); } [Test] public void TestErrorHandlingWithEmptyDatafile() { - var optimizelyEmptyDatafile = new Optimizely("", null, LoggerMock.Object, ErrorHandlerMock.Object, null, true); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Unable to parse empty datafile."), Times.Once); - ErrorHandlerMock.Verify(e => e.HandleError(It.Is(ex => ex.Message == "Unable to parse empty datafile.")), Times.Once); + var optimizelyEmptyDatafile = new Optimizely("", null, LoggerMock.Object, + ErrorHandlerMock.Object, null, true); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Unable to parse empty datafile."), + Times.Once); + ErrorHandlerMock.Verify( + e => e.HandleError(It.Is(ex => + ex.Message == "Unable to parse empty datafile.")), Times.Once); } [Test] public void TestErrorHandlingWithUnsupportedConfigVersion() { - var optimizelyUnsupportedVersion = new Optimizely(TestData.UnsupportedVersionDatafile, null, LoggerMock.Object, ErrorHandlerMock.Object, null, true); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, $"This version of the C# SDK does not support the given datafile version: 5"), Times.Once); - ErrorHandlerMock.Verify(e => e.HandleError(It.Is(ex => ex.Message == $"This version of the C# SDK does not support the given datafile version: 5")), Times.Once); + var optimizelyUnsupportedVersion = new Optimizely(TestData.UnsupportedVersionDatafile, + null, LoggerMock.Object, ErrorHandlerMock.Object, null, true); + LoggerMock.Verify( + l => l.Log(LogLevel.ERROR, + $"This version of the C# SDK does not support the given datafile version: 5"), + Times.Once); + ErrorHandlerMock.Verify( + e => e.HandleError(It.Is(ex => + ex.Message == + $"This version of the C# SDK does not support the given datafile version: 5")), + Times.Once); } [Test] @@ -515,16 +710,31 @@ public void TestValidatePreconditionsUserNotInForcedVariationInExperiment() { var attributes = new UserAttributes { - { "device_type", "iPhone" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "location", "San Francisco" + } }; var variation = Optimizely.GetVariation("test_experiment", "test_user", attributes); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"test_user\" is not in the forced variation map."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [test_user] is in variation [control] of experiment [test_experiment]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "This decision will not be saved since the UserProfileService is null."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "User \"test_user\" is not in the forced variation map."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "User [test_user] is in variation [control] of experiment [test_experiment]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "This decision will not be saved since the UserProfileService is null."), + Times.Once); Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, variation)); } @@ -533,8 +743,8 @@ public void TestValidatePreconditionsUserNotInForcedVariationInExperiment() public void TestActivateInvalidOptimizelyObject() { var optly = new Optimizely("Random datafile", null, LoggerMock.Object); - LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided 'datafile' has invalid schema."), Times.Once); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided 'datafile' has invalid schema."), + Times.Once); } //attributes can not be invalid beacuse now it is strongly typed. @@ -560,11 +770,19 @@ public void TestActivateUserInNoVariation() { var optly = Helper.CreatePrivateOptimizely(); - var result = optly.Invoke("Activate", "test_experiment", "not_in_variation_user", OptimizelyHelper.UserAttributes); + var result = optly.Invoke("Activate", "test_experiment", "not_in_variation_user", + OptimizelyHelper.UserAttributes); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [8495] to user [not_in_variation_user] with bucketing ID [not_in_variation_user]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [not_in_variation_user] is in no variation."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Not activating user not_in_variation_user."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "Assigned bucket [8495] to user [not_in_variation_user] with bucketing ID [not_in_variation_user]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, "User [not_in_variation_user] is in no variation."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, "Not activating user not_in_variation_user."), + Times.Once); Assert.IsNull(result); } @@ -574,22 +792,41 @@ public void TestActivateNoAudienceNoAttributes() { var parameters = new Dictionary { - { "param1", "val1" }, - { "param2", "val2" } + { + "param1", "val1" + }, + { + "param2", "val2" + } }; var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("EventProcessor", EventProcessorMock.Object); - var variation = (Variation)optly.Invoke("Activate", "group_experiment_1", "user_1", null); + var variation = + (Variation)optly.Invoke("Activate", "group_experiment_1", "user_1", null); EventProcessorMock.Verify(ep => ep.Process(It.IsAny()), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1922] to user [user_1] with bucketing ID [user_1]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [user_1] is in experiment [group_experiment_1] of group [7722400015]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [9525] to user [user_1] with bucketing ID [user_1]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [user_1] is in variation [group_exp_1_var_2] of experiment [group_experiment_1]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user user_1 in experiment group_experiment_1."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "Assigned bucket [1922] to user [user_1] with bucketing ID [user_1]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "User [user_1] is in experiment [group_experiment_1] of group [7722400015]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "Assigned bucket [9525] to user [user_1] with bucketing ID [user_1]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "User [user_1] is in variation [group_exp_1_var_2] of experiment [group_experiment_1]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "Activating user user_1 in experiment group_experiment_1."), Times.Once); Assert.IsTrue(TestData.CompareObjects(GroupVariation, variation)); } @@ -605,8 +842,12 @@ public void TestActivateAudienceNoAttributes() var variationkey = optly.Invoke("Activate", "test_experiment", "test_user", null); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User \"test_user\" does not meet conditions to be in experiment \"test_experiment\"."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Not activating user test_user."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "User \"test_user\" does not meet conditions to be in experiment \"test_experiment\"."), + Times.Once); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Not activating user test_user."), + Times.Once); Assert.IsNull(variationkey); } @@ -617,15 +858,27 @@ public void TestActivateWithAttributes() var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("EventProcessor", EventProcessorMock.Object); - var variation = (Variation)optly.Invoke("Activate", "test_experiment", "test_user", OptimizelyHelper.UserAttributes); + var variation = (Variation)optly.Invoke("Activate", "test_experiment", "test_user", + OptimizelyHelper.UserAttributes); EventProcessorMock.Verify(ep => ep.Process(It.IsAny()), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"test_user\" is not in the forced variation map."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [test_user] is in variation [control] of experiment [test_experiment]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "This decision will not be saved since the UserProfileService is null.")); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user test_user in experiment test_experiment."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "User \"test_user\" is not in the forced variation map."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "User [test_user] is in variation [control] of experiment [test_experiment]."), + Times.Once); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, + "This decision will not be saved since the UserProfileService is null.")); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "Activating user test_user in experiment test_experiment."), Times.Once); Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, variation)); } @@ -636,15 +889,26 @@ public void TestActivateWithNullAttributes() var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("EventProcessor", EventProcessorMock.Object); - var variation = (Variation)optly.Invoke("Activate", "test_experiment", "test_user", OptimizelyHelper.NullUserAttributes); + var variation = (Variation)optly.Invoke("Activate", "test_experiment", "test_user", + OptimizelyHelper.NullUserAttributes); EventProcessorMock.Verify(ep => ep.Process(It.IsAny()), Times.Once); //"User "test_user" is not in the forced variation map." - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"test_user\" is not in the forced variation map."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [test_user] is in variation [control] of experiment [test_experiment]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user test_user in experiment test_experiment."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "User \"test_user\" is not in the forced variation map."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "User [test_user] is in variation [control] of experiment [test_experiment]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "Activating user test_user in experiment test_experiment."), Times.Once); Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, variation)); } @@ -656,9 +920,11 @@ public void TestActivateExperimentNotRunning() var variationkey = optly.Invoke("Activate", "paused_experiment", "test_user", null); - LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(2)); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Experiment \"paused_experiment\" is not running."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Not activating user test_user."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, "Experiment \"paused_experiment\" is not running."), + Times.Once); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Not activating user test_user."), + Times.Once); Assert.IsNull(variationkey); } @@ -668,23 +934,42 @@ public void TestActivateWithTypedAttributes() { var userAttributes = new UserAttributes { - {"device_type", "iPhone" }, - {"location", "San Francisco" }, - {"boolean_key", true }, - {"integer_key", 15 }, - {"double_key", 3.14 } + { + "device_type", "iPhone" + }, + { + "location", "San Francisco" + }, + { + "boolean_key", true + }, + { + "integer_key", 15 + }, + { + "double_key", 3.14 + } }; var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("EventProcessor", EventProcessorMock.Object); - var variation = (Variation)optly.Invoke("Activate", "test_experiment", "test_user", userAttributes); + var variation = (Variation)optly.Invoke("Activate", "test_experiment", "test_user", + userAttributes); EventProcessorMock.Verify(ep => ep.Process(It.IsAny()), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [test_user] is in variation [control] of experiment [test_experiment]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user test_user in experiment test_experiment."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "User [test_user] is in variation [control] of experiment [test_experiment]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "Activating user test_user in experiment test_experiment."), Times.Once); Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, variation)); } @@ -698,8 +983,9 @@ public void TestGetVariationInvalidOptimizelyObject() { var optly = new Optimizely("Random datafile", null, LoggerMock.Object); var variationkey = optly.Activate("some_experiment", "some_user"); - LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(2)); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'Activate'."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'Activate'."), + Times.Once); //Assert.IsNull(variationkey); } @@ -724,15 +1010,28 @@ public void TestGetVariationAudienceMatch() { var attributes = new UserAttributes { - { "device_type", "iPhone" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "location", "San Francisco" + } }; var variation = Optimizely.GetVariation("test_experiment", "test_user", attributes); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [test_user] is in variation [control] of experiment [test_experiment]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "This decision will not be saved since the UserProfileService is null."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "User [test_user] is in variation [control] of experiment [test_experiment]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "This decision will not be saved since the UserProfileService is null."), + Times.Once); Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, variation)); } @@ -741,7 +1040,10 @@ public void TestGetVariationAudienceMatch() public void TestGetVariationAudienceNoMatch() { var variation = Optimizely.Activate("test_experiment", "test_user"); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User \"test_user\" does not meet conditions to be in experiment \"test_experiment\"."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "User \"test_user\" does not meet conditions to be in experiment \"test_experiment\"."), + Times.Once); Assert.IsNull(variation); } @@ -749,7 +1051,9 @@ public void TestGetVariationAudienceNoMatch() public void TestGetVariationExperimentNotRunning() { var variation = Optimizely.Activate("paused_experiment", "test_user"); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Experiment \"paused_experiment\" is not running."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, "Experiment \"paused_experiment\" is not running."), + Times.Once); Assert.IsNull(variation); } @@ -758,8 +1062,9 @@ public void TestTrackInvalidOptimizelyObject() { var optly = new Optimizely("Random datafile", null, LoggerMock.Object); optly.Track("some_event", "some_user"); - LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(2)); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'Track'."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'Track'."), + Times.Once); } #endregion Test GetVariation @@ -771,12 +1076,16 @@ public void TestTrackInvalidAttributes() { var attributes = new UserAttributes { - { "abc", "43" } + { + "abc", "43" + } }; Optimizely.Track("purchase", TestUserId, attributes); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, @"Attribute key ""abc"" is not in datafile."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.ERROR, @"Attribute key ""abc"" is not in datafile."), + Times.Once); } [Test] @@ -786,9 +1095,12 @@ public void TestTrackNoAttributesNoEventValue() optly.SetFieldOrProperty("EventProcessor", EventProcessorMock.Object); optly.Invoke("Track", "purchase", "test_user", null, null); - EventProcessorMock.Verify(processor => processor.Process(It.IsAny()), Times.Once); + EventProcessorMock.Verify(processor => processor.Process(It.IsAny()), + Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Tracking event purchase for user test_user."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, "Tracking event purchase for user test_user."), + Times.Once); } [Test] @@ -800,7 +1112,9 @@ public void TestTrackWithAttributesNoEventValue() optly.Invoke("Track", "purchase", "test_user", OptimizelyHelper.UserAttributes, null); EventProcessorMock.Verify(ep => ep.Process(It.IsAny()), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Tracking event purchase for user test_user."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, "Tracking event purchase for user test_user."), + Times.Once); } [Test] @@ -808,8 +1122,12 @@ public void TestTrackUnknownEventKey() { Optimizely.Track("unknown_event", "test_user"); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Event key \"unknown_event\" is not in datafile."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Not tracking user test_user for event unknown_event."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.ERROR, "Event key \"unknown_event\" is not in datafile."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, "Not tracking user test_user for event unknown_event."), + Times.Once); } [Test] @@ -820,11 +1138,15 @@ public void TestTrackNoAttributesWithEventValue() optly.Invoke("Track", "purchase", "test_user", null, new EventTags { - { "revenue", 42 } + { + "revenue", 42 + } }); EventProcessorMock.Verify(ep => ep.Process(It.IsAny()), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Tracking event purchase for user test_user."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, "Tracking event purchase for user test_user."), + Times.Once); } [Test] @@ -832,8 +1154,12 @@ public void TestTrackWithAttributesWithEventValue() { var attributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, }; var optly = Helper.CreatePrivateOptimizely(); @@ -841,12 +1167,16 @@ public void TestTrackWithAttributesWithEventValue() optly.Invoke("Track", "purchase", "test_user", attributes, new EventTags { - { "revenue", 42 } + { + "revenue", 42 + } }); EventProcessorMock.Verify(ep => ep.Process(It.IsAny()), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Tracking event purchase for user test_user."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, "Tracking event purchase for user test_user."), + Times.Once); } [Test] @@ -855,16 +1185,23 @@ public void TestTrackWithNullAttributesWithNullEventValue() var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("EventProcessor", EventProcessorMock.Object); - optly.Invoke("Track", "purchase", "test_user", OptimizelyHelper.NullUserAttributes, new EventTags - { - { "revenue", 42 }, - { "wont_send_null", null} - }); + optly.Invoke("Track", "purchase", "test_user", OptimizelyHelper.NullUserAttributes, + new EventTags + { + { + "revenue", 42 + }, + { + "wont_send_null", null + } + }); EventProcessorMock.Verify(ep => ep.Process(It.IsAny()), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "[EventTags] Null value for key wont_send_null removed and will not be sent to results.")); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Tracking event purchase for user test_user.")); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, + "[EventTags] Null value for key wont_send_null removed and will not be sent to results.")); + LoggerMock.Verify(l => + l.Log(LogLevel.INFO, "Tracking event purchase for user test_user.")); } #endregion Test Track @@ -877,16 +1214,26 @@ public void TestInvalidDispatchImpressionEvent() var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("EventDispatcher", new InvalidEventDispatcher()); - var variation = (Variation)optly.Invoke("Activate", "test_experiment", "test_user", OptimizelyHelper.UserAttributes); + var variation = (Variation)optly.Invoke("Activate", "test_experiment", "test_user", + OptimizelyHelper.UserAttributes); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [test_user] is in variation [control] of experiment [test_experiment]."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "User [test_user] is in variation [control] of experiment [test_experiment]."), + Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user test_user in experiment test_experiment."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "Activating user test_user in experiment test_experiment."), Times.Once); // Need to see how error handler can be verified. - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, It.IsAny()), Times.Once); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Attribute key \"company\" is not in datafile."), Times.Once); Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, variation)); } @@ -899,9 +1246,12 @@ public void TestInvalidDispatchConversionEvent() optly.Invoke("Track", "purchase", "test_user", null, null); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Tracking event purchase for user test_user."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, "Tracking event purchase for user test_user."), + Times.Once); } #endregion Test Invalid Dispatch @@ -917,7 +1267,9 @@ public void TestTrackNoAttributesWithInvalidEventValue() optly.Invoke("Track", "purchase", "test_user", null, new Dictionary { - {"revenue", 4200 } + { + "revenue", 4200 + } }); } @@ -930,15 +1282,19 @@ public void TestTrackNoAttributesWithDeprecatedEventValue() optly.SetFieldOrProperty("EventDispatcher", new ValidEventDispatcher()); optly.Invoke("Track", "purchase", "test_user", null, new Dictionary { - {"revenue", 42 } + { + "revenue", 42 + } }); } [Test] public void TestForcedVariationPreceedsWhitelistedVariation() { - var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); - var projectConfig = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, ErrorHandlerMock.Object); + var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, + LoggerMock.Object, ErrorHandlerMock.Object); + var projectConfig = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, + ErrorHandlerMock.Object); Variation expectedVariation1 = projectConfig.GetVariationFromKey("etag3", "vtag5"); Variation expectedVariation2 = projectConfig.GetVariationFromKey("etag3", "vtag6"); @@ -959,13 +1315,28 @@ public void TestForcedVariationPreceedsWhitelistedVariation() Assert.IsTrue(TestData.CompareObjects(expectedVariation1, variation)); - LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(7)); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"testUser1\" is not in the forced variation map."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User \"testUser1\" is forced in variation \"vtag5\"."), Times.Exactly(2)); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Set variation \"281\" for experiment \"224\" and user \"testUser1\" in the forced variation map."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Variation \"vtag6\" is mapped to experiment \"etag3\" and user \"testUser1\" in the forced variation map"), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Variation mapped to experiment \"etag3\" has been removed for user \"testUser1\"."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "No experiment \"etag3\" mapped to user \"testUser1\" in the forced variation map."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "User \"testUser1\" is not in the forced variation map."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, "User \"testUser1\" is forced in variation \"vtag5\"."), + Times.Exactly(2)); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "Set variation \"281\" for experiment \"224\" and user \"testUser1\" in the forced variation map."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "Variation \"vtag6\" is mapped to experiment \"etag3\" and user \"testUser1\" in the forced variation map"), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "Variation mapped to experiment \"etag3\" has been removed for user \"testUser1\"."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "No experiment \"etag3\" mapped to user \"testUser1\" in the forced variation map."), + Times.Once); } [Test] @@ -977,17 +1348,24 @@ public void TestForcedVariationPreceedsUserProfile() var variationKey = "vtag2"; var fbVariationKey = "vtag1"; - UserProfile userProfile = new UserProfile(userId, new Dictionary - { - { experimentKey, new Bucketing.Decision(variationKey)} - }); + UserProfile userProfile = new UserProfile(userId, + new Dictionary + { + { + experimentKey, new Bucketing.Decision(variationKey) + } + }); userProfileServiceMock.Setup(_ => _.Lookup(userId)).Returns(userProfile.ToMap()); - var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object, userProfileServiceMock.Object); - var projectConfig = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, ErrorHandlerMock.Object); - Variation expectedFbVariation = projectConfig.GetVariationFromKey(experimentKey, fbVariationKey); - Variation expectedVariation = projectConfig.GetVariationFromKey(experimentKey, variationKey); + var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, + LoggerMock.Object, ErrorHandlerMock.Object, userProfileServiceMock.Object); + var projectConfig = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, + ErrorHandlerMock.Object); + Variation expectedFbVariation = + projectConfig.GetVariationFromKey(experimentKey, fbVariationKey); + Variation expectedVariation = + projectConfig.GetVariationFromKey(experimentKey, variationKey); var variationUserProfile = optimizely.GetVariation(experimentKey, userId); Assert.IsTrue(TestData.CompareObjects(expectedVariation, variationUserProfile)); @@ -1004,16 +1382,43 @@ public void TestForcedVariationPreceedsUserProfile() variationUserProfile = optimizely.GetVariation(experimentKey, userId); Assert.IsTrue(TestData.CompareObjects(expectedVariation, variationUserProfile)); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"testUser3\" is not in the forced variation map."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "No previously activated variation of experiment \"etag1\" for user \"testUser3\" found in user profile."), Times.Exactly(2)); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [4969] to user [testUser3] with bucketing ID [testUser3]."), Times.Exactly(2)); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUser3] is in variation [vtag2] of experiment [etag1]."), Times.Exactly(2)); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Saved variation \"277\" of experiment \"223\" for user \"testUser3\"."), Times.Exactly(2)); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Set variation \"276\" for experiment \"223\" and user \"testUser3\" in the forced variation map."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Variation mapped to experiment \"etag1\" has been removed for user \"testUser3\"."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "No experiment \"etag1\" mapped to user \"testUser3\" in the forced variation map."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"testUser3\" is not in the forced variation map."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"testUser3\" is not in the forced variation map."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "User \"testUser3\" is not in the forced variation map."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "No previously activated variation of experiment \"etag1\" for user \"testUser3\" found in user profile."), + Times.Exactly(2)); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "Assigned bucket [4969] to user [testUser3] with bucketing ID [testUser3]."), + Times.Exactly(2)); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "User [testUser3] is in variation [vtag2] of experiment [etag1]."), + Times.Exactly(2)); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "Saved variation \"277\" of experiment \"223\" for user \"testUser3\"."), + Times.Exactly(2)); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "Set variation \"276\" for experiment \"223\" and user \"testUser3\" in the forced variation map."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "Variation mapped to experiment \"etag1\" has been removed for user \"testUser3\"."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "No experiment \"etag1\" mapped to user \"testUser3\" in the forced variation map."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "User \"testUser3\" is not in the forced variation map."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "User \"testUser3\" is not in the forced variation map."), Times.Once); } // check that a null variation key clears the forced variation @@ -1025,23 +1430,36 @@ public void TestSetForcedVariationNullVariation() var userAttributes = new UserAttributes { - {"device_type", "iPhone" }, - {"location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "location", "San Francisco" + } }; Optimizely.Activate(experimentKey, TestUserId, userAttributes); // set variation - Assert.IsTrue(Optimizely.SetForcedVariation(experimentKey, TestUserId, expectedForcedVariationKey), "Set forced variation to variation failed."); + Assert.IsTrue( + Optimizely.SetForcedVariation(experimentKey, TestUserId, + expectedForcedVariationKey), "Set forced variation to variation failed."); - var actualForcedVariation = Optimizely.GetVariation(experimentKey, TestUserId, userAttributes); - Assert.IsTrue(TestData.CompareObjects(VariationWithKeyVariation, actualForcedVariation), string.Format(@"Forced variation key should be variation, but got ""{0}"".", actualForcedVariation?.Key)); + var actualForcedVariation = + Optimizely.GetVariation(experimentKey, TestUserId, userAttributes); + Assert.IsTrue(TestData.CompareObjects(VariationWithKeyVariation, actualForcedVariation), + string.Format(@"Forced variation key should be variation, but got ""{0}"".", + actualForcedVariation?.Key)); // clear variation and check that the user gets bucketed normally - Assert.IsTrue(Optimizely.SetForcedVariation(experimentKey, TestUserId, null), "Clear forced variation failed."); + Assert.IsTrue(Optimizely.SetForcedVariation(experimentKey, TestUserId, null), + "Clear forced variation failed."); - var actualVariation = Optimizely.GetVariation("test_experiment", "test_user", userAttributes); - Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, actualVariation), string.Format(@"Variation key should be control, but got ""{0}"".", actualVariation?.Key)); + var actualVariation = + Optimizely.GetVariation("test_experiment", "test_user", userAttributes); + Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, actualVariation), + string.Format(@"Variation key should be control, but got ""{0}"".", + actualVariation?.Key)); } // check that the forced variation is set correctly @@ -1053,35 +1471,51 @@ public void TestSetForcedVariation() var userAttributes = new UserAttributes { - {"device_type", "iPhone" }, - {"location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "location", "San Francisco" + } }; Optimizely.Activate(experimentKey, TestUserId, userAttributes); // test invalid experiment -. normal bucketing should occur - Assert.IsFalse(Optimizely.SetForcedVariation("bad_experiment", TestUserId, "bad_control"), "Set variation to 'variation' should have failed because of invalid experiment."); + Assert.IsFalse( + Optimizely.SetForcedVariation("bad_experiment", TestUserId, "bad_control"), + "Set variation to 'variation' should have failed because of invalid experiment."); var variation = Optimizely.GetVariation(experimentKey, TestUserId, userAttributes); Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, variation)); // test invalid variation -. normal bucketing should occur - Assert.IsFalse(Optimizely.SetForcedVariation("test_experiment", TestUserId, "bad_variation"), "Set variation to 'bad_variation' should have failed."); + Assert.IsFalse( + Optimizely.SetForcedVariation("test_experiment", TestUserId, "bad_variation"), + "Set variation to 'bad_variation' should have failed."); variation = Optimizely.GetVariation("test_experiment", "test_user", userAttributes); Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, variation)); // test valid variation -. the user should be bucketed to the specified forced variation - Assert.IsTrue(Optimizely.SetForcedVariation(experimentKey, TestUserId, expectedForcedVariationKey), "Set variation to 'variation' failed."); + Assert.IsTrue( + Optimizely.SetForcedVariation(experimentKey, TestUserId, + expectedForcedVariationKey), "Set variation to 'variation' failed."); - var actualForcedVariation = Optimizely.GetVariation(experimentKey, TestUserId, userAttributes); - Assert.IsTrue(TestData.CompareObjects(VariationWithKeyVariation, actualForcedVariation)); + var actualForcedVariation = + Optimizely.GetVariation(experimentKey, TestUserId, userAttributes); + Assert.IsTrue(TestData.CompareObjects(VariationWithKeyVariation, + actualForcedVariation)); // make sure another setForcedVariation call sets a new forced variation correctly - Assert.IsTrue(Optimizely.SetForcedVariation(experimentKey, "test_user2", expectedForcedVariationKey), "Set variation to 'variation' failed."); - actualForcedVariation = Optimizely.GetVariation(experimentKey, "test_user2", userAttributes); + Assert.IsTrue( + Optimizely.SetForcedVariation(experimentKey, "test_user2", + expectedForcedVariationKey), "Set variation to 'variation' failed."); + actualForcedVariation = + Optimizely.GetVariation(experimentKey, "test_user2", userAttributes); - Assert.IsTrue(TestData.CompareObjects(VariationWithKeyVariation, actualForcedVariation)); + Assert.IsTrue(TestData.CompareObjects(VariationWithKeyVariation, + actualForcedVariation)); } [Test] @@ -1097,7 +1531,8 @@ public void TestSetForcedVariationWithInvalidExperimentKey() var userId = "test_user"; var variation = "variation"; - Assert.False(Optimizely.SetForcedVariation("test_experiment_not_in_datafile", userId, variation)); + Assert.False(Optimizely.SetForcedVariation("test_experiment_not_in_datafile", userId, + variation)); Assert.False(Optimizely.SetForcedVariation("", userId, variation)); Assert.False(Optimizely.SetForcedVariation(null, userId, variation)); } @@ -1108,7 +1543,8 @@ public void TestSetForcedVariationWithInvalidVariationKey() var userId = "test_user"; var experimentKey = "test_experiment"; - Assert.False(Optimizely.SetForcedVariation(experimentKey, userId, "variation_not_in_datafile")); + Assert.False(Optimizely.SetForcedVariation(experimentKey, userId, + "variation_not_in_datafile")); Assert.False(Optimizely.SetForcedVariation(experimentKey, userId, "")); } @@ -1117,20 +1553,35 @@ public void TestSetForcedVariationWithInvalidVariationKey() public void TestGetForcedVariation() { var experimentKey = "test_experiment"; - var expectedForcedVariation = new Variation { Key = "variation", Id = "7721010009" }; - var expectedForcedVariation2 = new Variation { Key = "variation", Id = "7721010509" }; + var expectedForcedVariation = new Variation + { + Key = "variation", + Id = "7721010009" + }; + var expectedForcedVariation2 = new Variation + { + Key = "variation", + Id = "7721010509" + }; var userAttributes = new UserAttributes { - {"device_type", "iPhone" }, - {"location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "location", "San Francisco" + } }; Optimizely.Activate(experimentKey, TestUserId, userAttributes); - Assert.IsTrue(Optimizely.SetForcedVariation(experimentKey, TestUserId, expectedForcedVariation.Key), "Set variation to 'variation' failed."); + Assert.IsTrue( + Optimizely.SetForcedVariation(experimentKey, TestUserId, + expectedForcedVariation.Key), "Set variation to 'variation' failed."); // call getForcedVariation with valid experiment key and valid user ID - var actualForcedVariation = Optimizely.GetForcedVariation("test_experiment", TestUserId); + var actualForcedVariation = + Optimizely.GetForcedVariation("test_experiment", TestUserId); Assert.IsTrue(TestData.CompareObjects(expectedForcedVariation, actualForcedVariation)); // call getForcedVariation with invalid experiment and valid userID @@ -1138,14 +1589,18 @@ public void TestGetForcedVariation() Assert.Null(actualForcedVariation); // call getForcedVariation with valid experiment and invalid userID - actualForcedVariation = Optimizely.GetForcedVariation("test_experiment", "invalid_user"); + actualForcedVariation = + Optimizely.GetForcedVariation("test_experiment", "invalid_user"); Assert.Null(actualForcedVariation); // call getForcedVariation with an experiment that"s not running - Assert.IsTrue(Optimizely.SetForcedVariation("paused_experiment", "test_user2", "variation"), "Set variation to 'variation' failed."); + Assert.IsTrue( + Optimizely.SetForcedVariation("paused_experiment", "test_user2", "variation"), + "Set variation to 'variation' failed."); - actualForcedVariation = Optimizely.GetForcedVariation("paused_experiment", "test_user2"); + actualForcedVariation = + Optimizely.GetForcedVariation("paused_experiment", "test_user2"); Assert.IsTrue(TestData.CompareObjects(expectedForcedVariation2, actualForcedVariation)); @@ -1188,16 +1643,26 @@ public void TestGetVariationAudienceMatchAfterSetForcedVariation() var attributes = new UserAttributes { - { "device_type", "iPhone" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "location", "San Francisco" + } }; - Assert.True(Optimizely.SetForcedVariation(experimentKey, userId, variationKey), "Set variation for paused experiment should have failed."); + Assert.True(Optimizely.SetForcedVariation(experimentKey, userId, variationKey), + "Set variation for paused experiment should have failed."); var variation = Optimizely.GetVariation(experimentKey, userId, attributes); - LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(2)); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"Set variation ""{0}"" for experiment ""{1}"" and user ""{2}"" in the forced variation map.", variationId, experimentId, userId))); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"Variation ""{0}"" is mapped to experiment ""{1}"" and user ""{2}"" in the forced variation map", variationKey, experimentKey, userId))); + LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, + string.Format( + @"Set variation ""{0}"" for experiment ""{1}"" and user ""{2}"" in the forced variation map.", + variationId, experimentId, userId))); + LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, + string.Format( + @"Variation ""{0}"" is mapped to experiment ""{1}"" and user ""{2}"" in the forced variation map", + variationKey, experimentKey, userId))); Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, variation)); } @@ -1213,16 +1678,24 @@ public void TestGetVariationExperimentNotRunningAfterSetForceVariation() var attributes = new UserAttributes { - { "device_type", "iPhone" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "location", "San Francisco" + } }; - Assert.True(Optimizely.SetForcedVariation(experimentKey, userId, variationKey), "Set variation for paused experiment should have failed."); + Assert.True(Optimizely.SetForcedVariation(experimentKey, userId, variationKey), + "Set variation for paused experiment should have failed."); var variation = Optimizely.GetVariation(experimentKey, userId, attributes); - LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(2)); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"Set variation ""{0}"" for experiment ""{1}"" and user ""{2}"" in the forced variation map.", variationId, experimentId, userId))); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("Experiment \"{0}\" is not running.", experimentKey))); + LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, + string.Format( + @"Set variation ""{0}"" for experiment ""{1}"" and user ""{2}"" in the forced variation map.", + variationId, experimentId, userId))); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, + string.Format("Experiment \"{0}\" is not running.", experimentKey))); Assert.Null(variation); } @@ -1236,12 +1709,18 @@ public void TestGetVariationWhitelistedUserAfterSetForcedVariation() var variationKey = "variation"; var variationId = "7721010009"; - Assert.True(Optimizely.SetForcedVariation(experimentKey, userId, variationKey), "Set variation for paused experiment should have passed."); + Assert.True(Optimizely.SetForcedVariation(experimentKey, userId, variationKey), + "Set variation for paused experiment should have passed."); var variation = Optimizely.GetVariation(experimentKey, userId); - LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(2)); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"Set variation ""{0}"" for experiment ""{1}"" and user ""{2}"" in the forced variation map.", variationId, experimentId, userId))); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"Variation ""{0}"" is mapped to experiment ""{1}"" and user ""{2}"" in the forced variation map", variationKey, experimentKey, userId))); + LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, + string.Format( + @"Set variation ""{0}"" for experiment ""{1}"" and user ""{2}"" in the forced variation map.", + variationId, experimentId, userId))); + LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, + string.Format( + @"Variation ""{0}"" is mapped to experiment ""{1}"" and user ""{2}"" in the forced variation map", + variationKey, experimentKey, userId))); Assert.IsTrue(TestData.CompareObjects(VariationWithKeyVariation, variation)); } @@ -1256,8 +1735,12 @@ public void TestActivateNoAudienceNoAttributesAfterSetForcedVariation() var variationId = "7722370027"; var parameters = new Dictionary { - { "param1", "val1" }, - { "param2", "val2" } + { + "param1", "val1" + }, + { + "param2", "val2" + } }; Experiment experiment = new Experiment(); @@ -1267,21 +1750,48 @@ public void TestActivateNoAudienceNoAttributesAfterSetForcedVariation() optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); // Set forced variation - Assert.True((bool)optly.Invoke("SetForcedVariation", experimentKey, userId, variationKey), "Set variation for paused experiment should have failed."); + Assert.True( + (bool)optly.Invoke("SetForcedVariation", experimentKey, userId, variationKey), + "Set variation for paused experiment should have failed."); // Activate - var variation = (Variation)optly.Invoke("Activate", "group_experiment_1", "user_1", null); - - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); - - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"Set variation ""{0}"" for experiment ""{1}"" and user ""{2}"" in the forced variation map.", variationId, experimentId, userId)), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"user_1\" is not in the forced variation map."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1922] to user [user_1] with bucketing ID [user_1]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [user_1] is in experiment [group_experiment_1] of group [7722400015]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [9525] to user [user_1] with bucketing ID [user_1]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [user_1] is in variation [group_exp_1_var_2] of experiment [group_experiment_1]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "This decision will not be saved since the UserProfileService is null."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user user_1 in experiment group_experiment_1."), Times.Once); + var variation = + (Variation)optly.Invoke("Activate", "group_experiment_1", "user_1", null); + + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Once); + + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + string.Format( + @"Set variation ""{0}"" for experiment ""{1}"" and user ""{2}"" in the forced variation map.", + variationId, experimentId, userId)), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, "User \"user_1\" is not in the forced variation map."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "Assigned bucket [1922] to user [user_1] with bucketing ID [user_1]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "User [user_1] is in experiment [group_experiment_1] of group [7722400015]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "Assigned bucket [9525] to user [user_1] with bucketing ID [user_1]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "User [user_1] is in variation [group_exp_1_var_2] of experiment [group_experiment_1]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "This decision will not be saved since the UserProfileService is null."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "Activating user user_1 in experiment group_experiment_1."), Times.Once); Assert.IsTrue(TestData.CompareObjects(GroupVariation, variation)); } @@ -1296,60 +1806,99 @@ public void TestTrackNoAttributesNoEventValueAfterSetForcedVariation() var variationId = "7722370027"; var parameters = new Dictionary { - { "param1", "val1" } + { + "param1", "val1" + } }; var optly = Helper.CreatePrivateOptimizely(); // Set forced variation - Assert.True((bool)optly.Invoke("SetForcedVariation", experimentKey, userId, variationKey), "Set variation for paused experiment should have failed."); + Assert.True( + (bool)optly.Invoke("SetForcedVariation", experimentKey, userId, variationKey), + "Set variation for paused experiment should have failed."); // Track optly.Invoke("Track", "purchase", "test_user", null, null); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"Set variation ""{0}"" for experiment ""{1}"" and user ""{2}"" in the forced variation map.", variationId, experimentId, userId)), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Tracking event purchase for user test_user."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + string.Format( + @"Set variation ""{0}"" for experiment ""{1}"" and user ""{2}"" in the forced variation map.", + variationId, experimentId, userId)), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, "Tracking event purchase for user test_user."), + Times.Once); } [Test] public void TestGetVariationBucketingIdAttribute() { - var testBucketingIdControl = "testBucketingIdControl!"; // generates bucketing number 3741 + var testBucketingIdControl = + "testBucketingIdControl!"; // generates bucketing number 3741 var testBucketingIdVariation = "123456789"; // generates bucketing number 4567 var userId = "test_user"; var experimentKey = "test_experiment"; var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; var userAttributesWithBucketingId = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" }, - { ControlAttributes.BUCKETING_ID_ATTRIBUTE, testBucketingIdVariation } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + }, + { + ControlAttributes.BUCKETING_ID_ATTRIBUTE, testBucketingIdVariation + } }; // confirm that a valid variation is bucketed without the bucketing ID var actualVariation = Optimizely.GetVariation(experimentKey, userId, userAttributes); - Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, actualVariation), string.Format("Invalid variation key \"{0}\" for getVariation.", actualVariation?.Key)); + Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, actualVariation), + string.Format("Invalid variation key \"{0}\" for getVariation.", + actualVariation?.Key)); // confirm that invalid audience returns null actualVariation = Optimizely.GetVariation(experimentKey, userId); - Assert.Null(actualVariation, string.Format("Invalid variation key \"{0}\" for getVariation with bucketing ID \"{1}\".", actualVariation?.Key, testBucketingIdControl)); + Assert.Null(actualVariation, + string.Format( + "Invalid variation key \"{0}\" for getVariation with bucketing ID \"{1}\".", + actualVariation?.Key, testBucketingIdControl)); // confirm that a valid variation is bucketed with the bucketing ID - actualVariation = Optimizely.GetVariation(experimentKey, userId, userAttributesWithBucketingId); - Assert.IsTrue(TestData.CompareObjects(VariationWithKeyVariation, actualVariation), string.Format("Invalid variation key \"{0}\" for getVariation with bucketing ID \"{1}\".", actualVariation?.Key, testBucketingIdVariation)); + actualVariation = + Optimizely.GetVariation(experimentKey, userId, userAttributesWithBucketingId); + Assert.IsTrue(TestData.CompareObjects(VariationWithKeyVariation, actualVariation), + string.Format( + "Invalid variation key \"{0}\" for getVariation with bucketing ID \"{1}\".", + actualVariation?.Key, testBucketingIdVariation)); // confirm that invalid experiment with the bucketing ID returns null - actualVariation = Optimizely.GetVariation("invalidExperimentKey", userId, userAttributesWithBucketingId); - Assert.Null(actualVariation, string.Format("Invalid variation key \"{0}\" for getVariation with bucketing ID \"{1}\".", actualVariation?.Key, testBucketingIdControl)); + actualVariation = Optimizely.GetVariation("invalidExperimentKey", userId, + userAttributesWithBucketingId); + Assert.Null(actualVariation, + string.Format( + "Invalid variation key \"{0}\" for getVariation with bucketing ID \"{1}\".", + actualVariation?.Key, testBucketingIdControl)); } #endregion Test Misc @@ -1366,21 +1915,35 @@ public void TestGetFeatureVariableBooleanReturnsCorrectValue() var variableKeyNull = "varNull"; var featureVariableType = "boolean"; - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyTrue, It.IsAny(), - It.IsAny(), featureVariableType)).Returns(true); - Assert.AreEqual(true, OptimizelyMock.Object.GetFeatureVariableBoolean(featureKey, variableKeyTrue, TestUserId, null)); - - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyFalse, It.IsAny(), - It.IsAny(), featureVariableType)).Returns(false); - Assert.AreEqual(false, OptimizelyMock.Object.GetFeatureVariableBoolean(featureKey, variableKeyFalse, TestUserId, null)); - - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyNonBoolean, It.IsAny(), - It.IsAny(), featureVariableType)).Returns("non_boolean_value"); - Assert.Null(OptimizelyMock.Object.GetFeatureVariableBoolean(featureKey, variableKeyNonBoolean, TestUserId, null)); - - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyNull, It.IsAny(), - It.IsAny(), featureVariableType)).Returns(null); - Assert.Null(OptimizelyMock.Object.GetFeatureVariableBoolean(featureKey, variableKeyNull, TestUserId, null)); + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), + variableKeyTrue, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns(true); + Assert.AreEqual(true, + OptimizelyMock.Object.GetFeatureVariableBoolean(featureKey, variableKeyTrue, + TestUserId, null)); + + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), + variableKeyFalse, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns(false); + Assert.AreEqual(false, + OptimizelyMock.Object.GetFeatureVariableBoolean(featureKey, variableKeyFalse, + TestUserId, null)); + + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), + variableKeyNonBoolean, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns("non_boolean_value"); + Assert.Null(OptimizelyMock.Object.GetFeatureVariableBoolean(featureKey, + variableKeyNonBoolean, TestUserId, null)); + + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), + variableKeyNull, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns(null); + Assert.Null(OptimizelyMock.Object.GetFeatureVariableBoolean(featureKey, variableKeyNull, + TestUserId, null)); } [Test] @@ -1391,12 +1954,15 @@ public void TestGetFeatureVariableDoubleFRCulture() var optimizely = new Optimizely(fallbackConfigManager); - var doubleValue = optimizely.GetFeatureVariableDouble("double_single_variable_feature", "double_variable", "testUser1"); + var doubleValue = optimizely.GetFeatureVariableDouble("double_single_variable_feature", + "double_variable", "testUser1"); Assert.AreEqual(doubleValue, 14.99); SetCulture("fr-FR"); - var doubleValueFR = optimizely.GetFeatureVariableDouble("double_single_variable_feature", "double_variable", "testUser1"); + var doubleValueFR = + optimizely.GetFeatureVariableDouble("double_single_variable_feature", + "double_variable", "testUser1"); Assert.AreEqual(doubleValueFR, 14.99); } @@ -1408,12 +1974,16 @@ public void TestGetFeatureVariableIntegerFRCulture() var optimizely = new Optimizely(fallbackConfigManager); - var integerValue = optimizely.GetFeatureVariableInteger("integer_single_variable_feature", "integer_variable", "testUser1"); + var integerValue = + optimizely.GetFeatureVariableInteger("integer_single_variable_feature", + "integer_variable", "testUser1"); Assert.AreEqual(integerValue, 13); SetCulture("fr-FR"); - var integerValueFR = optimizely.GetFeatureVariableInteger("integer_single_variable_feature", "integer_variable", "testUser1"); + var integerValueFR = + optimizely.GetFeatureVariableInteger("integer_single_variable_feature", + "integer_variable", "testUser1"); Assert.AreEqual(integerValueFR, 13); } @@ -1425,12 +1995,16 @@ public void TestGetFeatureVariableBooleanFRCulture() var optimizely = new Optimizely(fallbackConfigManager); - var booleanValue = optimizely.GetFeatureVariableBoolean("boolean_single_variable_feature", "boolean_variable", "testUser1"); + var booleanValue = + optimizely.GetFeatureVariableBoolean("boolean_single_variable_feature", + "boolean_variable", "testUser1"); Assert.AreEqual(booleanValue, false); SetCulture("fr-FR"); - var booleanValueFR = optimizely.GetFeatureVariableBoolean("boolean_single_variable_feature", "boolean_variable", "testUser1"); + var booleanValueFR = + optimizely.GetFeatureVariableBoolean("boolean_single_variable_feature", + "boolean_variable", "testUser1"); Assert.AreEqual(booleanValueFR, false); } @@ -1442,12 +2016,15 @@ public void TestGetFeatureVariableStringFRCulture() var optimizely = new Optimizely(fallbackConfigManager); - var stringValue = optimizely.GetFeatureVariableString("string_single_variable_feature", "string_variable", "testUser1"); + var stringValue = optimizely.GetFeatureVariableString("string_single_variable_feature", + "string_variable", "testUser1"); Assert.AreEqual(stringValue, "cta_1"); SetCulture("fr-FR"); - var stringValueFR = optimizely.GetFeatureVariableString("string_single_variable_feature", "string_variable", "testUser1"); + var stringValueFR = + optimizely.GetFeatureVariableString("string_single_variable_feature", + "string_variable", "testUser1"); Assert.AreEqual(stringValueFR, "cta_1"); } @@ -1458,27 +2035,39 @@ public void TestGetFeatureVariableJSONFRCulture() var expectedDict = new Dictionary() { - { "int_var", 1 }, - { "boolean_key", false} + { + "int_var", 1 + }, + { + "boolean_key", false + } }; SetCulture("en-US"); var optimizely = new Optimizely(fallbackConfigManager); - var optimizelyJsonValue = optimizely.GetFeatureVariableJSON("string_single_variable_feature", "json_var", "testUser1"); + var optimizelyJsonValue = + optimizely.GetFeatureVariableJSON("string_single_variable_feature", "json_var", + "testUser1"); - Assert.IsTrue(TestData.CompareObjects(optimizelyJsonValue.ToDictionary(), expectedDict)); + Assert.IsTrue(TestData.CompareObjects(optimizelyJsonValue.ToDictionary(), + expectedDict)); Assert.AreEqual(optimizelyJsonValue.GetValue("int_var"), 1); Assert.AreEqual(optimizelyJsonValue.GetValue("boolean_key"), false); - Assert.IsTrue(TestData.CompareObjects(optimizelyJsonValue.GetValue(""), expectedDict)); + Assert.IsTrue(TestData.CompareObjects(optimizelyJsonValue.GetValue(""), + expectedDict)); SetCulture("fr-FR"); - var optimizelyJsonValueFR = optimizely.GetFeatureVariableJSON("string_single_variable_feature", "json_var", "testUser1"); + var optimizelyJsonValueFR = + optimizely.GetFeatureVariableJSON("string_single_variable_feature", "json_var", + "testUser1"); - Assert.IsTrue(TestData.CompareObjects(optimizelyJsonValue.ToDictionary(), expectedDict)); + Assert.IsTrue(TestData.CompareObjects(optimizelyJsonValue.ToDictionary(), + expectedDict)); Assert.AreEqual(optimizelyJsonValueFR.GetValue("int_var"), 1); Assert.AreEqual(optimizelyJsonValueFR.GetValue("boolean_key"), false); - Assert.IsTrue(TestData.CompareObjects(optimizelyJsonValue.GetValue(""), expectedDict)); + Assert.IsTrue(TestData.CompareObjects(optimizelyJsonValue.GetValue(""), + expectedDict)); } [Test] @@ -1491,21 +2080,35 @@ public void TestGetFeatureVariableDoubleReturnsCorrectValue() var variableKeyNull = "varNull"; var featureVariableType = "double"; - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyDouble, It.IsAny(), - It.IsAny(), featureVariableType)).Returns(100.54); - Assert.AreEqual(100.54, OptimizelyMock.Object.GetFeatureVariableDouble(featureKey, variableKeyDouble, TestUserId, null)); - - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyInt, It.IsAny(), - It.IsAny(), featureVariableType)).Returns(100); - Assert.AreEqual(100, OptimizelyMock.Object.GetFeatureVariableDouble(featureKey, variableKeyInt, TestUserId, null)); - - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyNonDouble, It.IsAny(), - It.IsAny(), featureVariableType)).Returns("non_double_value"); - Assert.Null(OptimizelyMock.Object.GetFeatureVariableDouble(featureKey, variableKeyNonDouble, TestUserId, null)); - - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyNull, It.IsAny(), - It.IsAny(), featureVariableType)).Returns(null); - Assert.Null(OptimizelyMock.Object.GetFeatureVariableDouble(featureKey, variableKeyNull, TestUserId, null)); + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType( + It.IsAny(), variableKeyDouble, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns(100.54); + Assert.AreEqual(100.54, + OptimizelyMock.Object.GetFeatureVariableDouble(featureKey, variableKeyDouble, + TestUserId, null)); + + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType( + It.IsAny(), variableKeyInt, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns(100); + Assert.AreEqual(100, + OptimizelyMock.Object.GetFeatureVariableDouble(featureKey, variableKeyInt, + TestUserId, null)); + + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), + variableKeyNonDouble, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns("non_double_value"); + Assert.Null(OptimizelyMock.Object.GetFeatureVariableDouble(featureKey, + variableKeyNonDouble, TestUserId, null)); + + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType( + It.IsAny(), variableKeyNull, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns(null); + Assert.Null(OptimizelyMock.Object.GetFeatureVariableDouble(featureKey, variableKeyNull, + TestUserId, null)); } [Test] @@ -1517,17 +2120,27 @@ public void TestGetFeatureVariableIntegerReturnsCorrectValue() var variableKeyNull = "varNull"; var featureVariableType = "integer"; - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyInt, It.IsAny(), - It.IsAny(), featureVariableType)).Returns(100); - Assert.AreEqual(100, OptimizelyMock.Object.GetFeatureVariableInteger(featureKey, variableKeyInt, TestUserId, null)); + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), + variableKeyInt, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns(100); + Assert.AreEqual(100, + OptimizelyMock.Object.GetFeatureVariableInteger(featureKey, variableKeyInt, + TestUserId, null)); - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableNonInt, It.IsAny(), - It.IsAny(), featureVariableType)).Returns("non_integer_value"); - Assert.Null(OptimizelyMock.Object.GetFeatureVariableInteger(featureKey, variableNonInt, TestUserId, null)); + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), + variableNonInt, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns("non_integer_value"); + Assert.Null(OptimizelyMock.Object.GetFeatureVariableInteger(featureKey, variableNonInt, + TestUserId, null)); - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyNull, It.IsAny(), - It.IsAny(), featureVariableType)).Returns(null); - Assert.Null(OptimizelyMock.Object.GetFeatureVariableInteger(featureKey, variableKeyNull, TestUserId, null)); + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), + variableKeyNull, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns(null); + Assert.Null(OptimizelyMock.Object.GetFeatureVariableInteger(featureKey, variableKeyNull, + TestUserId, null)); } [Test] @@ -1539,17 +2152,28 @@ public void TestGetFeatureVariableStringReturnsCorrectValue() var variableKeyNull = "varNull"; var featureVariableType = "string"; - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyString, It.IsAny(), - It.IsAny(), featureVariableType)).Returns("Test String"); - Assert.AreEqual("Test String", OptimizelyMock.Object.GetFeatureVariableString(featureKey, variableKeyString, TestUserId, null)); + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), + variableKeyString, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns("Test String"); + Assert.AreEqual("Test String", + OptimizelyMock.Object.GetFeatureVariableString(featureKey, variableKeyString, + TestUserId, null)); - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyIntString, It.IsAny(), - It.IsAny(), featureVariableType)).Returns("123"); - Assert.AreEqual("123", OptimizelyMock.Object.GetFeatureVariableString(featureKey, variableKeyIntString, TestUserId, null)); + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), + variableKeyIntString, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns("123"); + Assert.AreEqual("123", + OptimizelyMock.Object.GetFeatureVariableString(featureKey, variableKeyIntString, + TestUserId, null)); - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyNull, It.IsAny(), - It.IsAny(), featureVariableType)).Returns(null); - Assert.Null(OptimizelyMock.Object.GetFeatureVariableString(featureKey, variableKeyNull, TestUserId, null)); + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), + variableKeyNull, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns(null); + Assert.Null(OptimizelyMock.Object.GetFeatureVariableString(featureKey, variableKeyNull, + TestUserId, null)); } [Test] @@ -1562,21 +2186,42 @@ public void TestGetFeatureVariableJSONReturnsCorrectValue() var variableKeyNull = "varNull"; var featureVariableType = "json"; - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyString, It.IsAny(), - It.IsAny(), featureVariableType)).Returns(new OptimizelyJSON("{\"string\": \"Test String\"}", ErrorHandlerMock.Object, LoggerMock.Object)); - Assert.AreEqual("Test String", OptimizelyMock.Object.GetFeatureVariableJSON(featureKey, variableKeyString, TestUserId, null).GetValue("string")); - - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyIntString, It.IsAny(), - It.IsAny(), featureVariableType)).Returns(new OptimizelyJSON("{ \"integer\": 123 }", ErrorHandlerMock.Object, LoggerMock.Object)); - Assert.AreEqual(123, OptimizelyMock.Object.GetFeatureVariableJSON(featureKey, variableKeyIntString, TestUserId, null).GetValue("integer")); - - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyDouble, It.IsAny(), - It.IsAny(), featureVariableType)).Returns(new OptimizelyJSON("{ \"double\": 123.28 }", ErrorHandlerMock.Object, LoggerMock.Object)); - Assert.AreEqual(123.28, OptimizelyMock.Object.GetFeatureVariableJSON(featureKey, variableKeyDouble, TestUserId, null).GetValue("double")); - - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyNull, It.IsAny(), - It.IsAny(), featureVariableType)).Returns(null); - Assert.Null(OptimizelyMock.Object.GetFeatureVariableJSON(featureKey, variableKeyNull, TestUserId, null)); + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType( + It.IsAny(), variableKeyString, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns(new OptimizelyJSON("{\"string\": \"Test String\"}", ErrorHandlerMock.Object, + LoggerMock.Object)); + Assert.AreEqual("Test String", + OptimizelyMock.Object. + GetFeatureVariableJSON(featureKey, variableKeyString, TestUserId, null). + GetValue("string")); + + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType( + It.IsAny(), variableKeyIntString, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns(new OptimizelyJSON("{ \"integer\": 123 }", ErrorHandlerMock.Object, + LoggerMock.Object)); + Assert.AreEqual(123, + OptimizelyMock.Object. + GetFeatureVariableJSON(featureKey, variableKeyIntString, TestUserId, null). + GetValue("integer")); + + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType( + It.IsAny(), variableKeyDouble, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns(new OptimizelyJSON("{ \"double\": 123.28 }", ErrorHandlerMock.Object, + LoggerMock.Object)); + Assert.AreEqual(123.28, + OptimizelyMock.Object. + GetFeatureVariableJSON(featureKey, variableKeyDouble, TestUserId, null). + GetValue("double")); + + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType( + It.IsAny(), variableKeyNull, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns(null); + Assert.Null(OptimizelyMock.Object.GetFeatureVariableJSON(featureKey, variableKeyNull, + TestUserId, null)); } [Test] @@ -1590,40 +2235,100 @@ public void TestGetFeatureVariableJSONReturnsCorrectValueWhenInitializedUsingDic var variableKeyNull = "varNull"; var featureVariableType = "json"; - var expectedStringDict = new Dictionary() { { "string", "Test String" } }; - var expectedIntegerDict = new Dictionary() { { "integer", 123 } }; - var expectedDoubleDict = new Dictionary() { { "double", 123.28 } }; - var expectedBooleanDict = new Dictionary() { { "boolean", true } }; - - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyString, It.IsAny(), - It.IsAny(), featureVariableType)).Returns(new OptimizelyJSON(expectedStringDict, ErrorHandlerMock.Object, LoggerMock.Object)); - Assert.IsTrue(TestData.CompareObjects(expectedStringDict, OptimizelyMock.Object.GetFeatureVariableJSON(featureKey, variableKeyString, TestUserId, null).ToDictionary())); - Assert.AreEqual("Test String", OptimizelyMock.Object.GetFeatureVariableJSON(featureKey, variableKeyString, TestUserId, null).GetValue("string")); - - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyIntString, It.IsAny(), - It.IsAny(), featureVariableType)).Returns(new OptimizelyJSON(expectedIntegerDict, ErrorHandlerMock.Object, LoggerMock.Object)); - Assert.IsTrue(TestData.CompareObjects(expectedIntegerDict, OptimizelyMock.Object.GetFeatureVariableJSON(featureKey, variableKeyIntString, TestUserId, null).ToDictionary())); - Assert.AreEqual(123, OptimizelyMock.Object.GetFeatureVariableJSON(featureKey, variableKeyIntString, TestUserId, null).GetValue("integer")); - - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyDouble, It.IsAny(), - It.IsAny(), featureVariableType)).Returns(new OptimizelyJSON(expectedDoubleDict, ErrorHandlerMock.Object, LoggerMock.Object)); - Assert.IsTrue(TestData.CompareObjects(expectedDoubleDict, OptimizelyMock.Object.GetFeatureVariableJSON(featureKey, variableKeyDouble, TestUserId, null).ToDictionary())); - Assert.AreEqual(123.28, OptimizelyMock.Object.GetFeatureVariableJSON(featureKey, variableKeyDouble, TestUserId, null).GetValue("double")); - - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyBoolean, It.IsAny(), - It.IsAny(), featureVariableType)).Returns(new OptimizelyJSON(expectedBooleanDict, ErrorHandlerMock.Object, LoggerMock.Object)); - Assert.IsTrue(TestData.CompareObjects(expectedBooleanDict, OptimizelyMock.Object.GetFeatureVariableJSON(featureKey, variableKeyBoolean, TestUserId, null).ToDictionary())); - Assert.AreEqual(true, OptimizelyMock.Object.GetFeatureVariableJSON(featureKey, variableKeyBoolean, TestUserId, null).GetValue("boolean")); + var expectedStringDict = new Dictionary() + { + { + "string", "Test String" + } + }; + var expectedIntegerDict = new Dictionary() + { + { + "integer", 123 + } + }; + var expectedDoubleDict = new Dictionary() + { + { + "double", 123.28 + } + }; + var expectedBooleanDict = new Dictionary() + { + { + "boolean", true + } + }; - OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyNull, It.IsAny(), - It.IsAny(), featureVariableType)).Returns(null); - Assert.Null(OptimizelyMock.Object.GetFeatureVariableJSON(featureKey, variableKeyNull, TestUserId, null)); + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType( + It.IsAny(), variableKeyString, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns(new OptimizelyJSON(expectedStringDict, ErrorHandlerMock.Object, + LoggerMock.Object)); + Assert.IsTrue(TestData.CompareObjects(expectedStringDict, + OptimizelyMock.Object. + GetFeatureVariableJSON(featureKey, variableKeyString, TestUserId, null). + ToDictionary())); + Assert.AreEqual("Test String", + OptimizelyMock.Object. + GetFeatureVariableJSON(featureKey, variableKeyString, TestUserId, null). + GetValue("string")); + + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType( + It.IsAny(), variableKeyIntString, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns(new OptimizelyJSON(expectedIntegerDict, ErrorHandlerMock.Object, + LoggerMock.Object)); + Assert.IsTrue(TestData.CompareObjects(expectedIntegerDict, + OptimizelyMock.Object. + GetFeatureVariableJSON(featureKey, variableKeyIntString, TestUserId, null). + ToDictionary())); + Assert.AreEqual(123, + OptimizelyMock.Object. + GetFeatureVariableJSON(featureKey, variableKeyIntString, TestUserId, null). + GetValue("integer")); + + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType( + It.IsAny(), variableKeyDouble, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns(new OptimizelyJSON(expectedDoubleDict, ErrorHandlerMock.Object, + LoggerMock.Object)); + Assert.IsTrue(TestData.CompareObjects(expectedDoubleDict, + OptimizelyMock.Object. + GetFeatureVariableJSON(featureKey, variableKeyDouble, TestUserId, null). + ToDictionary())); + Assert.AreEqual(123.28, + OptimizelyMock.Object. + GetFeatureVariableJSON(featureKey, variableKeyDouble, TestUserId, null). + GetValue("double")); + + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType( + It.IsAny(), variableKeyBoolean, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns(new OptimizelyJSON(expectedBooleanDict, ErrorHandlerMock.Object, + LoggerMock.Object)); + Assert.IsTrue(TestData.CompareObjects(expectedBooleanDict, + OptimizelyMock.Object. + GetFeatureVariableJSON(featureKey, variableKeyBoolean, TestUserId, null). + ToDictionary())); + Assert.AreEqual(true, + OptimizelyMock.Object. + GetFeatureVariableJSON(featureKey, variableKeyBoolean, TestUserId, null). + GetValue("boolean")); + + OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType( + It.IsAny(), variableKeyNull, It.IsAny(), + It.IsAny(), featureVariableType)). + Returns(null); + Assert.Null(OptimizelyMock.Object.GetFeatureVariableJSON(featureKey, variableKeyNull, + TestUserId, null)); } #region Feature Toggle Tests [Test] - public void TestGetFeatureVariableDoubleReturnsRightValueWhenUserBuckedIntoFeatureExperimentAndVariationIsToggleOn() + public void + TestGetFeatureVariableDoubleReturnsRightValueWhenUserBuckedIntoFeatureExperimentAndVariationIsToggleOn() { var featureKey = "double_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); @@ -1631,103 +2336,158 @@ public void TestGetFeatureVariableDoubleReturnsRightValueWhenUserBuckedIntoFeatu var expectedValue = 42.42; var experiment = Config.GetExperimentFromKey("test_experiment_double_feature"); var variation = Config.GetVariationFromKey("test_experiment_double_feature", "control"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); - var variableValue = (double)optly.Invoke("GetFeatureVariableDouble", featureKey, variableKey, TestUserId, null); + var variableValue = (double)optly.Invoke("GetFeatureVariableDouble", featureKey, + variableKey, TestUserId, null); Assert.AreEqual(expectedValue, variableValue); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, $@"Got variable value ""{variableValue}"" for variable ""{variableKey}"" of feature flag ""{featureKey}"".")); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, + $@"Got variable value ""{variableValue}"" for variable ""{variableKey + }"" of feature flag ""{featureKey}"".")); } [Test] - public void TestGetFeatureVariableIntegerReturnsRightValueWhenUserBuckedIntoFeatureExperimentAndVariationIsToggleOn() + public void + TestGetFeatureVariableIntegerReturnsRightValueWhenUserBuckedIntoFeatureExperimentAndVariationIsToggleOn() { var featureKey = "integer_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); var variableKey = "integer_variable"; var expectedValue = 13; var experiment = Config.GetExperimentFromKey("test_experiment_integer_feature"); - var variation = Config.GetVariationFromKey("test_experiment_integer_feature", "variation"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); + var variation = + Config.GetVariationFromKey("test_experiment_integer_feature", "variation"); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); - var variableValue = (int)optly.Invoke("GetFeatureVariableInteger", featureKey, variableKey, TestUserId, userAttributes); + var variableValue = (int)optly.Invoke("GetFeatureVariableInteger", featureKey, + variableKey, TestUserId, userAttributes); Assert.AreEqual(expectedValue, variableValue); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, $@"Got variable value ""{variableValue}"" for variable ""{variableKey}"" of feature flag ""{featureKey}"".")); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, + $@"Got variable value ""{variableValue}"" for variable ""{variableKey + }"" of feature flag ""{featureKey}"".")); } [Test] - public void TestGetFeatureVariableDoubleReturnsDefaultValueWhenUserBuckedIntoFeatureExperimentAndVariationIsToggleOff() + public void + TestGetFeatureVariableDoubleReturnsDefaultValueWhenUserBuckedIntoFeatureExperimentAndVariationIsToggleOff() { var featureKey = "double_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); var variableKey = "double_variable"; var expectedValue = 14.99; var experiment = Config.GetExperimentFromKey("test_experiment_double_feature"); - var variation = Config.GetVariationFromKey("test_experiment_double_feature", "variation"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); - - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); + var variation = + Config.GetVariationFromKey("test_experiment_double_feature", "variation"); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); + + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); - var variableValue = (double)optly.Invoke("GetFeatureVariableDouble", featureKey, variableKey, TestUserId, null); + var variableValue = (double)optly.Invoke("GetFeatureVariableDouble", featureKey, + variableKey, TestUserId, null); Assert.AreEqual(expectedValue, variableValue); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, $@"Feature ""{featureKey}"" is not enabled for user {TestUserId}. Returning the default variable value ""{variableValue}"".")); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, + $@"Feature ""{featureKey}"" is not enabled for user {TestUserId + }. Returning the default variable value ""{variableValue}"".")); } [Test] - public void TestGetFeatureVariableIntegerReturnsDefaultValueWhenUserBuckedIntoFeatureExperimentAndVariationIsToggleOff() + public void + TestGetFeatureVariableIntegerReturnsDefaultValueWhenUserBuckedIntoFeatureExperimentAndVariationIsToggleOff() { var featureKey = "integer_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); var variableKey = "integer_variable"; var expectedValue = 7; var experiment = Config.GetExperimentFromKey("test_experiment_integer_feature"); - var variation = Config.GetVariationFromKey("test_experiment_integer_feature", "control"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); + var variation = + Config.GetVariationFromKey("test_experiment_integer_feature", "control"); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); - var variableValue = (int)optly.Invoke("GetFeatureVariableInteger", featureKey, variableKey, TestUserId, userAttributes); + var variableValue = (int)optly.Invoke("GetFeatureVariableInteger", featureKey, + variableKey, TestUserId, userAttributes); Assert.AreEqual(expectedValue, variableValue); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, $@"Feature ""{featureKey}"" is not enabled for user {TestUserId}. Returning the default variable value ""{variableValue}"".")); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, + $@"Feature ""{featureKey}"" is not enabled for user {TestUserId + }. Returning the default variable value ""{variableValue}"".")); } [Test] - public void TestGetFeatureVariableBooleanReturnsRightValueWhenUserBuckedIntoRolloutAndVariationIsToggleOn() + public void + TestGetFeatureVariableBooleanReturnsRightValueWhenUserBuckedIntoRolloutAndVariationIsToggleOn() { var featureKey = "boolean_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); @@ -1735,21 +2495,31 @@ public void TestGetFeatureVariableBooleanReturnsRightValueWhenUserBuckedIntoRoll var expectedValue = true; var experiment = Config.GetRolloutFromId("166660").Experiments[0]; var variation = Config.GetVariationFromKey(experiment.Key, "177771"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); - var variableValue = (bool)optly.Invoke("GetFeatureVariableBoolean", featureKey, variableKey, TestUserId, null); + var variableValue = (bool)optly.Invoke("GetFeatureVariableBoolean", featureKey, + variableKey, TestUserId, null); Assert.AreEqual(expectedValue, variableValue); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, $@"Got variable value ""true"" for variable ""{variableKey}"" of feature flag ""{featureKey}"".")); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, + $@"Got variable value ""true"" for variable ""{variableKey}"" of feature flag ""{ + featureKey}"".")); } [Test] - public void TestGetFeatureVariableJSONReturnsRightValueWhenUserBucketIntoRolloutAndVariationIsToggleOn() + public void + TestGetFeatureVariableJSONReturnsRightValueWhenUserBucketIntoRolloutAndVariationIsToggleOn() { var featureKey = "string_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); @@ -1758,30 +2528,46 @@ public void TestGetFeatureVariableJSONReturnsRightValueWhenUserBucketIntoRollout var expectedIntValue = 4; var experiment = Config.GetRolloutFromId("166661").Experiments[0]; var variation = Config.GetVariationFromKey(experiment.Key, "177775"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - var variableValue = (OptimizelyJSON)optly.Invoke("GetFeatureVariableJSON", featureKey, variableKey, TestUserId, userAttributes); + var variableValue = (OptimizelyJSON)optly.Invoke("GetFeatureVariableJSON", featureKey, + variableKey, TestUserId, userAttributes); Assert.AreEqual(expectedIntValue, variableValue.GetValue("int_var")); Assert.AreEqual(expectedStringValue, variableValue.GetValue("string_var")); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, $@"Got variable value ""{variableValue}"" for variable ""{variableKey}"" of feature flag ""{featureKey}"".")); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, + $@"Got variable value ""{variableValue}"" for variable ""{variableKey + }"" of feature flag ""{featureKey}"".")); } [Test] - public void TestGetFeatureVariableJSONReturnsRightValueWhenUserBucketIntoRolloutAndVariationIsToggleOnTypeIsJson() + public void + TestGetFeatureVariableJSONReturnsRightValueWhenUserBucketIntoRolloutAndVariationIsToggleOnTypeIsJson() { var featureKey = "string_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); @@ -1790,30 +2576,46 @@ public void TestGetFeatureVariableJSONReturnsRightValueWhenUserBucketIntoRollout var expectedIntValue = 5; var experiment = Config.GetRolloutFromId("166661").Experiments[0]; var variation = Config.GetVariationFromKey(experiment.Key, "177775"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - var variableValue = (OptimizelyJSON)optly.Invoke("GetFeatureVariableJSON", featureKey, variableKey, TestUserId, userAttributes); + var variableValue = (OptimizelyJSON)optly.Invoke("GetFeatureVariableJSON", featureKey, + variableKey, TestUserId, userAttributes); Assert.AreEqual(expectedIntValue, variableValue.GetValue("int_var")); Assert.AreEqual(expectedStringValue, variableValue.GetValue("string_var")); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, $@"Got variable value ""{variableValue}"" for variable ""{variableKey}"" of feature flag ""{featureKey}"".")); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, + $@"Got variable value ""{variableValue}"" for variable ""{variableKey + }"" of feature flag ""{featureKey}"".")); } [Test] - public void TestGetFeatureVariableStringReturnsRightValueWhenUserBuckedIntoRolloutAndVariationIsToggleOn() + public void + TestGetFeatureVariableStringReturnsRightValueWhenUserBuckedIntoRolloutAndVariationIsToggleOn() { var featureKey = "string_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); @@ -1821,29 +2623,45 @@ public void TestGetFeatureVariableStringReturnsRightValueWhenUserBuckedIntoRollo var expectedValue = "cta_4"; var experiment = Config.GetRolloutFromId("166661").Experiments[0]; var variation = Config.GetVariationFromKey(experiment.Key, "177775"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - var variableValue = (string)optly.Invoke("GetFeatureVariableString", featureKey, variableKey, TestUserId, userAttributes); + var variableValue = (string)optly.Invoke("GetFeatureVariableString", featureKey, + variableKey, TestUserId, userAttributes); Assert.AreEqual(expectedValue, variableValue); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, $@"Got variable value ""{variableValue}"" for variable ""{variableKey}"" of feature flag ""{featureKey}"".")); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, + $@"Got variable value ""{variableValue}"" for variable ""{variableKey + }"" of feature flag ""{featureKey}"".")); } [Test] - public void TestGetFeatureVariableBooleanReturnsDefaultValueWhenUserBuckedIntoRolloutAndVariationIsToggleOff() + public void + TestGetFeatureVariableBooleanReturnsDefaultValueWhenUserBuckedIntoRolloutAndVariationIsToggleOff() { var featureKey = "boolean_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); @@ -1851,20 +2669,30 @@ public void TestGetFeatureVariableBooleanReturnsDefaultValueWhenUserBuckedIntoRo var expectedValue = true; var experiment = Config.GetRolloutFromId("166660").Experiments[3]; var variation = Config.GetVariationFromKey(experiment.Key, "177782"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); - var variableValue = (bool)optly.Invoke("GetFeatureVariableBoolean", featureKey, variableKey, TestUserId, null); + var variableValue = (bool)optly.Invoke("GetFeatureVariableBoolean", featureKey, + variableKey, TestUserId, null); Assert.AreEqual(expectedValue, variableValue); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, $@"Feature ""{featureKey}"" is not enabled for user {TestUserId}. Returning the default variable value ""true"".")); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, + $@"Feature ""{featureKey}"" is not enabled for user {TestUserId + }. Returning the default variable value ""true"".")); } [Test] - public void TestGetFeatureVariableStringReturnsDefaultValueWhenUserBuckedIntoRolloutAndVariationIsToggleOff() + public void + TestGetFeatureVariableStringReturnsDefaultValueWhenUserBuckedIntoRolloutAndVariationIsToggleOff() { var featureKey = "string_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); @@ -1872,45 +2700,70 @@ public void TestGetFeatureVariableStringReturnsDefaultValueWhenUserBuckedIntoRol var expectedValue = "wingardium leviosa"; var experiment = Config.GetRolloutFromId("166661").Experiments[2]; var variation = Config.GetVariationFromKey(experiment.Key, "177784"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - var variableValue = (string)optly.Invoke("GetFeatureVariableString", featureKey, variableKey, TestUserId, userAttributes); + var variableValue = (string)optly.Invoke("GetFeatureVariableString", featureKey, + variableKey, TestUserId, userAttributes); Assert.AreEqual(expectedValue, variableValue); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, $@"Feature ""{featureKey}"" is not enabled for user {TestUserId}. Returning the default variable value ""{variableValue}"".")); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, + $@"Feature ""{featureKey}"" is not enabled for user {TestUserId + }. Returning the default variable value ""{variableValue}"".")); } [Test] - public void TestGetFeatureVariableDoubleReturnsDefaultValueWhenUserNotBuckedIntoBothFeatureExperimentAndRollout() + public void + TestGetFeatureVariableDoubleReturnsDefaultValueWhenUserNotBuckedIntoBothFeatureExperimentAndRollout() { var featureKey = "double_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey("double_single_variable_feature"); var variableKey = "double_variable"; var expectedValue = 14.99; - var decision = Result.NewResult(new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); - var variableValue = (double)optly.Invoke("GetFeatureVariableDouble", featureKey, variableKey, TestUserId, null); + var variableValue = (double)optly.Invoke("GetFeatureVariableDouble", featureKey, + variableKey, TestUserId, null); Assert.AreEqual(expectedValue, variableValue); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, $@"User ""{TestUserId}"" is not in any variation for feature flag ""{featureKey}"", returning default value ""{variableValue}"".")); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, + $@"User ""{TestUserId}"" is not in any variation for feature flag ""{featureKey + }"", returning default value ""{variableValue}"".")); } #endregion Feature Toggle Tests @@ -1928,36 +2781,53 @@ public void TestGetFeatureVariableValueForTypeGivenNullOrEmptyArguments() var variableType = "boolean"; // Passing null and empty feature key. - Assert.IsNull(Optimizely.GetFeatureVariableValueForType(null, variableKey, TestUserId, null, variableType)); - Assert.IsNull(Optimizely.GetFeatureVariableValueForType("", variableKey, TestUserId, null, variableType)); + Assert.IsNull(Optimizely.GetFeatureVariableValueForType(null, variableKey, + TestUserId, null, variableType)); + Assert.IsNull(Optimizely.GetFeatureVariableValueForType("", variableKey, + TestUserId, null, variableType)); // Passing null and empty variable key. - Assert.IsNull(Optimizely.GetFeatureVariableValueForType(featureKey, null, TestUserId, null, variableType)); - Assert.IsNull(Optimizely.GetFeatureVariableValueForType(featureKey, "", TestUserId, null, variableType)); + Assert.IsNull(Optimizely.GetFeatureVariableValueForType(featureKey, null, + TestUserId, null, variableType)); + Assert.IsNull(Optimizely.GetFeatureVariableValueForType(featureKey, "", + TestUserId, null, variableType)); // Passing null and empty user Id. - Assert.IsNull(Optimizely.GetFeatureVariableValueForType(featureKey, variableKey, null, null, variableType)); - Assert.IsNull(Optimizely.GetFeatureVariableValueForType(featureKey, variableKey, "", null, variableType)); + Assert.IsNull(Optimizely.GetFeatureVariableValueForType(featureKey, variableKey, + null, null, variableType)); + Assert.IsNull(Optimizely.GetFeatureVariableValueForType(featureKey, variableKey, + "", null, variableType)); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided Feature Key is in invalid format."), Times.Exactly(2)); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided Variable Key is in invalid format."), Times.Exactly(2)); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided User Id is in invalid format."), Times.Exactly(1)); + LoggerMock.Verify( + l => l.Log(LogLevel.ERROR, "Provided Feature Key is in invalid format."), + Times.Exactly(2)); + LoggerMock.Verify( + l => l.Log(LogLevel.ERROR, "Provided Variable Key is in invalid format."), + Times.Exactly(2)); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided User Id is in invalid format."), + Times.Exactly(1)); } // Should return null and log error message when feature key or variable key does not get found. [Test] public void TestGetFeatureVariableValueForTypeGivenFeatureKeyOrVariableKeyNotFound() { - var featureKey = "this_feature_should_never_be_found_in_the_datafile_unless_the_datafile_creator_got_insane"; - var variableKey = "this_variable_should_never_be_found_in_the_datafile_unless_the_datafile_creator_got_insane"; + var featureKey = + "this_feature_should_never_be_found_in_the_datafile_unless_the_datafile_creator_got_insane"; + var variableKey = + "this_variable_should_never_be_found_in_the_datafile_unless_the_datafile_creator_got_insane"; var variableType = "boolean"; - Assert.IsNull(Optimizely.GetFeatureVariableValueForType(featureKey, variableKey, TestUserId, null, variableType)); - Assert.IsNull(Optimizely.GetFeatureVariableValueForType("double_single_variable_feature", variableKey, TestUserId, null, variableType)); + Assert.IsNull(Optimizely.GetFeatureVariableValueForType(featureKey, variableKey, + TestUserId, null, variableType)); + Assert.IsNull(Optimizely.GetFeatureVariableValueForType( + "double_single_variable_feature", variableKey, TestUserId, null, variableType)); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, $@"Feature key ""{featureKey}"" is not in datafile.")); + LoggerMock.Verify(l => + l.Log(LogLevel.ERROR, $@"Feature key ""{featureKey}"" is not in datafile.")); LoggerMock.Verify(l => l.Log(LogLevel.ERROR, - $@"No feature variable was found for key ""{variableKey}"" in feature flag ""double_single_variable_feature"".")); + $@"No feature variable was found for key ""{variableKey + }"" in feature flag ""double_single_variable_feature"".")); } // Should return null and log error message when variable type is invalid. @@ -1969,30 +2839,46 @@ public void TestGetFeatureVariableValueForTypeGivenInvalidVariableType() var variableTypeDouble = "double"; var variableTypeString = "string"; - Assert.IsNull(Optimizely.GetFeatureVariableValueForType("double_single_variable_feature", "double_variable", TestUserId, null, variableTypeBool)); - Assert.IsNull(Optimizely.GetFeatureVariableValueForType("boolean_single_variable_feature", "boolean_variable", TestUserId, null, variableTypeDouble)); - Assert.IsNull(Optimizely.GetFeatureVariableValueForType("integer_single_variable_feature", "integer_variable", TestUserId, null, variableTypeString)); - Assert.IsNull(Optimizely.GetFeatureVariableValueForType("string_single_variable_feature", "string_variable", TestUserId, null, variableTypeInt)); - Assert.IsNull(Optimizely.GetFeatureVariableValueForType("string_single_variable_feature", "json_var", TestUserId, null, variableTypeInt)); + Assert.IsNull(Optimizely.GetFeatureVariableValueForType( + "double_single_variable_feature", "double_variable", TestUserId, null, + variableTypeBool)); + Assert.IsNull(Optimizely.GetFeatureVariableValueForType( + "boolean_single_variable_feature", "boolean_variable", TestUserId, null, + variableTypeDouble)); + Assert.IsNull(Optimizely.GetFeatureVariableValueForType( + "integer_single_variable_feature", "integer_variable", TestUserId, null, + variableTypeString)); + Assert.IsNull(Optimizely.GetFeatureVariableValueForType( + "string_single_variable_feature", "string_variable", TestUserId, null, + variableTypeInt)); + Assert.IsNull(Optimizely.GetFeatureVariableValueForType( + "string_single_variable_feature", "json_var", TestUserId, null, variableTypeInt)); LoggerMock.Verify(l => l.Log(LogLevel.ERROR, - $@"Variable is of type ""double"", but you requested it as type ""{variableTypeBool}"".")); + $@"Variable is of type ""double"", but you requested it as type ""{variableTypeBool + }"".")); LoggerMock.Verify(l => l.Log(LogLevel.ERROR, - $@"Variable is of type ""boolean"", but you requested it as type ""{variableTypeDouble}"".")); + $@"Variable is of type ""boolean"", but you requested it as type ""{ + variableTypeDouble}"".")); LoggerMock.Verify(l => l.Log(LogLevel.ERROR, - $@"Variable is of type ""integer"", but you requested it as type ""{variableTypeString}"".")); + $@"Variable is of type ""integer"", but you requested it as type ""{ + variableTypeString}"".")); LoggerMock.Verify(l => l.Log(LogLevel.ERROR, - $@"Variable is of type ""string"", but you requested it as type ""{variableTypeInt}"".")); + $@"Variable is of type ""string"", but you requested it as type ""{variableTypeInt + }"".")); } [Test] public void TestUnsupportedVariableType() { - var featureVariableStringRandomType = Optimizely.GetFeatureVariableString("", "any_key", TestUserId); + var featureVariableStringRandomType = + Optimizely.GetFeatureVariableString("", "any_key", TestUserId); Assert.IsNull(featureVariableStringRandomType); // This is to test that only json subtype is parsing and all other will subtype will be stringify - var featureVariableStringRegexSubType = Optimizely.GetFeatureVariableString("unsupported_variabletype", "string_regex_key", TestUserId); + var featureVariableStringRegexSubType = + Optimizely.GetFeatureVariableString("unsupported_variabletype", "string_regex_key", + TestUserId); Assert.AreEqual(featureVariableStringRegexSubType, "^\\d+(\\.\\d+)?"); } @@ -2003,60 +2889,87 @@ public void TestGetFeatureVariableValueForTypeGivenFeatureFlagIsNotEnabledForUse var featureKey = "double_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey("double_single_variable_feature"); var experiment = Config.GetExperimentFromKey("test_experiment_double_feature"); - var variation = Config.GetVariationFromKey("test_experiment_double_feature", "variation"); + var variation = + Config.GetVariationFromKey("test_experiment_double_feature", "variation"); var variableKey = "double_variable"; var variableType = "double"; var expectedValue = 14.99; - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - var variableValue = (double?)optly.InvokeGeneric("GetFeatureVariableValueForType", new Type[] { typeof(double?) }, featureKey, variableKey, TestUserId, null, variableType); + var variableValue = (double?)optly.InvokeGeneric("GetFeatureVariableValueForType", + new Type[] + { + typeof(double?) + }, featureKey, variableKey, TestUserId, null, variableType); Assert.AreEqual(expectedValue, variableValue); LoggerMock.Verify(l => l.Log(LogLevel.INFO, - $@"Feature ""{featureKey}"" is not enabled for user {TestUserId}. Returning the default variable value ""{variableValue}"".")); + $@"Feature ""{featureKey}"" is not enabled for user {TestUserId + }. Returning the default variable value ""{variableValue}"".")); } // Should return default value and log message when feature is enabled for the user // but variable usage does not get found for the variation. [Test] - public void TestGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUserAndVaribaleNotInVariation() + public void + TestGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUserAndVaribaleNotInVariation() { var featureKey = "double_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey("double_single_variable_feature"); var experiment = Config.GetExperimentFromKey("test_experiment_integer_feature"); - var differentVariation = Config.GetVariationFromKey("test_experiment_integer_feature", "control"); - var expectedDecision = Result.NewResult(new FeatureDecision(experiment, differentVariation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); + var differentVariation = + Config.GetVariationFromKey("test_experiment_integer_feature", "control"); + var expectedDecision = Result.NewResult( + new FeatureDecision(experiment, differentVariation, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); var variableKey = "double_variable"; var variableType = "double"; var expectedValue = 14.99; // Mock GetVariationForFeature method to return variation of different feature. - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(expectedDecision); + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(expectedDecision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - var variableValue = (double?)optly.InvokeGeneric("GetFeatureVariableValueForType", new Type[] { typeof(double?) }, featureKey, variableKey, TestUserId, null, variableType); + var variableValue = (double?)optly.InvokeGeneric("GetFeatureVariableValueForType", + new Type[] + { + typeof(double?) + }, featureKey, variableKey, TestUserId, null, variableType); Assert.AreEqual(expectedValue, variableValue); LoggerMock.Verify(l => l.Log(LogLevel.INFO, - $@"Variable ""{variableKey}"" is not used in variation ""control"", returning default value ""{expectedValue}"".")); + $@"Variable ""{variableKey + }"" is not used in variation ""control"", returning default value ""{expectedValue + }"".")); } // Should return variable value from variation and log message when feature is enabled for the user // and variable usage has been found for the variation. [Test] - public void TestGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUserAndVaribaleIsInVariation() + public void + TestGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUserAndVaribaleIsInVariation() { var featureKey = "double_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey("double_single_variable_feature"); @@ -2065,19 +2978,31 @@ public void TestGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUserAn var expectedValue = 42.42; var experiment = Config.GetExperimentFromKey("test_experiment_double_feature"); var variation = Config.GetVariationFromKey("test_experiment_double_feature", "control"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - var variableValue = (double?)optly.InvokeGeneric("GetFeatureVariableValueForType", new Type[] { typeof(double?) }, featureKey, variableKey, TestUserId, null, variableType); + var variableValue = (double?)optly.InvokeGeneric("GetFeatureVariableValueForType", + new Type[] + { + typeof(double?) + }, featureKey, variableKey, TestUserId, null, variableType); Assert.AreEqual(expectedValue, variableValue); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, $@"Got variable value ""{variableValue}"" for variable ""{variableKey}"" of feature flag ""{featureKey}"".")); + LoggerMock.Verify(l => l.Log(LogLevel.INFO, + $@"Got variable value ""{variableValue}"" for variable ""{variableKey + }"" of feature flag ""{featureKey}"".")); } // Verify that GetFeatureVariableValueForType returns correct variable value for rollout rule. @@ -2090,10 +3015,16 @@ public void TestGetFeatureVariableValueForTypeWithRolloutRule() //experimentid - 177772 var experiment = Config.Rollouts[0].Experiments[1]; var variation = Config.GetVariationFromId(experiment.Key, "177773"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); var expectedVariableValue = false; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); @@ -2101,7 +3032,8 @@ public void TestGetFeatureVariableValueForTypeWithRolloutRule() optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); // Calling GetFeatureVariableBoolean to get GetFeatureVariableValueForType returned value casted in bool. - var actualVariableValue = (bool?)optly.Invoke("GetFeatureVariableBoolean", featureKey, variableKey, TestUserId, null); + var actualVariableValue = (bool?)optly.Invoke("GetFeatureVariableBoolean", featureKey, + variableKey, TestUserId, null); // Verify that variable value 'false' has been returned from GetFeatureVariableValueForType as it is the value // stored in rollout rule '177772'. @@ -2124,8 +3056,11 @@ public void TestIsFeatureEnabledGivenNullOrEmptyArguments() Assert.IsFalse(Optimizely.IsFeatureEnabled(null, TestUserId, null)); Assert.IsFalse(Optimizely.IsFeatureEnabled("", TestUserId, null)); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided Feature Key is in invalid format."), Times.Exactly(2)); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided User Id is in invalid format."), Times.Exactly(1)); + LoggerMock.Verify( + l => l.Log(LogLevel.ERROR, "Provided Feature Key is in invalid format."), + Times.Exactly(2)); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided User Id is in invalid format."), + Times.Exactly(1)); } // Should return false and log error message when feature flag key is not found in the datafile. @@ -2135,14 +3070,16 @@ public void TestIsFeatureEnabledGivenFeatureFlagNotFound() var featureKey = "feature_not_found"; Assert.IsFalse(Optimizely.IsFeatureEnabled(featureKey, TestUserId, null)); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, $@"Feature key ""{featureKey}"" is not in datafile.")); + LoggerMock.Verify(l => + l.Log(LogLevel.ERROR, $@"Feature key ""{featureKey}"" is not in datafile.")); } // Should return false and log error message when arguments are null or empty. [Test] public void TestIsFeatureEnabledGivenFeatureFlagContainsInvalidExperiment() { - var tempConfig = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, new NoOpErrorHandler()); + var tempConfig = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, + new NoOpErrorHandler()); var tempConfigManager = new FallbackProjectConfigManager(tempConfig); var featureFlag = tempConfig.GetFeatureFlagFromKey("multi_variate_feature"); @@ -2150,12 +3087,17 @@ public void TestIsFeatureEnabledGivenFeatureFlagContainsInvalidExperiment() optly.SetFieldOrProperty("ProjectConfigManager", tempConfigManager); // Set such an experiment to the list of experiment ids, that does not belong to the feature. - featureFlag.ExperimentIds = new List { "4209211" }; + featureFlag.ExperimentIds = new List + { + "4209211" + }; // Should return false when the experiment in feature flag does not get found in the datafile. - Assert.False((bool)optly.Invoke("IsFeatureEnabled", "multi_variate_feature", TestUserId, null)); + Assert.False((bool)optly.Invoke("IsFeatureEnabled", "multi_variate_feature", TestUserId, + null)); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, @"Experiment ID ""4209211"" is not in datafile.")); + LoggerMock.Verify(l => + l.Log(LogLevel.ERROR, @"Experiment ID ""4209211"" is not in datafile.")); } // Should return false and log message when feature is not enabled for the user. @@ -2165,10 +3107,17 @@ public void TestIsFeatureEnabledGivenFeatureFlagIsNotEnabledForUser() var featureKey = "double_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey("double_single_variable_feature"); var experiment = Config.GetExperimentFromKey("test_experiment_double_feature"); - var variation = Config.GetVariationFromKey("test_experiment_double_feature", "variation"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); - - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); + var variation = + Config.GetVariationFromKey("test_experiment_double_feature", "variation"); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); + + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); @@ -2191,9 +3140,15 @@ public void TestIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsNotBeingExperi var experiment = rollout.Experiments[0]; var variation = experiment.Variations[0]; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); @@ -2204,7 +3159,8 @@ public void TestIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsNotBeingExperi // SendImpressionEvent() does not get called. LoggerMock.Verify(l => l.Log(LogLevel.INFO, - $@"The user ""{TestUserId}"" is not being experimented on feature ""{featureKey}""."), Times.Once); + $@"The user ""{TestUserId}"" is not being experimented on feature ""{featureKey + }""."), Times.Once); LoggerMock.Verify(l => l.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is enabled for user ""{TestUserId}"".")); @@ -2219,9 +3175,15 @@ public void TestIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsBeingExperimen var experiment = Config.GetExperimentFromKey("test_experiment_double_feature"); var variation = Config.GetVariationFromKey("test_experiment_double_feature", "control"); var featureFlag = Config.GetFeatureFlagFromKey(featureKey); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); @@ -2232,7 +3194,8 @@ public void TestIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsBeingExperimen // SendImpressionEvent() gets called. LoggerMock.Verify(l => l.Log(LogLevel.INFO, - $@"The user ""{TestUserId}"" is not being experimented on feature ""{featureKey}""."), Times.Never); + $@"The user ""{TestUserId}"" is not being experimented on feature ""{featureKey + }""."), Times.Never); LoggerMock.Verify(l => l.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is enabled for user ""{TestUserId}"".")); @@ -2245,11 +3208,18 @@ public void TestIsFeatureEnabledGivenFeatureFlagIsNotEnabledAndUserIsBeingExperi { var featureKey = "double_single_variable_feature"; var experiment = Config.GetExperimentFromKey("test_experiment_double_feature"); - var variation = Config.GetVariationFromKey("test_experiment_double_feature", "variation"); + var variation = + Config.GetVariationFromKey("test_experiment_double_feature", "variation"); var featureFlag = Config.GetFeatureFlagFromKey(featureKey); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); @@ -2260,11 +3230,13 @@ public void TestIsFeatureEnabledGivenFeatureFlagIsNotEnabledAndUserIsBeingExperi // SendImpressionEvent() gets called. LoggerMock.Verify(l => l.Log(LogLevel.INFO, - $@"The user ""{TestUserId}"" is not being experimented on feature ""{featureKey}""."), Times.Never); + $@"The user ""{TestUserId}"" is not being experimented on feature ""{featureKey + }""."), Times.Never); LoggerMock.Verify(l => l.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is not enabled for user ""{TestUserId}"".")); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny())); + EventDispatcherMock.Verify(dispatcher => + dispatcher.DispatchEvent(It.IsAny())); } // Verify that IsFeatureEnabled returns true if a variation does not get found in the feature @@ -2275,19 +3247,44 @@ public void TestIsFeatureEnabledGivenVariationNotFoundInFeatureExperimentButInRo var featureKey = "boolean_single_variable_feature"; var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + } }; Assert.True(Optimizely.IsFeatureEnabled(featureKey, TestUserId, userAttributes)); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The feature flag \"boolean_single_variable_feature\" is not used in any experiments."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"testUserId\" does not meet the conditions for targeting rule \"1\"."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"testUserId\" does not meet the conditions for targeting rule \"2\"."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [8408] to user [testUserId] with bucketing ID [testUserId]."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The user \"testUserId\" is bucketed into a rollout for feature flag \"boolean_single_variable_feature\"."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The user \"testUserId\" is not being experimented on feature \"boolean_single_variable_feature\"."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Feature flag \"boolean_single_variable_feature\" is enabled for user \"testUserId\"."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "The feature flag \"boolean_single_variable_feature\" is not used in any experiments."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "User \"testUserId\" does not meet the conditions for targeting rule \"1\"."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "User \"testUserId\" does not meet the conditions for targeting rule \"2\"."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.DEBUG, + "Assigned bucket [8408] to user [testUserId] with bucketing ID [testUserId]."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "The user \"testUserId\" is bucketed into a rollout for feature flag \"boolean_single_variable_feature\"."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "The user \"testUserId\" is not being experimented on feature \"boolean_single_variable_feature\"."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.INFO, + "Feature flag \"boolean_single_variable_feature\" is enabled for user \"testUserId\"."), + Times.Once); } public void TestIsFeatureEnabledWithFeatureEnabledPropertyGivenFeatureExperiment() @@ -2295,15 +3292,29 @@ public void TestIsFeatureEnabledWithFeatureEnabledPropertyGivenFeatureExperiment var userId = "testUserId2"; var featureKey = "double_single_variable_feature"; var experiment = Config.GetExperimentFromKey("test_experiment_double_feature"); - var featureEnabledTrue = Config.GetVariationFromKey("test_experiment_double_feature", "control"); - var featureEnabledFalse = Config.GetVariationFromKey("test_experiment_double_feature", "variation"); + var featureEnabledTrue = + Config.GetVariationFromKey("test_experiment_double_feature", "control"); + var featureEnabledFalse = + Config.GetVariationFromKey("test_experiment_double_feature", "variation"); var featureFlag = Config.GetFeatureFlagFromKey(featureKey); - var decisionTrue = Result.NewResult(new FeatureDecision(experiment, featureEnabledTrue, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); - var decisionFalse = Result.NewResult(new FeatureDecision(experiment, featureEnabledFalse, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); - - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decisionTrue); - - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decisionFalse); + var decisionTrue = Result.NewResult( + new FeatureDecision(experiment, featureEnabledTrue, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); + var decisionFalse = Result.NewResult( + new FeatureDecision(experiment, featureEnabledFalse, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); + + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decisionTrue); + + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decisionFalse); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); @@ -2325,7 +3336,11 @@ public void TestIsFeatureEnabledWithFeatureEnabledPropertyGivenRolloutRule() // Verify that IsFeatureEnabled returns true when user is bucketed into the rollout rule's variation. Assert.True(Optimizely.IsFeatureEnabled("boolean_single_variable_feature", TestUserId)); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(null); + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(null); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); @@ -2349,9 +3364,15 @@ public void TestActivateListenerWithAttributes() { var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; TestActivateListener(userAttributes); @@ -2363,31 +3384,46 @@ public void TestActivateListener(UserAttributes userAttributes) var variationKey = "group_exp_1_var_1"; var featureKey = "boolean_feature"; var experiment = Config.GetExperimentFromKey(experimentKey); - var variation = Result.NewResult(Config.GetVariationFromKey(experimentKey, variationKey), DecisionReasons); + var variation = + Result.NewResult(Config.GetVariationFromKey(experimentKey, variationKey), + DecisionReasons); var featureFlag = Config.GetFeatureFlagFromKey(featureKey); - var decision = Result.NewResult(new FeatureDecision(experiment, variation.ResultObject, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation.ResultObject, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); // Mocking objects. - NotificationCallbackMock.Setup(nc => nc.TestActivateCallback(It.IsAny(), It.IsAny(), + NotificationCallbackMock.Setup(nc => nc.TestActivateCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); - NotificationCallbackMock.Setup(nc => nc.TestAnotherActivateCallback(It.IsAny(), It.IsAny(), + NotificationCallbackMock.Setup(nc => nc.TestAnotherActivateCallback( + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); - NotificationCallbackMock.Setup(nc => nc.TestTrackCallback(It.IsAny(), It.IsAny(), + NotificationCallbackMock.Setup(nc => nc.TestTrackCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); - Mock mockUserContext = new Mock(OptimizelyMock.Object, TestUserId, userAttributes, ErrorHandlerMock.Object, LoggerMock.Object); + Mock mockUserContext = + new Mock(OptimizelyMock.Object, TestUserId, userAttributes, + ErrorHandlerMock.Object, LoggerMock.Object); mockUserContext.Setup(ouc => ouc.GetUserId()).Returns(TestUserId); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), It.IsAny())).Returns(variation); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), It.IsAny())).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, + It.IsAny(), It.IsAny())). + Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, + It.IsAny(), It.IsAny())). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); var optStronglyTyped = optly.GetObject() as Optimizely; // Adding notification listeners. var notificationType = NotificationCenter.NotificationType.Activate; - optStronglyTyped.NotificationCenter.AddNotification(notificationType, NotificationCallbackMock.Object.TestActivateCallback); - optStronglyTyped.NotificationCenter.AddNotification(notificationType, NotificationCallbackMock.Object.TestAnotherActivateCallback); + optStronglyTyped.NotificationCenter.AddNotification(notificationType, + NotificationCallbackMock.Object.TestActivateCallback); + optStronglyTyped.NotificationCenter.AddNotification(notificationType, + NotificationCallbackMock.Object.TestAnotherActivateCallback); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); @@ -2396,10 +3432,15 @@ public void TestActivateListener(UserAttributes userAttributes) optStronglyTyped.IsFeatureEnabled(featureKey, TestUserId, userAttributes); // Verify that all the registered callbacks are called once for both Activate and IsFeatureEnabled. - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Exactly(2)); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Exactly(2)); - NotificationCallbackMock.Verify(nc => nc.TestActivateCallback(experiment, TestUserId, userAttributes, variation.ResultObject, It.IsAny()), Times.Exactly(2)); - NotificationCallbackMock.Verify(nc => nc.TestAnotherActivateCallback(experiment, TestUserId, userAttributes, variation.ResultObject, It.IsAny()), Times.Exactly(2)); + NotificationCallbackMock.Verify( + nc => nc.TestActivateCallback(experiment, TestUserId, userAttributes, + variation.ResultObject, It.IsAny()), Times.Exactly(2)); + NotificationCallbackMock.Verify( + nc => nc.TestAnotherActivateCallback(experiment, TestUserId, userAttributes, + variation.ResultObject, It.IsAny()), Times.Exactly(2)); } [Test] @@ -2413,9 +3454,15 @@ public void TestTrackListenerWithAttributesWithoutEventTags() { var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; TestTrackListener(userAttributes, null); @@ -2426,14 +3473,22 @@ public void TestTrackListenerWithAttributesAndEventTags() { var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; var eventTags = new EventTags { - { "revenue", 42 } + { + "revenue", 42 + } }; TestTrackListener(userAttributes, eventTags); @@ -2445,28 +3500,39 @@ public void TestTrackListener(UserAttributes userAttributes, EventTags eventTags var variationKey = "control"; var eventKey = "purchase"; var experiment = Config.GetExperimentFromKey(experimentKey); - var variation = Result.NewResult(Config.GetVariationFromKey(experimentKey, variationKey), DecisionReasons); - var logEvent = new LogEvent("https://logx.optimizely.com/v1/events", OptimizelyHelper.SingleParameter, - "POST", new Dictionary { }); + var variation = + Result.NewResult(Config.GetVariationFromKey(experimentKey, variationKey), + DecisionReasons); + var logEvent = new LogEvent("https://logx.optimizely.com/v1/events", + OptimizelyHelper.SingleParameter, + "POST", new Dictionary()); var optly = Helper.CreatePrivateOptimizely(); var optStronglyTyped = optly.GetObject() as Optimizely; // Mocking objects. - NotificationCallbackMock.Setup(nc => nc.TestTrackCallback(It.IsAny(), It.IsAny(), + NotificationCallbackMock.Setup(nc => nc.TestTrackCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); - NotificationCallbackMock.Setup(nc => nc.TestAnotherTrackCallback(It.IsAny(), It.IsAny(), + NotificationCallbackMock.Setup(nc => nc.TestAnotherTrackCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); - Mock mockUserContext = new Mock(Optimizely, TestUserId, new UserAttributes(), ErrorHandlerMock.Object, LoggerMock.Object); + Mock mockUserContext = + new Mock(Optimizely, TestUserId, new UserAttributes(), + ErrorHandlerMock.Object, LoggerMock.Object); mockUserContext.Setup(ouc => ouc.GetUserId()).Returns(TestUserId); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config)).Returns(variation); + DecisionServiceMock. + Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config)). + Returns(variation); // Adding notification listeners. var notificationType = NotificationCenter.NotificationType.Track; - optStronglyTyped.NotificationCenter.AddNotification(notificationType, NotificationCallbackMock.Object.TestTrackCallback); - optStronglyTyped.NotificationCenter.AddNotification(notificationType, NotificationCallbackMock.Object.TestAnotherTrackCallback); + optStronglyTyped.NotificationCenter.AddNotification(notificationType, + NotificationCallbackMock.Object.TestTrackCallback); + optStronglyTyped.NotificationCenter.AddNotification(notificationType, + NotificationCallbackMock.Object.TestAnotherTrackCallback); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); @@ -2474,9 +3540,14 @@ public void TestTrackListener(UserAttributes userAttributes, EventTags eventTags optly.Invoke("Track", eventKey, TestUserId, userAttributes, eventTags); // Verify that all the registered callbacks for Track are called. - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); - NotificationCallbackMock.Verify(nc => nc.TestTrackCallback(eventKey, TestUserId, userAttributes, eventTags, It.IsAny()), Times.Exactly(1)); - NotificationCallbackMock.Verify(nc => nc.TestAnotherTrackCallback(eventKey, TestUserId, userAttributes, eventTags, It.IsAny()), Times.Exactly(1)); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestTrackCallback(eventKey, TestUserId, userAttributes, eventTags, + It.IsAny()), Times.Exactly(1)); + NotificationCallbackMock.Verify( + nc => nc.TestAnotherTrackCallback(eventKey, TestUserId, userAttributes, eventTags, + It.IsAny()), Times.Exactly(1)); } #region Decision Listener @@ -2487,72 +3558,111 @@ public void TestActivateSendsDecisionNotificationWithActualVariationKey() var experimentKey = "test_experiment"; var variationKey = "variation"; var experiment = Config.GetExperimentFromKey(experimentKey); - var variation = Result.NewResult(Config.GetVariationFromKey(experimentKey, variationKey), DecisionReasons); + var variation = + Result.NewResult(Config.GetVariationFromKey(experimentKey, variationKey), + DecisionReasons); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; // Mocking objects. - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config)).Returns(variation); + DecisionServiceMock. + Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config)). + Returns(variation); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); var optStronglyTyped = optly.GetObject() as Optimizely; - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.Invoke("Activate", experimentKey, TestUserId, userAttributes); var decisionInfo = new Dictionary { - { "experimentKey", experimentKey }, - { "variationKey", variationKey }, + { + "experimentKey", experimentKey + }, + { + "variationKey", variationKey + }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.AB_TEST, TestUserId, userAttributes, decisionInfo), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.AB_TEST, TestUserId, + userAttributes, decisionInfo), Times.Once); } [Test] - public void TestActivateSendsDecisionNotificationWithVariationKeyAndTypeFeatureTestForFeatureExperiment() + public void + TestActivateSendsDecisionNotificationWithVariationKeyAndTypeFeatureTestForFeatureExperiment() { var experimentKey = "group_experiment_1"; var variationKey = "group_exp_1_var_1"; var experiment = Config.GetExperimentFromKey(experimentKey); - var variation = Result.NewResult(Config.GetVariationFromKey(experimentKey, variationKey), DecisionReasons); + var variation = + Result.NewResult(Config.GetVariationFromKey(experimentKey, variationKey), + DecisionReasons); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; // Mocking objects. - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config)).Returns(variation); + DecisionServiceMock. + Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config)). + Returns(variation); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); var optStronglyTyped = optly.GetObject() as Optimizely; - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.Invoke("Activate", experimentKey, TestUserId, userAttributes); var decisionInfo = new Dictionary { - { "experimentKey", experimentKey }, - { "variationKey", variationKey }, + { + "experimentKey", experimentKey + }, + { + "variationKey", variationKey + }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_TEST, TestUserId, userAttributes, decisionInfo), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_TEST, TestUserId, + userAttributes, decisionInfo), Times.Once); } [Test] @@ -2564,22 +3674,34 @@ public void TestActivateSendsDecisionNotificationWithNullVariationKey() var optly = Helper.CreatePrivateOptimizely(); var optStronglyTyped = optly.GetObject() as Optimizely; - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), It.IsAny(), null)).Returns(Result.NullResult(null)); + DecisionServiceMock. + Setup(ds => ds.GetVariation(experiment, It.IsAny(), + It.IsAny(), null)). + Returns(Result.NullResult(null)); - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.Invoke("Activate", experimentKey, TestUserId, null); var decisionInfo = new Dictionary { - { "experimentKey", experimentKey }, - { "variationKey", null }, + { + "experimentKey", experimentKey + }, + { + "variationKey", null + }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.AB_TEST, TestUserId, new UserAttributes(), decisionInfo), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.AB_TEST, TestUserId, + new UserAttributes(), decisionInfo), Times.Once); } [Test] @@ -2588,12 +3710,20 @@ public void TestGetVariationSendsDecisionNotificationWithActualVariationKey() var experimentKey = "test_experiment"; var variationKey = "variation"; var experiment = Config.GetExperimentFromKey(experimentKey); - var variation = Result.NewResult(Config.GetVariationFromKey(experimentKey, variationKey), DecisionReasons); + var variation = + Result.NewResult(Config.GetVariationFromKey(experimentKey, variationKey), + DecisionReasons); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; var optly = Helper.CreatePrivateOptimizely(); @@ -2602,65 +3732,100 @@ public void TestGetVariationSendsDecisionNotificationWithActualVariationKey() var optStronglyTyped = optly.GetObject() as Optimizely; // Mocking objects. - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); - Mock mockUserContext = new Mock(optStronglyTyped, TestUserId, new UserAttributes(), ErrorHandlerMock.Object, LoggerMock.Object); + Mock mockUserContext = + new Mock(optStronglyTyped, TestUserId, new UserAttributes(), + ErrorHandlerMock.Object, LoggerMock.Object); mockUserContext.Setup(ouc => ouc.GetUserId()).Returns(TestUserId); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config)).Returns(variation); + DecisionServiceMock. + Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config)). + Returns(variation); - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.Invoke("GetVariation", experimentKey, TestUserId, userAttributes); var decisionInfo = new Dictionary { - { "experimentKey", experimentKey }, - { "variationKey", variationKey }, + { + "experimentKey", experimentKey + }, + { + "variationKey", variationKey + }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.AB_TEST, TestUserId, userAttributes, decisionInfo), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.AB_TEST, TestUserId, + userAttributes, decisionInfo), Times.Once); } [Test] - public void TestGetVariationSendsDecisionNotificationWithVariationKeyAndTypeFeatureTestForFeatureExperiment() + public void + TestGetVariationSendsDecisionNotificationWithVariationKeyAndTypeFeatureTestForFeatureExperiment() { var experimentKey = "group_experiment_1"; var variationKey = "group_exp_1_var_1"; var experiment = Config.GetExperimentFromKey(experimentKey); - var variation = Result.NewResult(Config.GetVariationFromKey(experimentKey, variationKey), DecisionReasons); + var variation = + Result.NewResult(Config.GetVariationFromKey(experimentKey, variationKey), + DecisionReasons); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; var optly = Helper.CreatePrivateOptimizely(); var optStronglyTyped = optly.GetObject() as Optimizely; // Mocking objects. - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); - Mock mockUserContext = new Mock(optStronglyTyped, TestUserId, new UserAttributes(), ErrorHandlerMock.Object, LoggerMock.Object); + Mock mockUserContext = + new Mock(optStronglyTyped, TestUserId, new UserAttributes(), + ErrorHandlerMock.Object, LoggerMock.Object); mockUserContext.Setup(ouc => ouc.GetUserId()).Returns(TestUserId); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config)).Returns(variation); + DecisionServiceMock. + Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config)). + Returns(variation); - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); optly.Invoke("GetVariation", experimentKey, TestUserId, userAttributes); var decisionInfo = new Dictionary { - { "experimentKey", experimentKey }, - { "variationKey", variationKey }, + { + "experimentKey", experimentKey + }, + { + "variationKey", variationKey + }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_TEST, TestUserId, userAttributes, decisionInfo), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_TEST, TestUserId, + userAttributes, decisionInfo), Times.Once); } [Test] @@ -2672,79 +3837,132 @@ public void TestGetVariationSendsDecisionNotificationWithNullVariationKey() var optly = Helper.CreatePrivateOptimizely(); var optStronglyTyped = optly.GetObject() as Optimizely; - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); - DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny(), It.IsAny(), It.IsAny())).Returns(Result.NullResult(null)); + DecisionServiceMock. + Setup(ds => ds.GetVariation(It.IsAny(), + It.IsAny(), It.IsAny())). + Returns(Result.NullResult(null)); //DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, Config, null)).Returns(Result.NullResult(null)); - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.Invoke("GetVariation", experimentKey, TestUserId, null); var decisionInfo = new Dictionary { - { "experimentKey", experimentKey }, - { "variationKey", null }, + { + "experimentKey", experimentKey + }, + { + "variationKey", null + }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.AB_TEST, TestUserId, new UserAttributes(), decisionInfo), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.AB_TEST, TestUserId, + new UserAttributes(), decisionInfo), Times.Once); } - public void TestIsFeatureEnabledSendsDecisionNotificationWithFeatureEnabledTrueForFeatureExperiment() + public void + TestIsFeatureEnabledSendsDecisionNotificationWithFeatureEnabledTrueForFeatureExperiment() { var featureKey = "double_single_variable_feature"; var experiment = Config.GetExperimentFromKey("test_experiment_double_feature"); - var variation = Result.NewResult(Config.GetVariationFromKey("test_experiment_double_feature", "control"), DecisionReasons); + var variation = Result.NewResult( + Config.GetVariationFromKey("test_experiment_double_feature", "control"), + DecisionReasons); var featureFlag = Config.GetFeatureFlagFromKey(featureKey); - var decision = Result.NewResult(new FeatureDecision(experiment, variation.ResultObject, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); - - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + var decision = Result.NewResult( + new FeatureDecision(experiment, variation.ResultObject, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); + + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), ConfigManager.GetConfig(), null)).Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, + It.IsAny(), ConfigManager.GetConfig(), null)). + Returns(variation); var optly = Helper.CreatePrivateOptimizely(); var optStronglyTyped = optly.GetObject() as Optimizely; - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); bool result = (bool)optly.Invoke("IsFeatureEnabled", featureKey, TestUserId, null); Assert.True(result); var decisionInfo = new Dictionary - { - { "featureKey", featureKey }, - { "featureEnabled", true }, - { "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST }, - { "sourceExperimentKey", "test_experiment_double_feature" }, - { "sourceVariationKey", "control" }, + { + { + "featureKey", featureKey + }, + { + "featureEnabled", true + }, + { + "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST + }, + { + "sourceExperimentKey", "test_experiment_double_feature" + }, + { + "sourceVariationKey", "control" + }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, TestUserId, new UserAttributes(), decisionInfo), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, TestUserId, + new UserAttributes(), decisionInfo), Times.Once); } [Test] - public void TestIsFeatureEnabledSendsDecisionNotificationWithFeatureEnabledFalseForFeatureExperiment() + public void + TestIsFeatureEnabledSendsDecisionNotificationWithFeatureEnabledFalseForFeatureExperiment() { var featureKey = "double_single_variable_feature"; var experiment = Config.GetExperimentFromKey("test_experiment_double_feature"); - var variation = Result.NewResult(Config.GetVariationFromKey("test_experiment_double_feature", "variation"), DecisionReasons); + var variation = Result.NewResult( + Config.GetVariationFromKey("test_experiment_double_feature", "variation"), + DecisionReasons); var featureFlag = Config.GetFeatureFlagFromKey(featureKey); - var decision = Result.NewResult(new FeatureDecision(experiment, variation.ResultObject, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); - - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + var decision = Result.NewResult( + new FeatureDecision(experiment, variation.ResultObject, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); + + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config, null)).Returns(variation); + DecisionServiceMock. + Setup(ds => + ds.GetVariation(experiment, It.IsAny(), Config, null)). + Returns(variation); var optly = Helper.CreatePrivateOptimizely(); var optStronglyTyped = optly.GetObject() as Optimizely; - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); @@ -2753,113 +3971,192 @@ public void TestIsFeatureEnabledSendsDecisionNotificationWithFeatureEnabledFalse var decisionInfo = new Dictionary { - { "featureKey", featureKey }, - { "featureEnabled", false }, - { "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST }, - { "sourceInfo", new Dictionary + { + "featureKey", featureKey + }, + { + "featureEnabled", false + }, + { + "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST + }, + { + "sourceInfo", new Dictionary { - { "experimentKey", "test_experiment_double_feature" }, - { "variationKey", "variation" }, + { + "experimentKey", "test_experiment_double_feature" + }, + { + "variationKey", "variation" + }, } }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, TestUserId, new UserAttributes(), It.Is>(info => TestData.CompareObjects(info, decisionInfo))), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, TestUserId, + new UserAttributes(), + It.Is>(info => + TestData.CompareObjects(info, decisionInfo))), Times.Once); } [Test] - public void TestIsFeatureEnabledSendsDecisionNotificationWithFeatureEnabledTrueForFeatureRollout() + public void + TestIsFeatureEnabledSendsDecisionNotificationWithFeatureEnabledTrueForFeatureRollout() { var featureKey = "boolean_single_variable_feature"; var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + } }; var experiment = Config.GetRolloutFromId("166660").Experiments[0]; var variation = Config.GetVariationFromKey("177770", "177771"); var featureFlag = Config.GetFeatureFlagFromKey(featureKey); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); - - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); + + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); var optStronglyTyped = optly.GetObject() as Optimizely; - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - bool result = (bool)optly.Invoke("IsFeatureEnabled", featureKey, TestUserId, userAttributes); + bool result = (bool)optly.Invoke("IsFeatureEnabled", featureKey, TestUserId, + userAttributes); Assert.True(result); var decisionInfo = new Dictionary { - { "featureKey", featureKey }, - { "featureEnabled", true }, - { "source", FeatureDecision.DECISION_SOURCE_ROLLOUT }, - { "sourceInfo", new Dictionary() }, + { + "featureKey", featureKey + }, + { + "featureEnabled", true + }, + { + "source", FeatureDecision.DECISION_SOURCE_ROLLOUT + }, + { + "sourceInfo", new Dictionary() + }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, decisionInfo))), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, TestUserId, + userAttributes, + It.Is>(info => + TestData.CompareObjects(info, decisionInfo))), Times.Once); } [Test] - public void TestIsFeatureEnabledSendsDecisionNotificationWithFeatureEnabledFalseForFeatureRollout() + public void + TestIsFeatureEnabledSendsDecisionNotificationWithFeatureEnabledFalseForFeatureRollout() { var featureKey = "boolean_single_variable_feature"; var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + } }; var experiment = Config.GetRolloutFromId("166660").Experiments[3]; var variation = Config.GetVariationFromKey("188880", "188881"); var featureFlag = Config.GetFeatureFlagFromKey(featureKey); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); - - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); + + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); var optStronglyTyped = optly.GetObject() as Optimizely; - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); - bool result = (bool)optly.Invoke("IsFeatureEnabled", featureKey, TestUserId, userAttributes); + bool result = (bool)optly.Invoke("IsFeatureEnabled", featureKey, TestUserId, + userAttributes); Assert.False(result); var decisionInfo = new Dictionary { - { "featureKey", featureKey }, - { "featureEnabled", false }, - { "source", FeatureDecision.DECISION_SOURCE_ROLLOUT }, - { "sourceInfo", new Dictionary() }, + { + "featureKey", featureKey + }, + { + "featureEnabled", false + }, + { + "source", FeatureDecision.DECISION_SOURCE_ROLLOUT + }, + { + "sourceInfo", new Dictionary() + }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, decisionInfo))), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, TestUserId, + userAttributes, + It.Is>(info => + TestData.CompareObjects(info, decisionInfo))), Times.Once); } [Test] - public void TestIsFeatureEnabledSendsDecisionNotificationWithFeatureEnabledFalseWhenUserIsNotBucketed() + public void + TestIsFeatureEnabledSendsDecisionNotificationWithFeatureEnabledFalseWhenUserIsNotBucketed() { var featureKey = "boolean_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); - var decision = Result.NewResult(new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); - - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + var decision = Result.NewResult( + new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); + + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); var optStronglyTyped = optly.GetObject() as Optimizely; - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); @@ -2868,121 +4165,265 @@ public void TestIsFeatureEnabledSendsDecisionNotificationWithFeatureEnabledFalse var decisionInfo = new Dictionary { - { "featureKey", featureKey }, - { "featureEnabled", false }, - { "source", FeatureDecision.DECISION_SOURCE_ROLLOUT }, - { "sourceInfo", new Dictionary() }, + { + "featureKey", featureKey + }, + { + "featureEnabled", false + }, + { + "source", FeatureDecision.DECISION_SOURCE_ROLLOUT + }, + { + "sourceInfo", new Dictionary() + }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, TestUserId, new UserAttributes(), It.Is>(info => TestData.CompareObjects(info, decisionInfo))), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, TestUserId, + new UserAttributes(), + It.Is>(info => + TestData.CompareObjects(info, decisionInfo))), Times.Once); } [Test] - public void TestGetEnabledFeaturesSendDecisionNotificationForBothEnabledAndDisabledFeatures() + public void + TestGetEnabledFeaturesSendDecisionNotificationForBothEnabledAndDisabledFeatures() { string[] enabledFeatures = { - "double_single_variable_feature", - "boolean_single_variable_feature", + "double_single_variable_feature", "boolean_single_variable_feature", "string_single_variable_feature" }; var userAttributes = new UserAttributes - { - { "device_type", "iPhone" }, - { "location", "San Francisco" } + { + { + "device_type", "iPhone" + }, + { + "location", "San Francisco" + } }; - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny>())); - OptimizelyMock.Object.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), + It.IsAny(), It.IsAny>())); + OptimizelyMock.Object.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); - var actualFeaturesList = OptimizelyMock.Object.GetEnabledFeatures(TestUserId, userAttributes); + var actualFeaturesList = + OptimizelyMock.Object.GetEnabledFeatures(TestUserId, userAttributes); CollectionAssert.AreEquivalent(enabledFeatures, actualFeaturesList); - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, new Dictionary { - { "featureKey", "boolean_feature" }, - { "featureEnabled", false }, - { "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST }, - { "sourceInfo", new Dictionary + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback( + DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, + It.Is>(info => TestData.CompareObjects(info, + new Dictionary { - { "experimentKey", "group_experiment_2" }, - { "variationKey", "group_exp_2_var_1" }, - } - }, - }))), Times.Once); - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, new Dictionary { - { "featureKey", "double_single_variable_feature" }, - { "featureEnabled", true }, - { "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST }, - { "sourceInfo", new Dictionary + { + "featureKey", "boolean_feature" + }, + { + "featureEnabled", false + }, + { + "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST + }, + { + "sourceInfo", new Dictionary + { + { + "experimentKey", "group_experiment_2" + }, + { + "variationKey", "group_exp_2_var_1" + }, + } + }, + }))), Times.Once); + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback( + DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, + It.Is>(info => TestData.CompareObjects(info, + new Dictionary { - { "experimentKey", "test_experiment_double_feature" }, - { "variationKey", "control" } - } - }, - }))), Times.Once); - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, new Dictionary { - { "featureKey", "integer_single_variable_feature" }, - { "featureEnabled", false }, - { "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST }, - { "sourceInfo", new Dictionary + { + "featureKey", "double_single_variable_feature" + }, + { + "featureEnabled", true + }, + { + "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST + }, + { + "sourceInfo", new Dictionary + { + { + "experimentKey", "test_experiment_double_feature" + }, + { + "variationKey", "control" + } + } + }, + }))), Times.Once); + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback( + DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, + It.Is>(info => TestData.CompareObjects(info, + new Dictionary { - { "experimentKey", "test_experiment_integer_feature" }, - { "variationKey", "control" }, - } - }, - }))), Times.Once); - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, new Dictionary { - { "featureKey", "boolean_single_variable_feature" }, - { "featureEnabled", true }, - { "source", FeatureDecision.DECISION_SOURCE_ROLLOUT }, - { "sourceInfo", new Dictionary() }, - }))), Times.Once); - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, new Dictionary { - { "featureKey", "string_single_variable_feature" }, - { "featureEnabled", true }, - { "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST }, - { "sourceInfo", new Dictionary + { + "featureKey", "integer_single_variable_feature" + }, + { + "featureEnabled", false + }, + { + "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST + }, + { + "sourceInfo", new Dictionary + { + { + "experimentKey", "test_experiment_integer_feature" + }, + { + "variationKey", "control" + }, + } + }, + }))), Times.Once); + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback( + DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, + It.Is>(info => TestData.CompareObjects(info, + new Dictionary { - { "experimentKey", "test_experiment_with_feature_rollout" }, - { "variationKey", "variation" }, - } - }, - }))), Times.Once); - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, new Dictionary { - { "featureKey", "multi_variate_feature" }, - { "featureEnabled", false }, - { "source", FeatureDecision.DECISION_SOURCE_ROLLOUT }, - { "sourceInfo", new Dictionary() }, - }))), Times.Once); - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, new Dictionary { - { "featureKey", "mutex_group_feature" }, - { "featureEnabled", false }, - { "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST }, - { "sourceInfo", new Dictionary + { + "featureKey", "boolean_single_variable_feature" + }, + { + "featureEnabled", true + }, + { + "source", FeatureDecision.DECISION_SOURCE_ROLLOUT + }, + { + "sourceInfo", new Dictionary() + }, + }))), Times.Once); + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback( + DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, + It.Is>(info => TestData.CompareObjects(info, + new Dictionary { - { "experimentKey", "group_experiment_2" }, - { "variationKey", "group_exp_2_var_1" }, - } - }, - }))), Times.Once); - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, new Dictionary { - { "featureKey", "empty_feature" }, - { "featureEnabled", false }, - { "source", FeatureDecision.DECISION_SOURCE_ROLLOUT }, - { "sourceInfo", new Dictionary() }, - }))), Times.Once); - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, new Dictionary { - { "featureKey", "no_rollout_experiment_feature" }, - { "featureEnabled", false }, - { "source", FeatureDecision.DECISION_SOURCE_ROLLOUT }, - { "sourceInfo", new Dictionary() }, - }))), Times.Once); + { + "featureKey", "string_single_variable_feature" + }, + { + "featureEnabled", true + }, + { + "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST + }, + { + "sourceInfo", new Dictionary + { + { + "experimentKey", "test_experiment_with_feature_rollout" + }, + { + "variationKey", "variation" + }, + } + }, + }))), Times.Once); + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback( + DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, + It.Is>(info => TestData.CompareObjects(info, + new Dictionary + { + { + "featureKey", "multi_variate_feature" + }, + { + "featureEnabled", false + }, + { + "source", FeatureDecision.DECISION_SOURCE_ROLLOUT + }, + { + "sourceInfo", new Dictionary() + }, + }))), Times.Once); + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback( + DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, + It.Is>(info => TestData.CompareObjects(info, + new Dictionary + { + { + "featureKey", "mutex_group_feature" + }, + { + "featureEnabled", false + }, + { + "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST + }, + { + "sourceInfo", new Dictionary + { + { + "experimentKey", "group_experiment_2" + }, + { + "variationKey", "group_exp_2_var_1" + }, + } + }, + }))), Times.Once); + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback( + DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, + It.Is>(info => TestData.CompareObjects(info, + new Dictionary + { + { + "featureKey", "empty_feature" + }, + { + "featureEnabled", false + }, + { + "source", FeatureDecision.DECISION_SOURCE_ROLLOUT + }, + { + "sourceInfo", new Dictionary() + }, + }))), Times.Once); + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback( + DecisionNotificationTypes.FEATURE, TestUserId, userAttributes, + It.Is>(info => TestData.CompareObjects(info, + new Dictionary + { + { + "featureKey", "no_rollout_experiment_feature" + }, + { + "featureEnabled", false + }, + { + "source", FeatureDecision.DECISION_SOURCE_ROLLOUT + }, + { + "sourceInfo", new Dictionary() + }, + }))), Times.Once); } [Test] - public void TestGetFeatureVariableDoubleSendsNotificationWhenUserBuckedIntoFeatureExperimentAndVariationIsToggleOn() + public void + TestGetFeatureVariableDoubleSendsNotificationWhenUserBuckedIntoFeatureExperimentAndVariationIsToggleOn() { var featureKey = "double_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); @@ -2990,10 +4431,17 @@ public void TestGetFeatureVariableDoubleSendsNotificationWhenUserBuckedIntoFeatu var expectedValue = 42.42; var experiment = Config.GetExperimentFromKey("test_experiment_double_feature"); var variation = Config.GetVariationFromKey("test_experiment_double_feature", "control"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); - - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); + + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); @@ -3002,52 +4450,94 @@ public void TestGetFeatureVariableDoubleSendsNotificationWhenUserBuckedIntoFeatu optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); - var variableValue = (double)optly.Invoke("GetFeatureVariableDouble", featureKey, variableKey, TestUserId, null); + var variableValue = (double)optly.Invoke("GetFeatureVariableDouble", featureKey, + variableKey, TestUserId, null); Assert.AreEqual(expectedValue, variableValue); var decisionInfo = new Dictionary { - { "featureKey", featureKey }, - { "featureEnabled", true }, - { "variableKey", variableKey }, - { "variableValue", expectedValue }, - { "variableType", FEATUREVARIABLE_DOUBLETYPE }, - { "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST }, - { "sourceInfo", new Dictionary + { + "featureKey", featureKey + }, + { + "featureEnabled", true + }, + { + "variableKey", variableKey + }, + { + "variableValue", expectedValue + }, + { + "variableType", FEATUREVARIABLE_DOUBLETYPE + }, + { + "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST + }, + { + "sourceInfo", new Dictionary { - { "experimentKey", "test_experiment_double_feature" }, - { "variationKey", "control" }, + { + "experimentKey", "test_experiment_double_feature" + }, + { + "variationKey", "control" + }, } }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, TestUserId, new UserAttributes(), It.Is>(info => TestData.CompareObjects(info, decisionInfo))), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, + TestUserId, new UserAttributes(), + It.Is>(info => + TestData.CompareObjects(info, decisionInfo))), Times.Once); } [Test] - public void TestGetFeatureVariableJsonSendsNotificationWhenUserBuckedIntoFeatureExperimentAndVariationIsToggleOn() + public void + TestGetFeatureVariableJsonSendsNotificationWhenUserBuckedIntoFeatureExperimentAndVariationIsToggleOn() { var featureKey = "string_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); var variableKey = "json_var"; var expectedDict = new Dictionary() { - { "int_var", 4 }, - { "string_var", "cta_4"} + { + "int_var", 4 + }, + { + "string_var", "cta_4" + } }; var experiment = Config.GetRolloutFromId("166661").Experiments[0]; var variation = Config.GetVariationFromKey(experiment.Key, "177775"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); @@ -3056,43 +4546,79 @@ public void TestGetFeatureVariableJsonSendsNotificationWhenUserBuckedIntoFeature var optStronglyTyped = optly.GetObject() as Optimizely; optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); - var variableValue = (OptimizelyJSON)optly.Invoke("GetFeatureVariableJSON", featureKey, variableKey, TestUserId, userAttributes); + var variableValue = (OptimizelyJSON)optly.Invoke("GetFeatureVariableJSON", featureKey, + variableKey, TestUserId, userAttributes); Assert.IsTrue(TestData.CompareObjects(expectedDict, variableValue.ToDictionary())); var decisionInfo = new Dictionary { - { "featureKey", featureKey }, - { "featureEnabled", true }, - { "variableKey", variableKey }, - { "variableValue", expectedDict }, - { "variableType", FEATUREVARIABLE_JSONTYPE }, - { "source", FeatureDecision.DECISION_SOURCE_ROLLOUT }, - { "sourceInfo", new Dictionary() }, + { + "featureKey", featureKey + }, + { + "featureEnabled", true + }, + { + "variableKey", variableKey + }, + { + "variableValue", expectedDict + }, + { + "variableType", FEATUREVARIABLE_JSONTYPE + }, + { + "source", FeatureDecision.DECISION_SOURCE_ROLLOUT + }, + { + "sourceInfo", new Dictionary() + }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, decisionInfo))), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, + TestUserId, userAttributes, + It.Is>(info => + TestData.CompareObjects(info, decisionInfo))), Times.Once); } [Test] - public void TestGetFeatureVariableIntegerSendsNotificationWhenUserBuckedIntoFeatureExperimentAndVariationIsToggleOn() + public void + TestGetFeatureVariableIntegerSendsNotificationWhenUserBuckedIntoFeatureExperimentAndVariationIsToggleOn() { var featureKey = "integer_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); var variableKey = "integer_variable"; var expectedValue = 13; var experiment = Config.GetExperimentFromKey("test_experiment_integer_feature"); - var variation = Config.GetVariationFromKey("test_experiment_integer_feature", "variation"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); + var variation = + Config.GetVariationFromKey("test_experiment_integer_feature", "variation"); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); @@ -3101,42 +4627,75 @@ public void TestGetFeatureVariableIntegerSendsNotificationWhenUserBuckedIntoFeat var optStronglyTyped = optly.GetObject() as Optimizely; optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); - var variableValue = (int)optly.Invoke("GetFeatureVariableInteger", featureKey, variableKey, TestUserId, userAttributes); + var variableValue = (int)optly.Invoke("GetFeatureVariableInteger", featureKey, + variableKey, TestUserId, userAttributes); Assert.AreEqual(expectedValue, variableValue); var decisionInfo = new Dictionary { - { "featureKey", featureKey }, - { "featureEnabled", true }, - { "variableKey", variableKey }, - { "variableValue", expectedValue }, - { "variableType", FEATUREVARIABLE_INTEGERTYPE }, - { "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST }, - { "sourceInfo", new Dictionary + { + "featureKey", featureKey + }, + { + "featureEnabled", true + }, + { + "variableKey", variableKey + }, + { + "variableValue", expectedValue + }, + { + "variableType", FEATUREVARIABLE_INTEGERTYPE + }, + { + "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST + }, + { + "sourceInfo", new Dictionary { - { "experimentKey", "test_experiment_integer_feature" }, - { "variationKey", "variation" }, + { + "experimentKey", "test_experiment_integer_feature" + }, + { + "variationKey", "variation" + }, } }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, decisionInfo))), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, + TestUserId, userAttributes, + It.Is>(info => + TestData.CompareObjects(info, decisionInfo))), Times.Once); } [Test] - public void TestGetFeatureVariableDoubleSendsNotificationWhenUserBuckedIntoFeatureExperimentAndVariationIsToggleOff() + public void + TestGetFeatureVariableDoubleSendsNotificationWhenUserBuckedIntoFeatureExperimentAndVariationIsToggleOff() { var featureKey = "double_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); var variableKey = "double_variable"; var expectedValue = 14.99; var experiment = Config.GetExperimentFromKey("test_experiment_double_feature"); - var variation = Config.GetVariationFromKey("test_experiment_double_feature", "variation"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); - - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), + var variation = + Config.GetVariationFromKey("test_experiment_double_feature", "variation"); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); + + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); @@ -3144,48 +4703,87 @@ public void TestGetFeatureVariableDoubleSendsNotificationWhenUserBuckedIntoFeatu optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); - var variableValue = (double)optly.Invoke("GetFeatureVariableDouble", featureKey, variableKey, TestUserId, null); + var variableValue = (double)optly.Invoke("GetFeatureVariableDouble", featureKey, + variableKey, TestUserId, null); Assert.AreEqual(expectedValue, variableValue); var decisionInfo = new Dictionary { - { "featureKey", featureKey }, - { "featureEnabled", false }, - { "variableKey", variableKey }, - { "variableValue", expectedValue }, - { "variableType", FEATUREVARIABLE_DOUBLETYPE }, - { "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST }, - { "sourceInfo", new Dictionary - { - { "experimentKey", "test_experiment_double_feature" }, - { "variationKey", "variation" }, - } + { + "featureKey", featureKey }, - }; - - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, TestUserId, new UserAttributes(), It.Is>(info => TestData.CompareObjects(info, decisionInfo))), Times.Once); - } - - [Test] - public void TestGetFeatureVariableIntegerSendsNotificationWhenUserBuckedIntoFeatureExperimentAndVariationIsToggleOff() - { - var featureKey = "integer_single_variable_feature"; - var featureFlag = Config.GetFeatureFlagFromKey(featureKey); + { + "featureEnabled", false + }, + { + "variableKey", variableKey + }, + { + "variableValue", expectedValue + }, + { + "variableType", FEATUREVARIABLE_DOUBLETYPE + }, + { + "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST + }, + { + "sourceInfo", new Dictionary + { + { + "experimentKey", "test_experiment_double_feature" + }, + { + "variationKey", "variation" + }, + } + }, + }; + + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, + TestUserId, new UserAttributes(), + It.Is>(info => + TestData.CompareObjects(info, decisionInfo))), Times.Once); + } + + [Test] + public void + TestGetFeatureVariableIntegerSendsNotificationWhenUserBuckedIntoFeatureExperimentAndVariationIsToggleOff() + { + var featureKey = "integer_single_variable_feature"; + var featureFlag = Config.GetFeatureFlagFromKey(featureKey); var variableKey = "integer_variable"; var expectedValue = 7; var experiment = Config.GetExperimentFromKey("test_experiment_integer_feature"); - var variation = Config.GetVariationFromKey("test_experiment_integer_feature", "control"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); + var variation = + Config.GetVariationFromKey("test_experiment_integer_feature", "control"); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST), DecisionReasons); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); @@ -3193,31 +4791,56 @@ public void TestGetFeatureVariableIntegerSendsNotificationWhenUserBuckedIntoFeat var optStronglyTyped = optly.GetObject() as Optimizely; optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); - var variableValue = (int)optly.Invoke("GetFeatureVariableInteger", featureKey, variableKey, TestUserId, userAttributes); + var variableValue = (int)optly.Invoke("GetFeatureVariableInteger", featureKey, + variableKey, TestUserId, userAttributes); Assert.AreEqual(expectedValue, variableValue); var decisionInfo = new Dictionary { - { "featureKey", featureKey }, - { "featureEnabled", false }, - { "variableKey", variableKey }, - { "variableValue", expectedValue }, - { "variableType", FEATUREVARIABLE_INTEGERTYPE }, - { "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST }, - { "sourceInfo", new Dictionary + { + "featureKey", featureKey + }, + { + "featureEnabled", false + }, + { + "variableKey", variableKey + }, + { + "variableValue", expectedValue + }, + { + "variableType", FEATUREVARIABLE_INTEGERTYPE + }, + { + "source", FeatureDecision.DECISION_SOURCE_FEATURE_TEST + }, + { + "sourceInfo", new Dictionary { - { "experimentKey", "test_experiment_integer_feature" }, - { "variationKey", "control" }, + { + "experimentKey", "test_experiment_integer_feature" + }, + { + "variationKey", "control" + }, } }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, decisionInfo))), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, + TestUserId, userAttributes, + It.Is>(info => + TestData.CompareObjects(info, decisionInfo))), Times.Once); } [Test] - public void TestGetFeatureVariableBooleanSendsNotificationWhenUserBuckedIntoRolloutAndVariationIsToggleOn() + public void + TestGetFeatureVariableBooleanSendsNotificationWhenUserBuckedIntoRolloutAndVariationIsToggleOn() { var featureKey = "boolean_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); @@ -3225,10 +4848,17 @@ public void TestGetFeatureVariableBooleanSendsNotificationWhenUserBuckedIntoRoll var expectedValue = true; var experiment = Config.GetRolloutFromId("166660").Experiments[0]; var variation = Config.GetVariationFromKey(experiment.Key, "177771"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); - - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); + + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); @@ -3236,26 +4866,48 @@ public void TestGetFeatureVariableBooleanSendsNotificationWhenUserBuckedIntoRoll var optStronglyTyped = optly.GetObject() as Optimizely; optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); - var variableValue = (bool)optly.Invoke("GetFeatureVariableBoolean", featureKey, variableKey, TestUserId, null); + var variableValue = (bool)optly.Invoke("GetFeatureVariableBoolean", featureKey, + variableKey, TestUserId, null); Assert.AreEqual(expectedValue, variableValue); var decisionInfo = new Dictionary { - { "featureKey", featureKey }, - { "featureEnabled", true }, - { "variableKey", variableKey }, - { "variableValue", expectedValue }, - { "variableType", FEATUREVARIABLE_BOOLEANTYPE }, - { "source", FeatureDecision.DECISION_SOURCE_ROLLOUT }, - { "sourceInfo", new Dictionary() }, + { + "featureKey", featureKey + }, + { + "featureEnabled", true + }, + { + "variableKey", variableKey + }, + { + "variableValue", expectedValue + }, + { + "variableType", FEATUREVARIABLE_BOOLEANTYPE + }, + { + "source", FeatureDecision.DECISION_SOURCE_ROLLOUT + }, + { + "sourceInfo", new Dictionary() + }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, TestUserId, new UserAttributes(), It.Is>(info => TestData.CompareObjects(info, decisionInfo))), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, + TestUserId, new UserAttributes(), + It.Is>(info => + TestData.CompareObjects(info, decisionInfo))), Times.Once); } [Test] - public void TestGetFeatureVariableStringSendsNotificationWhenUserBuckedIntoRolloutAndVariationIsToggleOn() + public void + TestGetFeatureVariableStringSendsNotificationWhenUserBuckedIntoRolloutAndVariationIsToggleOn() { var featureKey = "string_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); @@ -3263,16 +4915,29 @@ public void TestGetFeatureVariableStringSendsNotificationWhenUserBuckedIntoRollo var expectedValue = "cta_4"; var experiment = Config.GetRolloutFromId("166661").Experiments[0]; var variation = Config.GetVariationFromKey(experiment.Key, "177775"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); @@ -3281,26 +4946,48 @@ public void TestGetFeatureVariableStringSendsNotificationWhenUserBuckedIntoRollo var optStronglyTyped = optly.GetObject() as Optimizely; optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); - var variableValue = (string)optly.Invoke("GetFeatureVariableString", featureKey, variableKey, TestUserId, userAttributes); + var variableValue = (string)optly.Invoke("GetFeatureVariableString", featureKey, + variableKey, TestUserId, userAttributes); Assert.AreEqual(expectedValue, variableValue); var decisionInfo = new Dictionary { - { "featureKey", featureKey }, - { "featureEnabled", true }, - { "variableKey", variableKey }, - { "variableValue", expectedValue }, - { "variableType", FEATUREVARIABLE_STRINGTYPE }, - { "source", FeatureDecision.DECISION_SOURCE_ROLLOUT }, - { "sourceInfo", new Dictionary() }, + { + "featureKey", featureKey + }, + { + "featureEnabled", true + }, + { + "variableKey", variableKey + }, + { + "variableValue", expectedValue + }, + { + "variableType", FEATUREVARIABLE_STRINGTYPE + }, + { + "source", FeatureDecision.DECISION_SOURCE_ROLLOUT + }, + { + "sourceInfo", new Dictionary() + }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, decisionInfo))), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, + TestUserId, userAttributes, + It.Is>(info => + TestData.CompareObjects(info, decisionInfo))), Times.Once); } [Test] - public void TestGetFeatureVariableBooleanSendsNotificationWhenUserBuckedIntoRolloutAndVariationIsToggleOff() + public void + TestGetFeatureVariableBooleanSendsNotificationWhenUserBuckedIntoRolloutAndVariationIsToggleOff() { var featureKey = "boolean_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); @@ -3308,10 +4995,17 @@ public void TestGetFeatureVariableBooleanSendsNotificationWhenUserBuckedIntoRoll var expectedValue = true; var experiment = Config.GetRolloutFromId("166660").Experiments[3]; var variation = Config.GetVariationFromKey(experiment.Key, "177782"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); - - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); + + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); @@ -3319,26 +5013,48 @@ public void TestGetFeatureVariableBooleanSendsNotificationWhenUserBuckedIntoRoll optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); - var variableValue = (bool)optly.Invoke("GetFeatureVariableBoolean", featureKey, variableKey, TestUserId, null); + var variableValue = (bool)optly.Invoke("GetFeatureVariableBoolean", featureKey, + variableKey, TestUserId, null); Assert.AreEqual(expectedValue, variableValue); var decisionInfo = new Dictionary { - { "featureKey", featureKey }, - { "featureEnabled", false }, - { "variableKey", variableKey }, - { "variableValue", expectedValue }, - { "variableType", FEATUREVARIABLE_BOOLEANTYPE }, - { "source", FeatureDecision.DECISION_SOURCE_ROLLOUT }, - { "sourceInfo", new Dictionary()}, + { + "featureKey", featureKey + }, + { + "featureEnabled", false + }, + { + "variableKey", variableKey + }, + { + "variableValue", expectedValue + }, + { + "variableType", FEATUREVARIABLE_BOOLEANTYPE + }, + { + "source", FeatureDecision.DECISION_SOURCE_ROLLOUT + }, + { + "sourceInfo", new Dictionary() + }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, TestUserId, new UserAttributes(), It.Is>(info => TestData.CompareObjects(info, decisionInfo))), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, + TestUserId, new UserAttributes(), + It.Is>(info => + TestData.CompareObjects(info, decisionInfo))), Times.Once); } [Test] - public void TestGetFeatureVariableStringSendsNotificationWhenUserBuckedIntoRolloutAndVariationIsToggleOff() + public void + TestGetFeatureVariableStringSendsNotificationWhenUserBuckedIntoRolloutAndVariationIsToggleOff() { var featureKey = "string_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); @@ -3346,16 +5062,29 @@ public void TestGetFeatureVariableStringSendsNotificationWhenUserBuckedIntoRollo var expectedValue = "wingardium leviosa"; var experiment = Config.GetRolloutFromId("166661").Experiments[2]; var variation = Config.GetVariationFromKey(experiment.Key, "177784"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); @@ -3363,35 +5092,64 @@ public void TestGetFeatureVariableStringSendsNotificationWhenUserBuckedIntoRollo var optStronglyTyped = optly.GetObject() as Optimizely; optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); - var variableValue = (string)optly.Invoke("GetFeatureVariableString", featureKey, variableKey, TestUserId, userAttributes); + var variableValue = (string)optly.Invoke("GetFeatureVariableString", featureKey, + variableKey, TestUserId, userAttributes); Assert.AreEqual(expectedValue, variableValue); var decisionInfo = new Dictionary { - { "featureKey", featureKey }, - { "featureEnabled", false }, - { "variableKey", variableKey }, - { "variableValue", expectedValue }, - { "variableType", FEATUREVARIABLE_STRINGTYPE }, - { "source", FeatureDecision.DECISION_SOURCE_ROLLOUT }, - { "sourceInfo", new Dictionary() }, + { + "featureKey", featureKey + }, + { + "featureEnabled", false + }, + { + "variableKey", variableKey + }, + { + "variableValue", expectedValue + }, + { + "variableType", FEATUREVARIABLE_STRINGTYPE + }, + { + "source", FeatureDecision.DECISION_SOURCE_ROLLOUT + }, + { + "sourceInfo", new Dictionary() + }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, decisionInfo))), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, + TestUserId, userAttributes, + It.Is>(info => + TestData.CompareObjects(info, decisionInfo))), Times.Once); } [Test] - public void TestGetFeatureVariableDoubleSendsNotificationWhenUserNotBuckedIntoBothFeatureExperimentAndRollout() + public void + TestGetFeatureVariableDoubleSendsNotificationWhenUserNotBuckedIntoBothFeatureExperimentAndRollout() { var featureKey = "double_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey("double_single_variable_feature"); var variableKey = "double_variable"; var expectedValue = 14.99; - var decision = Result.NewResult(new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); - - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), + var decision = Result.NewResult( + new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); + + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); @@ -3399,56 +5157,104 @@ public void TestGetFeatureVariableDoubleSendsNotificationWhenUserNotBuckedIntoBo optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); - var variableValue = (double)optly.Invoke("GetFeatureVariableDouble", featureKey, variableKey, TestUserId, null); + var variableValue = (double)optly.Invoke("GetFeatureVariableDouble", featureKey, + variableKey, TestUserId, null); Assert.AreEqual(expectedValue, variableValue); var decisionInfo = new Dictionary { - { "featureKey", featureKey }, - { "featureEnabled", false }, - { "variableKey", variableKey }, - { "variableValue", expectedValue }, - { "variableType", FEATUREVARIABLE_DOUBLETYPE }, - { "source", FeatureDecision.DECISION_SOURCE_ROLLOUT }, - { "sourceInfo", new Dictionary() }, + { + "featureKey", featureKey + }, + { + "featureEnabled", false + }, + { + "variableKey", variableKey + }, + { + "variableValue", expectedValue + }, + { + "variableType", FEATUREVARIABLE_DOUBLETYPE + }, + { + "source", FeatureDecision.DECISION_SOURCE_ROLLOUT + }, + { + "sourceInfo", new Dictionary() + }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, TestUserId, new UserAttributes(), It.Is>(info => TestData.CompareObjects(info, decisionInfo))), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE_VARIABLE, + TestUserId, new UserAttributes(), + It.Is>(info => + TestData.CompareObjects(info, decisionInfo))), Times.Once); } [Test] - public void TestGetAllFeatureVariablesSendsNotificationWhenUserBucketIntoRolloutAndVariationIsToggleOn() + public void + TestGetAllFeatureVariablesSendsNotificationWhenUserBucketIntoRolloutAndVariationIsToggleOn() { var featureKey = "string_single_variable_feature"; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); - var expectedValue = new Dictionary() { - { "string_variable", "cta_4" }, - { "json_var", new Dictionary() + var expectedValue = new Dictionary() + { + { + "string_variable", "cta_4" + }, + { + "json_var", new Dictionary() { - { "int_var", 4 }, - { "string_var", "cta_4" } + { + "int_var", 4 + }, + { + "string_var", "cta_4" + } } }, - { "true_json_var", new Dictionary() + { + "true_json_var", new Dictionary() { - { "int_var", 5 }, - { "string_var", "cta_5" } + { + "int_var", 5 + }, + { + "string_var", "cta_5" + } } } }; var experiment = Config.GetRolloutFromId("166661").Experiments[0]; var variation = Config.GetVariationFromKey(experiment.Key, "177775"); - var decision = Result.NewResult(new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); @@ -3457,20 +5263,37 @@ public void TestGetAllFeatureVariablesSendsNotificationWhenUserBucketIntoRollout var optStronglyTyped = optly.GetObject() as Optimizely; optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + optStronglyTyped.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); - var variableValues = (OptimizelyJSON)optly.Invoke("GetAllFeatureVariables", featureKey, TestUserId, userAttributes); + var variableValues = (OptimizelyJSON)optly.Invoke("GetAllFeatureVariables", featureKey, + TestUserId, userAttributes); Assert.IsTrue(TestData.CompareObjects(variableValues.ToDictionary(), expectedValue)); var decisionInfo = new Dictionary { - { "featureKey", featureKey }, - { "featureEnabled", true }, - { "variableValues", expectedValue }, - { "source", FeatureDecision.DECISION_SOURCE_ROLLOUT }, - { "sourceInfo", new Dictionary() }, + { + "featureKey", featureKey + }, + { + "featureEnabled", true + }, + { + "variableValues", expectedValue + }, + { + "source", FeatureDecision.DECISION_SOURCE_ROLLOUT + }, + { + "sourceInfo", new Dictionary() + }, }; - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.ALL_FEATURE_VARIABLE, TestUserId, userAttributes, It.Is>(info => TestData.CompareObjects(info, decisionInfo))), Times.Once); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.ALL_FEATURE_VARIABLE, + TestUserId, userAttributes, + It.Is>(info => + TestData.CompareObjects(info, decisionInfo))), Times.Once); } #endregion Decision Listener @@ -3482,23 +5305,34 @@ public void TestGetAllFeatureVariablesReturnsNullScenarios() { var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; - var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, + LoggerMock.Object, ErrorHandlerMock.Object); // Null Feature flag key var result = optimizely.GetAllFeatureVariables(null, TestUserId, userAttributes); Assert.Null(result); - LoggerMock.Verify(log => log.Log(LogLevel.WARN, "The featureKey parameter must be nonnull."), Times.Once); + LoggerMock.Verify( + log => log.Log(LogLevel.WARN, "The featureKey parameter must be nonnull."), + Times.Once); // Null User ID - var result2 = optimizely.GetAllFeatureVariables("string_single_variable_feature", null, userAttributes); + var result2 = optimizely.GetAllFeatureVariables("string_single_variable_feature", null, + userAttributes); Assert.Null(result2); - LoggerMock.Verify(log => log.Log(LogLevel.WARN, "The userId parameter must be nonnull."), Times.Once); + LoggerMock.Verify( + log => log.Log(LogLevel.WARN, "The userId parameter must be nonnull."), Times.Once); // Invalid featureKey var featureKey = "InvalidFeatureKey"; @@ -3506,16 +5340,22 @@ public void TestGetAllFeatureVariablesReturnsNullScenarios() var result3 = optimizely.GetAllFeatureVariables(featureKey, TestUserId, userAttributes); Assert.Null(result3); - LoggerMock.Verify(log => log.Log(LogLevel.INFO, "No feature flag was found for key \"" + featureKey + "\"."), Times.Once); + LoggerMock.Verify( + log => log.Log(LogLevel.INFO, + "No feature flag was found for key \"" + featureKey + "\"."), Times.Once); // Null Optimizely config var invalidOptly = new Optimizely("Random datafile", null, LoggerMock.Object); - var result4 = invalidOptly.GetAllFeatureVariables("validFeatureKey", TestUserId, userAttributes); + var result4 = + invalidOptly.GetAllFeatureVariables("validFeatureKey", TestUserId, userAttributes); Assert.Null(result4); - LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Optimizely instance is not valid, failing getAllFeatureVariableValues call. type"), Times.Once); + LoggerMock.Verify( + log => log.Log(LogLevel.ERROR, + "Optimizely instance is not valid, failing getAllFeatureVariableValues call. type"), + Times.Once); } [Test] @@ -3526,45 +5366,72 @@ public void TestGetAllFeatureVariablesRollout() var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "company", "Optimizely" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "company", "Optimizely" + }, + { + "location", "San Francisco" + } }; var featureFlag = Config.GetFeatureFlagFromKey(featureKey); - var decision = Result.NewResult(new FeatureDecision(experiment, null, FeatureDecision.DECISION_SOURCE_ROLLOUT), DecisionReasons); + var decision = Result.NewResult( + new FeatureDecision(experiment, null, FeatureDecision.DECISION_SOURCE_ROLLOUT), + DecisionReasons); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), Config)).Returns(decision); + DecisionServiceMock. + Setup(ds => + ds.GetVariationForFeature(featureFlag, It.IsAny(), + Config)). + Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); - var result = (OptimizelyJSON)optly.Invoke("GetAllFeatureVariables", featureKey, TestUserId, userAttributes); + var result = (OptimizelyJSON)optly.Invoke("GetAllFeatureVariables", featureKey, + TestUserId, userAttributes); Assert.NotNull(result); - LoggerMock.Verify(log => log.Log(LogLevel.INFO, "Feature \"" + featureKey + "\" is not enabled for user \"" + TestUserId + "\""), Times.Once); + LoggerMock.Verify( + log => log.Log(LogLevel.INFO, + "Feature \"" + featureKey + "\" is not enabled for user \"" + TestUserId + + "\""), Times.Once); - LoggerMock.Verify(log => log.Log(LogLevel.INFO, "User \"" + TestUserId + "\" was not bucketed into any variation for feature flag \"" + featureKey + "\". " + - "The default values are being returned."), Times.Once); + LoggerMock.Verify(log => log.Log(LogLevel.INFO, + "User \"" + TestUserId + + "\" was not bucketed into any variation for feature flag \"" + featureKey + "\". " + + "The default values are being returned."), Times.Once); } [Test] public void TestGetAllFeatureVariablesSourceFeatureTest() { var featureKey = "double_single_variable_feature"; - var expectedValue = new Dictionary() { - { "double_variable", 42.42} + var expectedValue = new Dictionary() + { + { + "double_variable", 42.42 + } }; - var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, + LoggerMock.Object, ErrorHandlerMock.Object); var variableValues = optimizely.GetAllFeatureVariables(featureKey, TestUserId, null); Assert.IsTrue(TestData.CompareObjects(variableValues.ToDictionary(), expectedValue)); - LoggerMock.Verify(log => log.Log(LogLevel.INFO, "Feature \"" + featureKey + "\" is enabled for user \"" + TestUserId + "\""), Times.Once); + LoggerMock.Verify( + log => log.Log(LogLevel.INFO, + "Feature \"" + featureKey + "\" is enabled for user \"" + TestUserId + "\""), + Times.Once); - LoggerMock.Verify(log => log.Log(LogLevel.INFO, "User \"" + TestUserId + "\" was not bucketed into any variation for feature flag \"" + featureKey + "\". " + - "The default values are being returned."), Times.Never); + LoggerMock.Verify(log => log.Log(LogLevel.INFO, + "User \"" + TestUserId + + "\" was not bucketed into any variation for feature flag \"" + featureKey + "\". " + + "The default values are being returned."), Times.Never); } #endregion Test GetAllFeatureVariables @@ -3575,23 +5442,26 @@ public void TestGetAllFeatureVariablesSourceFeatureTest() public void TestDFMNotificationWhenProjectConfigIsUpdated() { var httpClientMock = new Mock(); - var t = TestHttpProjectConfigManagerUtil.MockSendAsync(httpClientMock, TestData.Datafile, TimeSpan.FromMilliseconds(300)); + var t = TestHttpProjectConfigManagerUtil.MockSendAsync(httpClientMock, + TestData.Datafile, TimeSpan.FromMilliseconds(300)); TestHttpProjectConfigManagerUtil.SetClientFieldValue(httpClientMock.Object); NotificationCenter notificationCenter = new NotificationCenter(); NotificationCallbackMock.Setup(notification => notification.TestConfigUpdateCallback()); - var httpManager = new HttpProjectConfigManager.Builder() - .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") - .WithLogger(LoggerMock.Object) - .WithPollingInterval(TimeSpan.FromMilliseconds(1000)) - .WithStartByDefault(false) - .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)) - .WithNotificationCenter(notificationCenter) - .Build(true); + var httpManager = new HttpProjectConfigManager.Builder(). + WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). + WithLogger(LoggerMock.Object). + WithPollingInterval(TimeSpan.FromMilliseconds(1000)). + WithStartByDefault(false). + WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)). + WithNotificationCenter(notificationCenter). + Build(true); var optimizely = new Optimizely(httpManager, notificationCenter); - optimizely.NotificationCenter.AddNotification(NotificationCenter.NotificationType.OptimizelyConfigUpdate, NotificationCallbackMock.Object.TestConfigUpdateCallback); + optimizely.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.OptimizelyConfigUpdate, + NotificationCallbackMock.Object.TestConfigUpdateCallback); httpManager.Start(); // wait till 10 seconds max, to avoid stale state in worst case. @@ -3608,16 +5478,18 @@ public void TestDFMWhenDatafileProvidedDoesNotNotifyWithoutStart() var httpClientMock = new Mock(); TestHttpProjectConfigManagerUtil.SetClientFieldValue(httpClientMock.Object); - var httpManager = new HttpProjectConfigManager.Builder() - .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") - .WithDatafile(TestData.Datafile) - .WithLogger(LoggerMock.Object) - .WithPollingInterval(TimeSpan.FromMilliseconds(1000)) - .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)) - .Build(); + var httpManager = new HttpProjectConfigManager.Builder(). + WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). + WithDatafile(TestData.Datafile). + WithLogger(LoggerMock.Object). + WithPollingInterval(TimeSpan.FromMilliseconds(1000)). + WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)). + Build(); var optimizely = new Optimizely(httpManager); - optimizely.NotificationCenter.AddNotification(NotificationCenter.NotificationType.OptimizelyConfigUpdate, NotificationCallbackMock.Object.TestConfigUpdateCallback); + optimizely.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.OptimizelyConfigUpdate, + NotificationCallbackMock.Object.TestConfigUpdateCallback); // added 10 secs max wait to avoid stale state. httpManager.OnReady().Wait(10000); @@ -3639,7 +5511,9 @@ public void TestGetEnabledFeaturesWithInvalidDatafile() Assert.IsEmpty(optly.GetEnabledFeatures("some_user", null)); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetEnabledFeatures'."), Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.ERROR, + "Datafile has invalid format. Failing 'GetEnabledFeatures'."), Times.Once); } [Test] @@ -3647,11 +5521,18 @@ public void TestGetEnabledFeaturesWithNoFeatureEnabledForUser() { var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "location", "San Francisco" + } }; - OptimizelyMock.Setup(om => om.IsFeatureEnabled(It.IsAny(), TestUserId, It.IsAny())).Returns(false); + OptimizelyMock.Setup(om => + om.IsFeatureEnabled(It.IsAny(), TestUserId, + It.IsAny())). + Returns(false); Assert.IsEmpty(OptimizelyMock.Object.GetEnabledFeatures(TestUserId, userAttributes)); } @@ -3660,35 +5541,40 @@ public void TestGetEnabledFeaturesWithSomeFeaturesEnabledForUser() { string[] enabledFeatures = { - "boolean_feature", - "double_single_variable_feature", - "string_single_variable_feature", - "multi_variate_feature", - "empty_feature" + "boolean_feature", "double_single_variable_feature", + "string_single_variable_feature", "multi_variate_feature", "empty_feature" }; string[] notEnabledFeatures = { - "integer_single_variable_feature", - "boolean_single_variable_feature", - "mutex_group_feature", - "no_rollout_experiment_feature" + "integer_single_variable_feature", "boolean_single_variable_feature", + "mutex_group_feature", "no_rollout_experiment_feature" }; var userAttributes = new UserAttributes { - { "device_type", "iPhone" }, - { "location", "San Francisco" } + { + "device_type", "iPhone" + }, + { + "location", "San Francisco" + } }; - OptimizelyMock.Setup(om => om.IsFeatureEnabled(It.IsIn(enabledFeatures), TestUserId, - It.IsAny())).Returns(true); - OptimizelyMock.Setup(om => om.IsFeatureEnabled(It.IsIn(notEnabledFeatures), TestUserId, - It.IsAny())).Returns(false); + OptimizelyMock.Setup(om => om.IsFeatureEnabled(It.IsIn(enabledFeatures), + TestUserId, + It.IsAny())). + Returns(true); + OptimizelyMock.Setup(om => om.IsFeatureEnabled(It.IsIn(notEnabledFeatures), + TestUserId, + It.IsAny())). + Returns(false); - var actualFeaturesList = OptimizelyMock.Object.GetEnabledFeatures(TestUserId, userAttributes); + var actualFeaturesList = + OptimizelyMock.Object.GetEnabledFeatures(TestUserId, userAttributes); // Verify that the returned feature list contains only enabledFeatures. CollectionAssert.AreEquivalent(enabledFeatures, actualFeaturesList); - Array.ForEach(notEnabledFeatures, nef => CollectionAssert.DoesNotContain(actualFeaturesList, nef)); + Array.ForEach(notEnabledFeatures, + nef => CollectionAssert.DoesNotContain(actualFeaturesList, nef)); } #endregion Test GetEnabledFeatures @@ -3700,10 +5586,20 @@ public void TestValidateStringInputsWithValidValues() { var optly = Helper.CreatePrivateOptimizely(); - bool result = (bool)optly.Invoke("ValidateStringInputs", new Dictionary { { Optimizely.EXPERIMENT_KEY, "test_experiment" } }); + bool result = (bool)optly.Invoke("ValidateStringInputs", new Dictionary + { + { + Optimizely.EXPERIMENT_KEY, "test_experiment" + } + }); Assert.True(result); - result = (bool)optly.Invoke("ValidateStringInputs", new Dictionary { { Optimizely.EVENT_KEY, "buy_now_event" } }); + result = (bool)optly.Invoke("ValidateStringInputs", new Dictionary + { + { + Optimizely.EVENT_KEY, "buy_now_event" + } + }); Assert.True(result); } @@ -3712,10 +5608,20 @@ public void TestValidateStringInputsWithInvalidValues() { var optly = Helper.CreatePrivateOptimizely(); - bool result = (bool)optly.Invoke("ValidateStringInputs", new Dictionary { { Optimizely.EXPERIMENT_KEY, "" } }); + bool result = (bool)optly.Invoke("ValidateStringInputs", new Dictionary + { + { + Optimizely.EXPERIMENT_KEY, "" + } + }); Assert.False(result); - result = (bool)optly.Invoke("ValidateStringInputs", new Dictionary { { Optimizely.EVENT_KEY, null } }); + result = (bool)optly.Invoke("ValidateStringInputs", new Dictionary + { + { + Optimizely.EVENT_KEY, null + } + }); Assert.False(result); } @@ -3724,13 +5630,28 @@ public void TestValidateStringInputsWithUserId() { var optly = Helper.CreatePrivateOptimizely(); - bool result = (bool)optly.Invoke("ValidateStringInputs", new Dictionary { { Optimizely.USER_ID, "testUser" } }); + bool result = (bool)optly.Invoke("ValidateStringInputs", new Dictionary + { + { + Optimizely.USER_ID, "testUser" + } + }); Assert.True(result); - result = (bool)optly.Invoke("ValidateStringInputs", new Dictionary { { Optimizely.USER_ID, "" } }); + result = (bool)optly.Invoke("ValidateStringInputs", new Dictionary + { + { + Optimizely.USER_ID, "" + } + }); Assert.True(result); - result = (bool)optly.Invoke("ValidateStringInputs", new Dictionary { { Optimizely.USER_ID, null } }); + result = (bool)optly.Invoke("ValidateStringInputs", new Dictionary + { + { + Optimizely.USER_ID, null + } + }); Assert.False(result); } @@ -3739,13 +5660,19 @@ public void TestActivateValidateInputValues() { // Verify that ValidateStringInputs does not log error for valid values. var variation = Optimizely.Activate("test_experiment", "test_user"); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided User Id is in invalid format."), Times.Never); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided Experiment Key is in invalid format."), Times.Never); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided User Id is in invalid format."), + Times.Never); + LoggerMock.Verify( + l => l.Log(LogLevel.ERROR, "Provided Experiment Key is in invalid format."), + Times.Never); // Verify that ValidateStringInputs logs error for invalid values. variation = Optimizely.Activate("", null); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided User Id is in invalid format."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided Experiment Key is in invalid format."), Times.Once); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided User Id is in invalid format."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.ERROR, "Provided Experiment Key is in invalid format."), + Times.Once); } [Test] @@ -3753,13 +5680,19 @@ public void TestGetVariationValidateInputValues() { // Verify that ValidateStringInputs does not log error for valid values. var variation = Optimizely.GetVariation("test_experiment", "test_user"); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided User Id is in invalid format."), Times.Never); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided Experiment Key is in invalid format."), Times.Never); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided User Id is in invalid format."), + Times.Never); + LoggerMock.Verify( + l => l.Log(LogLevel.ERROR, "Provided Experiment Key is in invalid format."), + Times.Never); // Verify that ValidateStringInputs logs error for invalid values. variation = Optimizely.GetVariation("", null); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided User Id is in invalid format."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided Experiment Key is in invalid format."), Times.Once); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided User Id is in invalid format."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.ERROR, "Provided Experiment Key is in invalid format."), + Times.Once); } [Test] @@ -3767,13 +5700,18 @@ public void TestTrackValidateInputValues() { // Verify that ValidateStringInputs does not log error for valid values. Optimizely.Track("purchase", "test_user"); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided User Id is in invalid format."), Times.Never); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided Event Key is in invalid format."), Times.Never); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided User Id is in invalid format."), + Times.Never); + LoggerMock.Verify( + l => l.Log(LogLevel.ERROR, "Provided Event Key is in invalid format."), + Times.Never); // Verify that ValidateStringInputs logs error for invalid values. Optimizely.Track("", null); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided User Id is in invalid format."), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided Event Key is in invalid format."), Times.Once); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided User Id is in invalid format."), + Times.Once); + LoggerMock.Verify( + l => l.Log(LogLevel.ERROR, "Provided Event Key is in invalid format."), Times.Once); } #endregion Test ValidateStringInputs @@ -3783,33 +5721,45 @@ public void TestTrackValidateInputValues() [Test] public void TestActivateWithTypedAudiences() { - var variation = OptimizelyWithTypedAudiences.Activate("typed_audience_experiment", "user1", new UserAttributes - { - { "house", "Gryffindor" } - }); + var variation = OptimizelyWithTypedAudiences.Activate("typed_audience_experiment", + "user1", new UserAttributes + { + { + "house", "Gryffindor" + } + }); Assert.AreEqual("A", variation.Key); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Once); - variation = OptimizelyWithTypedAudiences.Activate("typed_audience_experiment", "user1", new UserAttributes - { - { "lasers", 45.5 } - }); + variation = OptimizelyWithTypedAudiences.Activate("typed_audience_experiment", "user1", + new UserAttributes + { + { + "lasers", 45.5 + } + }); Assert.AreEqual("A", variation.Key); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Exactly(2)); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Exactly(2)); } [Test] public void TestActivateExcludeUserFromExperimentWithTypedAudiences() { - var variation = OptimizelyWithTypedAudiences.Activate("typed_audience_experiment", "user1", new UserAttributes - { - { "house", "Hufflepuff" } - }); + var variation = OptimizelyWithTypedAudiences.Activate("typed_audience_experiment", + "user1", new UserAttributes + { + { + "house", "Hufflepuff" + } + }); Assert.Null(variation); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Never); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Never); } [Test] @@ -3817,37 +5767,50 @@ public void TestTrackWithTypedAudiences() { OptimizelyWithTypedAudiences.Track("item_bought", "user1", new UserAttributes { - { "house", "Welcome to Slytherin!" } + { + "house", "Welcome to Slytherin!" + } }); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Once); } [Test] - public void TestTrackDoesNotExcludeUserFromExperimentWhenAttributesMismatchWithTypedAudiences() + public void + TestTrackDoesNotExcludeUserFromExperimentWhenAttributesMismatchWithTypedAudiences() { OptimizelyWithTypedAudiences.Track("item_bought", "user1", new UserAttributes { - { "house", "Hufflepuff" } + { + "house", "Hufflepuff" + } }); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Once); } [Test] public void TestIsFeatureEnabledWithTypedAudiences() { - var featureEnabled = OptimizelyWithTypedAudiences.IsFeatureEnabled("feat_no_vars", "user1", new UserAttributes - { - { "favorite_ice_cream", "chocolate" } - }); + var featureEnabled = OptimizelyWithTypedAudiences.IsFeatureEnabled("feat_no_vars", + "user1", new UserAttributes + { + { + "favorite_ice_cream", "chocolate" + } + }); Assert.True(featureEnabled); - featureEnabled = OptimizelyWithTypedAudiences.IsFeatureEnabled("feat_no_vars", "user1", new UserAttributes - { - { "lasers", 45.5 } - }); + featureEnabled = OptimizelyWithTypedAudiences.IsFeatureEnabled("feat_no_vars", "user1", + new UserAttributes + { + { + "lasers", 45.5 + } + }); Assert.True(featureEnabled); } @@ -3855,24 +5818,31 @@ public void TestIsFeatureEnabledWithTypedAudiences() [Test] public void TestIsFeatureEnabledExcludeUserFromExperimentWithTypedAudiences() { - var featureEnabled = OptimizelyWithTypedAudiences.IsFeatureEnabled("feat", "user1", new UserAttributes { }); + var featureEnabled = OptimizelyWithTypedAudiences.IsFeatureEnabled("feat", "user1", + new UserAttributes()); Assert.False(featureEnabled); } [Test] public void TestGetFeatureVariableStringReturnVariableValueWithTypedAudiences() { - var variableValue = OptimizelyWithTypedAudiences.GetFeatureVariableString("feat_with_var", "x", "user1", new UserAttributes - { - { "lasers", 71 } - }); + var variableValue = OptimizelyWithTypedAudiences.GetFeatureVariableString( + "feat_with_var", "x", "user1", new UserAttributes + { + { + "lasers", 71 + } + }); Assert.AreEqual(variableValue, "xyz"); - variableValue = OptimizelyWithTypedAudiences.GetFeatureVariableString("feat_with_var", "x", "user1", new UserAttributes - { - { "should_do_it", true } - }); + variableValue = OptimizelyWithTypedAudiences.GetFeatureVariableString("feat_with_var", + "x", "user1", new UserAttributes + { + { + "should_do_it", true + } + }); Assert.AreEqual(variableValue, "xyz"); } @@ -3880,10 +5850,13 @@ public void TestGetFeatureVariableStringReturnVariableValueWithTypedAudiences() [Test] public void TestGetFeatureVariableStringReturnDefaultVariableValueWithTypedAudiences() { - var variableValue = OptimizelyWithTypedAudiences.GetFeatureVariableString("feat_with_var", "x", "user1", new UserAttributes - { - { "lasers", 50 } - }); + var variableValue = OptimizelyWithTypedAudiences.GetFeatureVariableString( + "feat_with_var", "x", "user1", new UserAttributes + { + { + "lasers", 50 + } + }); Assert.AreEqual(variableValue, "x"); } @@ -3897,15 +5870,22 @@ public void TestActivateIncludesUserInExperimentWithComplexAudienceConditions() { var userAttributes = new UserAttributes { - { "house", "Welcome to Slytherin!" }, - { "lasers", 45.5 } + { + "house", "Welcome to Slytherin!" + }, + { + "lasers", 45.5 + } }; // Should be included via substring match string audience with id '3988293898' and exact match number audience with id '3468206646' - var variation = OptimizelyWithTypedAudiences.Activate("audience_combinations_experiment", "user1", userAttributes); + var variation = + OptimizelyWithTypedAudiences.Activate("audience_combinations_experiment", "user1", + userAttributes); Assert.AreEqual("A", variation.Key); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Once); } [Test] @@ -3913,15 +5893,22 @@ public void TestActivateExcludesUserFromExperimentWithComplexAudienceConditions( { var userAttributes = new UserAttributes { - { "house", "Hufflepuff" }, - { "lasers", 45.5 } + { + "house", "Hufflepuff" + }, + { + "lasers", 45.5 + } }; // Should be excluded as substring audience with id '3988293898' does not match, so the overall conditions fail. - var variation = OptimizelyWithTypedAudiences.Activate("audience_combinations_experiment", "user1", userAttributes); + var variation = + OptimizelyWithTypedAudiences.Activate("audience_combinations_experiment", "user1", + userAttributes); Assert.Null(variation); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Never); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Never); } [Test] @@ -3929,29 +5916,40 @@ public void TestTrackIncludesUserInExperimentWithComplexAudienceConditions() { var userAttributes = new UserAttributes { - { "house", "Gryffindor" }, - { "should_do_it", true } + { + "house", "Gryffindor" + }, + { + "should_do_it", true + } }; // Should be included via exact match string audience with id '3468206642' and exact match boolean audience with id '3468206646' OptimizelyWithTypedAudiences.Track("user_signed_up", "user1", userAttributes); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Once); } [Test] - public void TestTrackDoesNotExcludesUserFromExperimentWhenAttributesMismatchWithAudienceConditions() + public void + TestTrackDoesNotExcludesUserFromExperimentWhenAttributesMismatchWithAudienceConditions() { var userAttributes = new UserAttributes { - { "house", "Gryffindor" }, - { "should_do_it", false } + { + "house", "Gryffindor" + }, + { + "should_do_it", false + } }; // Should be excluded as exact match boolean audience with id '3468206643' does not match so the overall conditions fail. OptimizelyWithTypedAudiences.Track("user_signed_up", "user1", userAttributes); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Once); } [Test] @@ -3959,12 +5957,17 @@ public void TestIsFeatureEnabledIncludesUserInRolloutWithComplexAudienceConditio { var userAttributes = new UserAttributes { - { "house", "Welcome to Slytherin!" }, - { "favorite_ice_cream", "walls" } + { + "house", "Welcome to Slytherin!" + }, + { + "favorite_ice_cream", "walls" + } }; // Should be included via substring match string audience with id '3988293898' and exists audience with id '3988293899' - var result = OptimizelyWithTypedAudiences.IsFeatureEnabled("feat2", "user1", userAttributes); + var result = + OptimizelyWithTypedAudiences.IsFeatureEnabled("feat2", "user1", userAttributes); Assert.True(result); } @@ -3973,13 +5976,19 @@ public void TestIsFeatureEnabledExcludesUserFromRolloutWithComplexAudienceCondit { var userAttributes = new UserAttributes { - { "house", "Ravenclaw" }, - { "lasers", 45.5 } + { + "house", "Ravenclaw" + }, + { + "lasers", 45.5 + } }; // Should be excluded - substring match string audience with id '3988293898' does not match, // and no audience in the other branch of the 'and' matches either - var result = OptimizelyWithTypedAudiences.IsFeatureEnabled("audience_combinations_experiment", "user1", userAttributes); + var result = + OptimizelyWithTypedAudiences.IsFeatureEnabled("audience_combinations_experiment", + "user1", userAttributes); Assert.False(result); } @@ -3988,22 +5997,30 @@ public void TestGetFeatureVariableIntegerReturnsVariableValueWithComplexAudience { var userAttributes = new UserAttributes { - { "house", "Gryffindor" }, - { "lasers", 700 } + { + "house", "Gryffindor" + }, + { + "lasers", 700 + } }; // Should be included via substring match string audience with id '3988293898' and exists audience with id '3988293899' - var value = OptimizelyWithTypedAudiences.GetFeatureVariableInteger("feat2_with_var", "z", "user1", userAttributes); + var value = + OptimizelyWithTypedAudiences.GetFeatureVariableInteger("feat2_with_var", "z", + "user1", userAttributes); Assert.AreEqual(150, value); } [Test] public void TestGetFeatureVariableIntegerReturnsDefaultValueWithComplexAudienceConditions() { - var userAttributes = new UserAttributes { }; + var userAttributes = new UserAttributes(); // Should be excluded - no audiences match with no attributes. - var value = OptimizelyWithTypedAudiences.GetFeatureVariableInteger("feat2_with_var", "z", "user1", userAttributes); + var value = + OptimizelyWithTypedAudiences.GetFeatureVariableInteger("feat2_with_var", "z", + "user1", userAttributes); Assert.AreEqual(10, value); } @@ -4014,12 +6031,12 @@ public void TestGetFeatureVariableIntegerReturnsDefaultValueWithComplexAudienceC [Test] public void TestOptimizelyDisposeAlsoDisposedConfigManager() { - var httpManager = new HttpProjectConfigManager.Builder() - .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") - .WithLogger(LoggerMock.Object) - .WithPollingInterval(TimeSpan.FromMilliseconds(5000)) - .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)) - .Build(); + var httpManager = new HttpProjectConfigManager.Builder(). + WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). + WithLogger(LoggerMock.Object). + WithPollingInterval(TimeSpan.FromMilliseconds(5000)). + WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)). + Build(); var optimizely = new Optimizely(httpManager); optimizely.Dispose(); @@ -4030,12 +6047,12 @@ public void TestOptimizelyDisposeAlsoDisposedConfigManager() [Test] public void TestDisposeInvalidateObject() { - var httpManager = new HttpProjectConfigManager.Builder() - .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") - .WithLogger(LoggerMock.Object) - .WithPollingInterval(TimeSpan.FromMilliseconds(5000)) - .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)) - .Build(); + var httpManager = new HttpProjectConfigManager.Builder(). + WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). + WithLogger(LoggerMock.Object). + WithPollingInterval(TimeSpan.FromMilliseconds(5000)). + WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)). + Build(); var optimizely = new Optimizely(httpManager); optimizely.Dispose(); @@ -4045,21 +6062,36 @@ public void TestDisposeInvalidateObject() [Test] public void TestAfterDisposeAPIsNoLongerValid() { - var httpManager = new HttpProjectConfigManager.Builder() - .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") - .WithDatafile(TestData.Datafile) - .WithLogger(LoggerMock.Object) - .WithPollingInterval(TimeSpan.FromMilliseconds(50000)) - .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)) - .Build(true); + var httpManager = new HttpProjectConfigManager.Builder(). + WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). + WithDatafile(TestData.Datafile). + WithLogger(LoggerMock.Object). + WithPollingInterval(TimeSpan.FromMilliseconds(50000)). + WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)). + Build(true); var optimizely = new Optimizely(httpManager); httpManager.Start(); - var activate = optimizely.Activate("test_experiment", TestUserId, new UserAttributes() { - { "device_type", "iPhone" }, { "location", "San Francisco" } }); + var activate = optimizely.Activate("test_experiment", TestUserId, new UserAttributes() + { + { + "device_type", "iPhone" + }, + { + "location", "San Francisco" + } + }); Assert.NotNull(activate); optimizely.Dispose(); - var activateAfterDispose = optimizely.Activate("test_experiment", TestUserId, new UserAttributes() { - { "device_type", "iPhone" }, { "location", "San Francisco" } }); + var activateAfterDispose = optimizely.Activate("test_experiment", TestUserId, + new UserAttributes() + { + { + "device_type", "iPhone" + }, + { + "location", "San Francisco" + } + }); Assert.Null(activateAfterDispose); httpManager.Dispose(); } @@ -4088,10 +6120,14 @@ public void TestAfterDisposeAPIsShouldNotCrash() optimizely.Track(string.Empty, string.Empty); Assert.IsFalse(optimizely.IsFeatureEnabled(string.Empty, string.Empty)); Assert.AreEqual(optimizely.GetEnabledFeatures(string.Empty).Count, 0); - Assert.IsNull(optimizely.GetFeatureVariableBoolean(string.Empty, string.Empty, string.Empty)); - Assert.IsNull(optimizely.GetFeatureVariableString(string.Empty, string.Empty, string.Empty)); - Assert.IsNull(optimizely.GetFeatureVariableDouble(string.Empty, string.Empty, string.Empty)); - Assert.IsNull(optimizely.GetFeatureVariableInteger(string.Empty, string.Empty, string.Empty)); + Assert.IsNull( + optimizely.GetFeatureVariableBoolean(string.Empty, string.Empty, string.Empty)); + Assert.IsNull( + optimizely.GetFeatureVariableString(string.Empty, string.Empty, string.Empty)); + Assert.IsNull( + optimizely.GetFeatureVariableDouble(string.Empty, string.Empty, string.Empty)); + Assert.IsNull( + optimizely.GetFeatureVariableInteger(string.Empty, string.Empty, string.Empty)); } #endregion Disposable Optimizely diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index 5e9ddad9f..5c8b2c276 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -1,12 +1,11 @@ -/** - * - * Copyright 2020-2021, Optimizely and contributors +/* + * Copyright 2020-2021, 2022-2023 Optimizely 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 + * https://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, @@ -26,11 +25,13 @@ using OptimizelySDK.Event.Dispatcher; using OptimizelySDK.Logger; using OptimizelySDK.Notifications; +using OptimizelySDK.Odp; using OptimizelySDK.OptimizelyDecisions; using OptimizelySDK.Tests.NotificationTests; using OptimizelySDK.Utils; using System; using System.Collections.Generic; +using System.Threading; namespace OptimizelySDK.Tests { @@ -54,16 +55,24 @@ public void SetUp() ErrorHandlerMock = new Mock(); ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny())); + EventDispatcherMock = new Mock(); - Optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + Optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, + LoggerMock.Object, ErrorHandlerMock.Object); } [Test] public void OptimizelyUserContextWithAttributes() { - var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + var attributes = new UserAttributes() + { + { + "house", "GRYFFINDOR" + } + }; + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, + ErrorHandlerMock.Object, LoggerMock.Object); Assert.AreEqual(user.GetOptimizely(), Optimizely); Assert.AreEqual(user.GetUserId(), UserID); @@ -73,7 +82,8 @@ public void OptimizelyUserContextWithAttributes() [Test] public void OptimizelyUserContextNoAttributes() { - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, + ErrorHandlerMock.Object, LoggerMock.Object); Assert.AreEqual(user.GetOptimizely(), Optimizely); Assert.AreEqual(user.GetUserId(), UserID); @@ -83,8 +93,14 @@ public void OptimizelyUserContextNoAttributes() [Test] public void SetAttribute() { - var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + var attributes = new UserAttributes() + { + { + "house", "GRYFFINDOR" + } + }; + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, + ErrorHandlerMock.Object, LoggerMock.Object); user.SetAttribute("k1", "v1"); user.SetAttribute("k2", true); @@ -104,7 +120,8 @@ public void SetAttribute() [Test] public void SetAttributeNoAttribute() { - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, + ErrorHandlerMock.Object, LoggerMock.Object); user.SetAttribute("k1", "v1"); user.SetAttribute("k2", true); @@ -119,8 +136,14 @@ public void SetAttributeNoAttribute() [Test] public void SetAttributeOverride() { - var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + var attributes = new UserAttributes() + { + { + "house", "GRYFFINDOR" + } + }; + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, + ErrorHandlerMock.Object, LoggerMock.Object); user.SetAttribute("k1", "v1"); user.SetAttribute("house", "v2"); @@ -133,8 +156,14 @@ public void SetAttributeOverride() [Test] public void SetAttributeNullValue() { - var attributes = new UserAttributes() { { "k1", null } }; - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + var attributes = new UserAttributes() + { + { + "k1", null + } + }; + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, + ErrorHandlerMock.Object, LoggerMock.Object); var newAttributes = user.GetAttributes(); Assert.AreEqual(newAttributes["k1"], null); @@ -151,7 +180,8 @@ public void SetAttributeNullValue() [Test] public void SetAttributeToOverrideAttribute() { - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, + ErrorHandlerMock.Object, LoggerMock.Object); Assert.AreEqual(user.GetOptimizely(), Optimizely); Assert.AreEqual(user.GetUserId(), UserID); @@ -362,12 +392,14 @@ public void DecideInvalidFlagKey() [Test] public void DecideWhenConfigIsNull() { - Optimizely optimizely = new Optimizely(TestData.UnsupportedVersionDatafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + Optimizely optimizely = new Optimizely(TestData.UnsupportedVersionDatafile, + EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); var flagKey = "multi_variate_feature"; var decisionExpected = OptimizelyDecision.NewErrorDecision( flagKey, - new OptimizelyUserContext(optimizely, UserID, new UserAttributes(), ErrorHandlerMock.Object, LoggerMock.Object), + new OptimizelyUserContext(optimizely, UserID, new UserAttributes(), + ErrorHandlerMock.Object, LoggerMock.Object), DecisionMessage.SDK_NOT_READY, ErrorHandlerMock.Object, LoggerMock.Object); @@ -385,7 +417,10 @@ public void DecideWhenConfigIsNull() public void DecideForKeysWithOneFlag() { var flagKey = "multi_variate_feature"; - var flagKeys = new string[] { flagKey }; + var flagKeys = new string[] + { + flagKey + }; var variablesExpected = Optimizely.GetAllFeatureVariables(flagKey, UserID); @@ -398,13 +433,13 @@ public void DecideForKeysWithOneFlag() var decision = decisions[flagKey]; OptimizelyDecision expDecision = new OptimizelyDecision( - "Gred", - false, - variablesExpected, - "test_experiment_multivariate", - flagKey, - user, - new string[0]); + "Gred", + false, + variablesExpected, + "test_experiment_multivariate", + flagKey, + user, + new string[0]); Assert.IsTrue(TestData.CompareObjects(decision, expDecision)); } @@ -413,7 +448,10 @@ public void DecideAllTwoFlag() { var flagKey1 = "multi_variate_feature"; var flagKey2 = "string_single_variable_feature"; - var flagKeys = new string[] { flagKey1, flagKey2 }; + var flagKeys = new string[] + { + flagKey1, flagKey2 + }; var variablesExpected1 = Optimizely.GetAllFeatureVariables(flagKey1, UserID); var variablesExpected2 = Optimizely.GetAllFeatureVariables(flagKey2, UserID); @@ -421,39 +459,46 @@ public void DecideAllTwoFlag() var user = Optimizely.CreateUserContext(UserID); user.SetAttribute("browser_type", "chrome"); // Mocking objects. - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); - Optimizely.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + Optimizely.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); var decisions = user.DecideForKeys(flagKeys); var userAttributes = new UserAttributes { - { "browser_type", "chrome" } + { + "browser_type", "chrome" + } }; Assert.True(decisions.Count == 2); - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FLAG, UserID, userAttributes, It.IsAny>()), - Times.Exactly(2)); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.FLAG, UserID, + userAttributes, It.IsAny>()), + Times.Exactly(2)); OptimizelyDecision expDecision1 = new OptimizelyDecision( - "Gred", - false, - variablesExpected1, - "test_experiment_multivariate", - flagKey1, - user, - new string[0]); + "Gred", + false, + variablesExpected1, + "test_experiment_multivariate", + flagKey1, + user, + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); OptimizelyDecision expDecision2 = new OptimizelyDecision( - "control", - true, - variablesExpected2, - "test_experiment_with_feature_rollout", - flagKey2, - user, - new string[0]); + "control", + true, + variablesExpected2, + "test_experiment_with_feature_rollout", + flagKey2, + user, + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey2], expDecision2)); } @@ -485,120 +530,127 @@ public void DecideAllAllFlags() var user = Optimizely.CreateUserContext(UserID); user.SetAttribute("browser_type", "chrome"); // Mocking objects. - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); - Optimizely.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + Optimizely.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); var decisions = user.DecideAll(); var userAttributes = new UserAttributes { - { "browser_type", "chrome" } + { + "browser_type", "chrome" + } }; Assert.True(decisions.Count == 10); - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FLAG, UserID, userAttributes, It.IsAny>()), - Times.Exactly(10)); + NotificationCallbackMock.Verify( + nc => nc.TestDecisionCallback(DecisionNotificationTypes.FLAG, UserID, + userAttributes, It.IsAny>()), + Times.Exactly(10)); OptimizelyDecision expDecision1 = new OptimizelyDecision( - null, - false, - variablesExpected1, - null, - flagKey1, - user, - new string[0]); + null, + false, + variablesExpected1, + null, + flagKey1, + user, + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); OptimizelyDecision expDecision2 = new OptimizelyDecision( - "variation", - false, - variablesExpected2, - "test_experiment_double_feature", - flagKey2, - user, - new string[0]); + "variation", + false, + variablesExpected2, + "test_experiment_double_feature", + flagKey2, + user, + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey2], expDecision2)); OptimizelyDecision expDecision3 = new OptimizelyDecision( - "control", - false, - variablesExpected3, - "test_experiment_integer_feature", - flagKey3, - user, - new string[0]); + "control", + false, + variablesExpected3, + "test_experiment_integer_feature", + flagKey3, + user, + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey3], expDecision3)); OptimizelyDecision expDecision4 = new OptimizelyDecision( - "188881", - false, - variablesExpected4, - "188880", - flagKey4, - user, - new string[0]); + "188881", + false, + variablesExpected4, + "188880", + flagKey4, + user, + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey4], expDecision4)); OptimizelyDecision expDecision5 = new OptimizelyDecision( - "control", - true, - variablesExpected5, - "test_experiment_with_feature_rollout", - flagKey5, - user, - new string[0]); + "control", + true, + variablesExpected5, + "test_experiment_with_feature_rollout", + flagKey5, + user, + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey5], expDecision5)); OptimizelyDecision expDecision6 = new OptimizelyDecision( - "Gred", - false, - variablesExpected6, - "test_experiment_multivariate", - flagKey6, - user, - new string[0]); + "Gred", + false, + variablesExpected6, + "test_experiment_multivariate", + flagKey6, + user, + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey6], expDecision6)); OptimizelyDecision expDecision7 = new OptimizelyDecision( - null, - false, - variablesExpected7, - null, - flagKey7, - user, - new string[0]); + null, + false, + variablesExpected7, + null, + flagKey7, + user, + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey7], expDecision7)); OptimizelyDecision expDecision8 = new OptimizelyDecision( - null, - false, - variablesExpected8, - null, - flagKey8, - user, - new string[0]); + null, + false, + variablesExpected8, + null, + flagKey8, + user, + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey8], expDecision8)); OptimizelyDecision expDecision9 = new OptimizelyDecision( - null, - false, - variablesExpected9, - null, - flagKey9, - user, - new string[0]); + null, + false, + variablesExpected9, + null, + flagKey9, + user, + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey9], expDecision9)); OptimizelyDecision expDecision10 = new OptimizelyDecision( - null, - false, - variablesExpected10, - null, - flagKey10, - user, - new string[0]); + null, + false, + variablesExpected10, + null, + flagKey10, + user, + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey10], expDecision10)); } @@ -609,7 +661,10 @@ public void DecideAllEnabledFlagsOnlyDecideOptions() var variablesExpected1 = Optimizely.GetAllFeatureVariables(flagKey1, UserID); - var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.ENABLED_FLAGS_ONLY }; + var decideOptions = new OptimizelyDecideOption[] + { + OptimizelyDecideOption.ENABLED_FLAGS_ONLY + }; var user = Optimizely.CreateUserContext(UserID); user.SetAttribute("browser_type", "chrome"); @@ -618,13 +673,13 @@ public void DecideAllEnabledFlagsOnlyDecideOptions() Assert.True(decisions.Count == 1); OptimizelyDecision expDecision1 = new OptimizelyDecision( - "control", - true, - variablesExpected1, - "test_experiment_with_feature_rollout", - flagKey1, - user, - new string[0]); + "control", + true, + variablesExpected1, + "test_experiment_with_feature_rollout", + flagKey1, + user, + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); } @@ -632,7 +687,10 @@ public void DecideAllEnabledFlagsOnlyDecideOptions() public void DecideAllEnabledFlagsDefaultDecideOptions() { var flagKey1 = "string_single_variable_feature"; - var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.ENABLED_FLAGS_ONLY }; + var decideOptions = new OptimizelyDecideOption[] + { + OptimizelyDecideOption.ENABLED_FLAGS_ONLY + }; var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, @@ -650,13 +708,13 @@ public void DecideAllEnabledFlagsDefaultDecideOptions() Assert.True(decisions.Count == 1); OptimizelyDecision expDecision1 = new OptimizelyDecision( - "control", - true, - variablesExpected1, - "test_experiment_with_feature_rollout", - flagKey1, - user, - new string[0]); + "control", + true, + variablesExpected1, + "test_experiment_with_feature_rollout", + flagKey1, + user, + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); } @@ -664,7 +722,10 @@ public void DecideAllEnabledFlagsDefaultDecideOptions() public void DecideAllEnabledFlagsDefaultDecideOptionsPlusApiOptions() { var flagKey1 = "string_single_variable_feature"; - var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.ENABLED_FLAGS_ONLY }; + var decideOptions = new OptimizelyDecideOption[] + { + OptimizelyDecideOption.ENABLED_FLAGS_ONLY + }; var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, @@ -674,20 +735,24 @@ public void DecideAllEnabledFlagsDefaultDecideOptionsPlusApiOptions() var user = optimizely.CreateUserContext(UserID); user.SetAttribute("browser_type", "chrome"); - decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.EXCLUDE_VARIABLES }; + decideOptions = new OptimizelyDecideOption[] + { + OptimizelyDecideOption.EXCLUDE_VARIABLES + }; var decisions = user.DecideAll(decideOptions); Assert.True(decisions.Count == 1); var expectedOptlyJson = new Dictionary(); OptimizelyDecision expDecision1 = new OptimizelyDecision( - "control", - true, - new OptimizelyJSON(expectedOptlyJson, ErrorHandlerMock.Object, LoggerMock.Object), - "test_experiment_with_feature_rollout", - flagKey1, - user, - new string[] { }); + "control", + true, + new OptimizelyJSON(expectedOptlyJson, ErrorHandlerMock.Object, LoggerMock.Object), + "test_experiment_with_feature_rollout", + flagKey1, + user, + new string[] + { }); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); } @@ -698,7 +763,10 @@ public void DecideExcludeVariablesDecideOptions() var variablesExpected = new Dictionary(); var user = Optimizely.CreateUserContext(UserID); user.SetAttribute("browser_type", "chrome"); - var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.EXCLUDE_VARIABLES }; + var decideOptions = new OptimizelyDecideOption[] + { + OptimizelyDecideOption.EXCLUDE_VARIABLES + }; var decision = user.Decide(flagKey, decideOptions); @@ -721,13 +789,18 @@ public void DecideIncludeReasonsDecideOptions() var decision = user.Decide(flagKey); Assert.True(decision.Reasons.Length == 1); - Assert.AreEqual(decision.Reasons[0], DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, flagKey)); + Assert.AreEqual(decision.Reasons[0], + DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, flagKey)); - var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.INCLUDE_REASONS }; + var decideOptions = new OptimizelyDecideOption[] + { + OptimizelyDecideOption.INCLUDE_REASONS + }; decision = user.Decide(flagKey, decideOptions); Assert.True(decision.Reasons.Length == 1); - Assert.AreEqual(decision.Reasons[0], DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, flagKey)); + Assert.AreEqual(decision.Reasons[0], + DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, flagKey)); flagKey = "multi_variate_feature"; decision = user.Decide(flagKey); @@ -743,8 +816,12 @@ public void DecideIncludeReasonsDecideOptions() decision = user.Decide(flagKey, decideOptions); Assert.True(decision.Reasons.Length > 0); - Assert.AreEqual("User [testUserID] is in variation [Gred] of experiment [test_experiment_multivariate].", decision.Reasons[1]); - Assert.AreEqual("The user \"testUserID\" is bucketed into experiment \"test_experiment_multivariate\" of feature \"multi_variate_feature\".", decision.Reasons[2]); + Assert.AreEqual( + "User [testUserID] is in variation [Gred] of experiment [test_experiment_multivariate].", + decision.Reasons[1]); + Assert.AreEqual( + "The user \"testUserID\" is bucketed into experiment \"test_experiment_multivariate\" of feature \"multi_variate_feature\".", + decision.Reasons[2]); } [Test] @@ -753,16 +830,22 @@ public void TestDoNotSendEventDecide() var flagKey = "multi_variate_feature"; var variablesExpected = Optimizely.GetAllFeatureVariables(flagKey, UserID); - var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, + LoggerMock.Object, ErrorHandlerMock.Object); var user = optimizely.CreateUserContext(UserID); user.SetAttribute("browser_type", "chrome"); - var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.DISABLE_DECISION_EVENT }; + var decideOptions = new OptimizelyDecideOption[] + { + OptimizelyDecideOption.DISABLE_DECISION_EVENT + }; var decision = user.Decide(flagKey, decideOptions); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Never); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Never); decision = user.Decide(flagKey); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Once); Assert.AreEqual(decision.VariationKey, "Gred"); Assert.False(decision.Enabled); @@ -778,7 +861,10 @@ public void TestDefaultDecideOptions() { var flagKey = "multi_variate_feature"; var variablesExpected = Optimizely.GetAllFeatureVariables(flagKey, UserID); - var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.DISABLE_DECISION_EVENT }; + var decideOptions = new OptimizelyDecideOption[] + { + OptimizelyDecideOption.DISABLE_DECISION_EVENT + }; var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, @@ -790,7 +876,8 @@ public void TestDefaultDecideOptions() user.SetAttribute("browser_type", "chrome"); var decision = user.Decide(flagKey); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Never); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Never); Assert.AreEqual(decision.VariationKey, "Gred"); Assert.False(decision.Enabled); @@ -809,36 +896,58 @@ public void TestDecisionNotification() var enabled = true; var variables = Optimizely.GetAllFeatureVariables(flagKey, UserID); var ruleKey = "test_experiment_with_feature_rollout"; - var reasons = new string[] { }; + var reasons = new string[] + { }; var user = Optimizely.CreateUserContext(UserID); user.SetAttribute("browser_type", "chrome"); var decisionInfo = new Dictionary { - { "flagKey", flagKey }, - { "enabled", enabled }, - { "variables", variables.ToDictionary() }, - { "variationKey", variationKey }, - { "ruleKey", ruleKey }, - { "reasons", reasons }, - { "decisionEventDispatched", true }, + { + "flagKey", flagKey + }, + { + "enabled", enabled + }, + { + "variables", variables.ToDictionary() + }, + { + "variationKey", variationKey + }, + { + "ruleKey", ruleKey + }, + { + "reasons", reasons + }, + { + "decisionEventDispatched", true + }, }; var userAttributes = new UserAttributes { - { "browser_type", "chrome" } + { + "browser_type", "chrome" + } }; // Mocking objects. - NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())); - Optimizely.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + Optimizely.NotificationCenter.AddNotification( + NotificationCenter.NotificationType.Decision, + NotificationCallbackMock.Object.TestDecisionCallback); user.Decide(flagKey); - NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FLAG, UserID, userAttributes, It.Is>(info => - TestData.CompareObjects(info, decisionInfo))), - Times.Once); + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback( + DecisionNotificationTypes.FLAG, UserID, userAttributes, + It.Is>(info => + TestData.CompareObjects(info, decisionInfo))), + Times.Once); } [Test] @@ -855,21 +964,28 @@ public void TestDecideOptionsByPassUPS() var userProfile = new UserProfile(userId, new Dictionary { - { experimentId, new Decision(fbVariationId)} + { + experimentId, new Decision(fbVariationId) + } }); userProfileServiceMock.Setup(_ => _.Lookup(userId)).Returns(userProfile.ToMap()); - var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object, userProfileServiceMock.Object); + var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, + LoggerMock.Object, ErrorHandlerMock.Object, userProfileServiceMock.Object); var user = optimizely.CreateUserContext(userId); - var projectConfig = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, ErrorHandlerMock.Object); + var projectConfig = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, + ErrorHandlerMock.Object); var variationUserProfile = user.Decide(flagKey); Assert.AreEqual(fbVariationKey, variationUserProfile.VariationKey); - var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE }; + var decideOptions = new OptimizelyDecideOption[] + { + OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE + }; variationUserProfile = user.Decide(flagKey, decideOptions); Assert.AreEqual(variationKey, variationUserProfile.VariationKey); } @@ -883,12 +999,17 @@ public void TestDecideOptionsByPassUPSNeverCallsSaveVariation() var userId = "testUser3"; var variationKey = "control"; - var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object, userProfileServiceMock.Object); + var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, + LoggerMock.Object, ErrorHandlerMock.Object, userProfileServiceMock.Object); var user = optimizely.CreateUserContext(userId); - var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE }; + var decideOptions = new OptimizelyDecideOption[] + { + OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE + }; var variationUserProfile = user.Decide(flagKey, decideOptions); - userProfileServiceMock.Verify(l => l.Save(It.IsAny>()), Times.Never); + userProfileServiceMock.Verify(l => l.Save(It.IsAny>()), + Times.Never); Assert.AreEqual(variationKey, variationUserProfile.VariationKey); } @@ -900,11 +1021,16 @@ public void TestDecideOptionsByPassUPSNeverCallsSaveVariation() [Test] public void TestTrackEventWithAudienceConditions() { - var OptimizelyWithTypedAudiences = new Optimizely(TestData.TypedAudienceDatafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + var OptimizelyWithTypedAudiences = new Optimizely(TestData.TypedAudienceDatafile, + EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); var userAttributes = new UserAttributes { - { "house", "Gryffindor" }, - { "should_do_it", false } + { + "house", "Gryffindor" + }, + { + "should_do_it", false + } }; var user = OptimizelyWithTypedAudiences.CreateUserContext(UserID, userAttributes); @@ -912,26 +1038,78 @@ public void TestTrackEventWithAudienceConditions() // Should be excluded as exact match boolean audience with id '3468206643' does not match so the overall conditions fail. user.TrackEvent("user_signed_up"); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Once); } [Test] public void TrackEventEmptyAttributesWithEventTags() { - var OptimizelyWithTypedAudiences = new Optimizely(TestData.TypedAudienceDatafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + var OptimizelyWithTypedAudiences = new Optimizely(TestData.TypedAudienceDatafile, + EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); var user = OptimizelyWithTypedAudiences.CreateUserContext(UserID); // Should be excluded as exact match boolean audience with id '3468206643' does not match so the overall conditions fail. user.TrackEvent("user_signed_up", new EventTags { - { "revenue", 42 }, - { "wont_send_null", null} + { + "revenue", 42 + }, + { + "wont_send_null", null + } }); - EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), + Times.Once); } #endregion TrackEvent + + [Test] + public void ShouldFetchQualifiedSegmentsAsyncThenCallCallback() + { + var odpManager = new OdpManager.Builder().Build(); + var cde = new CountdownEvent(1); + bool callbackResult = false; + var optimizely = new Optimizely(TestData.OdpIntegrationDatafile, + EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object, + odpManager: odpManager); + var context = new OptimizelyUserContext(optimizely, UserID, null, + ErrorHandlerMock.Object, LoggerMock.Object); + + context.FetchQualifiedSegments(success => + { + callbackResult = success; + cde.Signal(); + }); + cde.Wait(5000); + context.Dispose(); + + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, Constants.ODP_NOT_ENABLED_MESSAGE), + Times.Never); + Assert.IsTrue(callbackResult); + } + + [Test] + public void ShouldFetchQualifiedSegmentsSynchronously() + { + var odpManager = new OdpManager.Builder().Build(); + var mockLogger = new Mock(); + mockLogger.Setup(l => l.Log(It.IsAny(), It.IsAny())); + var optimizely = new Optimizely(TestData.OdpIntegrationDatafile, + EventDispatcherMock.Object, mockLogger.Object, ErrorHandlerMock.Object, + odpManager: odpManager); + var context = new OptimizelyUserContext(optimizely, UserID, null, + ErrorHandlerMock.Object, mockLogger.Object); + + var success = context.FetchQualifiedSegments(); + context.Dispose(); + + mockLogger.Verify(l => l.Log(LogLevel.ERROR, Constants.ODP_NOT_ENABLED_MESSAGE), + Times.Never); + Assert.IsTrue(success); + } } } diff --git a/OptimizelySDK.Tests/ProjectConfigTest.cs b/OptimizelySDK.Tests/ProjectConfigTest.cs index e9866ae7d..494fd2ef7 100644 --- a/OptimizelySDK.Tests/ProjectConfigTest.cs +++ b/OptimizelySDK.Tests/ProjectConfigTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022, Optimizely + * Copyright 2017-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,12 +44,18 @@ public void Setup() ErrorHandlerMock = new Mock(); ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny())); - Config = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, ErrorHandlerMock.Object); + Config = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, + ErrorHandlerMock.Object); } public static Dictionary CreateDictionary(string name, object entityObject) { - return new Dictionary() { { name, entityObject } }; + return new Dictionary() + { + { + name, entityObject + } + }; } [Test] @@ -80,18 +86,46 @@ public void TestInit() // Check Experiment Key Map var experimentKeyMap = new Dictionary() { - {"test_experiment",Config.GetExperimentFromKey("test_experiment") }, - { "paused_experiment",Config.GetExperimentFromKey("paused_experiment") }, - { "test_experiment_multivariate",Config.GetExperimentFromKey("test_experiment_multivariate") }, - { "test_experiment_with_feature_rollout",Config.GetExperimentFromKey("test_experiment_with_feature_rollout") }, - { "test_experiment_double_feature",Config.GetExperimentFromKey("test_experiment_double_feature") }, - { "test_experiment_integer_feature",Config.GetExperimentFromKey("test_experiment_integer_feature") }, - { "group_experiment_1",Config.GetExperimentFromKey("group_experiment_1") }, - {"group_experiment_2",Config.GetExperimentFromKey("group_experiment_2") }, - {"etag1",Config.GetExperimentFromKey("etag1") }, - {"etag2",Config.GetExperimentFromKey("etag2") }, - {"etag3",Config.GetExperimentFromKey("etag3") }, - {"etag4",Config.GetExperimentFromKey("etag4") } + { + "test_experiment", Config.GetExperimentFromKey("test_experiment") + }, + { + "paused_experiment", Config.GetExperimentFromKey("paused_experiment") + }, + { + "test_experiment_multivariate", + Config.GetExperimentFromKey("test_experiment_multivariate") + }, + { + "test_experiment_with_feature_rollout", + Config.GetExperimentFromKey("test_experiment_with_feature_rollout") + }, + { + "test_experiment_double_feature", + Config.GetExperimentFromKey("test_experiment_double_feature") + }, + { + "test_experiment_integer_feature", + Config.GetExperimentFromKey("test_experiment_integer_feature") + }, + { + "group_experiment_1", Config.GetExperimentFromKey("group_experiment_1") + }, + { + "group_experiment_2", Config.GetExperimentFromKey("group_experiment_2") + }, + { + "etag1", Config.GetExperimentFromKey("etag1") + }, + { + "etag2", Config.GetExperimentFromKey("etag2") + }, + { + "etag3", Config.GetExperimentFromKey("etag3") + }, + { + "etag4", Config.GetExperimentFromKey("etag4") + } }; Assert.IsTrue(TestData.CompareObjects(experimentKeyMap, Config.ExperimentKeyMap)); @@ -100,163 +134,317 @@ public void TestInit() var experimentIdMap = new Dictionary() { - {"7716830082",Config.GetExperimentFromId("7716830082") }, - {"7716830585",Config.GetExperimentFromId("7716830585") }, - {"122230",Config.GetExperimentFromId("122230") }, - {"122235",Config.GetExperimentFromId("122235") }, - {"122238",Config.GetExperimentFromId("122238") }, - {"122241",Config.GetExperimentFromId("122241") }, - { "7723330021",Config.GetExperimentFromId("7723330021") }, - { "7718750065",Config.GetExperimentFromId("7718750065") }, - { "223",Config.GetExperimentFromId("223") }, - { "118",Config.GetExperimentFromId("118") }, - { "224",Config.GetExperimentFromId("224") }, - { "119",Config.GetExperimentFromId("119") } + { + "7716830082", Config.GetExperimentFromId("7716830082") + }, + { + "7716830585", Config.GetExperimentFromId("7716830585") + }, + { + "122230", Config.GetExperimentFromId("122230") + }, + { + "122235", Config.GetExperimentFromId("122235") + }, + { + "122238", Config.GetExperimentFromId("122238") + }, + { + "122241", Config.GetExperimentFromId("122241") + }, + { + "7723330021", Config.GetExperimentFromId("7723330021") + }, + { + "7718750065", Config.GetExperimentFromId("7718750065") + }, + { + "223", Config.GetExperimentFromId("223") + }, + { + "118", Config.GetExperimentFromId("118") + }, + { + "224", Config.GetExperimentFromId("224") + }, + { + "119", Config.GetExperimentFromId("119") + } }; Assert.IsTrue(TestData.CompareObjects(experimentIdMap, Config.ExperimentIdMap)); // Check Event key Map - var eventKeyMap = new Dictionary { { "purchase", Config.GetEvent("purchase") } }; + var eventKeyMap = new Dictionary + { + { + "purchase", Config.GetEvent("purchase") + } + }; Assert.IsTrue(TestData.CompareObjects(eventKeyMap, Config.EventKeyMap)); // Check Attribute Key Map var attributeKeyMap = new Dictionary { - { "device_type", Config.GetAttribute("device_type") }, - { "location", Config.GetAttribute("location")}, - { "browser_type", Config.GetAttribute("browser_type")}, - { "boolean_key", Config.GetAttribute("boolean_key")}, - { "integer_key", Config.GetAttribute("integer_key")}, - { "double_key", Config.GetAttribute("double_key")} + { + "device_type", Config.GetAttribute("device_type") + }, + { + "location", Config.GetAttribute("location") + }, + { + "browser_type", Config.GetAttribute("browser_type") + }, + { + "boolean_key", Config.GetAttribute("boolean_key") + }, + { + "integer_key", Config.GetAttribute("integer_key") + }, + { + "double_key", Config.GetAttribute("double_key") + } }; Assert.IsTrue(TestData.CompareObjects(attributeKeyMap, Config.AttributeKeyMap)); // Check Audience ID Map var audienceIdMap = new Dictionary { - { "7718080042", Config.GetAudience("7718080042") }, - { "11154", Config.GetAudience("11154") }, - { "100", Config.GetAudience("100") } + { + "7718080042", Config.GetAudience("7718080042") + }, + { + "11154", Config.GetAudience("11154") + }, + { + "100", Config.GetAudience("100") + } }; Assert.IsTrue(TestData.CompareObjects(audienceIdMap, Config.AudienceIdMap)); // Check Variation Key Map var expectedVariationKeyMap = new Dictionary { - { "test_experiment", new Dictionary - { - { "control", Config.GetVariationFromKey("test_experiment", "control") }, - { "variation", Config.GetVariationFromKey("test_experiment", "variation")} - } - }, - { "paused_experiment", new Dictionary - { - { "control", Config.GetVariationFromKey("paused_experiment", "control") }, - { "variation", Config.GetVariationFromKey("paused_experiment", "variation") } - } - }, - { "group_experiment_1", new Dictionary - { - {"group_exp_1_var_1", Config.GetVariationFromKey("group_experiment_1", "group_exp_1_var_1") }, - { "group_exp_1_var_2", Config.GetVariationFromKey("group_experiment_1", "group_exp_1_var_2") } - } - }, - { "group_experiment_2", new Dictionary - { - {"group_exp_2_var_1", Config.GetVariationFromKey("group_experiment_2", "group_exp_2_var_1") }, - { "group_exp_2_var_2", Config.GetVariationFromKey("group_experiment_2", "group_exp_2_var_2") } - } - }, - { "test_experiment_multivariate", new Dictionary - { - {"Fred", Config.GetVariationFromKey("test_experiment_multivariate", "Fred") }, - { "Feorge", Config.GetVariationFromKey("test_experiment_multivariate", "Feorge") }, - { "Gred", Config.GetVariationFromKey("test_experiment_multivariate", "Gred") }, - { "George", Config.GetVariationFromKey("test_experiment_multivariate", "George") } - } - }, - { "test_experiment_with_feature_rollout", new Dictionary - { - {"control", Config.GetVariationFromKey("test_experiment_with_feature_rollout", "control") }, - { "variation", Config.GetVariationFromKey("test_experiment_with_feature_rollout", "variation") } - } - }, - { "test_experiment_double_feature", new Dictionary - { - {"control", Config.GetVariationFromKey("test_experiment_double_feature", "control") }, - { "variation", Config.GetVariationFromKey("test_experiment_double_feature", "variation") } - } - }, - { "test_experiment_integer_feature", new Dictionary - { - {"control", Config.GetVariationFromKey("test_experiment_integer_feature", "control") }, - { "variation", Config.GetVariationFromKey("test_experiment_integer_feature", "variation") } - } - }, - { "177770", new Dictionary - { - {"177771", Config.GetVariationFromKey("177770", "177771") } - } - }, - { "177772", new Dictionary - { - {"177773", Config.GetVariationFromKey("177772", "177773") } - } - }, - { "177776", new Dictionary - { - {"177778", Config.GetVariationFromKey("177776", "177778") } - } - }, - { "177774", new Dictionary - { - {"177775", Config.GetVariationFromKey("177774", "177775") } - } - }, - { "177779", new Dictionary - { - {"177780", Config.GetVariationFromKey("177779", "177780") } - } - }, - { "177781", new Dictionary - { - {"177782", Config.GetVariationFromKey("177781", "177782") } - } - }, - { "177783", new Dictionary - { - {"177784", Config.GetVariationFromKey("177783", "177784") } - } - }, - { "188880", new Dictionary - { - {"188881", Config.GetVariationFromKey("188880", "188881") } - } - }, - { "etag1", new Dictionary - { - {"vtag1", Config.GetVariationFromKey("etag1", "vtag1") }, - {"vtag2", Config.GetVariationFromKey("etag1", "vtag2") } - } - }, - { "etag2", new Dictionary - { - {"vtag3", Config.GetVariationFromKey("etag2", "vtag3") }, - {"vtag4", Config.GetVariationFromKey("etag2", "vtag4") } - } - }, - { "etag3", new Dictionary - { - {"vtag5", Config.GetVariationFromKey("etag3", "vtag5") }, - {"vtag6", Config.GetVariationFromKey("etag3", "vtag6") } - } - }, - { "etag4", new Dictionary - { - {"vtag7", Config.GetVariationFromKey("etag4", "vtag7") }, - {"vtag8", Config.GetVariationFromKey("etag4", "vtag8") } - } + { + "test_experiment", new Dictionary + { + { + "control", Config.GetVariationFromKey("test_experiment", "control") + }, + { + "variation", Config.GetVariationFromKey("test_experiment", "variation") + } + } + }, + { + "paused_experiment", new Dictionary + { + { + "control", Config.GetVariationFromKey("paused_experiment", "control") + }, + { + "variation", + Config.GetVariationFromKey("paused_experiment", "variation") + } + } + }, + { + "group_experiment_1", new Dictionary + { + { + "group_exp_1_var_1", + Config.GetVariationFromKey("group_experiment_1", "group_exp_1_var_1") + }, + { + "group_exp_1_var_2", + Config.GetVariationFromKey("group_experiment_1", "group_exp_1_var_2") + } + } + }, + { + "group_experiment_2", new Dictionary + { + { + "group_exp_2_var_1", + Config.GetVariationFromKey("group_experiment_2", "group_exp_2_var_1") + }, + { + "group_exp_2_var_2", + Config.GetVariationFromKey("group_experiment_2", "group_exp_2_var_2") + } + } + }, + { + "test_experiment_multivariate", new Dictionary + { + { + "Fred", + Config.GetVariationFromKey("test_experiment_multivariate", "Fred") + }, + { + "Feorge", + Config.GetVariationFromKey("test_experiment_multivariate", "Feorge") + }, + { + "Gred", + Config.GetVariationFromKey("test_experiment_multivariate", "Gred") + }, + { + "George", + Config.GetVariationFromKey("test_experiment_multivariate", "George") + } + } + }, + { + "test_experiment_with_feature_rollout", new Dictionary + { + { + "control", Config.GetVariationFromKey( + "test_experiment_with_feature_rollout", + "control") + }, + { + "variation", Config.GetVariationFromKey( + "test_experiment_with_feature_rollout", + "variation") + } + } + }, + { + "test_experiment_double_feature", new Dictionary + { + { + "control", + Config.GetVariationFromKey("test_experiment_double_feature", "control") + }, + { + "variation", Config.GetVariationFromKey( + "test_experiment_double_feature", + "variation") + } + } + }, + { + "test_experiment_integer_feature", new Dictionary + { + { + "control", + Config.GetVariationFromKey("test_experiment_integer_feature", "control") + }, + { + "variation", Config.GetVariationFromKey( + "test_experiment_integer_feature", + "variation") + } + } + }, + { + "177770", new Dictionary + { + { + "177771", Config.GetVariationFromKey("177770", "177771") + } + } + }, + { + "177772", new Dictionary + { + { + "177773", Config.GetVariationFromKey("177772", "177773") + } + } + }, + { + "177776", new Dictionary + { + { + "177778", Config.GetVariationFromKey("177776", "177778") + } + } + }, + { + "177774", new Dictionary + { + { + "177775", Config.GetVariationFromKey("177774", "177775") + } + } + }, + { + "177779", new Dictionary + { + { + "177780", Config.GetVariationFromKey("177779", "177780") + } + } + }, + { + "177781", new Dictionary + { + { + "177782", Config.GetVariationFromKey("177781", "177782") + } + } + }, + { + "177783", new Dictionary + { + { + "177784", Config.GetVariationFromKey("177783", "177784") + } + } + }, + { + "188880", new Dictionary + { + { + "188881", Config.GetVariationFromKey("188880", "188881") + } + } + }, + { + "etag1", new Dictionary + { + { + "vtag1", Config.GetVariationFromKey("etag1", "vtag1") + }, + { + "vtag2", Config.GetVariationFromKey("etag1", "vtag2") + } + } + }, + { + "etag2", new Dictionary + { + { + "vtag3", Config.GetVariationFromKey("etag2", "vtag3") + }, + { + "vtag4", Config.GetVariationFromKey("etag2", "vtag4") + } + } + }, + { + "etag3", new Dictionary + { + { + "vtag5", Config.GetVariationFromKey("etag3", "vtag5") + }, + { + "vtag6", Config.GetVariationFromKey("etag3", "vtag6") + } + } + }, + { + "etag4", new Dictionary + { + { + "vtag7", Config.GetVariationFromKey("etag4", "vtag7") + }, + { + "vtag8", Config.GetVariationFromKey("etag4", "vtag8") + } + } } }; @@ -265,119 +453,225 @@ public void TestInit() // Check Variation ID Map var expectedVariationIdMap = new Dictionary { - { "test_experiment", new Dictionary - { - {"7722370027", Config.GetVariationFromId("test_experiment", "7722370027") }, - { "7721010009", Config.GetVariationFromId("test_experiment", "7721010009") } - } - }, - { "paused_experiment", new Dictionary - { - {"7722370427", Config.GetVariationFromId("paused_experiment", "7722370427") }, - { "7721010509", Config.GetVariationFromId("paused_experiment", "7721010509") } - } - }, - { "test_experiment_multivariate", new Dictionary - { - { "122231", Config.GetVariationFromId("test_experiment_multivariate", "122231") }, - { "122232", Config.GetVariationFromId("test_experiment_multivariate", "122232") }, - { "122233", Config.GetVariationFromId("test_experiment_multivariate", "122233") }, - { "122234", Config.GetVariationFromId("test_experiment_multivariate", "122234") } - } - }, - { "test_experiment_with_feature_rollout", new Dictionary - { - { "122236", Config.GetVariationFromId("test_experiment_with_feature_rollout", "122236") }, - { "122237", Config.GetVariationFromId("test_experiment_with_feature_rollout", "122237") } - } - }, - { "test_experiment_double_feature", new Dictionary - { - { "122239", Config.GetVariationFromId("test_experiment_double_feature", "122239") }, - { "122240", Config.GetVariationFromId("test_experiment_double_feature", "122240") } - } - }, - { "test_experiment_integer_feature", new Dictionary - { - { "122242", Config.GetVariationFromId("test_experiment_integer_feature", "122242") }, - { "122243", Config.GetVariationFromId("test_experiment_integer_feature", "122243") } - } - }, - { "group_experiment_1", new Dictionary - { - {"7722260071", Config.GetVariationFromId("group_experiment_1", "7722260071") }, - { "7722360022", Config.GetVariationFromId("group_experiment_1", "7722360022")} - } - }, - { "group_experiment_2", new Dictionary - { - {"7713030086", Config.GetVariationFromId("group_experiment_2", "7713030086") }, - { "7725250007", Config.GetVariationFromId("group_experiment_2", "7725250007")} - } - }, - { "177770", new Dictionary - { - {"177771", Config.GetVariationFromId("177770", "177771") } - } - }, - { "177772", new Dictionary - { - {"177773", Config.GetVariationFromId("177772", "177773") } - } - }, - { "177776", new Dictionary - { - {"177778", Config.GetVariationFromId("177776", "177778") } - } - }, - { "177774", new Dictionary - { - {"177775", Config.GetVariationFromId("177774", "177775") } - } - }, - { "177779", new Dictionary - { - {"177780", Config.GetVariationFromId("177779", "177780") } - } - }, - { "177781", new Dictionary - { - {"177782", Config.GetVariationFromId("177781", "177782") } - } - }, - { "177783", new Dictionary - { - {"177784", Config.GetVariationFromId("177783", "177784") } - } - }, - { "188880", new Dictionary - { - {"188881", Config.GetVariationFromId("188880", "188881") } - } - }, - { "etag1", new Dictionary - { - {"276", Config.GetVariationFromId("etag1", "276") }, - {"277", Config.GetVariationFromId("etag1", "277") } - } - }, - { "etag2", new Dictionary - { - {"278", Config.GetVariationFromId("etag2", "278") }, - {"279", Config.GetVariationFromId("etag2", "279") } - } - }, - { "etag3", new Dictionary - { - {"280", Config.GetVariationFromId("etag3", "280") }, - {"281", Config.GetVariationFromId("etag3", "281") } - } - }, - { "etag4", new Dictionary - { - {"282", Config.GetVariationFromId("etag4", "282") }, - {"283", Config.GetVariationFromId("etag4", "283") } - } + { + "test_experiment", new Dictionary + { + { + "7722370027", Config.GetVariationFromId("test_experiment", "7722370027") + }, + { + "7721010009", Config.GetVariationFromId("test_experiment", "7721010009") + } + } + }, + { + "paused_experiment", new Dictionary + { + { + "7722370427", + Config.GetVariationFromId("paused_experiment", "7722370427") + }, + { + "7721010509", + Config.GetVariationFromId("paused_experiment", "7721010509") + } + } + }, + { + "test_experiment_multivariate", new Dictionary + { + { + "122231", + Config.GetVariationFromId("test_experiment_multivariate", "122231") + }, + { + "122232", + Config.GetVariationFromId("test_experiment_multivariate", "122232") + }, + { + "122233", + Config.GetVariationFromId("test_experiment_multivariate", "122233") + }, + { + "122234", + Config.GetVariationFromId("test_experiment_multivariate", "122234") + } + } + }, + { + "test_experiment_with_feature_rollout", new Dictionary + { + { + "122236", Config.GetVariationFromId( + "test_experiment_with_feature_rollout", + "122236") + }, + { + "122237", Config.GetVariationFromId( + "test_experiment_with_feature_rollout", + "122237") + } + } + }, + { + "test_experiment_double_feature", new Dictionary + { + { + "122239", + Config.GetVariationFromId("test_experiment_double_feature", "122239") + }, + { + "122240", + Config.GetVariationFromId("test_experiment_double_feature", "122240") + } + } + }, + { + "test_experiment_integer_feature", new Dictionary + { + { + "122242", + Config.GetVariationFromId("test_experiment_integer_feature", "122242") + }, + { + "122243", + Config.GetVariationFromId("test_experiment_integer_feature", "122243") + } + } + }, + { + "group_experiment_1", new Dictionary + { + { + "7722260071", + Config.GetVariationFromId("group_experiment_1", "7722260071") + }, + { + "7722360022", + Config.GetVariationFromId("group_experiment_1", "7722360022") + } + } + }, + { + "group_experiment_2", new Dictionary + { + { + "7713030086", + Config.GetVariationFromId("group_experiment_2", "7713030086") + }, + { + "7725250007", + Config.GetVariationFromId("group_experiment_2", "7725250007") + } + } + }, + { + "177770", new Dictionary + { + { + "177771", Config.GetVariationFromId("177770", "177771") + } + } + }, + { + "177772", new Dictionary + { + { + "177773", Config.GetVariationFromId("177772", "177773") + } + } + }, + { + "177776", new Dictionary + { + { + "177778", Config.GetVariationFromId("177776", "177778") + } + } + }, + { + "177774", new Dictionary + { + { + "177775", Config.GetVariationFromId("177774", "177775") + } + } + }, + { + "177779", new Dictionary + { + { + "177780", Config.GetVariationFromId("177779", "177780") + } + } + }, + { + "177781", new Dictionary + { + { + "177782", Config.GetVariationFromId("177781", "177782") + } + } + }, + { + "177783", new Dictionary + { + { + "177784", Config.GetVariationFromId("177783", "177784") + } + } + }, + { + "188880", new Dictionary + { + { + "188881", Config.GetVariationFromId("188880", "188881") + } + } + }, + { + "etag1", new Dictionary + { + { + "276", Config.GetVariationFromId("etag1", "276") + }, + { + "277", Config.GetVariationFromId("etag1", "277") + } + } + }, + { + "etag2", new Dictionary + { + { + "278", Config.GetVariationFromId("etag2", "278") + }, + { + "279", Config.GetVariationFromId("etag2", "279") + } + } + }, + { + "etag3", new Dictionary + { + { + "280", Config.GetVariationFromId("etag3", "280") + }, + { + "281", Config.GetVariationFromId("etag3", "281") + } + } + }, + { + "etag4", new Dictionary + { + { + "282", Config.GetVariationFromId("etag4", "282") + }, + { + "283", Config.GetVariationFromId("etag4", "283") + } + } } }; @@ -386,28 +680,69 @@ public void TestInit() // Check Variation returns correct variable usage var featureVariableUsageInstance = new List { - new FeatureVariableUsage{Id="155560", Value="F"}, - new FeatureVariableUsage{Id="155561", Value="red"}, + new FeatureVariableUsage + { + Id = "155560", + Value = "F" + }, + new FeatureVariableUsage + { + Id = "155561", + Value = "red" + }, }; - var expectedVariationUsage = new Variation { Id = "122231", Key = "Fred", FeatureVariableUsageInstances = featureVariableUsageInstance, FeatureEnabled = true }; - var actualVariationUsage = Config.GetVariationFromKey("test_experiment_multivariate", "Fred"); + var expectedVariationUsage = new Variation + { + Id = "122231", + Key = "Fred", + FeatureVariableUsageInstances = featureVariableUsageInstance, + FeatureEnabled = true + }; + var actualVariationUsage = + Config.GetVariationFromKey("test_experiment_multivariate", "Fred"); Assertions.AreEqual(expectedVariationUsage, actualVariationUsage); // Check Feature Key map. var expectedFeatureKeyMap = new Dictionary { - { "boolean_feature", Config.GetFeatureFlagFromKey("boolean_feature") }, - { "double_single_variable_feature", Config.GetFeatureFlagFromKey("double_single_variable_feature") }, - { "integer_single_variable_feature", Config.GetFeatureFlagFromKey("integer_single_variable_feature") }, - { "boolean_single_variable_feature", Config.GetFeatureFlagFromKey("boolean_single_variable_feature") }, - { "string_single_variable_feature", Config.GetFeatureFlagFromKey("string_single_variable_feature") }, - { "multi_variate_feature", Config.GetFeatureFlagFromKey("multi_variate_feature") }, - { "mutex_group_feature", Config.GetFeatureFlagFromKey("mutex_group_feature") }, - { "empty_feature", Config.GetFeatureFlagFromKey("empty_feature") }, - { "no_rollout_experiment_feature", Config.GetFeatureFlagFromKey("no_rollout_experiment_feature") }, - { "unsupported_variabletype", Config.GetFeatureFlagFromKey("unsupported_variabletype") } + { + "boolean_feature", Config.GetFeatureFlagFromKey("boolean_feature") + }, + { + "double_single_variable_feature", + Config.GetFeatureFlagFromKey("double_single_variable_feature") + }, + { + "integer_single_variable_feature", + Config.GetFeatureFlagFromKey("integer_single_variable_feature") + }, + { + "boolean_single_variable_feature", + Config.GetFeatureFlagFromKey("boolean_single_variable_feature") + }, + { + "string_single_variable_feature", + Config.GetFeatureFlagFromKey("string_single_variable_feature") + }, + { + "multi_variate_feature", Config.GetFeatureFlagFromKey("multi_variate_feature") + }, + { + "mutex_group_feature", Config.GetFeatureFlagFromKey("mutex_group_feature") + }, + { + "empty_feature", Config.GetFeatureFlagFromKey("empty_feature") + }, + { + "no_rollout_experiment_feature", + Config.GetFeatureFlagFromKey("no_rollout_experiment_feature") + }, + { + "unsupported_variabletype", + Config.GetFeatureFlagFromKey("unsupported_variabletype") + } }; Assertions.AreEquivalent(expectedFeatureKeyMap, Config.FeatureKeyMap); @@ -415,8 +750,12 @@ public void TestInit() // Check Feature Key map. var expectedRolloutIdMap = new Dictionary { - { "166660", Config.GetRolloutFromId("166660") }, - { "166661", Config.GetRolloutFromId("166661") } + { + "166660", Config.GetRolloutFromId("166660") + }, + { + "166661", Config.GetRolloutFromId("166661") + } }; Assert.IsTrue(TestData.CompareObjects(expectedRolloutIdMap, Config.RolloutIdMap)); @@ -429,36 +768,68 @@ public void TestFlagVariations() var expectedVariationDict = new Dictionary { - { "group_exp_1_var_1", new Variation + { + "group_exp_1_var_1", new Variation { FeatureEnabled = true, Id = "7722260071", Key = "group_exp_1_var_1", - FeatureVariableUsageInstances = new List { new FeatureVariableUsage { Id = "155563", Value= "groupie_1_v1" } } + FeatureVariableUsageInstances = new List + { + new FeatureVariableUsage + { + Id = "155563", + Value = "groupie_1_v1" + } + } } }, - { "group_exp_1_var_2", new Variation + { + "group_exp_1_var_2", new Variation { FeatureEnabled = true, Id = "7722360022", Key = "group_exp_1_var_2", - FeatureVariableUsageInstances = new List { new FeatureVariableUsage { Id = "155563", Value= "groupie_1_v2" } } + FeatureVariableUsageInstances = new List + { + new FeatureVariableUsage + { + Id = "155563", + Value = "groupie_1_v2" + } + } } }, - { "group_exp_2_var_1", new Variation + { + "group_exp_2_var_1", new Variation { FeatureEnabled = false, Id = "7713030086", Key = "group_exp_2_var_1", - FeatureVariableUsageInstances = new List { new FeatureVariableUsage { Id = "155563", Value= "groupie_2_v1" } } + FeatureVariableUsageInstances = new List + { + new FeatureVariableUsage + { + Id = "155563", + Value = "groupie_2_v1" + } + } } }, - { "group_exp_2_var_2", new Variation + { + "group_exp_2_var_2", new Variation { FeatureEnabled = false, Id = "7725250007", Key = "group_exp_2_var_2", - FeatureVariableUsageInstances = new List { new FeatureVariableUsage { Id = "155563", Value= "groupie_2_v2" } } + FeatureVariableUsageInstances = new List + { + new FeatureVariableUsage + { + Id = "155563", + Value = "groupie_2_v2" + } + } } } }; @@ -469,7 +840,8 @@ public void TestFlagVariations() [Test] public void TestIfSendFlagDecisionKeyIsMissingItShouldReturnFalse() { - var tempConfig = DatafileProjectConfig.Create(TestData.SimpleABExperimentsDatafile, LoggerMock.Object, ErrorHandlerMock.Object); + var tempConfig = DatafileProjectConfig.Create(TestData.SimpleABExperimentsDatafile, + LoggerMock.Object, ErrorHandlerMock.Object); Assert.IsFalse(tempConfig.SendFlagDecisions); } @@ -498,11 +870,14 @@ public void TestGetGroupInvalidId() { var group = Config.GetGroup("invalid_id"); - LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(1)); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, @"Group ID ""invalid_id"" is not in datafile.")); + LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), + Times.Exactly(1)); + LoggerMock.Verify(l => + l.Log(LogLevel.ERROR, @"Group ID ""invalid_id"" is not in datafile.")); ErrorHandlerMock.Verify(e => e.HandleError( - It.Is(ex => ex.Message == "Provided group is not in datafile.")), + It.Is(ex => + ex.Message == "Provided group is not in datafile.")), Times.Once, "Failed"); Assert.IsTrue(TestData.CompareObjects(group, new Entity.Group())); @@ -522,9 +897,12 @@ public void TestGetExperimentInvalidKey() var experiment = Config.GetExperimentFromKey("invalid_key"); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, @"Experiment key ""invalid_key"" is not in datafile.")); + LoggerMock.Verify(l => + l.Log(LogLevel.ERROR, @"Experiment key ""invalid_key"" is not in datafile.")); - ErrorHandlerMock.Verify(e => e.HandleError(It.Is(ex => ex.Message == "Provided experiment is not in datafile."))); + ErrorHandlerMock.Verify(e => + e.HandleError(It.Is(ex => + ex.Message == "Provided experiment is not in datafile."))); Assert.IsTrue(TestData.CompareObjects(new Entity.Experiment(), experiment)); } @@ -543,9 +921,12 @@ public void TestGetExperimentInvalidId() var experiment = Config.GetExperimentFromId("42"); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, @"Experiment ID ""42"" is not in datafile.")); + LoggerMock.Verify(l => + l.Log(LogLevel.ERROR, @"Experiment ID ""42"" is not in datafile.")); - ErrorHandlerMock.Verify(e => e.HandleError(It.Is(ex => ex.Message == "Provided experiment is not in datafile."))); + ErrorHandlerMock.Verify(e => + e.HandleError(It.Is(ex => + ex.Message == "Provided experiment is not in datafile."))); Assert.IsTrue(TestData.CompareObjects(new Entity.Experiment(), experiment)); } @@ -557,7 +938,10 @@ public void TestGetEventValidKey() Assert.AreEqual("purchase", ev.Key); Assert.AreEqual("7718020063", ev.Id); - Assert.IsTrue(TestData.CompareObjects(new object[] { "7716830082", "7723330021", "7718750065", "7716830585" }, ev.ExperimentIds)); + Assert.IsTrue(TestData.CompareObjects(new object[] + { + "7716830082", "7723330021", "7718750065", "7716830585" + }, ev.ExperimentIds)); } [Test] @@ -566,9 +950,12 @@ public void TestGetEventInvalidKey() var ev = Config.GetEvent("invalid_key"); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, @"Event key ""invalid_key"" is not in datafile.")); + LoggerMock.Verify(l => + l.Log(LogLevel.ERROR, @"Event key ""invalid_key"" is not in datafile.")); - ErrorHandlerMock.Verify(e => e.HandleError(It.Is(ex => ex.Message == "Provided event is not in datafile."))); + ErrorHandlerMock.Verify(e => + e.HandleError(It.Is(ex => + ex.Message == "Provided event is not in datafile."))); Assert.IsTrue(TestData.CompareObjects(new Entity.Event(), ev)); } @@ -588,9 +975,12 @@ public void TestGetAudienceInvalidKey() var audience = Config.GetAudience("invalid_id"); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, @"Audience ID ""invalid_id"" is not in datafile.")); + LoggerMock.Verify(l => + l.Log(LogLevel.ERROR, @"Audience ID ""invalid_id"" is not in datafile.")); - ErrorHandlerMock.Verify(e => e.HandleError(It.Is(ex => ex.Message == "Provided audience is not in datafile."))); + ErrorHandlerMock.Verify(e => + e.HandleError(It.Is(ex => + ex.Message == "Provided audience is not in datafile."))); Assert.IsTrue(TestData.CompareObjects(new Entity.Audience(), audience)); } @@ -609,9 +999,12 @@ public void TestGetAttributeInvalidKey() var attribute = Config.GetAttribute("invalid_key"); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, @"Attribute key ""invalid_key"" is not in datafile.")); + LoggerMock.Verify(l => + l.Log(LogLevel.ERROR, @"Attribute key ""invalid_key"" is not in datafile.")); - ErrorHandlerMock.Verify(e => e.HandleError(It.Is(ex => ex.Message == "Provided attribute is not in datafile."))); + ErrorHandlerMock.Verify(e => + e.HandleError(It.Is(ex => + ex.Message == "Provided attribute is not in datafile."))); Assert.AreEqual(new Entity.Attribute(), attribute); } @@ -638,9 +1031,12 @@ public void TestGetVariationFromKeyValidEKInvalidVK() var variation = Config.GetVariationFromKey("test_experiment", "invalid_key"); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, @"No variation key ""invalid_key"" defined in datafile for experiment ""test_experiment"".")); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, + @"No variation key ""invalid_key"" defined in datafile for experiment ""test_experiment"".")); - ErrorHandlerMock.Verify(e => e.HandleError(It.Is(ex => ex.Message == "Provided variation is not in datafile."))); + ErrorHandlerMock.Verify(e => + e.HandleError(It.Is(ex => + ex.Message == "Provided variation is not in datafile."))); Assert.AreEqual(new Entity.Variation(), variation); } @@ -651,9 +1047,12 @@ public void TestGetVariationFromKeyInvalidEK() var variation = Config.GetVariationFromKey("invalid_experiment", "control"); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, @"No variation key ""control"" defined in datafile for experiment ""invalid_experiment"".")); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, + @"No variation key ""control"" defined in datafile for experiment ""invalid_experiment"".")); - ErrorHandlerMock.Verify(e => e.HandleError(It.Is(ex => ex.Message == "Provided variation is not in datafile."))); + ErrorHandlerMock.Verify(e => + e.HandleError(It.Is(ex => + ex.Message == "Provided variation is not in datafile."))); Assert.AreEqual(new Entity.Variation(), variation); } @@ -671,9 +1070,12 @@ public void TestGetVariationFromIdValidEKInvalidVId() var variation = Config.GetVariationFromId("test_experiment", "invalid_id"); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, @"No variation ID ""invalid_id"" defined in datafile for experiment ""test_experiment"".")); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, + @"No variation ID ""invalid_id"" defined in datafile for experiment ""test_experiment"".")); - ErrorHandlerMock.Verify(e => e.HandleError(It.Is(ex => ex.Message == "Provided variation is not in datafile."))); + ErrorHandlerMock.Verify(e => + e.HandleError(It.Is(ex => + ex.Message == "Provided variation is not in datafile."))); Assert.AreEqual(new Entity.Variation(), variation); } @@ -683,16 +1085,20 @@ public void TestGetVariationFromIdInvalidEK() var variation = Config.GetVariationFromId("invalid_experiment", "7722370027"); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Once); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, @"No variation ID ""7722370027"" defined in datafile for experiment ""invalid_experiment"".")); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, + @"No variation ID ""7722370027"" defined in datafile for experiment ""invalid_experiment"".")); - ErrorHandlerMock.Verify(e => e.HandleError(It.Is(ex => ex.Message == "Provided variation is not in datafile."))); + ErrorHandlerMock.Verify(e => + e.HandleError(It.Is(ex => + ex.Message == "Provided variation is not in datafile."))); Assert.AreEqual(new Entity.Variation(), variation); } [Test] public void TempProjectConfigTest() { - ProjectConfig config = DatafileProjectConfig.Create(TestData.Datafile, new Mock().Object, new DefaultErrorHandler()); + ProjectConfig config = DatafileProjectConfig.Create(TestData.Datafile, + new Mock().Object, new DefaultErrorHandler()); Assert.IsNotNull(config); Assert.AreEqual("1592310167", config.AccountId); } @@ -701,7 +1107,8 @@ public void TempProjectConfigTest() [Test] public void TestProjectConfigDatafileIsSame() { - ProjectConfig config = DatafileProjectConfig.Create(TestData.Datafile, new Mock().Object, new DefaultErrorHandler()); + ProjectConfig config = DatafileProjectConfig.Create(TestData.Datafile, + new Mock().Object, new DefaultErrorHandler()); Assert.AreEqual(config.ToDatafile(), TestData.Datafile); } @@ -729,7 +1136,8 @@ public void TestBotFilteringValues() if (projConfig.TryGetValue("botFiltering", out JToken token)) { projConfig.Property("botFiltering").Remove(); - var configWithoutBotFilter = DatafileProjectConfig.Create(JsonConvert.SerializeObject(projConfig), + var configWithoutBotFilter = DatafileProjectConfig.Create( + JsonConvert.SerializeObject(projConfig), LoggerMock.Object, ErrorHandlerMock.Object); // Verify that bot filtering is null when not defined in datafile. @@ -741,23 +1149,35 @@ public void TestBotFilteringValues() public void TestGetAttributeIdWithReservedPrefix() { // Verify that attribute key is returned for reserved attribute key. - Assert.AreEqual(Config.GetAttributeId(ControlAttributes.USER_AGENT_ATTRIBUTE), ControlAttributes.USER_AGENT_ATTRIBUTE); + Assert.AreEqual(Config.GetAttributeId(ControlAttributes.USER_AGENT_ATTRIBUTE), + ControlAttributes.USER_AGENT_ATTRIBUTE); // Verify that attribute Id is returned for attribute key with reserved prefix that does not exist in datafile. - Assert.AreEqual(Config.GetAttributeId("$opt_reserved_prefix_attribute"), "$opt_reserved_prefix_attribute"); + Assert.AreEqual(Config.GetAttributeId("$opt_reserved_prefix_attribute"), + "$opt_reserved_prefix_attribute"); // Create config file copy with additional resered prefix attribute. string reservedPrefixAttrKey = "$opt_user_defined_attribute"; JObject projConfig = JObject.Parse(TestData.Datafile); var attributes = (JArray)projConfig["attributes"]; - var reservedAttr = new Entity.Attribute { Id = "7723348204", Key = reservedPrefixAttrKey }; + var reservedAttr = new Entity.Attribute + { + Id = "7723348204", + Key = reservedPrefixAttrKey + }; attributes.Add((JObject)JToken.FromObject(reservedAttr)); // Verify that attribute Id is returned and warning is logged for attribute key with reserved prefix that exists in datafile. - var reservedAttrConfig = DatafileProjectConfig.Create(JsonConvert.SerializeObject(projConfig), LoggerMock.Object, ErrorHandlerMock.Object); - Assert.AreEqual(reservedAttrConfig.GetAttributeId(reservedPrefixAttrKey), reservedAttrConfig.GetAttribute(reservedPrefixAttrKey).Id); - LoggerMock.Verify(l => l.Log(LogLevel.WARN, $@"Attribute {reservedPrefixAttrKey} unexpectedly has reserved prefix {DatafileProjectConfig.RESERVED_ATTRIBUTE_PREFIX}; using attribute ID instead of reserved attribute name.")); + var reservedAttrConfig = DatafileProjectConfig.Create( + JsonConvert.SerializeObject(projConfig), LoggerMock.Object, + ErrorHandlerMock.Object); + Assert.AreEqual(reservedAttrConfig.GetAttributeId(reservedPrefixAttrKey), + reservedAttrConfig.GetAttribute(reservedPrefixAttrKey).Id); + LoggerMock.Verify(l => l.Log(LogLevel.WARN, + $@"Attribute {reservedPrefixAttrKey} unexpectedly has reserved prefix { + DatafileProjectConfig.RESERVED_ATTRIBUTE_PREFIX + }; using attribute ID instead of reserved attribute name.")); } [Test] @@ -765,28 +1185,36 @@ public void TestGetAttributeIdWithInvalidAttributeKey() { // Verify that null is returned when provided attribute key is invalid. Assert.Null(Config.GetAttributeId("invalid_attribute")); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, @"Attribute key ""invalid_attribute"" is not in datafile.")); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, + @"Attribute key ""invalid_attribute"" is not in datafile.")); } [Test] public void TestCreateThrowsWithNullDatafile() { - var exception = Assert.Throws(() => DatafileProjectConfig.Create(null, null, null)); + var exception = + Assert.Throws(() => + DatafileProjectConfig.Create(null, null, null)); Assert.AreEqual("Unable to parse null datafile.", exception.Message); } [Test] public void TestCreateThrowsWithEmptyDatafile() { - var exception = Assert.Throws(() => DatafileProjectConfig.Create("", null, null)); + var exception = + Assert.Throws(() => + DatafileProjectConfig.Create("", null, null)); Assert.AreEqual("Unable to parse empty datafile.", exception.Message); } [Test] public void TestCreateThrowsWithUnsupportedDatafileVersion() { - var exception = Assert.Throws(() => DatafileProjectConfig.Create(TestData.UnsupportedVersionDatafile, null, null)); - Assert.AreEqual($"This version of the C# SDK does not support the given datafile version: 5", exception.Message); + var exception = Assert.Throws(() => + DatafileProjectConfig.Create(TestData.UnsupportedVersionDatafile, null, null)); + Assert.AreEqual( + $"This version of the C# SDK does not support the given datafile version: 5", + exception.Message); } [Test] @@ -798,17 +1226,23 @@ public void TestCreateDoesNotThrowWithValidDatafile() [Test] public void TestExperimentAudiencesRetrivedFromTypedAudiencesFirstThenFromAudiences() { - var typedConfig = DatafileProjectConfig.Create(TestData.TypedAudienceDatafile, null, null); + var typedConfig = + DatafileProjectConfig.Create(TestData.TypedAudienceDatafile, null, null); var experiment = typedConfig.GetExperimentFromKey("feat_with_var_test"); - var expectedAudienceIds = new string[] { "3468206642", "3988293898", "3988293899", "3468206646", "3468206647", "3468206644", "3468206643" }; + var expectedAudienceIds = new string[] + { + "3468206642", "3988293898", "3988293899", "3468206646", "3468206647", "3468206644", + "3468206643" + }; Assert.That(expectedAudienceIds, Is.EquivalentTo(experiment.AudienceIds)); } [Test] public void TestIsFeatureExperimentReturnsFalseForExperimentThatDoesNotBelongToAnyFeature() { - var typedConfig = DatafileProjectConfig.Create(TestData.TypedAudienceDatafile, null, null); + var typedConfig = + DatafileProjectConfig.Create(TestData.TypedAudienceDatafile, null, null); var experiment = typedConfig.GetExperimentFromKey("typed_audience_experiment"); Assert.False(typedConfig.IsFeatureExperiment(experiment.Id)); @@ -817,52 +1251,54 @@ public void TestIsFeatureExperimentReturnsFalseForExperimentThatDoesNotBelongToA [Test] public void TestIsFeatureExperimentReturnsTrueForExperimentThatBelongsToAFeature() { - var typedConfig = DatafileProjectConfig.Create(TestData.TypedAudienceDatafile, null, null); + var typedConfig = + DatafileProjectConfig.Create(TestData.TypedAudienceDatafile, null, null); var experiment = typedConfig.GetExperimentFromKey("feat2_with_var_test"); Assert.True(typedConfig.IsFeatureExperiment(experiment.Id)); } - + [Test] public void TestRolloutWithEmptyStringRolloutIdFromConfigFile() { - var projectConfig = DatafileProjectConfig.Create(TestData.EmptyRolloutDatafile, null, null); + var projectConfig = + DatafileProjectConfig.Create(TestData.EmptyRolloutDatafile, null, null); Assert.IsNotNull(projectConfig); var featureFlag = projectConfig.FeatureKeyMap["empty_rollout_id"]; - + var rollout = projectConfig.GetRolloutFromId(featureFlag.RolloutId); Assert.IsNull(rollout.Experiments); Assert.IsNull(rollout.Id); } - + [Test] public void TestRolloutWithEmptyStringRolloutId() { var rolloutId = string.Empty; - + var rollout = Config.GetRolloutFromId(rolloutId); Assert.IsNull(rollout.Experiments); Assert.IsNull(rollout.Id); } - + [Test] public void TestRolloutWithConsistingOfASingleSpaceRolloutId() { var rolloutId = " "; // single space - + var rollout = Config.GetRolloutFromId(rolloutId); Assert.IsNull(rollout.Experiments); Assert.IsNull(rollout.Id); } - + [Test] public void TestRolloutWithConsistingOfANullRolloutId() { string nullRolloutId = null; - + var rollout = Config.GetRolloutFromId(nullRolloutId); Assert.IsNull(rollout.Experiments); @@ -870,39 +1306,47 @@ public void TestRolloutWithConsistingOfANullRolloutId() } private const string ZAIUS_HOST = "https://api.zaius.com"; - private const string ZAIUS_PUBLIC_KEY = "W4WzcEs-ABgXorzY7h1LCQ"; - + private const string ZAIUS_PUBLIC_KEY = "fake-public-key"; + [Test] public void TestProjectConfigWithOdpIntegration() { - var datafileProjectConfig = DatafileProjectConfig.Create(TestData.OdpIntegrationDatafile, new NoOpLogger(), new ErrorHandler.NoOpErrorHandler()); + var datafileProjectConfig = DatafileProjectConfig.Create( + TestData.OdpIntegrationDatafile, new NoOpLogger(), + new ErrorHandler.NoOpErrorHandler()); Assert.AreEqual(ZAIUS_HOST, datafileProjectConfig.HostForOdp); Assert.AreEqual(ZAIUS_PUBLIC_KEY, datafileProjectConfig.PublicKeyForOdp); } - + [Test] public void TestProjectConfigWithOdpIntegrationIncludesOtherFields() { - var datafileProjectConfig = DatafileProjectConfig.Create(TestData.OdpIntegrationWithOtherFieldsDatafile, new NoOpLogger(), new ErrorHandler.NoOpErrorHandler()); + var datafileProjectConfig = DatafileProjectConfig.Create( + TestData.OdpIntegrationWithOtherFieldsDatafile, new NoOpLogger(), + new ErrorHandler.NoOpErrorHandler()); Assert.AreEqual(ZAIUS_HOST, datafileProjectConfig.HostForOdp); Assert.AreEqual(ZAIUS_PUBLIC_KEY, datafileProjectConfig.PublicKeyForOdp); } - + [Test] public void TestProjectConfigWithEmptyIntegrationCollection() { - var datafileProjectConfig = DatafileProjectConfig.Create(TestData.EmptyIntegrationDatafile, new NoOpLogger(), new ErrorHandler.NoOpErrorHandler()); + var datafileProjectConfig = DatafileProjectConfig.Create( + TestData.EmptyIntegrationDatafile, new NoOpLogger(), + new ErrorHandler.NoOpErrorHandler()); Assert.IsNull(datafileProjectConfig.HostForOdp); Assert.IsNull(datafileProjectConfig.PublicKeyForOdp); } - + [Test] public void TestProjectConfigWithOtherIntegrationsInCollection() { - var datafileProjectConfig = DatafileProjectConfig.Create(TestData.NonOdpIntegrationDatafile, new NoOpLogger(), new ErrorHandler.NoOpErrorHandler()); + var datafileProjectConfig = DatafileProjectConfig.Create( + TestData.NonOdpIntegrationDatafile, new NoOpLogger(), + new ErrorHandler.NoOpErrorHandler()); Assert.IsNull(datafileProjectConfig.HostForOdp); Assert.IsNull(datafileProjectConfig.PublicKeyForOdp); diff --git a/OptimizelySDK.Tests/Properties/AssemblyInfo.cs b/OptimizelySDK.Tests/Properties/AssemblyInfo.cs index f38aabf17..1ee5326f2 100644 --- a/OptimizelySDK.Tests/Properties/AssemblyInfo.cs +++ b/OptimizelySDK.Tests/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -10,7 +9,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("OptimizelySDK.Tests")] -[assembly: AssemblyCopyright("Copyright © 2017-2020")] +[assembly: AssemblyCopyright("Copyright © 2017-2023")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/OptimizelySDK.Tests/Utils/TestData.cs b/OptimizelySDK.Tests/Utils/TestData.cs index 2f3c8000e..6fde30de5 100644 --- a/OptimizelySDK.Tests/Utils/TestData.cs +++ b/OptimizelySDK.Tests/Utils/TestData.cs @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020, Optimizely + * Copyright 2017-2020, 2022-2023 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; @@ -31,6 +32,7 @@ public class TestData private static string emptyDatafile = null; private static string duplicateExpKeysDatafile = null; private static string duplicateRuleKeysDatafile = null; + private static string odpSegmentsDatafile = null; private static string emptyIntegrationDatafile = null; private static string nonOdpIntegrationDatafile = null; @@ -45,23 +47,30 @@ public static string Datafile } } - public static string DuplicateExpKeysDatafile { - get { - return duplicateExpKeysDatafile ?? (duplicateExpKeysDatafile = LoadJsonData("similar_exp_keys.json")); + public static string DuplicateExpKeysDatafile + { + get + { + return duplicateExpKeysDatafile ?? + (duplicateExpKeysDatafile = LoadJsonData("similar_exp_keys.json")); } } - public static string DuplicateRuleKeysDatafile { - get { - return duplicateRuleKeysDatafile ?? (duplicateRuleKeysDatafile = LoadJsonData("similar_rule_keys_bucketing.json")); + public static string DuplicateRuleKeysDatafile + { + get + { + return duplicateRuleKeysDatafile ?? (duplicateRuleKeysDatafile = + LoadJsonData("similar_rule_keys_bucketing.json")); } } public static string SimpleABExperimentsDatafile { - get + get { - return simpleABExperimentsDatafile ?? (simpleABExperimentsDatafile = LoadJsonData("simple_ab_experiments.json")); + return simpleABExperimentsDatafile ?? (simpleABExperimentsDatafile = + LoadJsonData("simple_ab_experiments.json")); } } @@ -69,47 +78,62 @@ public static string UnsupportedVersionDatafile { get { - return unsupportedVersionDatafile ?? (unsupportedVersionDatafile = LoadJsonData("unsupported_version_datafile.json")); + return unsupportedVersionDatafile ?? (unsupportedVersionDatafile = + LoadJsonData("unsupported_version_datafile.json")); } } - public static string EmptyRolloutDatafile { - get { - return emptyRolloutDatafile ?? (emptyRolloutDatafile = LoadJsonData("EmptyRolloutRule.json")); + public static string EmptyRolloutDatafile + { + get + { + return emptyRolloutDatafile ?? + (emptyRolloutDatafile = LoadJsonData("EmptyRolloutRule.json")); } } - public static string EmptyDatafile { - get { + public static string EmptyDatafile + { + get + { return emptyDatafile ?? (emptyDatafile = LoadJsonData("emptydatafile.json")); } } - - public static string EmptyIntegrationDatafile { - get + + public static string EmptyIntegrationDatafile + { + get { - return emptyIntegrationDatafile ?? (emptyIntegrationDatafile = LoadJsonData("IntegrationEmptyDatafile.json")); + return emptyIntegrationDatafile ?? (emptyIntegrationDatafile = + LoadJsonData("IntegrationEmptyDatafile.json")); } } - - public static string NonOdpIntegrationDatafile { - get + + public static string NonOdpIntegrationDatafile + { + get { - return nonOdpIntegrationDatafile ?? (nonOdpIntegrationDatafile = LoadJsonData("IntegrationNonOdpDatafile.json")); + return nonOdpIntegrationDatafile ?? (nonOdpIntegrationDatafile = + LoadJsonData("IntegrationNonOdpDatafile.json")); } } - - public static string OdpIntegrationDatafile { - get + + public static string OdpIntegrationDatafile + { + get { - return odpIntegrationDatafile ?? (odpIntegrationDatafile = LoadJsonData("IntegrationOdpDatafile.json")); + return odpIntegrationDatafile ?? (odpIntegrationDatafile = + LoadJsonData("IntegrationOdpDatafile.json")); } } - - public static string OdpIntegrationWithOtherFieldsDatafile { - get + + public static string OdpIntegrationWithOtherFieldsDatafile + { + get { - return odpIntegrationWithOtherFieldsDatafile ?? (odpIntegrationWithOtherFieldsDatafile = LoadJsonData("IntegrationOdpWithOtherFieldsDatafile.json")); + return odpIntegrationWithOtherFieldsDatafile ?? + (odpIntegrationWithOtherFieldsDatafile = + LoadJsonData("IntegrationOdpWithOtherFieldsDatafile.json")); } } @@ -117,7 +141,17 @@ public static string TypedAudienceDatafile { get { - return typedAudienceDatafile ?? (typedAudienceDatafile = LoadJsonData("typed_audience_datafile.json")); + return typedAudienceDatafile ?? (typedAudienceDatafile = + LoadJsonData("typed_audience_datafile.json")); + } + } + + public static string OdpSegmentsDatafile + { + get + { + return odpSegmentsDatafile ?? + (odpSegmentsDatafile = LoadJsonData("OdpSegmentsDatafile.json")); } } @@ -146,7 +180,9 @@ public static long SecondsSince1970() return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds; } - public static void ChangeGUIDAndTimeStamp(Dictionary paramsObj, long timeStamp, Guid guid) + public static void ChangeGUIDAndTimeStamp(Dictionary paramsObj, + long timeStamp, Guid guid + ) { // Path from where to find // visitors.[0].snapshots.[0].events.[0].uuid or timestamp diff --git a/OptimizelySDK.sln.DotSettings b/OptimizelySDK.sln.DotSettings index bc0d0ca31..3ccf7ffc1 100644 --- a/OptimizelySDK.sln.DotSettings +++ b/OptimizelySDK.sln.DotSettings @@ -50,6 +50,7 @@ True True True + True True True diff --git a/OptimizelySDK/Config/DatafileProjectConfig.cs b/OptimizelySDK/Config/DatafileProjectConfig.cs index 5b50177c6..4fd9bb4b0 100644 --- a/OptimizelySDK/Config/DatafileProjectConfig.cs +++ b/OptimizelySDK/Config/DatafileProjectConfig.cs @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022, Optimizely + * Copyright 2019-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,10 @@ * limitations under the License. */ +#if !(NET35 || NET40 || NETSTANDARD1_6) +#define USE_ODP +#endif + using Newtonsoft.Json; using OptimizelySDK.Entity; using OptimizelySDK.ErrorHandler; @@ -102,7 +106,7 @@ public enum OPTLYSDKVersion /// Configured host name for the Optimizely Data Platform. /// public string HostForOdp { get; private set; } - + /// /// Configured public key from the Optimizely Data Platform. /// @@ -111,7 +115,8 @@ public enum OPTLYSDKVersion /// /// Supported datafile versions list. /// - private static List SupportedVersions = new List { + private static List SupportedVersions = new List + { OPTLYSDKVersion.V2, OPTLYSDKVersion.V3, OPTLYSDKVersion.V4 @@ -147,7 +152,10 @@ private Dictionary _ExperimentIdMap private Dictionary> _VariationKeyMap = new Dictionary>(); - public Dictionary> VariationKeyMap { get { return _VariationKeyMap; } } + public Dictionary> VariationKeyMap + { + get { return _VariationKeyMap; } + } /// /// Associative array of experiment ID to associative array of variation key to variations @@ -155,7 +163,10 @@ private Dictionary> _VariationKeyMap private Dictionary> _VariationKeyMapByExperimentId = new Dictionary>(); - public Dictionary> VariationKeyMapByExperimentId { get { return _VariationKeyMapByExperimentId; } } + public Dictionary> VariationKeyMapByExperimentId + { + get { return _VariationKeyMapByExperimentId; } + } /// /// Associative array of experiment ID to associative array of variation key to variations @@ -163,7 +174,10 @@ private Dictionary> _VariationKeyMapByExpe private Dictionary> _VariationIdMapByExperimentId = new Dictionary>(); - public Dictionary> VariationKeyIdByExperimentId { get { return _VariationIdMapByExperimentId; } } + public Dictionary> VariationKeyIdByExperimentId + { + get { return _VariationIdMapByExperimentId; } + } /// /// Associative array of experiment key to associative array of variation ID to variations @@ -171,7 +185,10 @@ private Dictionary> _VariationIdMapByExper private Dictionary> _VariationIdMap = new Dictionary>(); - public Dictionary> VariationIdMap { get { return _VariationIdMap; } } + public Dictionary> VariationIdMap + { + get { return _VariationIdMap; } + } /// /// Associative array of event key to Event(s) in the datafile @@ -212,13 +229,19 @@ private Dictionary> _VariationIdMap /// Associative array of experiment IDs that exist in any feature /// for checking that experiment is a feature experiment. /// - private Dictionary> ExperimentFeatureMap = new Dictionary>(); + private Dictionary> ExperimentFeatureMap = + new Dictionary>(); /// /// Associated dictionary of flags to variations key value. /// - private Dictionary> _FlagVariationMap = new Dictionary>(); - public Dictionary> FlagVariationMap { get { return _FlagVariationMap; } } + private Dictionary> _FlagVariationMap = + new Dictionary>(); + + public Dictionary> FlagVariationMap + { + get { return _FlagVariationMap; } + } //========================= Interfaces =========================== @@ -273,12 +296,14 @@ private Dictionary> _VariationIdMap /// Associative list of Rollouts. /// public Rollout[] Rollouts { get; set; } - + /// /// Associative list of Integrations. /// public Integration[] Integrations { get; set; } + public string[] Segments { get; set; } + //========================= Initialization =========================== /// @@ -298,22 +323,33 @@ private void Initialize() Integrations = Integrations ?? new Integration[0]; _ExperimentKeyMap = new Dictionary(); - _GroupIdMap = ConfigParser.GenerateMap(entities: Groups, getKey: g => g.Id.ToString(), clone: true); - _ExperimentIdMap = ConfigParser.GenerateMap(entities: Experiments, getKey: e => e.Id, clone: true); - _EventKeyMap = ConfigParser.GenerateMap(entities: Events, getKey: e => e.Key, clone: true); - _AttributeKeyMap = ConfigParser.GenerateMap(entities: Attributes, getKey: a => a.Key, clone: true); - _AudienceIdMap = ConfigParser.GenerateMap(entities: Audiences, getKey: a => a.Id.ToString(), clone: true); - _FeatureKeyMap = ConfigParser.GenerateMap(entities: FeatureFlags, getKey: f => f.Key, clone: true); - _RolloutIdMap = ConfigParser.GenerateMap(entities: Rollouts, getKey: r => r.Id.ToString(), clone: true); + _GroupIdMap = ConfigParser.GenerateMap(entities: Groups, + getKey: g => g.Id.ToString(), clone: true); + _ExperimentIdMap = ConfigParser.GenerateMap(entities: Experiments, + getKey: e => e.Id, clone: true); + _EventKeyMap = + ConfigParser.GenerateMap(entities: Events, getKey: e => e.Key, + clone: true); + _AttributeKeyMap = ConfigParser.GenerateMap(entities: Attributes, + getKey: a => a.Key, clone: true); + _AudienceIdMap = ConfigParser.GenerateMap(entities: Audiences, + getKey: a => a.Id.ToString(), clone: true); + _FeatureKeyMap = ConfigParser.GenerateMap(entities: FeatureFlags, + getKey: f => f.Key, clone: true); + _RolloutIdMap = ConfigParser.GenerateMap(entities: Rollouts, + getKey: r => r.Id.ToString(), clone: true); // Overwrite similar items in audience id map with typed audience id map. - var typedAudienceIdMap = ConfigParser.GenerateMap(entities: TypedAudiences, getKey: a => a.Id.ToString(), clone: true); + var typedAudienceIdMap = ConfigParser.GenerateMap(entities: TypedAudiences, + getKey: a => a.Id.ToString(), clone: true); foreach (var item in typedAudienceIdMap) _AudienceIdMap[item.Key] = item.Value; foreach (Group group in Groups) { - var experimentsInGroup = ConfigParser.GenerateMap(group.Experiments, getKey: e => e.Id, clone: true); + var experimentsInGroup = + ConfigParser.GenerateMap(group.Experiments, getKey: e => e.Id, + clone: true); foreach (Experiment experiment in experimentsInGroup.Values) { experiment.GroupId = group.Id; @@ -354,8 +390,10 @@ private void Initialize() { _VariationKeyMap[rolloutRule.Key] = new Dictionary(); _VariationIdMap[rolloutRule.Key] = new Dictionary(); - _VariationIdMapByExperimentId[rolloutRule.Id] = new Dictionary(); - _VariationKeyMapByExperimentId[rolloutRule.Id] = new Dictionary(); + _VariationIdMapByExperimentId[rolloutRule.Id] = + new Dictionary(); + _VariationKeyMapByExperimentId[rolloutRule.Id] = + new Dictionary(); if (rolloutRule.Variations != null) { @@ -363,7 +401,8 @@ private void Initialize() { _VariationKeyMap[rolloutRule.Key][variation.Key] = variation; _VariationIdMap[rolloutRule.Key][variation.Id] = variation; - _VariationKeyMapByExperimentId[rolloutRule.Id][variation.Key] = variation; + _VariationKeyMapByExperimentId[rolloutRule.Id][variation.Key] = + variation; _VariationIdMapByExperimentId[rolloutRule.Id][variation.Id] = variation; } } @@ -373,7 +412,10 @@ private void Initialize() var integration = Integrations.FirstOrDefault(i => i.Key.ToLower() == "odp"); HostForOdp = integration?.Host; PublicKeyForOdp = integration?.PublicKey; - +#if USE_ODP + var rawSegments = TypedAudiences.SelectMany(ta => ta.GetSegments()).ToArray(); + Segments = new SortedSet(rawSegments).ToArray(); +#endif var flagToVariationsMap = new Dictionary>(); // Adding experiments in experiment-feature map and flag variation map to use. foreach (var feature in FeatureFlags) @@ -381,7 +423,9 @@ private void Initialize() var variationKeyToVariationDict = new Dictionary(); foreach (var experimentId in feature.ExperimentIds ?? new List()) { - foreach (var variationDictKV in ExperimentIdMap[experimentId].VariationKeyToVariationMap) { + foreach (var variationDictKV in ExperimentIdMap[experimentId]. + VariationKeyToVariationMap) + { variationKeyToVariationDict[variationDictKV.Key] = variationDictKV.Value; } @@ -390,19 +434,28 @@ private void Initialize() ExperimentFeatureMap[experimentId].Add(feature.Id); } else - { - ExperimentFeatureMap[experimentId] = new List { feature.Id }; + { + ExperimentFeatureMap[experimentId] = new List + { + feature.Id + }; } } - if (RolloutIdMap.TryGetValue(feature.RolloutId, out var rolloutRules)) { - var rolloutRulesVariations = rolloutRules.Experiments.SelectMany(ex => ex.Variations); - foreach (var rolloutRuleVariation in rolloutRulesVariations) { - variationKeyToVariationDict[rolloutRuleVariation.Key] = rolloutRuleVariation; + + if (RolloutIdMap.TryGetValue(feature.RolloutId, out var rolloutRules)) + { + var rolloutRulesVariations = + rolloutRules.Experiments.SelectMany(ex => ex.Variations); + foreach (var rolloutRuleVariation in rolloutRulesVariations) + { + variationKeyToVariationDict[rolloutRuleVariation.Key] = + rolloutRuleVariation; } } - + flagToVariationsMap[feature.Key] = variationKeyToVariationDict; } + _FlagVariationMap = flagToVariationsMap; } @@ -413,7 +466,9 @@ private void Initialize() /// Logger instance /// ErrorHandler instance /// ProjectConfig instance created from datafile string - public static ProjectConfig Create(string content, ILogger logger, IErrorHandler errorHandler) + public static ProjectConfig Create(string content, ILogger logger, + IErrorHandler errorHandler + ) { DatafileProjectConfig config = GetConfig(content); @@ -436,8 +491,11 @@ private static DatafileProjectConfig GetConfig(string configData) var config = JsonConvert.DeserializeObject(configData); config._datafile = configData; - if (SupportedVersions.TrueForAll((supportedVersion) => !(((int)supportedVersion).ToString() == config.Version))) - throw new ConfigParseException($@"This version of the C# SDK does not support the given datafile version: {config.Version}"); + if (SupportedVersions.TrueForAll((supportedVersion) => + !(((int)supportedVersion).ToString() == config.Version))) + throw new ConfigParseException( + $@"This version of the C# SDK does not support the given datafile version: { + config.Version}"); return config; } @@ -456,7 +514,8 @@ public Group GetGroup(string groupId) string message = $@"Group ID ""{groupId}"" is not in datafile."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidGroupException("Provided group is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidGroupException("Provided group is not in datafile.")); return new Group(); } @@ -472,7 +531,9 @@ public Experiment GetExperimentFromKey(string experimentKey) string message = $@"Experiment key ""{experimentKey}"" is not in datafile."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidExperimentException("Provided experiment is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidExperimentException( + "Provided experiment is not in datafile.")); return new Experiment(); } @@ -488,7 +549,9 @@ public Experiment GetExperimentFromId(string experimentId) string message = $@"Experiment ID ""{experimentId}"" is not in datafile."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidExperimentException("Provided experiment is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidExperimentException( + "Provided experiment is not in datafile.")); return new Experiment(); } @@ -504,7 +567,8 @@ public Entity.Event GetEvent(string eventKey) string message = $@"Event key ""{eventKey}"" is not in datafile."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidEventException("Provided event is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidEventException("Provided event is not in datafile.")); return new Entity.Event(); } @@ -520,7 +584,8 @@ public Audience GetAudience(string audienceId) string message = $@"Audience ID ""{audienceId}"" is not in datafile."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidAudienceException("Provided audience is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidAudienceException("Provided audience is not in datafile.")); return new Audience(); } @@ -536,7 +601,8 @@ public Attribute GetAttribute(string attributeKey) string message = $@"Attribute key ""{attributeKey}"" is not in datafile."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidAttributeException("Provided attribute is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidAttributeException("Provided attribute is not in datafile.")); return new Attribute(); } @@ -553,9 +619,11 @@ public Variation GetVariationFromKey(string experimentKey, string variationKey) _VariationKeyMap[experimentKey].ContainsKey(variationKey)) return _VariationKeyMap[experimentKey][variationKey]; - string message = $@"No variation key ""{variationKey}"" defined in datafile for experiment ""{experimentKey}""."; + string message = $@"No variation key ""{variationKey + }"" defined in datafile for experiment ""{experimentKey}""."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidVariationException("Provided variation is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidVariationException("Provided variation is not in datafile.")); return new Variation(); } @@ -572,9 +640,11 @@ public Variation GetVariationFromKeyByExperimentId(string experimentId, string v _VariationKeyMapByExperimentId[experimentId].ContainsKey(variationKey)) return _VariationKeyMapByExperimentId[experimentId][variationKey]; - string message = $@"No variation key ""{variationKey}"" defined in datafile for experiment ""{experimentId}""."; + string message = $@"No variation key ""{variationKey + }"" defined in datafile for experiment ""{experimentId}""."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidVariationException("Provided variation is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidVariationException("Provided variation is not in datafile.")); return new Variation(); } @@ -591,9 +661,11 @@ public Variation GetVariationFromId(string experimentKey, string variationId) _VariationIdMap[experimentKey].ContainsKey(variationId)) return _VariationIdMap[experimentKey][variationId]; - string message = $@"No variation ID ""{variationId}"" defined in datafile for experiment ""{experimentKey}""."; + string message = $@"No variation ID ""{variationId + }"" defined in datafile for experiment ""{experimentKey}""."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidVariationException("Provided variation is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidVariationException("Provided variation is not in datafile.")); return new Variation(); } @@ -610,9 +682,11 @@ public Variation GetVariationFromIdByExperimentId(string experimentId, string va _VariationIdMapByExperimentId[experimentId].ContainsKey(variationId)) return _VariationIdMapByExperimentId[experimentId][variationId]; - string message = $@"No variation ID ""{variationId}"" defined in datafile for experiment ""{experimentId}""."; + string message = $@"No variation ID ""{variationId + }"" defined in datafile for experiment ""{experimentId}""."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidVariationException("Provided variation is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidVariationException("Provided variation is not in datafile.")); return new Variation(); } @@ -628,7 +702,8 @@ public FeatureFlag GetFeatureFlagFromKey(string featureKey) string message = $@"Feature key ""{featureKey}"" is not in datafile."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidFeatureException("Provided feature is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidFeatureException("Provided feature is not in datafile.")); return new FeatureFlag(); } @@ -640,8 +715,8 @@ public FeatureFlag GetFeatureFlagFromKey(string featureKey) /// public Variation GetFlagVariationByKey(string flagKey, string variationKey) { - if (FlagVariationMap.TryGetValue(flagKey, out var variationsKeyMap)) { - + if (FlagVariationMap.TryGetValue(flagKey, out var variationsKeyMap)) + { variationsKeyMap.TryGetValue(variationKey, out var variation); return variation; } @@ -657,20 +732,21 @@ public Variation GetFlagVariationByKey(string flagKey, string variationKey) public Rollout GetRolloutFromId(string rolloutId) { #if NET35 || NET40 - if (string.IsNullOrEmpty(rolloutId) || string.IsNullOrEmpty(rolloutId.Trim())) + if (string.IsNullOrEmpty(rolloutId) || string.IsNullOrEmpty(rolloutId.Trim())) #else - if (string.IsNullOrWhiteSpace(rolloutId)) + if (string.IsNullOrWhiteSpace(rolloutId)) #endif { return new Rollout(); } - + if (_RolloutIdMap.ContainsKey(rolloutId)) return _RolloutIdMap[rolloutId]; string message = $@"Rollout ID ""{rolloutId}"" is not in datafile."; Logger.Log(LogLevel.ERROR, message); - ErrorHandler.HandleError(new Exceptions.InvalidRolloutException("Provided rollout is not in datafile.")); + ErrorHandler.HandleError( + new Exceptions.InvalidRolloutException("Provided rollout is not in datafile.")); return new Rollout(); } @@ -687,7 +763,10 @@ public string GetAttributeId(string attributeKey) { var attribute = _AttributeKeyMap[attributeKey]; if (hasReservedPrefix) - Logger.Log(LogLevel.WARN, $@"Attribute {attributeKey} unexpectedly has reserved prefix {RESERVED_ATTRIBUTE_PREFIX}; using attribute ID instead of reserved attribute name."); + Logger.Log(LogLevel.WARN, + $@"Attribute {attributeKey} unexpectedly has reserved prefix { + RESERVED_ATTRIBUTE_PREFIX + }; using attribute ID instead of reserved attribute name."); return attribute.Id; } diff --git a/OptimizelySDK/Config/FallbackProjectConfigManager.cs b/OptimizelySDK/Config/FallbackProjectConfigManager.cs index 737dc422c..efe1a5d51 100644 --- a/OptimizelySDK/Config/FallbackProjectConfigManager.cs +++ b/OptimizelySDK/Config/FallbackProjectConfigManager.cs @@ -1,11 +1,11 @@ /* - * Copyright 2019, Optimizely + * Copyright 2019, 2022-2023 Optimizely * * 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 + * https://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, @@ -24,8 +24,8 @@ namespace OptimizelySDK.Config /// public class FallbackProjectConfigManager : ProjectConfigManager { - private ProjectConfig ProjectConfig; - + private ProjectConfig ProjectConfig; + /// /// Initializes a new instance of the FallbackProjectConfigManager class /// with the given ProjectConfig instance. @@ -44,5 +44,28 @@ public ProjectConfig GetConfig() { return ProjectConfig; } + + /// + /// SDK Key for Fallback is not used and always null + /// + public virtual string SdkKey + { + get + { + return null; + } + } + + /// + /// Access to current cached project configuration + /// + /// ProjectConfig instance + public virtual ProjectConfig CachedProjectConfig + { + get + { + return ProjectConfig; + } + } } } diff --git a/OptimizelySDK/Config/HttpProjectConfigManager.cs b/OptimizelySDK/Config/HttpProjectConfigManager.cs index 8b909755b..a3d88f6cc 100644 --- a/OptimizelySDK/Config/HttpProjectConfigManager.cs +++ b/OptimizelySDK/Config/HttpProjectConfigManager.cs @@ -1,11 +1,11 @@ /* - * Copyright 2019-2021, Optimizely + * Copyright 2019-2021, 2023 Optimizely * * 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 + * https://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, @@ -14,6 +14,10 @@ * limitations under the License. */ +#if !(NET35 || NET40 || NETSTANDARD1_6) +#define USE_ODP +#endif + using OptimizelySDK.ErrorHandler; using OptimizelySDK.Logger; using OptimizelySDK.Notifications; @@ -29,18 +33,30 @@ public class HttpProjectConfigManager : PollingProjectConfigManager private string Url; private string LastModifiedSince = string.Empty; private string DatafileAccessToken = string.Empty; - private HttpProjectConfigManager(TimeSpan period, string url, TimeSpan blockingTimeout, bool autoUpdate, ILogger logger, IErrorHandler errorHandler) + + private HttpProjectConfigManager(TimeSpan period, string url, TimeSpan blockingTimeout, + bool autoUpdate, ILogger logger, IErrorHandler errorHandler, string sdkKey + ) : base(period, blockingTimeout, autoUpdate, logger, errorHandler) { Url = url; + SdkKey = sdkKey; } - private HttpProjectConfigManager(TimeSpan period, string url, TimeSpan blockingTimeout, bool autoUpdate, ILogger logger, IErrorHandler errorHandler, string datafileAccessToken) - : this(period, url, blockingTimeout, autoUpdate, logger, errorHandler) + private HttpProjectConfigManager(TimeSpan period, string url, TimeSpan blockingTimeout, + bool autoUpdate, ILogger logger, IErrorHandler errorHandler, string datafileAccessToken, + string sdkKey + ) + : this(period, url, blockingTimeout, autoUpdate, logger, errorHandler, sdkKey) { DatafileAccessToken = datafileAccessToken; } + /// + /// SDK key in use for this project + /// + public override string SdkKey { get; } + public Task OnReady() { return CompletableConfigManager.Task; @@ -59,21 +75,26 @@ public HttpClient() public HttpClient(System.Net.Http.HttpClient httpClient) : this() { - if (httpClient != null) { + if (httpClient != null) + { Client = httpClient; } } public static System.Net.Http.HttpClientHandler GetHttpClientHandler() { - var handler = new System.Net.Http.HttpClientHandler() { - AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate + var handler = new System.Net.Http.HttpClientHandler() + { + AutomaticDecompression = System.Net.DecompressionMethods.GZip | + System.Net.DecompressionMethods.Deflate }; return handler; } - public virtual Task SendAsync(System.Net.Http.HttpRequestMessage httpRequestMessage) + public virtual Task SendAsync( + System.Net.Http.HttpRequestMessage httpRequestMessage + ) { return Client.SendAsync(httpRequestMessage); } @@ -84,12 +105,13 @@ public static System.Net.Http.HttpClientHandler GetHttpClientHandler() static HttpProjectConfigManager() { Client = new HttpClient(); - } + } private string GetRemoteDatafileResponse() { Logger.Log(LogLevel.DEBUG, $"Making datafile request to url \"{Url}\""); - var request = new System.Net.Http.HttpRequestMessage { + var request = new System.Net.Http.HttpRequestMessage + { RequestUri = new Uri(Url), Method = System.Net.Http.HttpMethod.Get, }; @@ -98,16 +120,20 @@ private string GetRemoteDatafileResponse() if (!string.IsNullOrEmpty(LastModifiedSince)) request.Headers.Add("If-Modified-Since", LastModifiedSince); - if (!string.IsNullOrEmpty(DatafileAccessToken)) { - request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", DatafileAccessToken); + if (!string.IsNullOrEmpty(DatafileAccessToken)) + { + request.Headers.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", + DatafileAccessToken); } - var httpResponse = Client.SendAsync(request); + var httpResponse = Client.SendAsync(request); httpResponse.Wait(); // Return from here if datafile is not modified. var result = httpResponse.Result; - if (!result.IsSuccessStatusCode) { + if (!result.IsSuccessStatusCode) + { Logger.Log(LogLevel.ERROR, $"Error fetching datafile \"{result.StatusCode}\""); return null; } @@ -122,9 +148,9 @@ private string GetRemoteDatafileResponse() var content = result.Content.ReadAsStringAsync(); content.Wait(); - return content.Result; + return content.Result; } -#elif NET40 +#elif NET40 private string GetRemoteDatafileResponse() { var request = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(Url); @@ -164,19 +190,24 @@ protected override ProjectConfig Poll() return DatafileProjectConfig.Create(datafile, Logger, ErrorHandler); } - + public class Builder { private const long MAX_MILLISECONDS_LIMIT = 4294967294; private readonly TimeSpan DEFAULT_PERIOD = TimeSpan.FromMinutes(5); private readonly TimeSpan DEFAULT_BLOCKINGOUT_PERIOD = TimeSpan.FromSeconds(15); - private readonly string DEFAULT_FORMAT = "https://cdn.optimizely.com/datafiles/{0}.json"; - private readonly string DEFAULT_AUTHENTICATED_DATAFILE_FORMAT = "https://config.optimizely.com/datafiles/auth/{0}.json"; + + private readonly string DEFAULT_FORMAT = + "https://cdn.optimizely.com/datafiles/{0}.json"; + + private readonly string DEFAULT_AUTHENTICATED_DATAFILE_FORMAT = + "https://config.optimizely.com/datafiles/auth/{0}.json"; + private string Datafile; - private string DatafileAccessToken; + private string DatafileAccessToken; private string SdkKey; private string Url; - private string Format; + private string Format; private ILogger Logger; private IErrorHandler ErrorHandler; private TimeSpan Period; @@ -197,6 +228,7 @@ public Builder WithBlockingTimeoutPeriod(TimeSpan blockingTimeoutSpan) return this; } + public Builder WithDatafile(string datafile) { Datafile = datafile; @@ -241,7 +273,7 @@ public Builder WithFormat(string format) return this; } - + public Builder WithLogger(ILogger logger) { Logger = logger; @@ -263,7 +295,7 @@ public Builder WithAutoUpdate(bool autoUpdate) return this; } - public Builder WithStartByDefault(bool startByDefault=true) + public Builder WithStartByDefault(bool startByDefault = true) { StartByDefault = startByDefault; @@ -303,41 +335,62 @@ public HttpProjectConfigManager Build(bool defer) if (ErrorHandler == null) ErrorHandler = new DefaultErrorHandler(Logger, false); - if (string.IsNullOrEmpty(Format)) { - - if (string.IsNullOrEmpty(DatafileAccessToken)) { + if (string.IsNullOrEmpty(Format)) + { + if (string.IsNullOrEmpty(DatafileAccessToken)) + { Format = DEFAULT_FORMAT; - } else { + } + else + { Format = DEFAULT_AUTHENTICATED_DATAFILE_FORMAT; } } - if (string.IsNullOrEmpty(Url)) { - if (string.IsNullOrEmpty(SdkKey)) { + if (string.IsNullOrEmpty(Url)) + { + if (string.IsNullOrEmpty(SdkKey)) + { ErrorHandler.HandleError(new Exception("SdkKey cannot be null")); } + Url = string.Format(Format, SdkKey); } - if (IsPollingIntervalProvided && (Period.TotalMilliseconds <= 0 || Period.TotalMilliseconds > MAX_MILLISECONDS_LIMIT)) { - Logger.Log(LogLevel.DEBUG, $"Polling interval is not valid for periodic calls, using default period {DEFAULT_PERIOD.TotalMilliseconds}ms"); + if (IsPollingIntervalProvided && (Period.TotalMilliseconds <= 0 || + Period.TotalMilliseconds > + MAX_MILLISECONDS_LIMIT)) + { + Logger.Log(LogLevel.DEBUG, + $"Polling interval is not valid for periodic calls, using default period {DEFAULT_PERIOD.TotalMilliseconds}ms"); Period = DEFAULT_PERIOD; - } else if(!IsPollingIntervalProvided) { - Logger.Log(LogLevel.DEBUG, $"No polling interval provided, using default period {DEFAULT_PERIOD.TotalMilliseconds}ms"); + } + else if (!IsPollingIntervalProvided) + { + Logger.Log(LogLevel.DEBUG, + $"No polling interval provided, using default period {DEFAULT_PERIOD.TotalMilliseconds}ms"); Period = DEFAULT_PERIOD; } - - if (IsBlockingTimeoutProvided && (BlockingTimeoutSpan.TotalMilliseconds <= 0 || BlockingTimeoutSpan.TotalMilliseconds > MAX_MILLISECONDS_LIMIT)) { - Logger.Log(LogLevel.DEBUG, $"Blocking timeout is not valid, using default blocking timeout {DEFAULT_BLOCKINGOUT_PERIOD.TotalMilliseconds}ms"); + + if (IsBlockingTimeoutProvided && (BlockingTimeoutSpan.TotalMilliseconds <= 0 || + BlockingTimeoutSpan.TotalMilliseconds > + MAX_MILLISECONDS_LIMIT)) + { + Logger.Log(LogLevel.DEBUG, + $"Blocking timeout is not valid, using default blocking timeout {DEFAULT_BLOCKINGOUT_PERIOD.TotalMilliseconds}ms"); BlockingTimeoutSpan = DEFAULT_BLOCKINGOUT_PERIOD; - } else if(!IsBlockingTimeoutProvided) { - Logger.Log(LogLevel.DEBUG, $"No Blocking timeout provided, using default blocking timeout {DEFAULT_BLOCKINGOUT_PERIOD.TotalMilliseconds}ms"); + } + else if (!IsBlockingTimeoutProvided) + { + Logger.Log(LogLevel.DEBUG, + $"No Blocking timeout provided, using default blocking timeout {DEFAULT_BLOCKINGOUT_PERIOD.TotalMilliseconds}ms"); BlockingTimeoutSpan = DEFAULT_BLOCKINGOUT_PERIOD; } - - configManager = new HttpProjectConfigManager(Period, Url, BlockingTimeoutSpan, AutoUpdate, Logger, ErrorHandler, DatafileAccessToken); + + configManager = new HttpProjectConfigManager(Period, Url, BlockingTimeoutSpan, + AutoUpdate, Logger, ErrorHandler, DatafileAccessToken, SdkKey); if (Datafile != null) { @@ -351,11 +404,18 @@ public HttpProjectConfigManager Build(bool defer) Logger.Log(LogLevel.WARN, "Error parsing fallback datafile." + ex.Message); } } - - configManager.NotifyOnProjectConfigUpdate += () => { - NotificationCenter?.SendNotifications(NotificationCenter.NotificationType.OptimizelyConfigUpdate); + + configManager.NotifyOnProjectConfigUpdate += () => + { +#if USE_ODP + NotificationCenterRegistry.GetNotificationCenter(SdkKey). + SendNotifications( + NotificationCenter.NotificationType.OptimizelyConfigUpdate); +#endif + NotificationCenter?.SendNotifications(NotificationCenter.NotificationType. + OptimizelyConfigUpdate); }; - + if (StartByDefault) configManager.Start(); @@ -363,7 +423,7 @@ public HttpProjectConfigManager Build(bool defer) // Optionally block until config is available. if (!defer) configManager.GetConfig(); - + return configManager; } } diff --git a/OptimizelySDK/Config/PollingProjectConfigManager.cs b/OptimizelySDK/Config/PollingProjectConfigManager.cs index 93d284746..671e8c134 100644 --- a/OptimizelySDK/Config/PollingProjectConfigManager.cs +++ b/OptimizelySDK/Config/PollingProjectConfigManager.cs @@ -1,11 +1,11 @@ /* - * Copyright 2019-2020, Optimizely + * Copyright 2019-2020, 2022-2023 Optimizely * * 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 + * https://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, @@ -14,13 +14,13 @@ * limitations under the License. */ -using System; -using System.Threading; +using OptimizelySDK.ErrorHandler; using OptimizelySDK.Logger; +using OptimizelySDK.OptlyConfig; using OptimizelySDK.Utils; +using System; +using System.Threading; using System.Threading.Tasks; -using OptimizelySDK.ErrorHandler; -using OptimizelySDK.OptlyConfig; namespace OptimizelySDK.Config { @@ -30,7 +30,8 @@ namespace OptimizelySDK.Config /// Instances of this class, must implement the method /// which is responsible for fetching a given ProjectConfig. /// - public abstract class PollingProjectConfigManager : ProjectConfigManager, IOptimizelyConfigManager, IDisposable + public abstract class PollingProjectConfigManager : ProjectConfigManager, + IOptimizelyConfigManager, IDisposable { public bool Disposed { get; private set; } @@ -44,14 +45,22 @@ public abstract class PollingProjectConfigManager : ProjectConfigManager, IOptim protected ILogger Logger { get; set; } protected IErrorHandler ErrorHandler { get; set; } protected TimeSpan BlockingTimeout; - protected TaskCompletionSource CompletableConfigManager = new TaskCompletionSource(); + + public virtual string SdkKey { get; } + + protected TaskCompletionSource CompletableConfigManager = + new TaskCompletionSource(); + private OptimizelyConfig CurrentOptimizelyConfig; + // Variables to control blocking/syncing. public int resourceInUse = 0; public event Action NotifyOnProjectConfigUpdate; - public PollingProjectConfigManager(TimeSpan period, TimeSpan blockingTimeout, bool autoUpdate = true, ILogger logger = null, IErrorHandler errorHandler = null) + public PollingProjectConfigManager(TimeSpan period, TimeSpan blockingTimeout, + bool autoUpdate = true, ILogger logger = null, IErrorHandler errorHandler = null + ) { Logger = logger; ErrorHandler = errorHandler; @@ -80,15 +89,17 @@ public void Start() return; } - Logger.Log(LogLevel.WARN, $"Starting Config scheduler with interval: {PollingInterval}."); - SchedulerService.Change(TimeSpan.Zero, AutoUpdate ? PollingInterval : TimeSpan.FromMilliseconds(-1)); + Logger.Log(LogLevel.WARN, + $"Starting Config scheduler with interval: {PollingInterval}."); + SchedulerService.Change(TimeSpan.Zero, + AutoUpdate ? PollingInterval : TimeSpan.FromMilliseconds(-1)); IsStarted = true; } /// /// Stops datafile scheduler. /// - public void Stop() + public void Stop() { if (Disposed) return; // don't call now and onwards. @@ -116,12 +127,15 @@ public ProjectConfig GetConfig() { // Don't wait next time. BlockingTimeout = TimeSpan.FromMilliseconds(0); - Logger.Log(LogLevel.WARN, "Timeout exceeded waiting for ProjectConfig to be set, returning null."); + Logger.Log(LogLevel.WARN, + "Timeout exceeded waiting for ProjectConfig to be set, returning null."); } } catch (AggregateException ex) { - Logger.Log(LogLevel.WARN, "Interrupted waiting for valid ProjectConfig. Error: " + ex.GetAllMessages()); + Logger.Log(LogLevel.WARN, + "Interrupted waiting for valid ProjectConfig. Error: " + + ex.GetAllMessages()); throw; } @@ -132,29 +146,42 @@ public ProjectConfig GetConfig() return projectConfig ?? CurrentProjectConfig; } + /// + /// Access to current cached project configuration + /// + /// ProjectConfig instance + public virtual ProjectConfig CachedProjectConfig + { + get + { + return CurrentProjectConfig; + } + } + /// /// Sets the latest available ProjectConfig valid instance. /// /// ProjectConfig /// true if the ProjectConfig saved successfully, false otherwise public bool SetConfig(ProjectConfig projectConfig) - { + { if (projectConfig == null) return false; - - var previousVersion = CurrentProjectConfig == null ? "null" : CurrentProjectConfig.Revision; + + var previousVersion = + CurrentProjectConfig == null ? "null" : CurrentProjectConfig.Revision; if (projectConfig.Revision == previousVersion) return false; - + CurrentProjectConfig = projectConfig; SetOptimizelyConfig(CurrentProjectConfig); // SetResult raise exception if called again, that's why Try is used. CompletableConfigManager.TrySetResult(true); - - NotifyOnProjectConfigUpdate?.Invoke(); - + NotifyOnProjectConfigUpdate?.Invoke(); + + return true; } @@ -162,7 +189,8 @@ private void SetOptimizelyConfig(ProjectConfig projectConfig) { try { - CurrentOptimizelyConfig = new OptimizelyConfigService(projectConfig).GetOptimizelyConfig(); + CurrentOptimizelyConfig = + new OptimizelyConfigService(projectConfig).GetOptimizelyConfig(); } catch (Exception ex) { @@ -195,28 +223,36 @@ public virtual void Dispose() /// public virtual void Run() { - if (Interlocked.Exchange(ref resourceInUse, 1) == 0){ - try { + if (Interlocked.Exchange(ref resourceInUse, 1) == 0) + { + try + { var config = Poll(); // during in-flight, if PollingProjectConfigManagerStopped, then don't need to set. - if(IsStarted) + if (IsStarted) SetConfig(config); - - } catch (Exception exception) { - Logger.Log(LogLevel.ERROR, "Unable to get project config. Error: " + exception.GetAllMessages()); - } finally { + } + catch (Exception exception) + { + Logger.Log(LogLevel.ERROR, + "Unable to get project config. Error: " + exception.GetAllMessages()); + } + finally + { Interlocked.Exchange(ref resourceInUse, 0); // trigger now, due because of delayed latency response - if (!Disposed && scheduleWhenFinished && IsStarted) { + if (!Disposed && scheduleWhenFinished && IsStarted) + { // Call immediately, because it's due now. scheduleWhenFinished = false; SchedulerService.Change(TimeSpan.FromSeconds(0), PollingInterval); } } } - else { + else + { scheduleWhenFinished = true; } } diff --git a/OptimizelySDK/Config/ProjectConfigManager.cs b/OptimizelySDK/Config/ProjectConfigManager.cs index e84b78a90..cc19c14e1 100644 --- a/OptimizelySDK/Config/ProjectConfigManager.cs +++ b/OptimizelySDK/Config/ProjectConfigManager.cs @@ -1,11 +1,11 @@ /* - * Copyright 2019, Optimizely + * Copyright 2019, 2022-2023 Optimizely * * 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 + * https://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, @@ -14,8 +14,6 @@ * limitations under the License. */ -using System; - namespace OptimizelySDK.Config { /// @@ -28,5 +26,16 @@ public interface ProjectConfigManager /// /// ProjectConfig instance ProjectConfig GetConfig(); + + /// + /// SDK key in use for this project + /// + string SdkKey { get; } + + /// + /// Access to current cached project configuration + /// + /// ProjectConfig instance or null if project config is not ready + ProjectConfig CachedProjectConfig { get; } } } diff --git a/OptimizelySDK/Entity/Audience.cs b/OptimizelySDK/Entity/Audience.cs index 186535ef6..8dd2673f8 100644 --- a/OptimizelySDK/Entity/Audience.cs +++ b/OptimizelySDK/Entity/Audience.cs @@ -18,6 +18,7 @@ using Newtonsoft.Json.Linq; using OptimizelySDK.AudienceConditions; using OptimizelySDK.Utils; +using System.Collections.Generic; namespace OptimizelySDK.Entity { @@ -90,5 +91,48 @@ public string ConditionsString return _conditionsString; } } + + public HashSet GetSegments() + { + return GetSegments(ConditionList); + } + + public static HashSet GetSegments(ICondition conditions) + { + if (conditions == null) + { + return new HashSet(); + } + + var segments = new HashSet(); + + if (conditions is BaseCondition baseCondition) + { + if (baseCondition.Match == BaseCondition.QUALIFIED) + { + segments.Add(baseCondition.Value.ToString()); + } + } + else if (conditions is AndCondition andCondition) + { + foreach (var nestedCondition in andCondition.Conditions) + { + segments.UnionWith(GetSegments(nestedCondition)); + } + } + else if (conditions is OrCondition orCondition) + { + foreach (var nestedCondition in orCondition.Conditions) + { + segments.UnionWith(GetSegments(nestedCondition)); + } + } + else if (conditions is NotCondition notCondition) + { + segments.UnionWith(GetSegments(notCondition.Condition)); + } + + return segments; + } } } diff --git a/OptimizelySDK/IOptimizely.cs b/OptimizelySDK/IOptimizely.cs index 836cdb922..b8031884d 100644 --- a/OptimizelySDK/IOptimizely.cs +++ b/OptimizelySDK/IOptimizely.cs @@ -1,11 +1,11 @@ -/** - * Copyright 2018, Optimizely, Inc. and contributors +/* + * Copyright 2018, 2022-2023 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 + * https://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, @@ -13,132 +13,158 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +#if !(NET35 || NET40 || NETSTANDARD1_6) +#define USE_ODP +#endif + using OptimizelySDK.Entity; using System.Collections.Generic; namespace OptimizelySDK { - public interface IOptimizely - { - /// - /// Returns true if the IOptimizely instance was initialized with a valid datafile - /// - bool IsValid { get; } - - /// - /// Buckets visitor and sends impression event to Optimizely. - /// - /// experimentKey string Key identifying the experiment - /// string ID for user - /// associative array of Attributes for the user - /// null|Variation Representing variation - Variation Activate(string experimentKey, string userId, UserAttributes userAttributes = null); - - /// - /// Create a context of the user for which decision APIs will be called. - /// A user context will be created successfully even when the SDK is not fully configured yet. - /// - /// The user ID to be used for bucketing. - /// The user's attributes - /// OptimizelyUserContext | An OptimizelyUserContext associated with this OptimizelyClient. - OptimizelyUserContext CreateUserContext(string userId, UserAttributes userAttributes = null); - - /// - /// Sends conversion event to Optimizely. - /// - /// Event key representing the event which needs to be recorded - /// ID for user - /// Attributes of the user - /// eventTags array Hash representing metadata associated with the event. - void Track(string eventKey, string userId, UserAttributes userAttributes = null, EventTags eventTags = null); - - /// - /// Get variation where user will be bucketed - /// - /// experimentKey string Key identifying the experiment - /// ID for the user - /// Attributes for the users - /// null|Variation Representing variation - Variation GetVariation(string experimentKey, string userId, UserAttributes userAttributes = null); - - /// - /// Force a user into a variation for a given experiment. - /// - /// The experiment key - /// The user ID - /// The variation key specifies the variation which the user will be forced into. - /// If null, then clear the existing experiment-to-variation mapping. - /// A boolean value that indicates if the set completed successfully. - bool SetForcedVariation(string experimentKey, string userId, string variationKey); - - /// - /// Gets the forced variation key for the given user and experiment. - /// - /// The experiment key - /// The user ID - /// null|string The variation key. - Variation GetForcedVariation(string experimentKey, string userId); - - #region FeatureFlag APIs - - /// - /// Determine whether a feature is enabled. - /// Send an impression event if the user is bucketed into an experiment using the feature. - /// - /// The feature key - /// The user ID - /// The user's attributes. - /// True if feature is enabled, false or null otherwise - bool IsFeatureEnabled(string featureKey, string userId, UserAttributes userAttributes = null); - - /// - /// Gets boolean feature variable value. - /// - /// The feature flag key - /// The variable key - /// The user ID - /// The user's attributes - /// bool | Feature variable value or null - bool? GetFeatureVariableBoolean(string featureKey, string variableKey, string userId, UserAttributes userAttributes = null); - - /// - /// Gets double feature variable value. - /// - /// The feature flag key - /// The variable key - /// The user ID - /// The user's attributes - /// double | Feature variable value or null - double? GetFeatureVariableDouble(string featureKey, string variableKey, string userId, UserAttributes userAttributes = null); - - /// - /// Gets integer feature variable value. - /// - /// The feature flag key - /// The variable key - /// The user ID - /// The user's attributes - /// int | Feature variable value or null - int? GetFeatureVariableInteger(string featureKey, string variableKey, string userId, UserAttributes userAttributes = null); - - /// - /// Gets string feature variable value. - /// - /// The feature flag key - /// The variable key - /// The user ID - /// The user's attributes - /// string | Feature variable value or null - string GetFeatureVariableString(string featureKey, string variableKey, string userId, UserAttributes userAttributes = null); - - /// - /// Get the list of features that are enabled for the user. - /// - /// The user Id - /// The user's attributes - /// List of the feature keys that are enabled for the user. - List GetEnabledFeatures(string userId, UserAttributes userAttributes = null); - - #endregion - } + public interface IOptimizely + { + /// + /// Returns true if the IOptimizely instance was initialized with a valid datafile + /// + bool IsValid { get; } + + /// + /// Buckets visitor and sends impression event to Optimizely. + /// + /// experimentKey string Key identifying the experiment + /// string ID for user + /// associative array of Attributes for the user + /// null|Variation Representing variation + Variation Activate(string experimentKey, string userId, UserAttributes userAttributes = null + ); + + /// + /// Create a context of the user for which decision APIs will be called. + /// A user context will be created successfully even when the SDK is not fully configured yet. + /// + /// The user ID to be used for bucketing. + /// The user's attributes + /// OptimizelyUserContext | An OptimizelyUserContext associated with this OptimizelyClient. + OptimizelyUserContext + CreateUserContext(string userId, UserAttributes userAttributes = null); + + /// + /// Sends conversion event to Optimizely. + /// + /// Event key representing the event which needs to be recorded + /// ID for user + /// Attributes of the user + /// eventTags array Hash representing metadata associated with the event. + void Track(string eventKey, string userId, UserAttributes userAttributes = null, + EventTags eventTags = null + ); + + /// + /// Get variation where user will be bucketed + /// + /// experimentKey string Key identifying the experiment + /// ID for the user + /// Attributes for the users + /// null|Variation Representing variation + Variation GetVariation(string experimentKey, string userId, + UserAttributes userAttributes = null + ); + + /// + /// Force a user into a variation for a given experiment. + /// + /// The experiment key + /// The user ID + /// The variation key specifies the variation which the user will be forced into. + /// If null, then clear the existing experiment-to-variation mapping. + /// A boolean value that indicates if the set completed successfully. + bool SetForcedVariation(string experimentKey, string userId, string variationKey); + + /// + /// Gets the forced variation key for the given user and experiment. + /// + /// The experiment key + /// The user ID + /// null|string The variation key. + Variation GetForcedVariation(string experimentKey, string userId); + + #region FeatureFlag APIs + + /// + /// Determine whether a feature is enabled. + /// Send an impression event if the user is bucketed into an experiment using the feature. + /// + /// The feature key + /// The user ID + /// The user's attributes. + /// True if feature is enabled, false or null otherwise + bool IsFeatureEnabled(string featureKey, string userId, UserAttributes userAttributes = null + ); + + /// + /// Gets boolean feature variable value. + /// + /// The feature flag key + /// The variable key + /// The user ID + /// The user's attributes + /// bool | Feature variable value or null + bool? GetFeatureVariableBoolean(string featureKey, string variableKey, string userId, + UserAttributes userAttributes = null + ); + + /// + /// Gets double feature variable value. + /// + /// The feature flag key + /// The variable key + /// The user ID + /// The user's attributes + /// double | Feature variable value or null + double? GetFeatureVariableDouble(string featureKey, string variableKey, string userId, + UserAttributes userAttributes = null + ); + + /// + /// Gets integer feature variable value. + /// + /// The feature flag key + /// The variable key + /// The user ID + /// The user's attributes + /// int | Feature variable value or null + int? GetFeatureVariableInteger(string featureKey, string variableKey, string userId, + UserAttributes userAttributes = null + ); + + /// + /// Gets string feature variable value. + /// + /// The feature flag key + /// The variable key + /// The user ID + /// The user's attributes + /// string | Feature variable value or null + string GetFeatureVariableString(string featureKey, string variableKey, string userId, + UserAttributes userAttributes = null + ); + + /// + /// Get the list of features that are enabled for the user. + /// + /// The user Id + /// The user's attributes + /// List of the feature keys that are enabled for the user. + List GetEnabledFeatures(string userId, UserAttributes userAttributes = null); + + #endregion + +#if USE_ODP + void SendOdpEvent(string type, string action, Dictionary identifiers, + Dictionary data + ); +#endif + } } diff --git a/OptimizelySDK/Notifications/NotificationCenterRegistry.cs b/OptimizelySDK/Notifications/NotificationCenterRegistry.cs new file mode 100644 index 000000000..ecbbda84d --- /dev/null +++ b/OptimizelySDK/Notifications/NotificationCenterRegistry.cs @@ -0,0 +1,82 @@ +/* + * Copyright 2023, Optimizely + * + * 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 + * + * https://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. + */ + +using OptimizelySDK.Logger; +using System.Collections.Generic; + +namespace OptimizelySDK.Notifications +{ + internal static class NotificationCenterRegistry + { + private static readonly object _mutex = new object(); + + private static Dictionary _notificationCenters = + new Dictionary(); + + /// + /// Thread-safe access to the NotificationCenter + /// + /// Retrieve NotificationCenter based on SDK key + /// Logger to record events + /// NotificationCenter instance per SDK key + public static NotificationCenter GetNotificationCenter(string sdkKey, ILogger logger = null) + { + if (sdkKey == null) + { + logger?.Log(LogLevel.ERROR, "No SDK key provided to GetNotificationCenter"); + return default; + } + + NotificationCenter notificationCenter; + lock (_mutex) + { + if (_notificationCenters.ContainsKey(sdkKey)) + { + notificationCenter = _notificationCenters[sdkKey]; + } + else + { + notificationCenter = new NotificationCenter(logger); + _notificationCenters[sdkKey] = notificationCenter; + } + } + + return notificationCenter; + } + + /// + /// Thread-safe removal of a NotificationCenter from the Registry + /// + /// SDK key identifying the target + public static void RemoveNotificationCenter(string sdkKey) + { + if (sdkKey == null) + { + return; + } + + lock (_mutex) + { + if (_notificationCenters.TryGetValue(sdkKey, + out NotificationCenter notificationCenter)) + { + notificationCenter.ClearAllNotifications(); + _notificationCenters.Remove(sdkKey); + } + } + } + } +} diff --git a/OptimizelySDK/Odp/Entity/OdpEvent.cs b/OptimizelySDK/Odp/Entity/OdpEvent.cs index 652f71e3b..95380c489 100644 --- a/OptimizelySDK/Odp/Entity/OdpEvent.cs +++ b/OptimizelySDK/Odp/Entity/OdpEvent.cs @@ -54,7 +54,7 @@ public OdpEvent(string type, string action, Dictionary identifie Dictionary data = null ) { - Type = type; + Type = type ?? Constants.ODP_EVENT_TYPE; Action = action; Identifiers = identifiers ?? new Dictionary(); Data = data ?? new Dictionary(); diff --git a/OptimizelySDK/Odp/Enums.cs b/OptimizelySDK/Odp/Enums.cs index 7aeb27535..c69923f64 100644 --- a/OptimizelySDK/Odp/Enums.cs +++ b/OptimizelySDK/Odp/Enums.cs @@ -32,7 +32,7 @@ public enum OdpUserKeyType /// public enum OdpSegmentOption { - IgnoreCache = 0, - ResetCache = 1, + IGNORE_CACHE = 0, + RESET_CACHE = 1, } } diff --git a/OptimizelySDK/Odp/IOdpEventManager.cs b/OptimizelySDK/Odp/IOdpEventManager.cs index f2cf96fb2..2967b4106 100644 --- a/OptimizelySDK/Odp/IOdpEventManager.cs +++ b/OptimizelySDK/Odp/IOdpEventManager.cs @@ -15,6 +15,7 @@ */ using OptimizelySDK.Odp.Entity; +using System; namespace OptimizelySDK.Odp { diff --git a/OptimizelySDK/Odp/LruCache.cs b/OptimizelySDK/Odp/LruCache.cs index a475c1175..11e387427 100644 --- a/OptimizelySDK/Odp/LruCache.cs +++ b/OptimizelySDK/Odp/LruCache.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022, Optimizely + * Copyright 2022-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,7 @@ namespace OptimizelySDK.Odp { - public class LruCache : ICache - where T : class + public class LruCache : ICache where T : class { /// /// The maximum number of elements that should be stored @@ -193,13 +192,11 @@ public ItemWrapper(T value) } /// - /// Read the current cache index/linked list for unit testing + /// For Testing Only: thread-safe retrieval of current cache keys /// - /// - public string[] _readCurrentCacheKeys() + /// Current cache keys + internal string[] CurrentCacheKeysForTesting() { - _logger.Log(LogLevel.WARN, "_readCurrentCacheKeys used for non-testing purpose"); - string[] cacheKeys; lock (_mutex) { @@ -208,5 +205,15 @@ public string[] _readCurrentCacheKeys() return cacheKeys; } + + /// + /// For Testing Only: Retrieve the current cache timout + /// + internal TimeSpan TimeoutForTesting { get { return _timeout; } } + + /// + /// For Testing Only: Retrieve hte current maximum cache size + /// + internal int MaxSizeForTesting { get { return _maxSize; } } } } diff --git a/OptimizelySDK/Odp/OdpEventManager.cs b/OptimizelySDK/Odp/OdpEventManager.cs index 070f4d8b8..3039e7f25 100644 --- a/OptimizelySDK/Odp/OdpEventManager.cs +++ b/OptimizelySDK/Odp/OdpEventManager.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022, Optimizely + * Copyright 2022-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -91,6 +91,11 @@ public class OdpEventManager : IOdpEventManager, IDisposable /// private Dictionary _commonData; + /// + /// Indicates if OdpEventManager should start upon Build() and UpdateSettings() + /// + private bool _autoStart; + /// /// Clear all entries from the queue /// @@ -107,7 +112,7 @@ private void DropQueue() /// public void Start() { - if (!_odpConfig.IsReady()) + if (_odpConfig == null || !_odpConfig.IsReady()) { _logger.Log(LogLevel.WARN, Constants.ODP_NOT_INTEGRATED_MESSAGE); @@ -141,35 +146,45 @@ protected virtual void Run() { while (true) { - if (DateTime.Now.MillisecondsSince1970() > _flushingIntervalDeadline) + object item; + // If batch has events, set the timeout to remaining time for flush interval, + // otherwise wait for the new event indefinitely + if (_currentBatch.Count > 0) { - _logger.Log(LogLevel.DEBUG, $"Flushing queue."); - FlushQueue(); + _eventQueue.TryTake(out item, (int)(_flushingIntervalDeadline - DateTime.Now.MillisecondsSince1970())); + } + else + { + item = _eventQueue.Take(); + Thread.Sleep(1); // TODO: need to figure out why this is allowing item to read shutdown signal. } - if (!_eventQueue.TryTake(out object item, 50)) + if (item == null) { - Thread.Sleep(50); + // null means no new events received and flush interval is over, dispatch whatever is in the batch. + if (_currentBatch.Count != 0) + { + _logger.Log(LogLevel.DEBUG, $"Flushing queue."); + FlushQueue(); + } continue; } - - if (item == _shutdownSignal) + else if (item == _shutdownSignal) { _logger.Log(LogLevel.INFO, "Received shutdown signal."); break; } - - if (item == _flushSignal) + else if (item == _flushSignal) { _logger.Log(LogLevel.DEBUG, "Received flush signal."); FlushQueue(); continue; } - - if (item is OdpEvent odpEvent) + else if (item is OdpEvent odpEvent) { AddToBatch(odpEvent); } + } } catch (InvalidOperationException ioe) @@ -250,7 +265,7 @@ public void Stop() _eventQueue.Add(_shutdownSignal); - if (!_executionThread.Join(_timeoutInterval)) + if (_executionThread != null && !_executionThread.Join(_timeoutInterval)) { _logger.Log(LogLevel.ERROR, $"Timeout exceeded attempting to close for {_timeoutInterval.Milliseconds} ms"); @@ -266,7 +281,7 @@ public void Stop() /// Event to enqueue public void SendEvent(OdpEvent odpEvent) { - if (!_odpConfig.IsReady()) + if (_odpConfig == null || !_odpConfig.IsReady()) { _logger.Log(LogLevel.WARN, Constants.ODP_NOT_INTEGRATED_MESSAGE); return; @@ -334,9 +349,7 @@ public void IdentifyUser(string userId) { var identifiers = new Dictionary { - { - OdpUserKeyType.FS_USER_ID.ToString(), userId - }, + { OdpUserKeyType.FS_USER_ID.ToString(), userId }, }; var odpEvent = new OdpEvent(Constants.ODP_EVENT_TYPE, "identified", identifiers); @@ -344,12 +357,24 @@ public void IdentifyUser(string userId) } /// - /// Update ODP configuration settings + /// Update ODP configuration settings with a implied flush of the queued events /// /// Configuration object containing new values public void UpdateSettings(OdpConfig odpConfig) { + if (odpConfig == null) + { + return; + } + + Flush(); + _odpConfig = odpConfig; + + if (_autoStart) + { + Start(); + } } /// @@ -402,56 +427,84 @@ public class Builder private BlockingCollection _eventQueue = new BlockingCollection(Constants.DEFAULT_QUEUE_CAPACITY); - private OdpConfig _odpConfig; private IOdpEventApiManager _odpEventApiManager; - private int _batchSize; private TimeSpan _flushInterval; private TimeSpan _timeoutInterval; private ILogger _logger; private IErrorHandler _errorHandler; + private bool? _autoStart; - public Builder WithEventQueue(BlockingCollection eventQueue) + /// + /// Indicates if OdpEventManager should start upon Build() and UpdateSettings() + /// + /// + /// + public Builder WithAutoStart(bool autoStart) { - _eventQueue = eventQueue; + _autoStart = autoStart; return this; } - public Builder WithOdpConfig(OdpConfig odpConfig) + /// + /// Provide an Event Queue + /// + /// Concrete implementation of an event queue + /// Current Builder instance + public Builder WithEventQueue(BlockingCollection eventQueue) { - _odpConfig = odpConfig; + _eventQueue = eventQueue; return this; } + /// + /// Provide an ODP Event Manager API + /// + /// Concrete implementation of an Event API Manager + /// Current Builder instance public Builder WithOdpEventApiManager(IOdpEventApiManager odpEventApiManager) { _odpEventApiManager = odpEventApiManager; return this; } - public Builder WithBatchSize(int batchSize) - { - _batchSize = batchSize; - return this; - } - + /// + /// Provide an flush interval + /// + /// Frequency to flush the queue + /// Current Builder instance public Builder WithFlushInterval(TimeSpan flushInterval) { _flushInterval = flushInterval; return this; } + /// + /// Provide a timeout to wait for network communication + /// + /// Span to allow for communication timeout + /// Current Builder instance public Builder WithTimeoutInterval(TimeSpan timeout) { _timeoutInterval = timeout; return this; } + /// + /// Provide a logger to record code events + /// + /// Concrete implementation of a logger + /// Current Builder instance public Builder WithLogger(ILogger logger = null) { _logger = logger; return this; } + /// + /// Provide an error handler + /// + /// Concrete implementation of an error handler + /// Current Builder instance public Builder WithErrorHandler(IErrorHandler errorHandler = null) { _errorHandler = errorHandler; @@ -461,24 +514,24 @@ public Builder WithErrorHandler(IErrorHandler errorHandler = null) /// /// Build OdpEventManager instance using collected parameters /// - /// Should start event processor upon initialization /// OdpEventProcessor instance - public OdpEventManager Build(bool startImmediately = true) + public OdpEventManager Build() { var manager = new OdpEventManager(); manager._eventQueue = _eventQueue; - manager._odpConfig = _odpConfig; manager._odpEventApiManager = _odpEventApiManager; - manager._batchSize = - _batchSize < 1 ? Constants.DEFAULT_BATCH_SIZE : _batchSize; - manager._flushInterval = _flushInterval <= TimeSpan.FromSeconds(0) ? - Constants.DEFAULT_FLUSH_INTERVAL : - _flushInterval; - manager._timeoutInterval = _timeoutInterval <= TimeSpan.FromSeconds(0) ? + manager._flushInterval = (_flushInterval > TimeSpan.Zero) ? + _flushInterval : + Constants.DEFAULT_FLUSH_INTERVAL; + manager._batchSize = (_flushInterval == TimeSpan.Zero) ? + 1 : + Constants.DEFAULT_BATCH_SIZE; + manager._timeoutInterval = _timeoutInterval <= TimeSpan.Zero ? Constants.DEFAULT_TIMEOUT_INTERVAL : _timeoutInterval; manager._logger = _logger ?? new NoOpLogger(); manager._errorHandler = _errorHandler ?? new NoOpErrorHandler(); + manager._autoStart = _autoStart ?? true; manager._validOdpDataTypes = new List() { @@ -496,21 +549,13 @@ public OdpEventManager Build(bool startImmediately = true) manager._commonData = new Dictionary { - { - "idempotence_id", Guid.NewGuid() - }, - { - "data_source_type", "sdk" - }, - { - "data_source", Optimizely.SDK_TYPE - }, - { - "data_source_version", Optimizely.SDK_VERSION - }, + { "idempotence_id", Guid.NewGuid() }, + { "data_source_type", "sdk" }, + { "data_source", Optimizely.SDK_TYPE }, + { "data_source_version", Optimizely.SDK_VERSION }, }; - if (startImmediately) + if (manager._autoStart) { manager.Start(); } @@ -519,9 +564,20 @@ public OdpEventManager Build(bool startImmediately = true) } } - public OdpConfig _readOdpConfigForTesting() - { - return _odpConfig; - } + /// + /// For Testing Only: Read the current ODP config + /// + /// Current ODP settings + internal OdpConfig OdpConfigForTesting { get { return _odpConfig; } } + + /// + /// For Testing Only: Read the current flush interval + /// + internal TimeSpan FlushIntervalForTesting { get { return _flushInterval; } } + + /// + /// For Testing Only: Read the current timeout interval + /// + internal TimeSpan TimeoutIntervalForTesting { get { return _timeoutInterval; } } } } diff --git a/OptimizelySDK/Odp/OdpManager.cs b/OptimizelySDK/Odp/OdpManager.cs index 0a57ca197..bf9a53244 100644 --- a/OptimizelySDK/Odp/OdpManager.cs +++ b/OptimizelySDK/Odp/OdpManager.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 Optimizely + * Copyright 2022-2023 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,6 +52,8 @@ public class OdpManager : IOdpManager, IDisposable /// private ILogger _logger; + private OdpManager() { } + /// /// Update the settings being used for ODP configuration and reset/restart dependent processes /// @@ -62,7 +64,7 @@ public class OdpManager : IOdpManager, IDisposable public bool UpdateSettings(string apiKey, string apiHost, List segmentsToCheck) { var newConfig = new OdpConfig(apiKey, apiHost, segmentsToCheck); - if (_odpConfig.Equals(newConfig)) + if (_odpConfig != null && _odpConfig.Equals(newConfig)) { return false; } @@ -91,7 +93,7 @@ public string[] FetchQualifiedSegments(string userId, List opt return null; } - return SegmentManager.FetchQualifiedSegments(userId, options).ToArray(); + return SegmentManager.FetchQualifiedSegments(userId, options)?.ToArray(); } /// @@ -147,63 +149,85 @@ public void Dispose() /// public class Builder { - private OdpConfig _odpConfig; private IOdpEventManager _eventManager; private IOdpSegmentManager _segmentManager; - private int _cacheSize; - private int _cacheTimeoutSeconds; private ILogger _logger; private IErrorHandler _errorHandler; private ICache> _cache; + private int? _maxSize; + private TimeSpan? _itemTimeout; + /// + /// Provide a Segment Manager + /// + /// Concrete implementation of a SegmentManager + /// Current Builder instance public Builder WithSegmentManager(IOdpSegmentManager segmentManager) { _segmentManager = segmentManager; return this; } + /// + /// Provide an Event Manager + /// + /// Concrete implementation of an Event Manager + /// Current Builder instance public Builder WithEventManager(IOdpEventManager eventManager) { _eventManager = eventManager; return this; } - public Builder WithOdpConfig(OdpConfig odpConfig) - { - _odpConfig = odpConfig; - return this; - } - - public Builder WithCacheSize(int cacheSize) - { - _cacheSize = cacheSize; - return this; - } - - public Builder WithCacheTimeout(int seconds) - { - _cacheTimeoutSeconds = seconds; - return this; - } - + /// + /// Provide a handler for logging + /// + /// Concrete implementation of a log handler + /// Current Builder instance public Builder WithLogger(ILogger logger = null) { _logger = logger; return this; } + /// + /// Provide handler for errors + /// + /// Concrete implementation of an error handler + /// Current Builder instance public Builder WithErrorHandler(IErrorHandler errorHandler = null) { _errorHandler = errorHandler; return this; } - public Builder WithCacheImplementation(ICache> cache) + /// + /// Provide a custom caching mechanism + /// + /// Concrete implementation of a cache + /// Current Builder instance + public Builder WithCache(ICache> cache) { _cache = cache; return this; } + /// + /// Provide a specification for the default cache mechanism + /// + /// Maximum number of elements to cache + /// Maximum time an element can be cached + /// Current Builder instance + public Builder WithCache(int? maxSize = null, + TimeSpan? itemTimeout = null + ) + { + _maxSize = maxSize; + _itemTimeout = itemTimeout; + + return this; + } + /// /// Build OdpManager instance using collected parameters /// @@ -213,26 +237,20 @@ public OdpManager Build(bool asEnabled = true) { _logger = _logger ?? new DefaultLogger(); _errorHandler = _errorHandler ?? new NoOpErrorHandler(); - _odpConfig = _odpConfig ?? new OdpConfig(); var manager = new OdpManager { - _odpConfig = _odpConfig, _logger = _logger, _enabled = asEnabled, }; - if (!manager._enabled) - { - return manager; - } - if (_eventManager == null) { var eventApiManager = new OdpEventApiManager(_logger, _errorHandler); manager.EventManager = new OdpEventManager.Builder(). - WithOdpConfig(_odpConfig). + WithTimeoutInterval(Constants.DEFAULT_TIMEOUT_INTERVAL). + WithFlushInterval(Constants.DEFAULT_FLUSH_INTERVAL). WithOdpEventApiManager(eventApiManager). WithLogger(_logger). WithErrorHandler(_errorHandler). @@ -243,25 +261,26 @@ public OdpManager Build(bool asEnabled = true) manager.EventManager = _eventManager; } + manager.EventManager.Start(); + if (_segmentManager == null) { - var cacheTimeout = TimeSpan.FromSeconds(_cacheTimeoutSeconds <= 0 ? - Constants.DEFAULT_CACHE_SECONDS : - _cacheTimeoutSeconds); var apiManager = new OdpSegmentApiManager(_logger, _errorHandler); - manager.SegmentManager = new OdpSegmentManager(_odpConfig, apiManager, - _cacheSize, cacheTimeout, _logger, _cache); + manager.SegmentManager = new OdpSegmentManager(apiManager, GetCache(), _logger); } else { manager.SegmentManager = _segmentManager; } - manager.EventManager.Start(); - return manager; } + + private ICache> GetCache() + { + return _cache ?? new LruCache>(_maxSize, _itemTimeout, _logger); + } } /// @@ -270,7 +289,7 @@ public OdpManager Build(bool asEnabled = true) /// True if EventManager can process events otherwise False private bool EventManagerOrConfigNotReady() { - return EventManager == null || !_enabled || !_odpConfig.IsReady(); + return EventManager == null || !_enabled || _odpConfig == null || !_odpConfig.IsReady(); } /// @@ -279,7 +298,8 @@ private bool EventManagerOrConfigNotReady() /// True if SegmentManager can fetch audience segments otherwise False private bool SegmentManagerOrConfigNotReady() { - return SegmentManager == null || !_enabled || !_odpConfig.IsReady(); + return SegmentManager == null || !_enabled || _odpConfig == null || + !_odpConfig.IsReady(); } } } diff --git a/OptimizelySDK/Odp/OdpSegmentApiManager.cs b/OptimizelySDK/Odp/OdpSegmentApiManager.cs index 95ea81da8..ec7fdae31 100644 --- a/OptimizelySDK/Odp/OdpSegmentApiManager.cs +++ b/OptimizelySDK/Odp/OdpSegmentApiManager.cs @@ -150,22 +150,7 @@ IEnumerable segmentsToCheck { return @"{ - ""query"": ""{ - query($userId: String, $audiences: [String]) { - { - customer({userKey}: $userId) { - audiences(subset: $audiences) { - edges { - node { - name - state - } - } - } - } - } - } - }"", + ""query"": ""query($userId: String, $audiences: [String]) {customer({userKey}: $userId) {audiences(subset: $audiences) {edges {node {name state}}}}}"", ""variables"" : { ""userId"": ""{userValue}"", ""audiences"": {audiences} @@ -210,7 +195,7 @@ private string QuerySegments(string apiKey, string endpoint, string query) _logger.Log(LogLevel.ERROR, $"{AUDIENCE_FETCH_FAILURE_MESSAGE} ({Constants.NETWORK_ERROR_REASON})"); - return default; + return null; } return response.Content.ReadAsStringAsync().Result; diff --git a/OptimizelySDK/Odp/OdpSegmentManager.cs b/OptimizelySDK/Odp/OdpSegmentManager.cs index c155a755b..381f315b4 100644 --- a/OptimizelySDK/Odp/OdpSegmentManager.cs +++ b/OptimizelySDK/Odp/OdpSegmentManager.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 Optimizely + * Copyright 2022-2023 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,27 +47,29 @@ public class OdpSegmentManager : IOdpSegmentManager /// private readonly ICache> _segmentsCache; - public OdpSegmentManager(OdpConfig odpConfig, IOdpSegmentApiManager apiManager, - int? cacheSize = null, TimeSpan? itemTimeout = null, - ILogger logger = null, ICache> cache = null + public OdpSegmentManager(IOdpSegmentApiManager apiManager, + ICache> cache = null, + ILogger logger = null ) { _apiManager = apiManager; - _odpConfig = odpConfig; _logger = logger ?? new DefaultLogger(); - itemTimeout = itemTimeout ?? TimeSpan.FromSeconds(Constants.DEFAULT_CACHE_SECONDS); - if (itemTimeout < TimeSpan.Zero) - { - _logger.Log(LogLevel.WARN, - "Negative item timeout provided. Items will not expire in cache."); - itemTimeout = TimeSpan.Zero; - } + _segmentsCache = + cache ?? new LruCache>(Constants.DEFAULT_MAX_CACHE_SIZE, + TimeSpan.FromSeconds(Constants.DEFAULT_CACHE_SECONDS), logger); + } - cacheSize = cacheSize ?? Constants.DEFAULT_MAX_CACHE_SIZE; + public OdpSegmentManager(IOdpSegmentApiManager apiManager, + int? cacheSize = null, + TimeSpan? itemTimeout = null, + ILogger logger = null + ) + { + _apiManager = apiManager; + _logger = logger ?? new DefaultLogger(); - _segmentsCache = - cache ?? new LruCache>(cacheSize.Value, itemTimeout, logger); + _segmentsCache = new LruCache>(cacheSize, itemTimeout, logger); } /// @@ -81,7 +83,7 @@ public List FetchQualifiedSegments(string fsUserId, List options = null ) { - if (!_odpConfig.IsReady()) + if (_odpConfig == null || !_odpConfig.IsReady()) { _logger.Log(LogLevel.WARN, Constants.ODP_NOT_INTEGRATED_MESSAGE); return null; @@ -99,12 +101,12 @@ public List FetchQualifiedSegments(string fsUserId, List qualifiedSegments; var cacheKey = GetCacheKey(OdpUserKeyType.FS_USER_ID.ToString().ToLower(), fsUserId); - if (options.Contains(OdpSegmentOption.ResetCache)) + if (options.Contains(OdpSegmentOption.RESET_CACHE)) { _segmentsCache.Reset(); } - if (!options.Contains(OdpSegmentOption.IgnoreCache)) + if (!options.Contains(OdpSegmentOption.IGNORE_CACHE)) { qualifiedSegments = _segmentsCache.Lookup(cacheKey); if (qualifiedSegments != null) @@ -124,7 +126,7 @@ public List FetchQualifiedSegments(string fsUserId, _odpConfig.SegmentsToCheck)?. ToList(); - if (qualifiedSegments != null && !options.Contains(OdpSegmentOption.IgnoreCache)) + if (qualifiedSegments != null && !options.Contains(OdpSegmentOption.IGNORE_CACHE)) { _segmentsCache.Save(cacheKey, qualifiedSegments); } @@ -159,5 +161,10 @@ public void ResetCache() { _segmentsCache.Reset(); } + + /// + /// For Testing Only: Retrieve the current segment cache + /// + internal ICache> SegmentsCacheForTesting { get { return _segmentsCache; } } } } diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 7a9f53dd6..16d6d8f34 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022, Optimizely + * Copyright 2017-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use file except in compliance with the License. @@ -14,6 +14,10 @@ * limitations under the License. */ +#if !(NET35 || NET40 || NETSTANDARD1_6) +#define USE_ODP +#endif + using OptimizelySDK.Bucketing; using OptimizelySDK.Entity; using OptimizelySDK.ErrorHandler; @@ -29,10 +33,13 @@ using OptimizelySDK.Config; using OptimizelySDK.Event; using OptimizelySDK.OptlyConfig; -using System.Net; using OptimizelySDK.OptimizelyDecisions; using System.Linq; +#if USE_ODP +using OptimizelySDK.Odp; +#endif + namespace OptimizelySDK { #if NET35 @@ -65,12 +72,18 @@ public class Optimizely : IOptimizely, IDisposable private OptimizelyDecideOption[] DefaultDecideOptions; +#if USE_ODP + private IOdpManager OdpManager; +#endif + /// /// It returns true if the ProjectConfig is valid otherwise false. /// Also, it may block execution if GetConfig() blocks execution to get ProjectConfig. /// - public bool IsValid { - get { + public bool IsValid + { + get + { return ProjectConfigManager?.GetConfig() != null; } } @@ -88,7 +101,8 @@ public static String SDK_VERSION // Microsoft Major.Minor.Build.Revision // Semantic Major.Minor.Patch Version version = assembly.GetName().Version; - String answer = String.Format("{0}.{1}.{2}", version.Major, version.Minor, version.Build); + String answer = String.Format("{0}.{1}.{2}", version.Major, version.Minor, + version.Build); return answer; } } @@ -117,28 +131,52 @@ public static String SDK_TYPE /// EventDispatcherInterface /// LoggerInterface /// ErrorHandlerInterface + /// User profile service /// boolean representing whether JSON schema validation needs to be performed /// EventProcessor + /// Default Decide options + /// Optional ODP Manager public Optimizely(string datafile, - IEventDispatcher eventDispatcher = null, - ILogger logger = null, - IErrorHandler errorHandler = null, - UserProfileService userProfileService = null, - bool skipJsonValidation = false, - EventProcessor eventProcessor = null, - OptimizelyDecideOption[] defaultDecideOptions = null) + IEventDispatcher eventDispatcher = null, + ILogger logger = null, + IErrorHandler errorHandler = null, + UserProfileService userProfileService = null, + bool skipJsonValidation = false, + EventProcessor eventProcessor = null, + OptimizelyDecideOption[] defaultDecideOptions = null +#if USE_ODP + , IOdpManager odpManager = null +#endif + ) { - try { - InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, null, eventProcessor, defaultDecideOptions); + try + { +#if USE_ODP + InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, + null, eventProcessor, defaultDecideOptions, odpManager); +#else + InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, + null, eventProcessor, defaultDecideOptions); +#endif - if (ValidateInputs(datafile, skipJsonValidation)) { + if (ValidateInputs(datafile, skipJsonValidation)) + { var config = DatafileProjectConfig.Create(datafile, Logger, ErrorHandler); ProjectConfigManager = new FallbackProjectConfigManager(config); - } else { +#if USE_ODP + // No need to setup notification for datafile updates. This constructor + // is for hardcoded datafile which should not be changed using this method. + OdpManager?.UpdateSettings(config.PublicKeyForOdp, config.HostForOdp, + config.Segments.ToList()); +#endif + } + else + { Logger.Log(LogLevel.ERROR, "Provided 'datafile' has invalid schema."); } } - catch (Exception ex) { + catch (Exception ex) + { string error = String.Empty; if (ex.GetType() == typeof(ConfigParseException)) error = ex.Message; @@ -154,32 +192,73 @@ public Optimizely(string datafile, /// Initializes a new instance of the class. /// /// Config manager. + /// Notification center /// Event dispatcher. /// Logger. /// Error handler. /// User profile service. /// EventProcessor + /// Default Decide options + /// Optional ODP Manager public Optimizely(ProjectConfigManager configManager, - NotificationCenter notificationCenter = null, - IEventDispatcher eventDispatcher = null, - ILogger logger = null, - IErrorHandler errorHandler = null, - UserProfileService userProfileService = null, - EventProcessor eventProcessor = null, - OptimizelyDecideOption[] defaultDecideOptions = null) + NotificationCenter notificationCenter = null, + IEventDispatcher eventDispatcher = null, + ILogger logger = null, + IErrorHandler errorHandler = null, + UserProfileService userProfileService = null, + EventProcessor eventProcessor = null, + OptimizelyDecideOption[] defaultDecideOptions = null +#if USE_ODP + , IOdpManager odpManager = null +#endif + ) { ProjectConfigManager = configManager; - InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, notificationCenter, eventProcessor, defaultDecideOptions); +#if USE_ODP + InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, + notificationCenter, eventProcessor, defaultDecideOptions, odpManager); + + var projectConfig = ProjectConfigManager.CachedProjectConfig; + + if (ProjectConfigManager.CachedProjectConfig != null) + { + // in case Project config is instantly available + OdpManager?.UpdateSettings(projectConfig.PublicKeyForOdp, projectConfig.HostForOdp, + projectConfig.Segments.ToList()); + } + + if (ProjectConfigManager.SdkKey != null) + { + NotificationCenterRegistry.GetNotificationCenter(configManager.SdkKey, logger) + ?.AddNotification(NotificationCenter.NotificationType.OptimizelyConfigUpdate, + () => + { + projectConfig = ProjectConfigManager.CachedProjectConfig; + + OdpManager?.UpdateSettings(projectConfig.PublicKeyForOdp, + projectConfig.HostForOdp, + projectConfig.Segments.ToList()); + }); + } + +#else + InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, + notificationCenter, eventProcessor, defaultDecideOptions); +#endif } private void InitializeComponents(IEventDispatcher eventDispatcher = null, - ILogger logger = null, - IErrorHandler errorHandler = null, - UserProfileService userProfileService = null, - NotificationCenter notificationCenter = null, - EventProcessor eventProcessor = null, - OptimizelyDecideOption[] defaultDecideOptions = null) + ILogger logger = null, + IErrorHandler errorHandler = null, + UserProfileService userProfileService = null, + NotificationCenter notificationCenter = null, + EventProcessor eventProcessor = null, + OptimizelyDecideOption[] defaultDecideOptions = null +#if USE_ODP + , IOdpManager odpManager = null +#endif + ) { Logger = logger ?? new NoOpLogger(); EventDispatcher = eventDispatcher ?? new DefaultEventDispatcher(Logger); @@ -188,9 +267,15 @@ private void InitializeComponents(IEventDispatcher eventDispatcher = null, EventBuilder = new EventBuilder(Bucketer, Logger); UserProfileService = userProfileService; NotificationCenter = notificationCenter ?? new NotificationCenter(Logger); - DecisionService = new DecisionService(Bucketer, ErrorHandler, userProfileService, Logger); - EventProcessor = eventProcessor ?? new ForwardingEventProcessor(EventDispatcher, NotificationCenter, Logger); + DecisionService = + new DecisionService(Bucketer, ErrorHandler, userProfileService, Logger); + EventProcessor = eventProcessor ?? new ForwardingEventProcessor(EventDispatcher, + NotificationCenter, + Logger); DefaultDecideOptions = defaultDecideOptions ?? new OptimizelyDecideOption[] { }; +#if USE_ODP + OdpManager = odpManager; +#endif } /// @@ -200,7 +285,9 @@ private void InitializeComponents(IEventDispatcher eventDispatcher = null, /// string ID for user /// associative array of Attributes for the user /// null|Variation Representing variation - public Variation Activate(string experimentKey, string userId, UserAttributes userAttributes = null) + public Variation Activate(string experimentKey, string userId, + UserAttributes userAttributes = null + ) { var config = ProjectConfigManager?.GetConfig(); @@ -210,11 +297,7 @@ public Variation Activate(string experimentKey, string userId, UserAttributes us return null; } - var inputValues = new Dictionary - { - { USER_ID, userId }, - { EXPERIMENT_KEY, experimentKey } - }; + var inputValues = new Dictionary { { USER_ID, userId }, { EXPERIMENT_KEY, experimentKey } }; if (!ValidateStringInputs(inputValues)) return null; @@ -235,7 +318,8 @@ public Variation Activate(string experimentKey, string userId, UserAttributes us return null; } - SendImpressionEvent(experiment, variation, userId, userAttributes, config, SOURCE_TYPE_EXPERIMENT, true); + SendImpressionEvent(experiment, variation, userId, userAttributes, config, + SOURCE_TYPE_EXPERIMENT, true); return variation; } @@ -258,7 +342,9 @@ private bool ValidateInputs(string datafile, bool skipJsonValidation) /// ID for user /// Attributes of the user /// eventTags array Hash representing metadata associated with the event. - public void Track(string eventKey, string userId, UserAttributes userAttributes = null, EventTags eventTags = null) + public void Track(string eventKey, string userId, UserAttributes userAttributes = null, + EventTags eventTags = null + ) { var config = ProjectConfigManager?.GetConfig(); @@ -268,11 +354,7 @@ public void Track(string eventKey, string userId, UserAttributes userAttributes return; } - var inputValues = new Dictionary - { - { USER_ID, userId }, - { EVENT_KEY, eventKey } - }; + var inputValues = new Dictionary { { USER_ID, userId }, { EVENT_KEY, eventKey } }; if (!ValidateStringInputs(inputValues)) return; @@ -281,7 +363,8 @@ public void Track(string eventKey, string userId, UserAttributes userAttributes if (eevent.Key == null) { - Logger.Log(LogLevel.INFO, string.Format("Not tracking user {0} for event {1}.", userId, eventKey)); + Logger.Log(LogLevel.INFO, + string.Format("Not tracking user {0} for event {1}.", userId, eventKey)); return; } @@ -290,15 +373,19 @@ public void Track(string eventKey, string userId, UserAttributes userAttributes eventTags = eventTags.FilterNullValues(Logger); } - var userEvent = UserEventFactory.CreateConversionEvent(config, eventKey, userId, userAttributes, eventTags); + var userEvent = UserEventFactory.CreateConversionEvent(config, eventKey, userId, + userAttributes, eventTags); EventProcessor.Process(userEvent); - Logger.Log(LogLevel.INFO, string.Format("Tracking event {0} for user {1}.", eventKey, userId)); + Logger.Log(LogLevel.INFO, + string.Format("Tracking event {0} for user {1}.", eventKey, userId)); - if (NotificationCenter.GetNotificationCount(NotificationCenter.NotificationType.Track) > 0) + if (NotificationCenter.GetNotificationCount(NotificationCenter.NotificationType.Track) > + 0) { var conversionEvent = EventFactory.CreateLogEvent(userEvent, Logger); - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Track, eventKey, userId, - userAttributes, eventTags, conversionEvent); + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Track, + eventKey, userId, + userAttributes, eventTags, conversionEvent); } } @@ -309,7 +396,9 @@ public void Track(string eventKey, string userId, UserAttributes userAttributes /// ID for the user /// Attributes for the users /// null|Variation Representing variation - public Variation GetVariation(string experimentKey, string userId, UserAttributes userAttributes = null) + public Variation GetVariation(string experimentKey, string userId, + UserAttributes userAttributes = null + ) { var config = ProjectConfigManager?.GetConfig(); return GetVariation(experimentKey, userId, config, userAttributes); @@ -323,7 +412,9 @@ public Variation GetVariation(string experimentKey, string userId, UserAttribute /// ProjectConfig to be used for variation /// Attributes for the users /// null|Variation Representing variation - private Variation GetVariation(string experimentKey, string userId, ProjectConfig config, UserAttributes userAttributes = null) + private Variation GetVariation(string experimentKey, string userId, ProjectConfig config, + UserAttributes userAttributes = null + ) { if (config == null) { @@ -331,11 +422,7 @@ private Variation GetVariation(string experimentKey, string userId, ProjectConfi return null; } - var inputValues = new Dictionary - { - { USER_ID, userId }, - { EXPERIMENT_KEY, experimentKey } - }; + var inputValues = new Dictionary { { USER_ID, userId }, { EXPERIMENT_KEY, experimentKey } }; if (!ValidateStringInputs(inputValues)) return null; @@ -345,16 +432,18 @@ private Variation GetVariation(string experimentKey, string userId, ProjectConfi return null; userAttributes = userAttributes ?? new UserAttributes(); - var userContext = CreateUserContext(userId, userAttributes); + var userContext = CreateUserContextCopy(userId, userAttributes); var variation = DecisionService.GetVariation(experiment, userContext, config)?.ResultObject; var decisionInfo = new Dictionary { - { "experimentKey", experimentKey }, - { "variationKey", variation?.Key }, + { "experimentKey", experimentKey }, { "variationKey", variation?.Key }, }; - var decisionNotificationType = config.IsFeatureExperiment(experiment.Id) ? DecisionNotificationTypes.FEATURE_TEST : DecisionNotificationTypes.AB_TEST; - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, decisionNotificationType, userId, + var decisionNotificationType = config.IsFeatureExperiment(experiment.Id) + ? DecisionNotificationTypes.FEATURE_TEST + : DecisionNotificationTypes.AB_TEST; + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, + decisionNotificationType, userId, userAttributes, decisionInfo); return variation; } @@ -376,12 +465,9 @@ public bool SetForcedVariation(string experimentKey, string userId, string varia return false; } - var inputValues = new Dictionary - { - { USER_ID, userId }, - { EXPERIMENT_KEY, experimentKey } - }; - return ValidateStringInputs(inputValues) && DecisionService.SetForcedVariation(experimentKey, userId, variationKey, config); + var inputValues = new Dictionary { { USER_ID, userId }, { EXPERIMENT_KEY, experimentKey } }; + return ValidateStringInputs(inputValues) && + DecisionService.SetForcedVariation(experimentKey, userId, variationKey, config); } /// @@ -398,11 +484,7 @@ public Variation GetForcedVariation(string experimentKey, string userId) return null; } - var inputValues = new Dictionary - { - { USER_ID, userId }, - { EXPERIMENT_KEY, experimentKey } - }; + var inputValues = new Dictionary { { USER_ID, userId }, { EXPERIMENT_KEY, experimentKey } }; if (!ValidateStringInputs(inputValues)) return null; @@ -420,22 +502,21 @@ public Variation GetForcedVariation(string experimentKey, string userId) /// The user ID /// The user's attributes. /// True if feature is enabled, false or null otherwise - public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttributes userAttributes = null) + public virtual bool IsFeatureEnabled(string featureKey, string userId, + UserAttributes userAttributes = null + ) { var config = ProjectConfigManager?.GetConfig(); if (config == null) { - Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'IsFeatureEnabled'."); + Logger.Log(LogLevel.ERROR, + "Datafile has invalid format. Failing 'IsFeatureEnabled'."); return false; } - var inputValues = new Dictionary - { - { USER_ID, userId }, - { FEATURE_KEY, featureKey } - }; + var inputValues = new Dictionary { { USER_ID, userId }, { FEATURE_KEY, featureKey } }; if (!ValidateStringInputs(inputValues)) return false; @@ -449,7 +530,10 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri bool featureEnabled = false; var sourceInfo = new Dictionary(); - var decision = DecisionService.GetVariationForFeature(featureFlag, CreateUserContext(userId, userAttributes), config).ResultObject; + var decision = DecisionService.GetVariationForFeature(featureFlag, + CreateUserContextCopy(userId, userAttributes), + config) + .ResultObject; var variation = decision?.Variation; var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT; @@ -467,14 +551,18 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri } else { - Logger.Log(LogLevel.INFO, $@"The user ""{userId}"" is not being experimented on feature ""{featureKey}""."); + Logger.Log(LogLevel.INFO, + $@"The user ""{userId}"" is not being experimented on feature ""{featureKey + }""."); } } if (featureEnabled == true) - Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is enabled for user ""{userId}""."); + Logger.Log(LogLevel.INFO, + $@"Feature flag ""{featureKey}"" is enabled for user ""{userId}""."); else - Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is not enabled for user ""{userId}""."); + Logger.Log(LogLevel.INFO, + $@"Feature flag ""{featureKey}"" is not enabled for user ""{userId}""."); var decisionInfo = new Dictionary { @@ -484,10 +572,12 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri { "sourceInfo", sourceInfo }, }; - SendImpressionEvent(decision?.Experiment, variation, userId, userAttributes, config, featureKey, decisionSource, featureEnabled); + SendImpressionEvent(decision?.Experiment, variation, userId, userAttributes, config, + featureKey, decisionSource, featureEnabled); - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId, - userAttributes ?? new UserAttributes(), decisionInfo); + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, + DecisionNotificationTypes.FEATURE, userId, + userAttributes ?? new UserAttributes(), decisionInfo); return featureEnabled; } @@ -500,21 +590,23 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri /// The user's attributes /// Variable type /// string | null Feature variable value - public virtual T GetFeatureVariableValueForType(string featureKey, string variableKey, string userId, - UserAttributes userAttributes, string variableType) + public virtual T GetFeatureVariableValueForType(string featureKey, string variableKey, + string userId, + UserAttributes userAttributes, string variableType + ) { var config = ProjectConfigManager?.GetConfig(); if (config == null) { - Logger.Log(LogLevel.ERROR, $@"Datafile has invalid format. Failing '{FeatureVariable.GetFeatureVariableTypeName(variableType)}'."); + Logger.Log(LogLevel.ERROR, + $@"Datafile has invalid format. Failing '{ + FeatureVariable.GetFeatureVariableTypeName(variableType)}'."); return default(T); } var inputValues = new Dictionary { - { USER_ID, userId }, - { FEATURE_KEY, featureKey }, - { VARIABLE_KEY, variableKey } + { USER_ID, userId }, { FEATURE_KEY, featureKey }, { VARIABLE_KEY, variableKey } }; if (!ValidateStringInputs(inputValues)) @@ -528,47 +620,60 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var if (featureVariable == null) { Logger.Log(LogLevel.ERROR, - $@"No feature variable was found for key ""{variableKey}"" in feature flag ""{featureKey}""."); + $@"No feature variable was found for key ""{variableKey}"" in feature flag ""{ + featureKey}""."); return default(T); } else if (featureVariable.Type != variableType) { Logger.Log(LogLevel.ERROR, - $@"Variable is of type ""{featureVariable.Type}"", but you requested it as type ""{variableType}""."); + $@"Variable is of type ""{featureVariable.Type + }"", but you requested it as type ""{variableType}""."); return default(T); } var featureEnabled = false; var variableValue = featureVariable.DefaultValue; - var decision = DecisionService.GetVariationForFeature(featureFlag, CreateUserContext(userId, userAttributes), config).ResultObject; + var decision = DecisionService.GetVariationForFeature(featureFlag, + CreateUserContextCopy(userId, userAttributes), + config) + .ResultObject; if (decision?.Variation != null) { var variation = decision.Variation; featureEnabled = variation.FeatureEnabled.GetValueOrDefault(); - var featureVariableUsageInstance = variation.GetFeatureVariableUsageFromId(featureVariable.Id); + var featureVariableUsageInstance = + variation.GetFeatureVariableUsageFromId(featureVariable.Id); if (featureVariableUsageInstance != null) { if (variation.FeatureEnabled == true) { variableValue = featureVariableUsageInstance.Value; - Logger.Log(LogLevel.INFO, $@"Got variable value ""{variableValue}"" for variable ""{variableKey}"" of feature flag ""{featureKey}""."); + Logger.Log(LogLevel.INFO, + $@"Got variable value ""{variableValue}"" for variable ""{variableKey + }"" of feature flag ""{featureKey}""."); } else { - Logger.Log(LogLevel.INFO, $@"Feature ""{featureKey}"" is not enabled for user {userId}. Returning the default variable value ""{variableValue}""."); + Logger.Log(LogLevel.INFO, + $@"Feature ""{featureKey}"" is not enabled for user {userId + }. Returning the default variable value ""{variableValue}""."); } } else { - Logger.Log(LogLevel.INFO, $@"Variable ""{variableKey}"" is not used in variation ""{variation.Key}"", returning default value ""{variableValue}""."); + Logger.Log(LogLevel.INFO, + $@"Variable ""{variableKey}"" is not used in variation ""{variation.Key + }"", returning default value ""{variableValue}""."); } } else { Logger.Log(LogLevel.INFO, - $@"User ""{userId}"" is not in any variation for feature flag ""{featureKey}"", returning default value ""{variableValue}""."); + $@"User ""{userId}"" is not in any variation for feature flag ""{featureKey + }"", returning default value ""{variableValue}""."); } var sourceInfo = new Dictionary(); @@ -584,14 +689,19 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var { "featureKey", featureKey }, { "featureEnabled", featureEnabled }, { "variableKey", variableKey }, - { "variableValue", typeCastedValue is OptimizelyJSON? ((OptimizelyJSON)typeCastedValue).ToDictionary() : typeCastedValue }, + { + "variableValue", typeCastedValue is OptimizelyJSON + ? ((OptimizelyJSON)typeCastedValue).ToDictionary() + : typeCastedValue + }, { "variableType", variableType.ToString().ToLower() }, { "source", decision?.Source }, { "sourceInfo", sourceInfo }, }; - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE_VARIABLE, userId, - userAttributes ?? new UserAttributes(), decisionInfo); + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, + DecisionNotificationTypes.FEATURE_VARIABLE, userId, + userAttributes ?? new UserAttributes(), decisionInfo); return (T)typeCastedValue; } @@ -603,9 +713,12 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var /// The user ID /// The user's attributes /// bool | Feature variable value or null - public bool? GetFeatureVariableBoolean(string featureKey, string variableKey, string userId, UserAttributes userAttributes = null) + public bool? GetFeatureVariableBoolean(string featureKey, string variableKey, string userId, + UserAttributes userAttributes = null + ) { - return GetFeatureVariableValueForType(featureKey, variableKey, userId, userAttributes, FeatureVariable.BOOLEAN_TYPE); + return GetFeatureVariableValueForType(featureKey, variableKey, userId, + userAttributes, FeatureVariable.BOOLEAN_TYPE); } /// @@ -616,9 +729,12 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var /// The user ID /// The user's attributes /// double | Feature variable value or null - public double? GetFeatureVariableDouble(string featureKey, string variableKey, string userId, UserAttributes userAttributes = null) + public double? GetFeatureVariableDouble(string featureKey, string variableKey, + string userId, UserAttributes userAttributes = null + ) { - return GetFeatureVariableValueForType(featureKey, variableKey, userId, userAttributes, FeatureVariable.DOUBLE_TYPE); + return GetFeatureVariableValueForType(featureKey, variableKey, userId, + userAttributes, FeatureVariable.DOUBLE_TYPE); } /// @@ -629,9 +745,12 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var /// The user ID /// The user's attributes /// int | Feature variable value or null - public int? GetFeatureVariableInteger(string featureKey, string variableKey, string userId, UserAttributes userAttributes = null) + public int? GetFeatureVariableInteger(string featureKey, string variableKey, string userId, + UserAttributes userAttributes = null + ) { - return GetFeatureVariableValueForType(featureKey, variableKey, userId, userAttributes, FeatureVariable.INTEGER_TYPE); + return GetFeatureVariableValueForType(featureKey, variableKey, userId, + userAttributes, FeatureVariable.INTEGER_TYPE); } /// @@ -642,9 +761,12 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var /// The user ID /// The user's attributes /// string | Feature variable value or null - public string GetFeatureVariableString(string featureKey, string variableKey, string userId, UserAttributes userAttributes = null) + public string GetFeatureVariableString(string featureKey, string variableKey, string userId, + UserAttributes userAttributes = null + ) { - return GetFeatureVariableValueForType(featureKey, variableKey, userId, userAttributes, FeatureVariable.STRING_TYPE); + return GetFeatureVariableValueForType(featureKey, variableKey, userId, + userAttributes, FeatureVariable.STRING_TYPE); } /// @@ -655,9 +777,12 @@ public string GetFeatureVariableString(string featureKey, string variableKey, st /// The user ID /// The user's attributes /// OptimizelyJson | Feature variable value or null - public OptimizelyJSON GetFeatureVariableJSON(string featureKey, string variableKey, string userId, UserAttributes userAttributes = null) + public OptimizelyJSON GetFeatureVariableJSON(string featureKey, string variableKey, + string userId, UserAttributes userAttributes = null + ) { - return GetFeatureVariableValueForType(featureKey, variableKey, userId, userAttributes, FeatureVariable.JSON_TYPE); + return GetFeatureVariableValueForType(featureKey, variableKey, userId, + userAttributes, FeatureVariable.JSON_TYPE); } /// @@ -668,12 +793,10 @@ public OptimizelyJSON GetFeatureVariableJSON(string featureKey, string variableK /// The user's attributes /// OptimizelyUserContext | An OptimizelyUserContext associated with this OptimizelyClient. public OptimizelyUserContext CreateUserContext(string userId, - UserAttributes userAttributes = null) + UserAttributes userAttributes = null + ) { - var inputValues = new Dictionary - { - { USER_ID, userId }, - }; + var inputValues = new Dictionary { { USER_ID, userId }, }; if (!ValidateStringInputs(inputValues)) return null; @@ -681,6 +804,22 @@ public OptimizelyUserContext CreateUserContext(string userId, return new OptimizelyUserContext(this, userId, userAttributes, ErrorHandler, Logger); } + private OptimizelyUserContext CreateUserContextCopy(string userId, + UserAttributes userAttributes = null + ) + { + var inputValues = new Dictionary { { USER_ID, userId }, }; + + if (!ValidateStringInputs(inputValues)) + return null; + + return new OptimizelyUserContext(this, userId, userAttributes, null, null, ErrorHandler, Logger +#if USE_ODP + , shouldIdentifyUser: false +#endif + ); + } + /// /// Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context, which contains all data required to deliver the flag. ///
    @@ -691,14 +830,16 @@ public OptimizelyUserContext CreateUserContext(string userId, /// A list of options for decision-making. /// A decision result. internal OptimizelyDecision Decide(OptimizelyUserContext user, - string key, - OptimizelyDecideOption[] options) + string key, + OptimizelyDecideOption[] options + ) { var config = ProjectConfigManager?.GetConfig(); if (config == null) { - return OptimizelyDecision.NewErrorDecision(key, user, DecisionMessage.SDK_NOT_READY, ErrorHandler, Logger); + return OptimizelyDecision.NewErrorDecision(key, user, DecisionMessage.SDK_NOT_READY, + ErrorHandler, Logger); } if (key == null) @@ -726,12 +867,14 @@ internal OptimizelyDecision Decide(OptimizelyUserContext user, FeatureDecision decision = null; var decisionContext = new OptimizelyDecisionContext(flag.Key); - var forcedDecisionVariation = DecisionService.ValidatedForcedDecision(decisionContext, config, user); + var forcedDecisionVariation = + DecisionService.ValidatedForcedDecision(decisionContext, config, user); decisionReasons += forcedDecisionVariation.DecisionReasons; if (forcedDecisionVariation.ResultObject != null) { - decision = new FeatureDecision(null, forcedDecisionVariation.ResultObject, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); + decision = new FeatureDecision(null, forcedDecisionVariation.ResultObject, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST); } else { @@ -755,29 +898,34 @@ internal OptimizelyDecision Decide(OptimizelyUserContext user, if (featureEnabled) { - Logger.Log(LogLevel.INFO, "Feature \"" + key + "\" is enabled for user \"" + userId + "\""); + Logger.Log(LogLevel.INFO, + "Feature \"" + key + "\" is enabled for user \"" + userId + "\""); } else { - Logger.Log(LogLevel.INFO, "Feature \"" + key + "\" is not enabled for user \"" + userId + "\""); + Logger.Log(LogLevel.INFO, + "Feature \"" + key + "\" is not enabled for user \"" + userId + "\""); } var variableMap = new Dictionary(); - if (flag?.Variables != null && !allOptions.Contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) + if (flag?.Variables != null && + !allOptions.Contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) { foreach (var featureVariable in flag?.Variables) { string variableValue = featureVariable.DefaultValue; if (featureEnabled) { - var featureVariableUsageInstance = decision?.Variation.GetFeatureVariableUsageFromId(featureVariable.Id); + var featureVariableUsageInstance = + decision?.Variation.GetFeatureVariableUsageFromId(featureVariable.Id); if (featureVariableUsageInstance != null) { variableValue = featureVariableUsageInstance.Value; } } - var typeCastedValue = GetTypeCastedVariableValue(variableValue, featureVariable.Type); + var typeCastedValue = + GetTypeCastedVariableValue(variableValue, featureVariable.Type); if (typeCastedValue is OptimizelyJSON) typeCastedValue = ((OptimizelyJSON)typeCastedValue).ToDictionary(); @@ -791,9 +939,13 @@ internal OptimizelyDecision Decide(OptimizelyUserContext user, var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT; if (!allOptions.Contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) { - decisionEventDispatched = SendImpressionEvent(decision?.Experiment, decision?.Variation, userId, userAttributes, config, key, decisionSource, featureEnabled); + decisionEventDispatched = SendImpressionEvent(decision?.Experiment, + decision?.Variation, userId, userAttributes, config, key, decisionSource, + featureEnabled); } - var reasonsToReport = decisionReasons.ToReport(allOptions.Contains(OptimizelyDecideOption.INCLUDE_REASONS)).ToArray(); + + var reasonsToReport = decisionReasons.ToReport(allOptions.Contains(OptimizelyDecideOption.INCLUDE_REASONS)) + .ToArray(); var variationKey = decision?.Variation?.Key; // TODO: add ruleKey values when available later. use a copy of experimentKey until then. @@ -807,11 +959,12 @@ internal OptimizelyDecision Decide(OptimizelyUserContext user, { "variationKey", variationKey }, { "ruleKey", ruleKey }, { "reasons", reasonsToReport }, - { "decisionEventDispatched", decisionEventDispatched } + { "decisionEventDispatched", decisionEventDispatched } }; - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FLAG, userId, - userAttributes ?? new UserAttributes(), decisionInfo); + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, + DecisionNotificationTypes.FLAG, userId, + userAttributes ?? new UserAttributes(), decisionInfo); return new OptimizelyDecision( variationKey, @@ -824,14 +977,16 @@ internal OptimizelyDecision Decide(OptimizelyUserContext user, } internal Dictionary DecideAll(OptimizelyUserContext user, - OptimizelyDecideOption[] options) + OptimizelyDecideOption[] options + ) { var decisionMap = new Dictionary(); var projectConfig = ProjectConfigManager?.GetConfig(); if (projectConfig == null) { - Logger.Log(LogLevel.ERROR, "Optimizely instance is not valid, failing isFeatureEnabled call."); + Logger.Log(LogLevel.ERROR, + "Optimizely instance is not valid, failing isFeatureEnabled call."); return decisionMap; } @@ -842,15 +997,17 @@ internal Dictionary DecideAll(OptimizelyUserContext } internal Dictionary DecideForKeys(OptimizelyUserContext user, - string[] keys, - OptimizelyDecideOption[] options) + string[] keys, + OptimizelyDecideOption[] options + ) { var decisionDictionary = new Dictionary(); var projectConfig = ProjectConfigManager?.GetConfig(); if (projectConfig == null) { - Logger.Log(LogLevel.ERROR, "Optimizely instance is not valid, failing isFeatureEnabled call."); + Logger.Log(LogLevel.ERROR, + "Optimizely instance is not valid, failing isFeatureEnabled call."); return decisionDictionary; } @@ -864,7 +1021,8 @@ internal Dictionary DecideForKeys(OptimizelyUserCont foreach (string key in keys) { var decision = Decide(user, key, options); - if (!allOptions.Contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) || decision.Enabled) + if (!allOptions.Contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) || + decision.Enabled) { decisionDictionary.Add(key, decision); } @@ -880,6 +1038,7 @@ private OptimizelyDecideOption[] GetAllOptions(OptimizelyDecideOption[] options) { copiedOptions = options.Union(DefaultDecideOptions).ToArray(); } + return copiedOptions; } @@ -892,10 +1051,12 @@ private OptimizelyDecideOption[] GetAllOptions(OptimizelyDecideOption[] options) /// The user's attributes /// It can either be experiment in case impression event is sent from activate or it's feature-test or rollout private void SendImpressionEvent(Experiment experiment, Variation variation, string userId, - UserAttributes userAttributes, ProjectConfig config, - string ruleType, bool enabled) + UserAttributes userAttributes, ProjectConfig config, + string ruleType, bool enabled + ) { - SendImpressionEvent(experiment, variation, userId, userAttributes, config, "", ruleType, enabled); + SendImpressionEvent(experiment, variation, userId, userAttributes, config, "", ruleType, + enabled); } /// @@ -908,34 +1069,43 @@ private void SendImpressionEvent(Experiment experiment, Variation variation, str /// It can either be experiment key in case if ruleType is experiment or it's feature key in case ruleType is feature-test or rollout /// It can either be experiment in case impression event is sent from activate or it's feature-test or rollout private bool SendImpressionEvent(Experiment experiment, Variation variation, string userId, - UserAttributes userAttributes, ProjectConfig config, - string flagKey, string ruleType, bool enabled) + UserAttributes userAttributes, ProjectConfig config, + string flagKey, string ruleType, bool enabled + ) { if (experiment != null && !experiment.IsExperimentRunning) { - Logger.Log(LogLevel.ERROR, @"Experiment has ""Launched"" status so not dispatching event during activation."); + Logger.Log(LogLevel.ERROR, + @"Experiment has ""Launched"" status so not dispatching event during activation."); } - var userEvent = UserEventFactory.CreateImpressionEvent(config, experiment, variation, userId, userAttributes, flagKey, ruleType, enabled); + var userEvent = UserEventFactory.CreateImpressionEvent(config, experiment, variation, + userId, userAttributes, flagKey, ruleType, enabled); if (userEvent == null) { return false; } + EventProcessor.Process(userEvent); if (experiment != null) { - Logger.Log(LogLevel.INFO, $"Activating user {userId} in experiment {experiment.Key}."); + Logger.Log(LogLevel.INFO, + $"Activating user {userId} in experiment {experiment.Key}."); } + // Kept For backwards compatibility. // This notification is deprecated and the new DecisionNotifications // are sent via their respective method calls. - if (NotificationCenter.GetNotificationCount(NotificationCenter.NotificationType.Activate) > 0) + if (NotificationCenter.GetNotificationCount( + NotificationCenter.NotificationType.Activate) > 0) { var impressionEvent = EventFactory.CreateLogEvent(userEvent, Logger); - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Activate, experiment, userId, - userAttributes, variation, impressionEvent); + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Activate, + experiment, userId, + userAttributes, variation, impressionEvent); } + return true; } @@ -953,7 +1123,8 @@ public List GetEnabledFeatures(string userId, UserAttributes userAttribu if (config == null) { - Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetEnabledFeatures'."); + Logger.Log(LogLevel.ERROR, + "Datafile has invalid format. Failing 'GetEnabledFeatures'."); return enabledFeaturesList; } @@ -978,12 +1149,14 @@ public List GetEnabledFeatures(string userId, UserAttributes userAttribu /// The user's attributes /// string | null An OptimizelyJSON instance for all variable values. public OptimizelyJSON GetAllFeatureVariables(string featureKey, string userId, - UserAttributes userAttributes = null) + UserAttributes userAttributes = null + ) { var config = ProjectConfigManager?.GetConfig(); if (config == null) { - Logger.Log(LogLevel.ERROR, "Optimizely instance is not valid, failing getAllFeatureVariableValues call. type"); + Logger.Log(LogLevel.ERROR, + "Optimizely instance is not valid, failing getAllFeatureVariableValues call. type"); return null; } @@ -1001,7 +1174,8 @@ public OptimizelyJSON GetAllFeatureVariables(string featureKey, string userId, var featureFlag = config.GetFeatureFlagFromKey(featureKey); if (string.IsNullOrEmpty(featureFlag.Key)) { - Logger.Log(LogLevel.INFO, "No feature flag was found for key \"" + featureKey + "\"."); + Logger.Log(LogLevel.INFO, + "No feature flag was found for key \"" + featureKey + "\"."); return null; } @@ -1009,7 +1183,8 @@ public OptimizelyJSON GetAllFeatureVariables(string featureKey, string userId, return null; var featureEnabled = false; - var decisionResult = DecisionService.GetVariationForFeature(featureFlag, CreateUserContext(userId, userAttributes), config); + var decisionResult = DecisionService.GetVariationForFeature(featureFlag, + CreateUserContextCopy(userId, userAttributes), config); var variation = decisionResult.ResultObject?.Variation; if (variation != null) @@ -1018,38 +1193,46 @@ public OptimizelyJSON GetAllFeatureVariables(string featureKey, string userId, } else { - Logger.Log(LogLevel.INFO, "User \"" + userId + "\" was not bucketed into any variation for feature flag \"" + featureKey + "\". " + - "The default values are being returned."); + Logger.Log(LogLevel.INFO, "User \"" + userId + + "\" was not bucketed into any variation for feature flag \"" + + featureKey + "\". " + + "The default values are being returned."); } if (featureEnabled) { - Logger.Log(LogLevel.INFO, "Feature \"" + featureKey + "\" is enabled for user \"" + userId + "\""); + Logger.Log(LogLevel.INFO, + "Feature \"" + featureKey + "\" is enabled for user \"" + userId + "\""); } else { - Logger.Log(LogLevel.INFO, "Feature \"" + featureKey + "\" is not enabled for user \"" + userId + "\""); + Logger.Log(LogLevel.INFO, + "Feature \"" + featureKey + "\" is not enabled for user \"" + userId + "\""); } + var valuesMap = new Dictionary(); foreach (var featureVariable in featureFlag.Variables) { string variableValue = featureVariable.DefaultValue; if (featureEnabled) { - var featureVariableUsageInstance = variation.GetFeatureVariableUsageFromId(featureVariable.Id); + var featureVariableUsageInstance = + variation.GetFeatureVariableUsageFromId(featureVariable.Id); if (featureVariableUsageInstance != null) { variableValue = featureVariableUsageInstance.Value; } } - var typeCastedValue = GetTypeCastedVariableValue(variableValue, featureVariable.Type); + var typeCastedValue = + GetTypeCastedVariableValue(variableValue, featureVariable.Type); if (typeCastedValue is OptimizelyJSON) typeCastedValue = ((OptimizelyJSON)typeCastedValue).ToDictionary(); valuesMap.Add(featureVariable.Key, typeCastedValue); } + var sourceInfo = new Dictionary(); if (decisionResult.ResultObject?.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST) { @@ -1066,7 +1249,8 @@ public OptimizelyJSON GetAllFeatureVariables(string featureKey, string userId, { "sourceInfo", sourceInfo }, }; - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.ALL_FEATURE_VARIABLE, userId, + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, + DecisionNotificationTypes.ALL_FEATURE_VARIABLE, userId, userAttributes ?? new UserAttributes(), decisionInfo); return new OptimizelyJSON(valuesMap, ErrorHandler, Logger); @@ -1082,7 +1266,8 @@ public OptimizelyConfig GetOptimizelyConfig() if (config == null) { - Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetOptimizelyConfig'."); + Logger.Log(LogLevel.ERROR, + "Datafile has invalid format. Failing 'GetOptimizelyConfig'."); return null; } @@ -1093,13 +1278,52 @@ public OptimizelyConfig GetOptimizelyConfig() return ((IOptimizelyConfigManager)ProjectConfigManager).GetOptimizelyConfig(); } - Logger.Log(LogLevel.DEBUG, "ProjectConfigManager is not instance of IOptimizelyConfigManager, generating new OptimizelyConfigObject as a fallback"); + Logger.Log(LogLevel.DEBUG, + "ProjectConfigManager is not instance of IOptimizelyConfigManager, generating new OptimizelyConfigObject as a fallback"); return new OptimizelyConfigService(config).GetOptimizelyConfig(); } #endregion FeatureFlag APIs +#if USE_ODP + /// + /// Attempts to fetch and return a list of a user's qualified segments. + /// + /// FS User ID + /// Options used during segment cache handling + /// Qualified segments for the user from the cache or the ODP server + internal string[] FetchQualifiedSegments(string userId, + List segmentOptions + ) + { + return OdpManager?.FetchQualifiedSegments(userId, segmentOptions); + } + + /// + /// Send identification event to Optimizely Data Platform + /// + /// FS User ID to send + internal void IdentifyUser(string userId) + { + OdpManager?.IdentifyUser(userId); + } + + /// + /// Add event to queue for sending to ODP + /// + /// Type of event (typically `fullstack` from server-side SDK events) + /// Subcategory of the event type + /// Key-value map of user identifiers + /// Event data in a key-value pair format + public void SendOdpEvent(string type, string action, Dictionary identifiers, + Dictionary data + ) + { + OdpManager?.SendEvent(type, action, identifiers, data); + } +#endif + /// /// Validate all string inputs are not null or empty. /// @@ -1144,7 +1368,8 @@ private object GetTypeCastedVariableValue(string value, string type) break; case FeatureVariable.DOUBLE_TYPE: - double.TryParse(value, System.Globalization.NumberStyles.Number, System.Globalization.CultureInfo.InvariantCulture, out double doubleValue); + double.TryParse(value, System.Globalization.NumberStyles.Number, + System.Globalization.CultureInfo.InvariantCulture, out double doubleValue); result = doubleValue; break; @@ -1163,21 +1388,37 @@ private object GetTypeCastedVariableValue(string value, string type) } if (result == null) - Logger.Log(LogLevel.ERROR, $@"Unable to cast variable value ""{value}"" to type ""{type}""."); + Logger.Log(LogLevel.ERROR, + $@"Unable to cast variable value ""{value}"" to type ""{type}""."); return result; } public void Dispose() { - if (Disposed) return; + if (Disposed) + { + return; + } Disposed = true; - (ProjectConfigManager as IDisposable)?.Dispose(); - (EventProcessor as IDisposable)?.Dispose(); + if (ProjectConfigManager is IDisposable) + { +#if USE_ODP + NotificationCenterRegistry.RemoveNotificationCenter(ProjectConfigManager.SdkKey); +#endif + + (ProjectConfigManager as IDisposable)?.Dispose(); + } ProjectConfigManager = null; + + (EventProcessor as IDisposable)?.Dispose(); + +#if USE_ODP + OdpManager?.Dispose(); +#endif } } } diff --git a/OptimizelySDK/OptimizelySDK.csproj b/OptimizelySDK/OptimizelySDK.csproj index b667a2fb5..bdf89ec49 100644 --- a/OptimizelySDK/OptimizelySDK.csproj +++ b/OptimizelySDK/OptimizelySDK.csproj @@ -1,213 +1,214 @@  - - - Debug - AnyCPU - {4DDE7FAA-110D-441C-AB3B-3F31B593E8BF} - Library - Properties - OptimizelySDK - OptimizelySDK - v4.5 - 512 - - 1.2.1 - ..\keypair.snk - - - true - full - false - bin\Debug\ - TRACE;DEBUG - prompt - 4 - false - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - - - NET35 - NET40 - - - - - - - ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - True - - - ..\packages\NJsonSchema.8.30.6304.31883\lib\net45\NJsonSchema.dll - True - - - - - - - - - - - ..\packages\murmurhash-signed.1.0.2\lib\net45\MurmurHash.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + Debug + AnyCPU + {4DDE7FAA-110D-441C-AB3B-3F31B593E8BF} + Library + Properties + OptimizelySDK + OptimizelySDK + v4.5 + 512 + + 1.2.1 + ..\keypair.snk + + + true + full + false + bin\Debug\ + TRACE;DEBUG + prompt + 4 + false + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + NET35 + NET40 + + + + + + + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + True + + + ..\packages\NJsonSchema.8.30.6304.31883\lib\net45\NJsonSchema.dll + True + + + + + + + + + + + ..\packages\murmurhash-signed.1.0.2\lib\net45\MurmurHash.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index 6ab4a1691..89fc3cac4 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -14,18 +14,29 @@ * limitations under the License. */ -using OptimizelySDK.Logger; -using System.Collections.Generic; +#if !(NET35 || NET40 || NETSTANDARD1_6) +#define USE_ODP +#endif + using OptimizelySDK.ErrorHandler; using OptimizelySDK.Entity; +using OptimizelySDK.Logger; using OptimizelySDK.OptimizelyDecisions; +using System; +using System.Collections.Generic; + +#if USE_ODP +using OptimizelySDK.Odp; +using System.Linq; +using System.Threading.Tasks; +#endif namespace OptimizelySDK { /// /// OptimizelyUserContext defines user contexts that the SDK will use to make decisions for /// - public class OptimizelyUserContext + public class OptimizelyUserContext : IDisposable { private ILogger Logger; private IErrorHandler ErrorHandler; @@ -43,20 +54,33 @@ public class OptimizelyUserContext // Optimizely object to be used. private Optimizely Optimizely; - private ForcedDecisionsStore ForcedDecisionsStore { get; set; } + /// + /// Determine if User Context has already been disposed + /// + public bool Disposed { get; private set; } + private ForcedDecisionsStore ForcedDecisionsStore { get; set; } - public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttributes userAttributes, IErrorHandler errorHandler, ILogger logger) : - this(optimizely, userId, userAttributes, null, null, errorHandler, logger) + public OptimizelyUserContext(Optimizely optimizely, string userId, + UserAttributes userAttributes, IErrorHandler errorHandler, ILogger logger + ) : this(optimizely, userId, userAttributes, null, null, errorHandler, logger) { } - public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttributes userAttributes, ForcedDecisionsStore forcedDecisionsStore, IErrorHandler errorHandler, ILogger logger) : - this(optimizely, userId, userAttributes, forcedDecisionsStore, null, errorHandler, logger) + public OptimizelyUserContext(Optimizely optimizely, string userId, + UserAttributes userAttributes, ForcedDecisionsStore forcedDecisionsStore, + IErrorHandler errorHandler, ILogger logger + ) : this(optimizely, userId, userAttributes, forcedDecisionsStore, null, errorHandler, logger) { } - public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttributes userAttributes, ForcedDecisionsStore forcedDecisionsStore, List qualifiedSegments, IErrorHandler errorHandler, ILogger logger) + public OptimizelyUserContext(Optimizely optimizely, string userId, + UserAttributes userAttributes, ForcedDecisionsStore forcedDecisionsStore, + List qualifiedSegments, IErrorHandler errorHandler, ILogger logger +#if USE_ODP + , bool shouldIdentifyUser = true +#endif + ) { ErrorHandler = errorHandler; Logger = logger; @@ -65,9 +89,22 @@ public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttribute ForcedDecisionsStore = forcedDecisionsStore ?? new ForcedDecisionsStore(); UserId = userId; QualifiedSegments = qualifiedSegments ?? new List(); + +#if USE_ODP + if (shouldIdentifyUser) + { + optimizely.IdentifyUser(UserId); + } +#endif } - private OptimizelyUserContext Copy() => new OptimizelyUserContext(Optimizely, UserId, GetAttributes(), GetForcedDecisionsStore(), GetQualifiedSegments(), ErrorHandler, Logger); + private OptimizelyUserContext Copy() => + new OptimizelyUserContext(Optimizely, UserId, GetAttributes(), + GetForcedDecisionsStore(), GetQualifiedSegments(), ErrorHandler, Logger +#if USE_ODP + , false +#endif + ); /// /// Returns Optimizely instance associated with the UserContext. @@ -93,10 +130,13 @@ public virtual string GetUserId() /// List of qualified segments public List GetQualifiedSegments() { - List qualifiedSegmentsCopy; + List qualifiedSegmentsCopy = null; lock (mutex) { - qualifiedSegmentsCopy = new List(QualifiedSegments); + if (QualifiedSegments != null) + { + qualifiedSegmentsCopy = new List(QualifiedSegments); + } } return qualifiedSegmentsCopy; @@ -110,11 +150,22 @@ public void SetQualifiedSegments(List qualifiedSegments) { lock (mutex) { - QualifiedSegments.Clear(); - QualifiedSegments.AddRange(qualifiedSegments); + if (qualifiedSegments == null) + { + QualifiedSegments = null; + } + else if (QualifiedSegments == null) + { + QualifiedSegments = new List(qualifiedSegments); + } + else + { + QualifiedSegments.Clear(); + QualifiedSegments.AddRange(qualifiedSegments); + } } } - + /// /// Returns true if the user is qualified for the given segment name /// @@ -124,10 +175,46 @@ public bool IsQualifiedFor(string segment) { lock (mutex) { - return QualifiedSegments.Contains(segment); + return QualifiedSegments?.Contains(segment) ?? false; } } +#if USE_ODP + /// + /// Fetch all qualified segments for the user context. + /// + /// Options used during segment cache handling + /// True if ODP segments were fetched successfully otherwise False + public bool FetchQualifiedSegments(List segmentOptions = null) + { + var segments = Optimizely.FetchQualifiedSegments(UserId, segmentOptions); + + var success = segments != null; + + SetQualifiedSegments(segments?.ToList()); + + return success; + } + + /// + /// Asynchronously fetch all qualified segments for the user context. + /// + /// Callback function to invoke when results are available + /// Options used during segment cache handling + /// True if ODP segments were fetched successfully otherwise False + public void FetchQualifiedSegments(Action callback, + List segmentOptions = null + ) + { + Task.Run(() => + { + var success = FetchQualifiedSegments(segmentOptions); + + callback?.Invoke(success); + }); + } +#endif + /// /// Returns copy of UserAttributes associated with UserContext. /// @@ -155,7 +242,8 @@ public ForcedDecisionsStore GetForcedDecisionsStore() if (ForcedDecisionsStore.Count == 0) { copiedForcedDecisionsStore = ForcedDecisionsStore.NullForcedDecision(); - } else + } + else { copiedForcedDecisionsStore = new ForcedDecisionsStore(ForcedDecisionsStore); } @@ -207,7 +295,8 @@ public virtual OptimizelyDecision Decide(string key) /// A list of options for decision-making. /// A decision result. public virtual OptimizelyDecision Decide(string key, - OptimizelyDecideOption[] options) + OptimizelyDecideOption[] options + ) { var optimizelyUserContext = Copy(); return Optimizely.Decide(optimizelyUserContext, key, options); @@ -218,7 +307,9 @@ public virtual OptimizelyDecision Decide(string key, /// /// list of flag keys for which a decision will be made. /// A dictionary of all decision results, mapped by flag keys. - public virtual Dictionary DecideForKeys(string[] keys, OptimizelyDecideOption[] options) + public virtual Dictionary DecideForKeys(string[] keys, + OptimizelyDecideOption[] options + ) { var optimizelyUserContext = Copy(); return Optimizely.DecideForKeys(optimizelyUserContext, keys, options); @@ -248,7 +339,9 @@ public virtual Dictionary DecideAll() /// /// A list of options for decision-making. /// All decision results mapped by flag keys. - public virtual Dictionary DecideAll(OptimizelyDecideOption[] options) + public virtual Dictionary DecideAll( + OptimizelyDecideOption[] options + ) { var optimizelyUserContext = Copy(); return Optimizely.DecideAll(optimizelyUserContext, options); @@ -269,7 +362,8 @@ public virtual void TrackEvent(string eventName) /// The event name. /// A map of event tag names to event tag values. public virtual void TrackEvent(string eventName, - EventTags eventTags) + EventTags eventTags + ) { Optimizely.Track(eventName, UserId, Attributes, eventTags); } @@ -280,7 +374,9 @@ public virtual void TrackEvent(string eventName, /// The context object containing flag and rule key. /// OptimizelyForcedDecision object containing variation key. /// - public bool SetForcedDecision(OptimizelyDecisionContext context, OptimizelyForcedDecision decision) + public bool SetForcedDecision(OptimizelyDecisionContext context, + OptimizelyForcedDecision decision + ) { lock (mutex) { @@ -350,5 +446,17 @@ public bool RemoveAllForcedDecisions() return true; } + + public void Dispose() + { + if (Disposed) + { + return; + } + + Disposed = true; + + Optimizely?.Dispose(); + } } } diff --git a/OptimizelySDK/ProjectConfig.cs b/OptimizelySDK/ProjectConfig.cs index 4a5368b3c..a84c46173 100644 --- a/OptimizelySDK/ProjectConfig.cs +++ b/OptimizelySDK/ProjectConfig.cs @@ -15,7 +15,6 @@ */ using OptimizelySDK.Entity; -using OptimizelySDK.Config; using System.Collections.Generic; namespace OptimizelySDK @@ -71,7 +70,7 @@ public interface ProjectConfig /// Configured host name for the Optimizely Data Platform. ///
string HostForOdp { get; } - + /// /// Configured public key from the Optimizely Data Platform. /// @@ -181,6 +180,11 @@ public interface ProjectConfig ///
Integration[] Integrations { get; } + /// + /// Array of ODP segments / audience names + /// + string[] Segments { get; set; } + //========================= Getters =========================== /// diff --git a/OptimizelySDK/Properties/AssemblyInfo.cs b/OptimizelySDK/Properties/AssemblyInfo.cs index 314ab5f60..34797b629 100644 --- a/OptimizelySDK/Properties/AssemblyInfo.cs +++ b/OptimizelySDK/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("OptimizelySDK")] -[assembly: AssemblyCopyright("Copyright © 2017-2020")] +[assembly: AssemblyCopyright("Copyright © 2017-2023")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -22,7 +22,11 @@ // Make types and members with internal scope visible to friend // OptimizelySDK.Tests unit tests. #pragma warning disable 1700 -[assembly: InternalsVisibleTo("OptimizelySDK.Tests, PublicKey=ThePublicKey")] +#if DEBUG +[assembly: InternalsVisibleTo("OptimizelySDK.Tests")] +#else +[assembly: InternalsVisibleTo("OptimizelySDK.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001006b0705c5f697a2522639be5d5bc02835aaef2e2cd4adf47c3bbf5ed97187298c17448701597b5a610d29eed362f36f056062bbccd424fc830dd5966a9378302c61e3ddd77effcd9dcfaf739f3ca88149e961f55f23d5ce1948703da33e261f6cc0c681a19ce62ccbfdeca8bd286f93395e4f67e4a2ea7782af581062edab8083")] +#endif #pragma warning restore 1700 // The following GUID is for the ID of the typelib if this project is exposed to COM