Skip to content

Commit

Permalink
GetAllValueDetails feature (#55)
Browse files Browse the repository at this point in the history
* Adds and implements GetAllValueDetails/GetAllValueDetailsAsync methods (+ revises RolloutEvaluatorExtensions)

* Deprecates GetVariationId/GetVariationIdAsync and GetAllVariationId/GetAllVariationIdAsync

* Adds tests
  • Loading branch information
adams85 authored Dec 16, 2022
1 parent ed8d45d commit 755fdc1
Show file tree
Hide file tree
Showing 8 changed files with 471 additions and 99 deletions.
83 changes: 83 additions & 0 deletions src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,89 @@ static void Configure(ConfigCatClientOptions options)
}
}

[DataRow(ClientCreationStrategy.Singleton)]
[DataRow(ClientCreationStrategy.Constructor)]
[DataRow(ClientCreationStrategy.Builder)]
[DataTestMethod]
public void GetAllValueDetails(ClientCreationStrategy creationStrategy)
{
static void Configure(ConfigCatClientOptions options)
{
options.PollingMode = PollingModes.ManualPoll;
options.Logger = consoleLogger;
options.HttpClientHandler = sharedHandler;
}

using IConfigCatClient client = creationStrategy switch
{
ClientCreationStrategy.Singleton => ConfigCatClient.Get(SDKKEY, Configure),
ClientCreationStrategy.Constructor => new ConfigCatClient(options => { Configure(options); options.SdkKey = SDKKEY; }),
ClientCreationStrategy.Builder => ConfigCatClientBuilder
.Initialize(SDKKEY)
.WithLogger(consoleLogger)
.WithManualPoll()
.WithHttpClientHandler(sharedHandler)
.Create(),
_ => throw new ArgumentOutOfRangeException(nameof(creationStrategy))
};

client.ForceRefresh();

var flagEvaluatedEvents = new List<FlagEvaluatedEventArgs>();
client.FlagEvaluated += (s, e) => flagEvaluatedEvents.Add(e);

var detailsList = client.GetAllValueDetails();

Assert.AreEqual(16, detailsList.Count);
var details = detailsList.FirstOrDefault(details => details.Key == "stringDefaultCat");
Assert.IsNotNull(details);
Assert.AreEqual("Cat", details.Value);
Assert.IsFalse(details.IsDefaultValue);

CollectionAssert.AreEqual(detailsList.ToArray(), flagEvaluatedEvents.Select(e => e.EvaluationDetails).ToArray());
}

[DataRow(ClientCreationStrategy.Singleton)]
[DataRow(ClientCreationStrategy.Constructor)]
[DataRow(ClientCreationStrategy.Builder)]
[DataTestMethod]
public async Task GetAllValueDetailsAsync(ClientCreationStrategy creationStrategy)
{
static void Configure(ConfigCatClientOptions options)
{
options.PollingMode = PollingModes.ManualPoll;
options.Logger = consoleLogger;
options.HttpClientHandler = sharedHandler;
}

using IConfigCatClient client = creationStrategy switch
{
ClientCreationStrategy.Singleton => ConfigCatClient.Get(SDKKEY, Configure),
ClientCreationStrategy.Constructor => new ConfigCatClient(options => { Configure(options); options.SdkKey = SDKKEY; }),
ClientCreationStrategy.Builder => ConfigCatClientBuilder
.Initialize(SDKKEY)
.WithLogger(consoleLogger)
.WithManualPoll()
.WithHttpClientHandler(sharedHandler)
.Create(),
_ => throw new ArgumentOutOfRangeException(nameof(creationStrategy))
};

var flagEvaluatedEvents = new List<FlagEvaluatedEventArgs>();
client.FlagEvaluated += (s, e) => flagEvaluatedEvents.Add(e);

await client.ForceRefreshAsync();
var detailsList = await client.GetAllValueDetailsAsync();

Assert.AreEqual(16, detailsList.Count);
var details = detailsList.FirstOrDefault(details => details.Key == "stringDefaultCat");
Assert.IsNotNull(details);
Assert.AreEqual("Cat", details.Value);
Assert.IsFalse(details.IsDefaultValue);

CollectionAssert.AreEqual(detailsList.ToArray(), flagEvaluatedEvents.Select(e => e.EvaluationDetails).ToArray());
}

