Skip to content

Commit

Permalink
Add tests for advanced flag override use cases
Browse files Browse the repository at this point in the history
  • Loading branch information
adams85 committed Sep 7, 2023
1 parent 2848429 commit 852aa98
Show file tree
Hide file tree
Showing 15 changed files with 408 additions and 135 deletions.
6 changes: 6 additions & 0 deletions src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,10 @@
</None>
</ItemGroup>

<ItemGroup>
<None Update="data\test_list_truncation.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
92 changes: 79 additions & 13 deletions src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using ConfigCat.Client.Configuration;
using ConfigCat.Client.Evaluation;
using ConfigCat.Client.Tests.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace ConfigCat.Client.Tests;

Expand Down Expand Up @@ -92,21 +94,15 @@ public void SegmentMatrixTests(string configLocation, string settingKey, string
[TestMethod]
public void CircularDependencyTest()
{
var config = new ConfigLocation.LocalFile("data", "sample_circulardependency_v6.json").FetchConfig();
var config = new ConfigLocation.LocalFile("data", "test_circulardependency_v6.json").FetchConfig();

var logEvents = new List<(LogLevel Level, LogEventId EventId, FormattableLogMessage Message, Exception? Exception)>();
var logEvents = new List<LogEvent>();
var logger = LoggingHelper.CreateCapturingLogger(logEvents);

var loggerMock = new Mock<IConfigCatLogger>();
loggerMock.SetupGet(logger => logger.LogLevel).Returns(LogLevel.Info);
loggerMock.Setup(logger => logger.Log(It.IsAny<LogLevel>(), It.IsAny<LogEventId>(), ref It.Ref<FormattableLogMessage>.IsAny, It.IsAny<Exception>()))
.Callback(delegate (LogLevel level, LogEventId eventId, ref FormattableLogMessage msg, Exception ex) { logEvents.Add((level, eventId, msg, ex)); });

var loggerWrapper = loggerMock.Object.AsWrapper();

var evaluator = new RolloutEvaluator(loggerWrapper);
var evaluator = new RolloutEvaluator(logger);

const string key = "key1";
var evaluationDetails = evaluator.Evaluate<object?>(config!.Settings, key, defaultValue: null, user: null, remoteConfig: null, loggerWrapper);
var evaluationDetails = evaluator.Evaluate<object?>(config!.Settings, key, defaultValue: null, user: null, remoteConfig: null, logger);

Assert.AreEqual(4, logEvents.Count);

Expand Down Expand Up @@ -138,7 +134,77 @@ public void CircularDependencyTest()
StringAssert.Matches((string?)evaluateLogEvent.Message.ArgValues[0], new Regex(
"THEN 'key3-prereq1' => " + Regex.Escape(RolloutEvaluator.CircularDependencyError) + Environment.NewLine
+ @"\s+" + Regex.Escape(RolloutEvaluator.TargetingRuleIgnoredMessage)));
}

var inv = loggerMock.Invocations[0];
[DataTestMethod]
[DataRow("stringDependsOnString", "1", "[email protected]", null, "Dog")]
[DataRow("stringDependsOnString", "1", "[email protected]", OverrideBehaviour.RemoteOverLocal, "Dog")]
[DataRow("stringDependsOnString", "1", "[email protected]", OverrideBehaviour.LocalOverRemote, "Dog")]
[DataRow("stringDependsOnString", "1", "[email protected]", OverrideBehaviour.LocalOnly, null)]
[DataRow("stringDependsOnString", "2", "[email protected]", null, "Cat")]
[DataRow("stringDependsOnString", "2", "[email protected]", OverrideBehaviour.RemoteOverLocal, "Cat")]
[DataRow("stringDependsOnString", "2", "[email protected]", OverrideBehaviour.LocalOverRemote, "Dog")]
[DataRow("stringDependsOnString", "2", "[email protected]", OverrideBehaviour.LocalOnly, null)]
[DataRow("stringDependsOnInt", "1", "[email protected]", null, "Dog")]
[DataRow("stringDependsOnInt", "1", "[email protected]", OverrideBehaviour.RemoteOverLocal, "Dog")]
[DataRow("stringDependsOnInt", "1", "[email protected]", OverrideBehaviour.LocalOverRemote, "Cat")]
[DataRow("stringDependsOnInt", "1", "[email protected]", OverrideBehaviour.LocalOnly, null)]
[DataRow("stringDependsOnInt", "2", "[email protected]", null, "Cat")]
[DataRow("stringDependsOnInt", "2", "[email protected]", OverrideBehaviour.RemoteOverLocal, "Cat")]
[DataRow("stringDependsOnInt", "2", "[email protected]", OverrideBehaviour.LocalOverRemote, "Dog")]
[DataRow("stringDependsOnInt", "2", "[email protected]", OverrideBehaviour.LocalOnly, null)]
public async Task PrerequisiteFlagOverrideTest(string key, string userId, string email, OverrideBehaviour? overrideBehaviour, object expectedValue)
{
var cdnLocation = (ConfigLocation.Cdn)new FlagDependencyMatrixTestsDescriptor().ConfigLocation;

var options = new ConfigCatClientOptions
{
// The flag override uses a different config json salt than the downloaded one and overrides the following segments:
// * Beta Users: User.Email IS ONE OF ['[email protected]']
// * Developers: User.Email IS ONE OF ['[email protected]']
FlagOverrides = overrideBehaviour is not null
? FlagOverrides.LocalFile(Path.Combine("data", "test_override_flagdependency_v6.json"), autoReload: false, overrideBehaviour.Value)
: null,
PollingMode = PollingModes.ManualPoll,
};
cdnLocation.ConfigureBaseUrl(options);

using var client = new ConfigCatClient(cdnLocation.SdkKey, options);
await client.ForceRefreshAsync();
var actualValue = await client.GetValueAsync(key, (object?)null, new User(userId) { Email = email });

Assert.AreEqual(expectedValue, actualValue);
}

[DataTestMethod]
[DataRow("developerAndBetaUserSegment", "1", "[email protected]", null, false)]
[DataRow("developerAndBetaUserSegment", "1", "[email protected]", OverrideBehaviour.RemoteOverLocal, false)]
[DataRow("developerAndBetaUserSegment", "1", "[email protected]", OverrideBehaviour.LocalOverRemote, true)]
[DataRow("developerAndBetaUserSegment", "1", "[email protected]", OverrideBehaviour.LocalOnly, true)]
[DataRow("notDeveloperAndNotBetaUserSegment", "2", "[email protected]", null, true)]
[DataRow("notDeveloperAndNotBetaUserSegment", "2", "[email protected]", OverrideBehaviour.RemoteOverLocal, true)]
[DataRow("notDeveloperAndNotBetaUserSegment", "2", "[email protected]", OverrideBehaviour.LocalOverRemote, true)]
[DataRow("notDeveloperAndNotBetaUserSegment", "2", "[email protected]", OverrideBehaviour.LocalOnly, null)]
public async Task ConfigSaltAndSegmentsOverrideTest(string key, string userId, string email, OverrideBehaviour? overrideBehaviour, object expectedValue)
{
var cdnLocation = (ConfigLocation.Cdn)new SegmentMatrixTestsDescriptor().ConfigLocation;

var options = new ConfigCatClientOptions
{
// The flag override uses a different config json salt than the downloaded one and overrides the following segments:
// * Beta Users: User.Email IS ONE OF ['[email protected]']
// * Developers: User.Email IS ONE OF ['[email protected]']
FlagOverrides = overrideBehaviour is not null
? FlagOverrides.LocalFile(Path.Combine("data", "test_override_segments_v6.json"), autoReload: false, overrideBehaviour.Value)
: null,
PollingMode = PollingModes.ManualPoll,
};
cdnLocation.ConfigureBaseUrl(options);

using var client = new ConfigCatClient(cdnLocation.SdkKey, options);
await client.ForceRefreshAsync();
var actualValue = await client.GetValueAsync(key, (object?)null, new User(userId) { Email = email });

Assert.AreEqual(expectedValue, actualValue);
}
}
55 changes: 39 additions & 16 deletions src/ConfigCat.Client.Tests/EvaluationLogTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using ConfigCat.Client.Tests.Helpers;
using ConfigCat.Client.Utils;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

