Skip to content

Commit cdd0f6b

Browse files
[FSSDK-11168] test addition
1 parent 44cc34a commit cdd0f6b

File tree

3 files changed

+387
-2
lines changed

3 files changed

+387
-2
lines changed
Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
/*
2+
* Copyright 2025, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System;
18+
using System.Collections.Generic;
19+
using Moq;
20+
using NUnit.Framework;
21+
using OptimizelySDK.Cmab;
22+
using OptimizelySDK.Entity;
23+
using OptimizelySDK.ErrorHandler;
24+
using OptimizelySDK.Logger;
25+
using OptimizelySDK.Odp;
26+
using OptimizelySDK.OptimizelyDecisions;
27+
using AttributeEntity = OptimizelySDK.Entity.Attribute;
28+
29+
namespace OptimizelySDK.Tests.CmabTests
30+
{
31+
[TestFixture]
32+
public class DefaultCmabServiceTest
33+
{
34+
private Mock<ICmabClient> _mockCmabClient;
35+
private LruCache<CmabCacheEntry> _cmabCache;
36+
private DefaultCmabService _cmabService;
37+
private ILogger _logger;
38+
39+
[SetUp]
40+
public void SetUp()
41+
{
42+
_mockCmabClient = new Mock<ICmabClient>(MockBehavior.Strict);
43+
_logger = new NoOpLogger();
44+
_cmabCache = new LruCache<CmabCacheEntry>(maxSize: 10, itemTimeout: TimeSpan.FromMinutes(5), logger: _logger);
45+
_cmabService = new DefaultCmabService(_cmabCache, _mockCmabClient.Object, _logger);
46+
}
47+
48+
[Test]
49+
public void ReturnsDecisionFromCacheWhenHashMatches()
50+
{
51+
var ruleId = "exp1";
52+
var userId = "user123";
53+
var experiment = CreateExperiment(ruleId, new List<string> { "66" });
54+
var attributeMap = new Dictionary<string, AttributeEntity>
55+
{
56+
{ "66", new AttributeEntity { Id = "66", Key = "age" } }
57+
};
58+
var projectConfig = CreateProjectConfig(ruleId, experiment, attributeMap);
59+
var userContext = CreateUserContext(userId, new Dictionary<string, object> { { "age", 25 } });
60+
var filteredAttributes = new UserAttributes(new Dictionary<string, object> { { "age", 25 } });
61+
var cacheKey = DefaultCmabService.GetCacheKey(userId, ruleId);
62+
63+
_cmabCache.Save(cacheKey, new CmabCacheEntry
64+
{
65+
AttributesHash = DefaultCmabService.HashAttributes(filteredAttributes),
66+
CmabUuid = "uuid-cached",
67+
VariationId = "varA"
68+
});
69+
70+
var decision = _cmabService.GetDecision(projectConfig, userContext, ruleId, null);
71+
72+
Assert.AreEqual("varA", decision.VariationId);
73+
Assert.AreEqual("uuid-cached", decision.CmabUuid);
74+
_mockCmabClient.Verify(c => c.FetchDecision(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDictionary<string, object>>(), It.IsAny<string>(), It.IsAny<TimeSpan?>()), Times.Never);
75+
}
76+
77+
[Test]
78+
public void IgnoresCacheWhenOptionSpecified()
79+
{
80+
var ruleId = "exp1";
81+
var userId = "user123";
82+
var experiment = CreateExperiment(ruleId, new List<string> { "66" });
83+
var attributeMap = new Dictionary<string, AttributeEntity>
84+
{
85+
{ "66", new AttributeEntity { Id = "66", Key = "age" } }
86+
};
87+
var projectConfig = CreateProjectConfig(ruleId, experiment, attributeMap);
88+
var userContext = CreateUserContext(userId, new Dictionary<string, object> { { "age", 25 } });
89+
var cacheKey = DefaultCmabService.GetCacheKey(userId, ruleId);
90+
91+
_mockCmabClient.Setup(c => c.FetchDecision(ruleId, userId,
92+
It.Is<IDictionary<string, object>>(attrs => attrs != null && attrs.Count == 1 && attrs.ContainsKey("age") && (int)attrs["age"] == 25),
93+
It.IsAny<string>(),
94+
It.IsAny<TimeSpan?>())).Returns("varB");
95+
96+
var decision = _cmabService.GetDecision(projectConfig, userContext, ruleId,
97+
new[] { OptimizelyDecideOption.IGNORE_CMAB_CACHE });
98+
99+
Assert.AreEqual("varB", decision.VariationId);
100+
Assert.IsNull(_cmabCache.Lookup(cacheKey));
101+
_mockCmabClient.VerifyAll();
102+
}
103+
104+
[Test]
105+
public void ResetsCacheWhenOptionSpecified()
106+
{
107+
var ruleId = "exp1";
108+
var userId = "user123";
109+
var experiment = CreateExperiment(ruleId, new List<string> { "66" });
110+
var attributeMap = new Dictionary<string, AttributeEntity>
111+
{
112+
{ "66", new AttributeEntity { Id = "66", Key = "age" } }
113+
};
114+
var projectConfig = CreateProjectConfig(ruleId, experiment, attributeMap);
115+
var userContext = CreateUserContext(userId, new Dictionary<string, object> { { "age", 25 } });
116+
var cacheKey = DefaultCmabService.GetCacheKey(userId, ruleId);
117+
118+
_cmabCache.Save(cacheKey, new CmabCacheEntry
119+
{
120+
AttributesHash = "stale",
121+
CmabUuid = "uuid-old",
122+
VariationId = "varOld"
123+
});
124+
125+
_mockCmabClient.Setup(c => c.FetchDecision(ruleId, userId,
126+
It.Is<IDictionary<string, object>>(attrs => attrs.Count == 1 && (int)attrs["age"] == 25),
127+
It.IsAny<string>(),
128+
It.IsAny<TimeSpan?>())).Returns("varNew");
129+
130+
var decision = _cmabService.GetDecision(projectConfig, userContext, ruleId,
131+
new[] { OptimizelyDecideOption.RESET_CMAB_CACHE });
132+
133+
Assert.AreEqual("varNew", decision.VariationId);
134+
var cachedEntry = _cmabCache.Lookup(cacheKey);
135+
Assert.IsNotNull(cachedEntry);
136+
Assert.AreEqual("varNew", cachedEntry.VariationId);
137+
Assert.AreEqual(decision.CmabUuid, cachedEntry.CmabUuid);
138+
_mockCmabClient.VerifyAll();
139+
}
140+
141+
[Test]
142+
public void InvalidatesUserEntryWhenOptionSpecified()
143+
{
144+
var ruleId = "exp1";
145+
var userId = "user123";
146+
var otherUserId = "other";
147+
var experiment = CreateExperiment(ruleId, new List<string> { "66" });
148+
var attributeMap = new Dictionary<string, AttributeEntity>
149+
{
150+
{ "66", new AttributeEntity { Id = "66", Key = "age" } }
151+
};
152+
var projectConfig = CreateProjectConfig(ruleId, experiment, attributeMap);
153+
var userContext = CreateUserContext(userId, new Dictionary<string, object> { { "age", 25 } });
154+
155+
var targetKey = DefaultCmabService.GetCacheKey(userId, ruleId);
156+
var otherKey = DefaultCmabService.GetCacheKey(otherUserId, ruleId);
157+
158+
_cmabCache.Save(targetKey, new CmabCacheEntry
159+
{
160+
AttributesHash = "old_hash",
161+
CmabUuid = "uuid-old",
162+
VariationId = "varOld"
163+
});
164+
_cmabCache.Save(otherKey, new CmabCacheEntry
165+
{
166+
AttributesHash = "other_hash",
167+
CmabUuid = "uuid-other",
168+
VariationId = "varOther"
169+
});
170+
171+
_mockCmabClient.Setup(c => c.FetchDecision(ruleId, userId,
172+
It.Is<IDictionary<string, object>>(attrs => attrs.Count == 1 && (int)attrs["age"] == 25),
173+
It.IsAny<string>(),
174+
It.IsAny<TimeSpan?>())).Returns("varNew");
175+
176+
var decision = _cmabService.GetDecision(projectConfig, userContext, ruleId,
177+
new[] { OptimizelyDecideOption.INVALIDATE_USER_CMAB_CACHE });
178+
179+
Assert.AreEqual("varNew", decision.VariationId);
180+
var updatedEntry = _cmabCache.Lookup(targetKey);
181+
Assert.IsNotNull(updatedEntry);
182+
Assert.AreEqual(decision.CmabUuid, updatedEntry.CmabUuid);
183+
Assert.AreEqual("varNew", updatedEntry.VariationId);
184+
185+
var otherEntry = _cmabCache.Lookup(otherKey);
186+
Assert.IsNotNull(otherEntry);
187+
Assert.AreEqual("varOther", otherEntry.VariationId);
188+
_mockCmabClient.VerifyAll();
189+
}
190+
191+
[Test]
192+
public void FetchesNewDecisionWhenHashDiffers()
193+
{
194+
var ruleId = "exp1";
195+
var userId = "user123";
196+
var experiment = CreateExperiment(ruleId, new List<string> { "66" });
197+
var attributeMap = new Dictionary<string, AttributeEntity>
198+
{
199+
{ "66", new AttributeEntity { Id = "66", Key = "age" } }
200+
};
201+
var projectConfig = CreateProjectConfig(ruleId, experiment, attributeMap);
202+
var userContext = CreateUserContext(userId, new Dictionary<string, object> { { "age", 25 } });
203+
204+
var cacheKey = DefaultCmabService.GetCacheKey(userId, ruleId);
205+
_cmabCache.Save(cacheKey, new CmabCacheEntry
206+
{
207+
AttributesHash = "different_hash",
208+
CmabUuid = "uuid-old",
209+
VariationId = "varOld"
210+
});
211+
212+
_mockCmabClient.Setup(c => c.FetchDecision(ruleId, userId,
213+
It.Is<IDictionary<string, object>>(attrs => attrs.Count == 1 && (int)attrs["age"] == 25),
214+
It.IsAny<string>(),
215+
It.IsAny<TimeSpan?>())).Returns("varUpdated");
216+
217+
var decision = _cmabService.GetDecision(projectConfig, userContext, ruleId, null);
218+
219+
Assert.AreEqual("varUpdated", decision.VariationId);
220+
var cachedEntry = _cmabCache.Lookup(cacheKey);
221+
Assert.IsNotNull(cachedEntry);
222+
Assert.AreEqual("varUpdated", cachedEntry.VariationId);
223+
Assert.AreEqual(decision.CmabUuid, cachedEntry.CmabUuid);
224+
_mockCmabClient.VerifyAll();
225+
}
226+
227+
[Test]
228+
public void FiltersAttributesBeforeCallingClient()
229+
{
230+
var ruleId = "exp1";
231+
var userId = "user123";
232+
var experiment = CreateExperiment(ruleId, new List<string> { "66", "77" });
233+
var attributeMap = new Dictionary<string, AttributeEntity>
234+
{
235+
{ "66", new AttributeEntity { Id = "66", Key = "age" } },
236+
{ "77", new AttributeEntity { Id = "77", Key = "location" } }
237+
};
238+
var projectConfig = CreateProjectConfig(ruleId, experiment, attributeMap);
239+
var userContext = CreateUserContext(userId, new Dictionary<string, object>
240+
{
241+
{ "age", 25 },
242+
{ "location", "USA" },
243+
{ "extra", "value" }
244+
});
245+
246+
_mockCmabClient.Setup(c => c.FetchDecision(ruleId, userId,
247+
It.Is<IDictionary<string, object>>(attrs => attrs.Count == 2 &&
248+
(int)attrs["age"] == 25 &&
249+
(string)attrs["location"] == "USA" &&
250+
!attrs.ContainsKey("extra")),
251+
It.IsAny<string>(),
252+
It.IsAny<TimeSpan?>())).Returns("varFiltered");
253+
254+
var decision = _cmabService.GetDecision(projectConfig, userContext, ruleId, null);
255+
256+
Assert.AreEqual("varFiltered", decision.VariationId);
257+
_mockCmabClient.VerifyAll();
258+
}
259+
260+
[Test]
261+
public void HandlesMissingCmabConfiguration()
262+
{
263+
var ruleId = "exp1";
264+
var userId = "user123";
265+
var experiment = CreateExperiment(ruleId, null);
266+
var attributeMap = new Dictionary<string, AttributeEntity>();
267+
var projectConfig = CreateProjectConfig(ruleId, experiment, attributeMap);
268+
var userContext = CreateUserContext(userId, new Dictionary<string, object> { { "age", 25 } });
269+
270+
_mockCmabClient.Setup(c => c.FetchDecision(ruleId, userId,
271+
It.Is<IDictionary<string, object>>(attrs => attrs.Count == 0),
272+
It.IsAny<string>(),
273+
It.IsAny<TimeSpan?>())).Returns("varDefault");
274+
275+
var decision = _cmabService.GetDecision(projectConfig, userContext, ruleId, null);
276+
277+
Assert.AreEqual("varDefault", decision.VariationId);
278+
_mockCmabClient.VerifyAll();
279+
}
280+
281+
[Test]
282+
public void AttributeHashIsStableRegardlessOfOrder()
283+
{
284+
var ruleId = "exp1";
285+
var userId = "user123";
286+
var experiment = CreateExperiment(ruleId, new List<string> { "66", "77" });
287+
var attributeMap = new Dictionary<string, AttributeEntity>
288+
{
289+
{ "66", new AttributeEntity { Id = "66", Key = "a" } },
290+
{ "77", new AttributeEntity { Id = "77", Key = "b" } }
291+
};
292+
var projectConfig = CreateProjectConfig(ruleId, experiment, attributeMap);
293+
294+
var firstContext = CreateUserContext(userId, new Dictionary<string, object>
295+
{
296+
{ "b", 2 },
297+
{ "a", 1 }
298+
});
299+
300+
_mockCmabClient.Setup(c => c.FetchDecision(ruleId, userId,
301+
It.IsAny<IDictionary<string, object>>(),
302+
It.IsAny<string>(),
303+
It.IsAny<TimeSpan?>())).Returns("varStable");
304+
305+
var firstDecision = _cmabService.GetDecision(projectConfig, firstContext, ruleId, null);
306+
Assert.AreEqual("varStable", firstDecision.VariationId);
307+
308+
var secondContext = CreateUserContext(userId, new Dictionary<string, object>
309+
{
310+
{ "a", 1 },
311+
{ "b", 2 }
312+
});
313+
314+
var secondDecision = _cmabService.GetDecision(projectConfig, secondContext, ruleId, null);
315+
316+
Assert.AreEqual("varStable", secondDecision.VariationId);
317+
_mockCmabClient.Verify(c => c.FetchDecision(ruleId, userId,
318+
It.IsAny<IDictionary<string, object>>(), It.IsAny<string>(), It.IsAny<TimeSpan?>()), Times.Once);
319+
}
320+
321+
[Test]
322+
public void UsesExpectedCacheKeyFormat()
323+
{
324+
var ruleId = "exp1";
325+
var userId = "user123";
326+
var experiment = CreateExperiment(ruleId, new List<string> { "66" });
327+
var attributeMap = new Dictionary<string, AttributeEntity>
328+
{
329+
{ "66", new AttributeEntity { Id = "66", Key = "age" } }
330+
};
331+
var projectConfig = CreateProjectConfig(ruleId, experiment, attributeMap);
332+
var userContext = CreateUserContext(userId, new Dictionary<string, object> { { "age", 25 } });
333+
334+
_mockCmabClient.Setup(c => c.FetchDecision(ruleId, userId,
335+
It.IsAny<IDictionary<string, object>>(),
336+
It.IsAny<string>(),
337+
It.IsAny<TimeSpan?>())).Returns("varKey");
338+
339+
var decision = _cmabService.GetDecision(projectConfig, userContext, ruleId, null);
340+
Assert.AreEqual("varKey", decision.VariationId);
341+
342+
var cacheKey = DefaultCmabService.GetCacheKey(userId, ruleId);
343+
var cachedEntry = _cmabCache.Lookup(cacheKey);
344+
Assert.IsNotNull(cachedEntry);
345+
Assert.AreEqual(decision.CmabUuid, cachedEntry.CmabUuid);
346+
}
347+
348+
private static OptimizelyUserContext CreateUserContext(string userId, IDictionary<string, object> attributes)
349+
{
350+
var userAttributes = new UserAttributes(attributes);
351+
return new OptimizelyUserContext(null, userId, userAttributes, null, null,
352+
new NoOpErrorHandler(), new NoOpLogger()
353+
#if !(NET35 || NET40 || NETSTANDARD1_6)
354+
, false
355+
#endif
356+
);
357+
}
358+
359+
private static ProjectConfig CreateProjectConfig(string ruleId, Experiment experiment,
360+
Dictionary<string, AttributeEntity> attributeMap)
361+
{
362+
var mockConfig = new Mock<ProjectConfig>();
363+
var experimentMap = new Dictionary<string, Experiment>();
364+
if (experiment != null)
365+
{
366+
experimentMap[ruleId] = experiment;
367+
}
368+
369+
mockConfig.SetupGet(c => c.ExperimentIdMap).Returns(experimentMap);
370+
mockConfig.SetupGet(c => c.AttributeIdMap).Returns(attributeMap ?? new Dictionary<string, AttributeEntity>());
371+
return mockConfig.Object;
372+
}
373+
374+
private static Experiment CreateExperiment(string ruleId, List<string> attributeIds)
375+
{
376+
return new Experiment
377+
{
378+
Id = ruleId,
379+
Cmab = attributeIds == null ? null : new Entity.Cmab(attributeIds)
380+
};
381+
}
382+
383+
}
384+
}

OptimizelySDK.Tests/OptimizelySDK.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
<ItemGroup>
7272
<Compile Include="Assertions.cs"/>
7373
<Compile Include="CmabTests\DefaultCmabClientTest.cs"/>
74+
<Compile Include="CmabTests\DefaultCmabServiceTest.cs"/>
7475
<Compile Include="AudienceConditionsTests\ConditionEvaluationTest.cs"/>
7576
<Compile Include="AudienceConditionsTests\ConditionsTest.cs"/>
7677
<Compile Include="AudienceConditionsTests\SegmentsTests.cs"/>

0 commit comments

Comments
 (0)