private static void GetValueAndAssert(IConfigCatClient client, string key, string defaultValue, string expectedValue)
{
var flagEvaluatedEvents = new List<FlagEvaluatedEventArgs>();
Expand Down
18 changes: 7 additions & 11 deletions src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class BasicConfigEvaluatorTests : ConfigEvaluatorTestsBase
[TestMethod]
public void GetValue_WithSimpleKey_ShouldReturnCat()
{
string actual = configEvaluator.Evaluate(config, "stringDefaultCat", string.Empty, user: null, null, this.logger, out _);
string actual = configEvaluator.Evaluate(config, "stringDefaultCat", string.Empty, user: null, null, this.logger).Value;

Assert.AreNotEqual(string.Empty, actual);
Assert.AreEqual("Cat", actual);
Expand All @@ -28,15 +28,15 @@ public void GetValue_WithSimpleKey_ShouldReturnCat()
[TestMethod]
public void GetValue_WithNonExistingKey_ShouldReturnDefaultValue()
{
string actual = configEvaluator.Evaluate(config, "NotExistsKey", "NotExistsValue", user: null, null, this.logger, out _);
string actual = configEvaluator.Evaluate(config, "NotExistsKey", "NotExistsValue", user: null, null, this.logger).Value;

Assert.AreEqual("NotExistsValue", actual);
}

[TestMethod]
public void GetValue_WithEmptyProjectConfig_ShouldReturnDefaultValue()
{
string actual = configEvaluator.Evaluate(new Dictionary<string, Setting>(), "stringDefaultCat", "Default", user: null, null, this.logger, out _);
string actual = configEvaluator.Evaluate(new Dictionary<string, Setting>(), "stringDefaultCat", "Default", user: null, null, this.logger).Value;

Assert.AreEqual("Default", actual);
}
Expand All @@ -49,13 +49,13 @@ public void GetValue_WithUser_ShouldReturnEvaluatedValue()
Email = "[email protected]",
Country = "United Kingdom",
Custom = new Dictionary<string, string> { { "Custom1", "admin" } }
}, null, this.logger, out _);
}, null, this.logger).Value;

Assert.AreEqual(3.1415, actual);
}

private delegate object EvaluateDelegate(IRolloutEvaluator evaluator, IDictionary<string, Setting> settings, string key, object defaultValue, User user,
ProjectConfig remoteConfig, ILogger logger, out EvaluationDetails<object> evaluationDetails);
private delegate EvaluationDetails<object> EvaluateDelegate(IRolloutEvaluator evaluator, IDictionary<string, Setting> settings, string key, object defaultValue, User user,
ProjectConfig remoteConfig, ILogger logger);

private static readonly MethodInfo evaluateMethodDefinition = new EvaluateDelegate(RolloutEvaluatorExtensions.Evaluate).Method.GetGenericMethodDefinition();

Expand Down Expand Up @@ -84,13 +84,10 @@ public void GetValue_WithCompatibleDefaultValue_ShouldSucceed(string key, object
null,
null,
this.logger,
null
};

var actualValue = evaluateMethodDefinition.MakeGenericMethod(settingClrType).Invoke(null, args);
var evaluationDetails = (EvaluationDetails)args.Last();
var evaluationDetails = (EvaluationDetails)evaluateMethodDefinition.MakeGenericMethod(settingClrType).Invoke(null, args);

Assert.AreEqual(expectedValue, actualValue);
Assert.AreEqual(expectedValue, evaluationDetails.Value);
}

Expand All @@ -110,7 +107,6 @@ public void GetValue_WithIncompatibleDefaultValueType_ShouldThrowWithImprovedErr
null,
null,
this.logger,
null
};

var ex = Assert.ThrowsException<InvalidOperationException>(() =>
Expand Down
218 changes: 218 additions & 0 deletions src/ConfigCat.Client.Tests/ConfigCatClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,224 @@ public async Task GetValueDetails_EvaluateServiceThrowException_ShouldReturnDefa
Assert.AreSame(actual, flagEvaluatedEvents[0].EvaluationDetails);
}