#if NET45
using Newtonsoft.Json;
Expand Down Expand Up @@ -198,6 +197,36 @@ public void SemVerValidationTests(string testSetName, string? sdkKey, string? ba
}
}

[TestMethod]
public void ComparisonValueListTruncation()
{
var config = new ConfigLocation.LocalFile("data", "test_list_truncation.json").FetchConfig();

var logEvents = new List<LogEvent>();
var logger = LoggingHelper.CreateCapturingLogger(logEvents);

var evaluator = new RolloutEvaluator(logger);
var evaluationDetails = evaluator.Evaluate(config.Settings, "key1", (bool?)null, new User("12"), remoteConfig: null, logger);
var actualReturnValue = evaluationDetails.Value;

Assert.AreEqual(true, actualReturnValue);
Assert.AreEqual(1, logEvents.Count);

var expectedLogLines = new[]
{
"INFO [5000] Evaluating 'key1' for User '{\"Identifier\":\"12\"}'",
" Evaluating targeting rules and applying the first match if any:",
" - IF User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] => true",
" AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10' ... <1 more value>] => true",
" AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10' ... <2 more values>] => true",
" THEN 'True' => MATCH, applying rule",
" Returning 'True'.",
};

var evt = logEvents[0];
Assert.AreEqual(string.Join(Environment.NewLine, expectedLogLines), FormatLogEvent(ref evt));
}

