Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
887fbdf
[FSSDK-11177] initial changes
junaed-optimizely Oct 13, 2025
35a475c
[FSSDK-11177] improvements
junaed-optimizely Oct 15, 2025
8d4a7ff
[FSSDK-11177] bucketer cmab test
junaed-optimizely Oct 15, 2025
a327881
[FSSDK-11177] decision service cmab test
junaed-optimizely Oct 15, 2025
d579956
[FSSDK-11177] test addition
junaed-optimizely Oct 16, 2025
3733e9d
[FSSDK-11177] test fix
junaed-optimizely Oct 16, 2025
9dc900f
[FSSDK-11981] fixes
junaed-optimizely Oct 21, 2025
8ad779a
[FSSDK-11981] plan removal
junaed-optimizely Oct 21, 2025
2904f1a
[FSSDK-11177] test adjustment
junaed-optimizely Oct 22, 2025
ae7b3dd
[FSSDK-11177] format change
junaed-optimizely Oct 22, 2025
40843ac
[FSSDK-11177] public api exposed for cmab cache
junaed-optimizely Oct 23, 2025
a568a45
[FSSDK-11177] format fix
junaed-optimizely Oct 23, 2025
c0ba24d
[FSSDK-11177] format fix
junaed-optimizely Oct 23, 2025
90f228c
[FSSDK-11177] format fix 3
junaed-optimizely Oct 23, 2025
13791c0
[FSSDK-11177] format fix 4
junaed-optimizely Oct 23, 2025
a217982
[FSSDK-11177] bucketer common logic extraction
junaed-optimizely Oct 27, 2025
2c9fad8
[FSSDK-11177] icache instead of lru
junaed-optimizely Oct 27, 2025
ab8eefc
[FSSDK-11177] cmab service constructor adjustment
junaed-optimizely Oct 28, 2025
f1a33cc
[FSSDK-11177] format fix
junaed-optimizely Oct 28, 2025
70a880d
[FSSDK-11177] lock implementation
junaed-optimizely Oct 29, 2025
252ba9e
[FSSDK-11177] Cache with remove interface addition
junaed-optimizely Oct 31, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@
<Compile Include="..\OptimizelySDK\Bucketing\UserProfileUtil.cs">
<Link>Bucketing\UserProfileUtil.cs</Link>
</Compile>
<Compile Include="..\OptimizelySDK\Bucketing\VariationDecisionResult.cs">
<Link>Bucketing\VariationDecisionResult.cs</Link>
</Compile>
<Compile Include="..\OptimizelySDK\Config\DatafileProjectConfig.cs">
<Link>Config\DatafileProjectConfig.cs</Link>
</Compile>
Expand Down Expand Up @@ -196,6 +199,9 @@
<Compile Include="..\OptimizelySDK\Cmab\CmabRetryConfig.cs">
<Link>Cmab\CmabRetryConfig.cs</Link>
</Compile>
<Compile Include="..\OptimizelySDK\Cmab\CmabConfig.cs">
<Link>Cmab\CmabConfig.cs</Link>
</Compile>
<Compile Include="..\OptimizelySDK\Cmab\CmabModels.cs">
<Link>Cmab\CmabModels.cs</Link>
</Compile>
Expand Down Expand Up @@ -369,6 +375,9 @@
<Compile Include="..\OptimizelySDK\Utils\Validator.cs">
<Link>Utils\Validator.cs</Link>
</Compile>
<Compile Include="..\OptimizelySDK\Utils\ICacheWithRemove.cs">
<Link>Utils\ICacheWithRemove.cs</Link>
</Compile>
<Compile Include="..\OptimizelySDK\Event\BatchEventProcessor.cs">
<Link>Event\BatchEventProcessor.cs</Link>
</Compile>
Expand Down
286 changes: 286 additions & 0 deletions OptimizelySDK.Tests/BucketerBucketToEntityIdTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
/*
* Copyright 2025, 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
*
* 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 System.Collections.Generic;
using Moq;
using Newtonsoft.Json;
using NUnit.Framework;
using OptimizelySDK.Bucketing;
using OptimizelySDK.Config;
using OptimizelySDK.Entity;
using OptimizelySDK.ErrorHandler;
using OptimizelySDK.Logger;

namespace OptimizelySDK.Tests
{
[TestFixture]
public class BucketerBucketToEntityIdTest
{
[SetUp]
public void SetUp()
{
_loggerMock = new Mock<ILogger>();
}

private const string ExperimentId = "bucket_entity_exp";
private const string ExperimentKey = "bucket_entity_experiment";
private const string GroupId = "group_1";

private Mock<ILogger> _loggerMock;

[Test]
public void BucketToEntityIdAllowsBucketingWhenNoGroup()
{
var config = CreateConfig(new ConfigSetup { IncludeGroup = false });
var experiment = config.GetExperimentFromKey(ExperimentKey);
var bucketer = new Bucketer(_loggerMock.Object);

var fullAllocation = CreateTrafficAllocations(new TrafficAllocation
{
EntityId = "entity_123",
EndOfRange = 10000,
});
var fullResult = bucketer.BucketToEntityId(config, experiment, "bucketing_id", "user",
fullAllocation);
Assert.IsNotNull(fullResult.ResultObject);
Assert.AreEqual("entity_123", fullResult.ResultObject);

var zeroAllocation = CreateTrafficAllocations(new TrafficAllocation
{
EntityId = "entity_123",
EndOfRange = 0,
});
var zeroResult = bucketer.BucketToEntityId(config, experiment, "bucketing_id", "user",
zeroAllocation);
Assert.IsNull(zeroResult.ResultObject);
}

[Test]
public void BucketToEntityIdReturnsEntityIdWhenGroupAllowsUser()
{
var config = CreateConfig(new ConfigSetup
{
IncludeGroup = true,
GroupPolicy = "random",
GroupEndOfRange = 10000,
});

var experiment = config.GetExperimentFromKey(ExperimentKey);
var bucketer = new Bucketer(_loggerMock.Object);

var testCases = new[]
{
new { BucketingId = "ppid1", EntityId = "entity1" },
new { BucketingId = "ppid2", EntityId = "entity2" },
new { BucketingId = "ppid3", EntityId = "entity3" },
new
{
BucketingId =
"a very very very very very very very very very very very very very very very long ppd string",
EntityId = "entity4",
},
};

foreach (var testCase in testCases)
{
var allocation = CreateTrafficAllocations(new TrafficAllocation
{
EntityId = testCase.EntityId,
EndOfRange = 10000,
});
var result = bucketer.BucketToEntityId(config, experiment, testCase.BucketingId,
testCase.BucketingId, allocation);
Assert.AreEqual(testCase.EntityId, result.ResultObject,
$"Failed for {testCase.BucketingId}");
}
}

[Test]
public void BucketToEntityIdReturnsNullWhenGroupRejectsUser()
{
var config = CreateConfig(new ConfigSetup
{
IncludeGroup = true,
GroupPolicy = "random",
GroupEndOfRange = 0,
});

var experiment = config.GetExperimentFromKey(ExperimentKey);
var bucketer = new Bucketer(_loggerMock.Object);

var allocation = CreateTrafficAllocations(new TrafficAllocation
{
EntityId = "entity1",
EndOfRange = 10000,
});
var testCases = new[]
{
"ppid1",
"ppid2",
"ppid3",
"a very very very very very very very very very very very very very very very long ppd string",
};

foreach (var bucketingId in testCases)
{
var result = bucketer.BucketToEntityId(config, experiment, bucketingId, bucketingId,
allocation);
Assert.IsNull(result.ResultObject, $"Expected null for {bucketingId}");
}
}

[Test]
public void BucketToEntityIdAllowsBucketingWhenGroupOverlapping()
{
var config = CreateConfig(new ConfigSetup
{
IncludeGroup = true,
GroupPolicy = "overlapping",
GroupEndOfRange = 10000,
});

var experiment = config.GetExperimentFromKey(ExperimentKey);
var bucketer = new Bucketer(_loggerMock.Object);

var allocation = CreateTrafficAllocations(new TrafficAllocation
{
EntityId = "entity_overlapping",
EndOfRange = 10000,
});
var result =
bucketer.BucketToEntityId(config, experiment, "bucketing_id", "user", allocation);
Assert.AreEqual("entity_overlapping", result.ResultObject);
}

private static IList<TrafficAllocation> CreateTrafficAllocations(
params TrafficAllocation[] allocations
)
{
return new List<TrafficAllocation>(allocations);
}

private ProjectConfig CreateConfig(ConfigSetup setup)
{
if (setup == null)
{
setup = new ConfigSetup();
}

var datafile = BuildDatafile(setup);
return DatafileProjectConfig.Create(datafile, _loggerMock.Object,
new NoOpErrorHandler());
}

private static string BuildDatafile(ConfigSetup setup)
{
var variations = new object[]
{
new Dictionary<string, object>
{
{ "id", "var_1" },
{ "key", "variation_1" },
{ "variables", new object[0] },
},
};

var experiment = new Dictionary<string, object>
{
{ "status", "Running" },
{ "key", ExperimentKey },
{ "layerId", "layer_1" },
{ "id", ExperimentId },
{ "audienceIds", new string[0] },
{ "audienceConditions", "[]" },
{ "forcedVariations", new Dictionary<string, string>() },
{ "variations", variations },
{
"trafficAllocation", new object[]
{
new Dictionary<string, object>
{
{ "entityId", "var_1" },
{ "endOfRange", 10000 },
},
}
},
};

object[] groups;
if (setup.IncludeGroup)
{
var groupExperiment = new Dictionary<string, object>(experiment);
groupExperiment["trafficAllocation"] = new object[0];

groups = new object[]
{
new Dictionary<string, object>
{
{ "id", GroupId },
{ "policy", setup.GroupPolicy },
{
"trafficAllocation", new object[]
{
new Dictionary<string, object>
{
{ "entityId", ExperimentId },
{ "endOfRange", setup.GroupEndOfRange },
},
}
},
{ "experiments", new object[] { groupExperiment } },
},
};
}
else
{
groups = new object[0];
}

var datafile = new Dictionary<string, object>
{
{ "version", "4" },
{ "projectId", "project_1" },
{ "accountId", "account_1" },
{ "revision", "1" },
{ "environmentKey", string.Empty },
{ "sdkKey", string.Empty },
{ "sendFlagDecisions", false },
{ "anonymizeIP", false },
{ "botFiltering", false },
{ "attributes", new object[0] },
{ "audiences", new object[0] },
{ "typedAudiences", new object[0] },
{ "events", new object[0] },
{ "featureFlags", new object[0] },
{ "rollouts", new object[0] },
{ "integrations", new object[0] },
{ "holdouts", new object[0] },
{ "groups", groups },
{ "experiments", new object[] { experiment } },
{ "segments", new object[0] },
};

return JsonConvert.SerializeObject(datafile);
}

private class ConfigSetup
{
public bool IncludeGroup { get; set; }
public string GroupPolicy { get; set; }
public int GroupEndOfRange { get; set; }
}
}
}
Loading