Skip to content

Commit

Permalink
Provide a way to mock ConfigCatClientSnapshot
Browse files Browse the repository at this point in the history
  • Loading branch information
adams85 committed Mar 27, 2024
1 parent 398325c commit 6f5579c
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 68 deletions.
71 changes: 71 additions & 0 deletions src/ConfigCat.Client.Tests/ConfigCatClientSnapshotTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using ConfigCat.Client.Cache;
using ConfigCat.Client.ConfigService;
using ConfigCat.Client.Configuration;
using ConfigCat.Client.Evaluation;
using ConfigCat.Client.Override;
using ConfigCat.Client.Tests.Fakes;
using ConfigCat.Client.Tests.Helpers;
using ConfigCat.Client.Utils;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace ConfigCat.Client.Tests;

[TestClass]
public class ConfigCatClientSnapshotTests
{
[TestMethod]
public void DefaultInstanceDoesNotThrow()
{
const string key = "key";
const string defaultValue = "";

var snapshot = default(ConfigCatClientSnapshot);

Assert.AreEqual(ClientCacheState.NoFlagData, snapshot.CacheState);
Assert.IsNull(snapshot.FetchedConfig);
CollectionAssert.AreEqual(ArrayUtils.EmptyArray<string>(), snapshot.GetAllKeys().ToArray());
Assert.AreEqual("", snapshot.GetValue(key, defaultValue));
var evaluationDetails = snapshot.GetValueDetails(key, defaultValue);
Assert.IsNotNull(evaluationDetails);
Assert.AreEqual(key, evaluationDetails.Key);
Assert.AreEqual(defaultValue, evaluationDetails.Value);
Assert.IsTrue(evaluationDetails.IsDefaultValue);
Assert.IsNotNull(evaluationDetails.ErrorMessage);
}

[TestMethod]
public void CanMockSnapshot()
{
const ClientCacheState cacheState = ClientCacheState.HasUpToDateFlagData;
var fetchedConfig = new Config();
var keys = new[] { "key1", "key2" };
var evaluationDetails = new EvaluationDetails<string>("key1", "value");

var mock = new Mock<IConfigCatClientSnapshot>();
mock.SetupGet(m => m.CacheState).Returns(cacheState);
mock.SetupGet(m => m.FetchedConfig).Returns(fetchedConfig);
mock.Setup(m => m.GetAllKeys()).Returns(keys);
mock.Setup(m => m.GetValue(evaluationDetails.Key, It.IsAny<string>(), It.IsAny<User?>())).Returns(evaluationDetails.Value);
mock.Setup(m => m.GetValueDetails(evaluationDetails.Key, It.IsAny<string>(), It.IsAny<User?>())).Returns(evaluationDetails);

var snapshot = new ConfigCatClientSnapshot(mock.Object);

Assert.AreEqual(cacheState, snapshot.CacheState);
Assert.AreEqual(fetchedConfig, snapshot.FetchedConfig);
CollectionAssert.AreEqual(keys, snapshot.GetAllKeys().ToArray());
Assert.AreEqual(evaluationDetails.Value, snapshot.GetValue(evaluationDetails.Key, ""));
Assert.AreSame(evaluationDetails, snapshot.GetValueDetails(evaluationDetails.Key, ""));
}
}
120 changes: 53 additions & 67 deletions src/ConfigCatClient/ConfigCatClientSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,68 +10,66 @@ namespace ConfigCat.Client;
/// <summary>
/// Represents the state of <see cref="IConfigCatClient"/> captured at a specific point in time.
/// </summary>
public readonly struct ConfigCatClientSnapshot
public readonly struct ConfigCatClientSnapshot : IConfigCatClientSnapshot
{
private readonly EvaluationServices evaluationServices;
private readonly object? evaluationServicesOrFakeImpl; // an instance of either EvaluationServices or IConfigCatClientSnapshot
private readonly SettingsWithRemoteConfig settings;
private readonly User? defaultUser;

private LoggerWrapper Logger => this.evaluationServices.Logger;
private IRolloutEvaluator ConfigEvaluator => this.evaluationServices.Evaluator;
private SafeHooksWrapper Hooks => this.evaluationServices.Hooks;
private readonly ClientCacheState cacheState;

internal ConfigCatClientSnapshot(EvaluationServices evaluationServices, SettingsWithRemoteConfig settings, User? defaultUser, ClientCacheState cacheState)
{
this.evaluationServices = evaluationServices;
this.evaluationServicesOrFakeImpl = evaluationServices;
this.settings = settings;
this.defaultUser = defaultUser;
CacheState = cacheState;
this.cacheState = cacheState;
}

/// <summary>
/// The state of the local cache at the time the snapshot was created.
/// For testing purposes. This constructor allows you to create an instance
/// which will use the fake implementation you provide instead of executing the built-in logic.
/// </summary>
public ClientCacheState CacheState { get; }
public ConfigCatClientSnapshot(IConfigCatClientSnapshot impl)
{
this.evaluationServicesOrFakeImpl = impl;
this.settings = default;
this.defaultUser = default;
this.cacheState = default;
}

/// <summary>
/// The latest config which has been fetched from the remote server.
/// </summary>
public IConfig? FetchedConfig => this.settings.RemoteConfig?.Config;
/// <inheritdoc/>>
public ClientCacheState CacheState => this.evaluationServicesOrFakeImpl is EvaluationServices
? this.cacheState
: ((IConfigCatClientSnapshot?)this.evaluationServicesOrFakeImpl)?.CacheState ?? ClientCacheState.NoFlagData;

/// <summary>
/// Returns the available setting keys.
/// </summary>
/// <remarks>
/// In case the client is configured to use flag override, this will also include the keys provided by the flag override.
/// </remarks>
/// <returns>The collection of keys.</returns>
/// <inheritdoc/>>
public IConfig? FetchedConfig => this.evaluationServicesOrFakeImpl is EvaluationServices
? this.settings.RemoteConfig?.Config
: ((IConfigCatClientSnapshot?)this.evaluationServicesOrFakeImpl)?.FetchedConfig ?? null;

/// <inheritdoc/>>
public IReadOnlyCollection<string> GetAllKeys()
{
if (this.evaluationServicesOrFakeImpl is not EvaluationServices)
{
return this.evaluationServicesOrFakeImpl is not null
? ((IConfigCatClientSnapshot)this.evaluationServicesOrFakeImpl).GetAllKeys()
: ArrayUtils.EmptyArray<string>();
}

return this.settings.Value is { } settings ? settings.ReadOnlyKeys() : ArrayUtils.EmptyArray<string>();
}

/// <summary>
/// Returns the value of a feature flag or setting identified by <paramref name="key"/> synchronously, based on the snapshot.
/// </summary>
/// <remarks>
/// It is important to provide an argument for the <paramref name="defaultValue"/> parameter, specifically for the <typeparamref name="T"/> generic type parameter,
/// that matches the type of the feature flag or setting you are evaluating.<br/>
/// Please refer to <see href="https://configcat.com/docs/sdk-reference/dotnet/#setting-type-mapping">this table</see> for the corresponding types.
/// </remarks>
/// <typeparam name="T">
/// The type of the value. Only the following types are allowed:
/// <see cref="string"/>, <see cref="bool"/>, <see cref="int"/>, <see cref="long"/>, <see cref="double"/> and <see cref="object"/> (both nullable and non-nullable).<br/>
/// The type must correspond to the setting type, otherwise <paramref name="defaultValue"/> will be returned.
/// </typeparam>
/// <param name="key">Key of the feature flag or setting.</param>
/// <param name="defaultValue">In case of failure, this value will be returned.</param>
/// <param name="user">The User Object to use for evaluating targeting rules and percentage options.</param>
/// <returns>The value of the feature flag or setting.</returns>
/// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="key"/> is an empty string.</exception>
/// <exception cref="ArgumentException"><typeparamref name="T"/> is not an allowed type.</exception>
/// <inheritdoc/>>
public T GetValue<T>(string key, T defaultValue, User? user = null)
{
if (this.evaluationServicesOrFakeImpl is not EvaluationServices evaluationServices)
{
return this.evaluationServicesOrFakeImpl is not null
? ((IConfigCatClientSnapshot)this.evaluationServicesOrFakeImpl).GetValue(key, defaultValue, user)
: defaultValue;
}

if (key is null)
{
throw new ArgumentNullException(nameof(key));
Expand All @@ -91,42 +89,30 @@ public T GetValue<T>(string key, T defaultValue, User? user = null)
try
{
settings = this.settings;
evaluationDetails = ConfigEvaluator.Evaluate(settings.Value, key, defaultValue, user, settings.RemoteConfig, Logger);
evaluationDetails = evaluationServices.Evaluator.Evaluate(settings.Value, key, defaultValue, user, settings.RemoteConfig, evaluationServices.Logger);
value = evaluationDetails.Value;
}
catch (Exception ex)
{
Logger.SettingEvaluationError($"{nameof(ConfigCatClientSnapshot)}.{nameof(GetValue)}", key, nameof(defaultValue), defaultValue, ex);
evaluationServices.Logger.SettingEvaluationError($"{nameof(ConfigCatClientSnapshot)}.{nameof(GetValue)}", key, nameof(defaultValue), defaultValue, ex);
evaluationDetails = EvaluationDetails.FromDefaultValue(key, defaultValue, fetchTime: settings.RemoteConfig?.TimeStamp, user, ex.Message, ex);
value = defaultValue;
}

Hooks.RaiseFlagEvaluated(evaluationDetails);
evaluationServices.Hooks.RaiseFlagEvaluated(evaluationDetails);
return value;
}

/// <summary>
/// Returns the value along with evaluation details of a feature flag or setting identified by <paramref name="key"/> synchronously, based on the snapshot.
/// </summary>
/// <remarks>
/// It is important to provide an argument for the <paramref name="defaultValue"/> parameter, specifically for the <typeparamref name="T"/> generic type parameter,
/// that matches the type of the feature flag or setting you are evaluating.<br/>
/// Please refer to <see href="https://configcat.com/docs/sdk-reference/dotnet/#setting-type-mapping">this table</see> for the corresponding types.
/// </remarks>
/// <typeparam name="T">
/// The type of the value. Only the following types are allowed:
/// <see cref="string"/>, <see cref="bool"/>, <see cref="int"/>, <see cref="long"/>, <see cref="double"/> and <see cref="object"/> (both nullable and non-nullable).<br/>
/// The type must correspond to the setting type, otherwise <paramref name="defaultValue"/> will be returned.
/// </typeparam>
/// <param name="key">Key of the feature flag or setting.</param>
/// <param name="defaultValue">In case of failure, this value will be returned.</param>
/// <param name="user">The User Object to use for evaluating targeting rules and percentage options.</param>
/// <returns>The value along with the details of evaluation of the feature flag or setting.</returns>
/// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="key"/> is an empty string.</exception>
/// <exception cref="ArgumentException"><typeparamref name="T"/> is not an allowed type.</exception>
/// <inheritdoc/>>
public EvaluationDetails<T> GetValueDetails<T>(string key, T defaultValue, User? user = null)
{
if (this.evaluationServicesOrFakeImpl is not EvaluationServices evaluationServices)
{
return this.evaluationServicesOrFakeImpl is not null
? ((IConfigCatClientSnapshot)this.evaluationServicesOrFakeImpl).GetValueDetails(key, defaultValue, user)
: EvaluationDetails.FromDefaultValue(key, defaultValue, null, user, $"{nameof(GetValueDetails)} was called on the default instance of {nameof(ConfigCatClientSnapshot)}.");
}

if (key is null)
{
throw new ArgumentNullException(nameof(key));
Expand All @@ -145,15 +131,15 @@ public EvaluationDetails<T> GetValueDetails<T>(string key, T defaultValue, User?
try
{
settings = this.settings;
evaluationDetails = ConfigEvaluator.Evaluate(settings.Value, key, defaultValue, user, settings.RemoteConfig, Logger);
evaluationDetails = evaluationServices.Evaluator.Evaluate(settings.Value, key, defaultValue, user, settings.RemoteConfig, evaluationServices.Logger);
}
catch (Exception ex)
{
Logger.SettingEvaluationError($"{nameof(ConfigCatClientSnapshot)}.{nameof(GetValueDetails)}", key, nameof(defaultValue), defaultValue, ex);
evaluationServices.Logger.SettingEvaluationError($"{nameof(ConfigCatClientSnapshot)}.{nameof(GetValueDetails)}", key, nameof(defaultValue), defaultValue, ex);
evaluationDetails = EvaluationDetails.FromDefaultValue(key, defaultValue, fetchTime: settings.RemoteConfig?.TimeStamp, user, ex.Message, ex);
}

Hooks.RaiseFlagEvaluated(evaluationDetails);
evaluationServices.Hooks.RaiseFlagEvaluated(evaluationDetails);
return evaluationDetails;
}
}
1 change: 0 additions & 1 deletion src/ConfigCatClient/IConfigCatClient.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using ConfigCat.Client.Configuration;
Expand Down
73 changes: 73 additions & 0 deletions src/ConfigCatClient/IConfigCatClientSnapshot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;

namespace ConfigCat.Client;

/// <summary>
/// Defines the public interface of the <see cref="ConfigCatClientSnapshot"/> struct.
/// </summary>
public interface IConfigCatClientSnapshot
{
/// <summary>
/// The state of the local cache at the time the snapshot was created.
/// </summary>
ClientCacheState CacheState { get; }

/// <summary>
/// The latest config which has been fetched from the remote server.
/// </summary>
IConfig? FetchedConfig { get; }

/// <summary>
/// Returns the available setting keys.
/// </summary>
/// <remarks>
/// In case the client is configured to use flag override, this will also include the keys provided by the flag override.
/// </remarks>
/// <returns>The collection of keys.</returns>
IReadOnlyCollection<string> GetAllKeys();

/// <summary>
/// Returns the value of a feature flag or setting identified by <paramref name="key"/> synchronously, based on the snapshot.
/// </summary>
/// <remarks>
/// It is important to provide an argument for the <paramref name="defaultValue"/> parameter, specifically for the <typeparamref name="T"/> generic type parameter,
/// that matches the type of the feature flag or setting you are evaluating.<br/>
/// Please refer to <see href="https://configcat.com/docs/sdk-reference/dotnet/#setting-type-mapping">this table</see> for the corresponding types.
/// </remarks>
/// <typeparam name="T">
/// The type of the value. Only the following types are allowed:
/// <see cref="string"/>, <see cref="bool"/>, <see cref="int"/>, <see cref="long"/>, <see cref="double"/> and <see cref="object"/> (both nullable and non-nullable).<br/>
/// The type must correspond to the setting type, otherwise <paramref name="defaultValue"/> will be returned.
/// </typeparam>
/// <param name="key">Key of the feature flag or setting.</param>
/// <param name="defaultValue">In case of failure, this value will be returned.</param>
/// <param name="user">The User Object to use for evaluating targeting rules and percentage options.</param>
/// <returns>The value of the feature flag or setting.</returns>
/// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="key"/> is an empty string.</exception>
/// <exception cref="ArgumentException"><typeparamref name="T"/> is not an allowed type.</exception>
T GetValue<T>(string key, T defaultValue, User? user = null);

/// <summary>
/// Returns the value along with evaluation details of a feature flag or setting identified by <paramref name="key"/> synchronously, based on the snapshot.
/// </summary>
/// <remarks>
/// It is important to provide an argument for the <paramref name="defaultValue"/> parameter, specifically for the <typeparamref name="T"/> generic type parameter,
/// that matches the type of the feature flag or setting you are evaluating.<br/>
/// Please refer to <see href="https://configcat.com/docs/sdk-reference/dotnet/#setting-type-mapping">this table</see> for the corresponding types.
/// </remarks>
/// <typeparam name="T">
/// The type of the value. Only the following types are allowed:
/// <see cref="string"/>, <see cref="bool"/>, <see cref="int"/>, <see cref="long"/>, <see cref="double"/> and <see cref="object"/> (both nullable and non-nullable).<br/>
/// The type must correspond to the setting type, otherwise <paramref name="defaultValue"/> will be returned.
/// </typeparam>
/// <param name="key">Key of the feature flag or setting.</param>
/// <param name="defaultValue">In case of failure, this value will be returned.</param>
/// <param name="user">The User Object to use for evaluating targeting rules and percentage options.</param>
/// <returns>The value along with the details of evaluation of the feature flag or setting.</returns>
/// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="key"/> is an empty string.</exception>
/// <exception cref="ArgumentException"><typeparamref name="T"/> is not an allowed type.</exception>
EvaluationDetails<T> GetValueDetails<T>(string key, T defaultValue, User? user = null);
}

0 comments on commit 6f5579c

Please sign in to comment.