private static void RunTest(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, string key, string? defaultValue, string? userObject, string? expectedReturnValue, string expectedLogFileName)
{
var defaultValueParsed = defaultValue?.Deserialize<JsonValue>()!.ToSettingValue(out var settingType).GetValue();
Expand Down Expand Up @@ -232,13 +261,8 @@ private static void RunTest(string testSetName, string? sdkKey, string? baseUrlO
user = null;
}

var logEvents = new List<(LogLevel Level, LogEventId EventId, FormattableLogMessage Message, Exception? Exception)>();

var loggerMock = new Mock<IConfigCatLogger>();
loggerMock.SetupGet(logger => logger.LogLevel).Returns(LogLevel.Info);
loggerMock.Setup(logger => logger.Log(It.IsAny<LogLevel>(), It.IsAny<LogEventId>(), ref It.Ref<FormattableLogMessage>.IsAny, It.IsAny<Exception>()))
.Callback(delegate (LogLevel level, LogEventId eventId, ref FormattableLogMessage msg, Exception ex) { logEvents.Add((level, eventId, msg, ex)); });
var logger = loggerMock.Object.AsWrapper();
var logEvents = new List<LogEvent>();
var logger = LoggingHelper.CreateCapturingLogger(logEvents);

ConfigLocation configLocation = sdkKey is { Length: > 0 }
? new ConfigLocation.Cdn(sdkKey, baseUrlOrOverrideFileName)
Expand All @@ -255,28 +279,27 @@ private static void RunTest(string testSetName, string? sdkKey, string? baseUrlO
var expectedLogFilePath = Path.Combine(TestDataRootPath, testSetName, expectedLogFileName);
var expectedLogText = string.Join(Environment.NewLine, File.ReadAllLines(expectedLogFilePath));

var actualLogText = string.Join(Environment.NewLine, logEvents
.Select(evt => FormatLogEvent(evt.Level, evt.EventId, ref evt.Message, evt.Exception)));
var actualLogText = string.Join(Environment.NewLine, logEvents.Select(evt => FormatLogEvent(ref evt)));

Assert.AreEqual(expectedLogText, actualLogText);
}

private static string FormatLogEvent(LogLevel level, LogEventId eventId, ref FormattableLogMessage message, Exception? exception)
private static string FormatLogEvent(ref LogEvent evt)
{
var levelString = level switch
var levelString = evt.Level switch
{
LogLevel.Debug => "DEBUG",
LogLevel.Info => "INFO",
LogLevel.Warning => "WARNING",
LogLevel.Error => "ERROR",
_ => level.ToString().ToUpperInvariant().PadRight(5)
_ => evt.Level.ToString().ToUpperInvariant().PadRight(5)
};

var eventIdString = eventId.Id.ToString(CultureInfo.InvariantCulture);
var eventIdString = evt.EventId.Id.ToString(CultureInfo.InvariantCulture);

var exceptionString = exception is null ? string.Empty : Environment.NewLine + exception;
var exceptionString = evt.Exception is null ? string.Empty : Environment.NewLine + evt.Exception;

return $"{levelString} [{eventIdString}] {message.InvariantFormattedMessage}{exceptionString}";
return $"{levelString} [{eventIdString}] {evt.Message.InvariantFormattedMessage}{exceptionString}";
}

#pragma warning disable IDE1006 // Naming Styles
Expand Down
17 changes: 10 additions & 7 deletions src/ConfigCat.Client.Tests/Helpers/ConfigLocation.Cdn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@ public sealed record class Cdn : ConfigLocation
public string SdkKey { get; }
public string? BaseUrl { get; }

public override string RealLocation
public override string GetRealLocation()
{
get
{
var options = new ConfigCatClientOptions { BaseUrl = BaseUrl is not null ? new Uri(BaseUrl) : ConfigCatClientOptions.BaseUrlEu };
return options.CreateUri(SdkKey).ToString();
}
var options = new ConfigCatClientOptions();
ConfigureBaseUrl(options);
return options.CreateUri(SdkKey).ToString();
}

internal override Config FetchConfig()
Expand All @@ -27,8 +25,8 @@ internal override Config FetchConfig()
{
PollingMode = PollingModes.ManualPoll,
Logger = new ConsoleLogger(),
BaseUrl = BaseUrl is not null ? new Uri(BaseUrl) : ConfigCatClientOptions.BaseUrlEu
};
ConfigureBaseUrl(options);

using var configFetcher = new HttpConfigFetcher(
options.CreateUri(SdkKey),
Expand All @@ -43,5 +41,10 @@ internal override Config FetchConfig()
? fetchResult.Config.Config!
: throw new InvalidOperationException("Could not fetch config from CDN: " + fetchResult.ErrorMessage);
}

internal void ConfigureBaseUrl(ConfigCatClientOptions options)
{
options.BaseUrl = BaseUrl is not null ? new Uri(BaseUrl) : ConfigCatClientOptions.BaseUrlEu;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public sealed record class LocalFile : ConfigLocation

public string FilePath { get; }

public override string RealLocation => FilePath;
public override string GetRealLocation() => FilePath;

internal override Config FetchConfig()
{
Expand Down
2 changes: 1 addition & 1 deletion src/ConfigCat.Client.Tests/Helpers/ConfigLocation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public abstract partial record class ConfigLocation
{
private ConfigLocation() { }

public abstract string RealLocation { get; }
public abstract string GetRealLocation();

internal abstract Config FetchConfig();
}
9 changes: 0 additions & 9 deletions src/ConfigCat.Client.Tests/Helpers/LoggerExtensions.cs

This file was deleted.

38 changes: 38 additions & 0 deletions src/ConfigCat.Client.Tests/Helpers/LoggingHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Moq;
using System.Collections.Generic;
using System;

namespace ConfigCat.Client;

internal struct LogEvent
{
public LogEvent(LogLevel level, LogEventId eventId, ref FormattableLogMessage message, Exception? exception)
{
(this.Level, this.EventId, this.Message, this.Exception) = (level, eventId, message, exception);
}

public readonly LogLevel Level;
public readonly LogEventId EventId;
public FormattableLogMessage Message;
public readonly Exception? Exception;
}

internal static class LoggingHelper
{
public static LoggerWrapper AsWrapper(this IConfigCatLogger logger, Hooks? hooks = null)
{
return new LoggerWrapper(logger, hooks);
}

public static LoggerWrapper CreateCapturingLogger(List<LogEvent> logEvents, LogLevel logLevel = LogLevel.Info)
{
var loggerMock = new Mock<IConfigCatLogger>();

loggerMock.SetupGet(logger => logger.LogLevel).Returns(logLevel);

loggerMock.Setup(logger => logger.Log(It.IsAny<LogLevel>(), It.IsAny<LogEventId>(), ref It.Ref<FormattableLogMessage>.IsAny, It.IsAny<Exception>()))
.Callback(delegate (LogLevel level, LogEventId eventId, ref FormattableLogMessage msg, Exception ex) { logEvents.Add(new LogEvent(level, eventId, ref msg, ex)); });

return loggerMock.Object.AsWrapper();
}
}
2 changes: 1 addition & 1 deletion src/ConfigCat.Client.Tests/MatrixTestRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class MatrixTestRunner<TDescriptor> : MatrixTestRunnerBase<TDescriptor>

protected override bool AssertValue<T>(string expected, Func<string, T> parse, T actual, string keyName, string? userId)
{
Assert.AreEqual(parse(expected), actual, $"config: {DescriptorInstance.ConfigLocation.RealLocation} | keyName: {keyName} | userId: {userId}");
Assert.AreEqual(parse(expected), actual, $"config: {DescriptorInstance.ConfigLocation.GetRealLocation()} | keyName: {keyName} | userId: {userId}");
return true;
}
}
Loading

0 comments on commit 852aa98

Please sign in to comment.