[DataRow(false)]
[DataRow(true)]
[DataTestMethod]
public async Task GetAllValueDetails_ShouldReturnCorrectEvaluationDetails(bool isAsync)
{
// Arrange

const string cacheKey = "123";
var configJsonFilePath = Path.Combine("data", "sample_variationid_v5.json");
var timeStamp = DateTime.UtcNow;

var client = CreateClientWithMockedFetcher(cacheKey, loggerMock, fetcherMock,
onFetch: _ => FetchResult.Success(new ProjectConfig { JsonString = File.ReadAllText(configJsonFilePath), HttpETag = "12345", TimeStamp = timeStamp }),
configServiceFactory: (fetcher, cacheParams, loggerWrapper) =>
{
return new ManualPollConfigService(fetcherMock.Object, cacheParams, loggerWrapper);
},
evaluatorFactory: loggerWrapper => new RolloutEvaluator(loggerWrapper), new Hooks(),
out var configService, out _);

if (isAsync)
{
await client.ForceRefreshAsync();
}
else
{
client.ForceRefresh();
}

var flagEvaluatedEvents = new List<FlagEvaluatedEventArgs>();
client.FlagEvaluated += (s, e) => flagEvaluatedEvents.Add(e);

var user = new User("[email protected]") { Email = "[email protected]" };

// Act

var actual = isAsync
? await client.GetAllValueDetailsAsync(user)
: client.GetAllValueDetails(user);

// Assert

var expected = new[]
{
new { Key = "boolean", Value = (object)true, VariationId = "67787ae4" },
new { Key = "text", Value = (object)"true", VariationId = "9bdc6a1f" },
new { Key = "whole", Value = (object)1, VariationId = "ab30533b" },
new { Key = "decimal", Value = (object)-2147483647.2147484, VariationId = "8f9559cf" },
};

foreach (var expectedItem in expected)
{
var actualDetails = actual.FirstOrDefault(details => details.Key == expectedItem.Key);

Assert.IsNotNull(actualDetails);
Assert.AreEqual(expectedItem.Value, actualDetails.Value);
Assert.IsFalse(actualDetails.IsDefaultValue);
Assert.AreEqual(expectedItem.VariationId, actualDetails.VariationId);
Assert.AreEqual(timeStamp, actualDetails.FetchTime);
Assert.AreSame(user, actualDetails.User);
Assert.IsNull(actualDetails.ErrorMessage);
Assert.IsNull(actualDetails.ErrorException);
Assert.IsNotNull(actualDetails.MatchedEvaluationRule);
Assert.IsNull(actualDetails.MatchedEvaluationPercentageRule);

var flagEvaluatedDetails = flagEvaluatedEvents.Select(e => e.EvaluationDetails).FirstOrDefault(details => details.Key == expectedItem.Key);

Assert.IsNotNull(flagEvaluatedDetails);
Assert.AreSame(actualDetails, flagEvaluatedDetails);
}
}

[DataRow(false)]
[DataRow(true)]
[DataTestMethod]
public async Task GetAllValueDetails_DeserializeFailed_ShouldReturnWithEmptyArray(bool isAsync)
{
// Arrange

configServiceMock.Setup(m => m.GetConfig()).Returns(ProjectConfig.Empty);
configServiceMock.Setup(m => m.GetConfigAsync()).ReturnsAsync(ProjectConfig.Empty);
var o = new SettingsWithPreferences();
configDeserializerMock
.Setup(m => m.TryDeserialize(It.IsAny<string>(), It.IsAny<string>(), out o))
.Returns(false);

using IConfigCatClient client = new ConfigCatClient(configServiceMock.Object, loggerMock.Object, evaluatorMock.Object, configDeserializerMock.Object, new Hooks());

var flagEvaluatedEvents = new List<FlagEvaluatedEventArgs>();
client.FlagEvaluated += (s, e) => flagEvaluatedEvents.Add(e);

// Act

var actual = isAsync
? await client.GetAllValueDetailsAsync()
: client.GetAllValueDetails();

// Assert

Assert.IsNotNull(actual);
Assert.AreEqual(0, actual.Count);
Assert.AreEqual(0, flagEvaluatedEvents.Count);
loggerMock.Verify(m => m.Error(It.IsAny<string>()), Times.Once);
}

[DataRow(false)]
[DataRow(true)]
[DataTestMethod]
public async Task GetAllValueDetails_ConfigServiceThrowException_ShouldReturnEmptyEnumerable(bool isAsync)
{
// Arrange

configServiceMock
.Setup(m => m.GetConfigAsync())
.Throws<Exception>();

using IConfigCatClient client = new ConfigCatClient(configServiceMock.Object, loggerMock.Object, evaluatorMock.Object, configDeserializerMock.Object, new Hooks());

var flagEvaluatedEvents = new List<FlagEvaluatedEventArgs>();
client.FlagEvaluated += (s, e) => flagEvaluatedEvents.Add(e);

// Act

var actual = isAsync
? await client.GetAllValueDetailsAsync()
: client.GetAllValueDetails();

// Assert

Assert.IsNotNull(actual);
Assert.AreEqual(0, actual.Count);
Assert.AreEqual(0, flagEvaluatedEvents.Count);
}

[DataRow(false)]
[DataRow(true)]
[DataTestMethod]
public async Task GetAllValueDetails_EvaluateServiceThrowException_ShouldReturnDefaultValue(bool isAsync)
{
// Arrange

const string errorMessage = "Error";

const string cacheKey = "123";
var configJsonFilePath = Path.Combine("data", "sample_variationid_v5.json");
var timeStamp = DateTime.UtcNow;

evaluatorMock
.Setup(m => m.Evaluate(It.IsAny<Setting>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<User>(), It.IsAny<ProjectConfig>(), It.IsNotNull<EvaluationDetailsFactory>()))
.Throws(new ApplicationException(errorMessage));

var client = CreateClientWithMockedFetcher(cacheKey, loggerMock, fetcherMock,
onFetch: _ => FetchResult.Success(new ProjectConfig { JsonString = File.ReadAllText(configJsonFilePath), HttpETag = "12345", TimeStamp = timeStamp }),
configServiceFactory: (fetcher, cacheParams, loggerWrapper) =>
{
return new ManualPollConfigService(fetcherMock.Object, cacheParams, loggerWrapper);
},
evaluatorFactory: _ => evaluatorMock.Object, new Hooks(),
out var configService, out _);

if (isAsync)
{
await client.ForceRefreshAsync();
}
else
{
client.ForceRefresh();
}

var flagEvaluatedEvents = new List<FlagEvaluatedEventArgs>();
var errorEvents = new List<ConfigCatClientErrorEventArgs>();
client.FlagEvaluated += (s, e) => flagEvaluatedEvents.Add(e);
client.Error += (s, e) => errorEvents.Add(e);

var user = new User("[email protected]") { Email = "[email protected]" };

// Act

var actual = isAsync
? await client.GetAllValueDetailsAsync(user)
: client.GetAllValueDetails(user);

// Assert

foreach (var key in new[] { "boolean", "text", "whole", "decimal" })
{
var actualDetails = actual.FirstOrDefault(details => details.Key == key);

Assert.IsNotNull(actualDetails);
Assert.AreEqual(key, actualDetails.Key);
Assert.IsNull(actualDetails.Value);
Assert.IsTrue(actualDetails.IsDefaultValue);
Assert.IsNull(actualDetails.VariationId);
Assert.AreEqual(timeStamp, actualDetails.FetchTime);
Assert.AreSame(user, actualDetails.User);
Assert.AreEqual(errorMessage, actualDetails?.ErrorMessage);
Assert.IsInstanceOfType(actualDetails.ErrorException, typeof(ApplicationException));
Assert.IsNull(actualDetails.MatchedEvaluationRule);
Assert.IsNull(actualDetails.MatchedEvaluationPercentageRule);

var flagEvaluatedDetails = flagEvaluatedEvents.Select(e => e.EvaluationDetails).FirstOrDefault(details => details.Key == key);

Assert.IsNotNull(flagEvaluatedDetails);
Assert.AreSame(actualDetails, flagEvaluatedDetails);
}

Assert.AreEqual(1, errorEvents.Count);
var errorEventArgs = errorEvents[0];
StringAssert.Contains(errorEventArgs.Message, isAsync ? nameof(IConfigCatClient.GetAllValueDetailsAsync) : nameof(IConfigCatClient.GetAllValueDetails));
Assert.IsInstanceOfType(errorEventArgs.Exception, typeof(AggregateException));
var actualException = (AggregateException)errorEventArgs.Exception;
Assert.AreEqual(actual.Count, actualException.InnerExceptions.Count);
foreach (var ex in actualException.InnerExceptions)
{
Assert.IsInstanceOfType(ex, typeof(ApplicationException));
}
}

[TestMethod]
public async Task GetAllKeys_ConfigServiceThrowException_ShouldReturnsWithEmptyArray()
{
Expand Down
Loading

0 comments on commit 755fdc1

Please sign in to comment.