Skip to content

Commit 6214c3d

Browse files
committed
PR comments and unit test
Signed-off-by: Torben Hørup <torben@t-hoerup.dk>
1 parent d1ca06b commit 6214c3d

File tree

4 files changed

+120
-27
lines changed

4 files changed

+120
-27
lines changed

src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Net;
66
using System.Net.Http;
77
using System.Net.Http.Headers;
8+
using System.Runtime.CompilerServices;
89
using System.Text;
910
using System.Text.Json;
1011
using System.Threading;
@@ -19,6 +20,7 @@
1920
using OpenFeature.Model;
2021
using ZiggyCreatures.Caching.Fusion;
2122

23+
[assembly: InternalsVisibleTo("OpenFeature.Contrib.Providers.GOFeatureFlag.Test")]
2224
namespace OpenFeature.Contrib.Providers.GOFeatureFlag
2325
{
2426
/// <summary>
@@ -30,7 +32,8 @@ public class GoFeatureFlagProvider : FeatureProvider
3032
private ExporterMetadata _exporterMetadata;
3133
private HttpClient _httpClient;
3234

33-
private IFusionCache _cache = null;
35+
internal IFusionCache _cache = null;
36+
internal MemoryCache _backingCache = null;
3437

3538
/// <summary>
3639
/// Constructor of the provider.
@@ -93,27 +96,25 @@ private void InitializeProvider(GoFeatureFlagProviderOptions options)
9396
new AuthenticationHeaderValue("Bearer", options.ApiKey);
9497

9598

96-
var cacheMaxSize = options.CacheMaxSize ?? 10000;
97-
var cacheMaxTTL = options.CacheMaxTTL ?? TimeSpan.FromSeconds(60);
9899

99-
var backingMemoryCache = new MemoryCache(new MemoryCacheOptions
100+
_backingCache = new MemoryCache(new MemoryCacheOptions
100101
{
101-
SizeLimit = cacheMaxSize,
102-
CompactionPercentage = 0.1,
102+
SizeLimit = options.CacheMaxSize,
103+
CompactionPercentage = 0.1,
103104
});
104-
105+
105106
_cache = new FusionCache(new FusionCacheOptions
106107
{
107108
DefaultEntryOptions = new FusionCacheEntryOptions
108109
{
109110
Size = 1,
110-
Duration = cacheMaxTTL,
111+
Duration = options.CacheMaxTTL,
111112
},
112-
113-
}, backingMemoryCache);
114-
113+
114+
}, _backingCache);
115115
}
116116

117+
117118
/// <summary>
118119
/// Return the metadata associated to this provider.
119120
/// </summary>
@@ -176,7 +177,7 @@ public override async Task<ResolutionDetails<string>> ResolveStringValueAsync(st
176177
try
177178
{
178179
var key = GenerateCacheKey(flagKey, context);
179-
return await _cache.GetOrSetAsync<ResolutionDetails<string>>(key, async (_,_) =>
180+
return await _cache.GetOrSetAsync<ResolutionDetails<string>>(key, async (_, _) =>
180181
{
181182
var resp = await CallApi(flagKey, defaultValue, context).ConfigureAwait(false);
182183
if (!(resp.Value is JsonElement element && element.ValueKind == JsonValueKind.String))
@@ -213,8 +214,8 @@ public override async Task<ResolutionDetails<int>> ResolveIntegerValueAsync(stri
213214
try
214215
{
215216
var key = GenerateCacheKey(flagKey, context);
216-
return await _cache.GetOrSetAsync<ResolutionDetails<int>>(key, async (_,_) =>
217-
{
217+
return await _cache.GetOrSetAsync<ResolutionDetails<int>>(key, async (_, _) =>
218+
{
218219
var resp = await CallApi(flagKey, defaultValue, context).ConfigureAwait(false);
219220
return new ResolutionDetails<int>(flagKey, int.Parse(resp.Value.ToString()), ErrorType.None,
220221
resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata());
@@ -249,7 +250,7 @@ public override async Task<ResolutionDetails<double>> ResolveDoubleValueAsync(st
249250
try
250251
{
251252
var key = GenerateCacheKey(flagKey, context);
252-
return await _cache.GetOrSetAsync<ResolutionDetails<double>>(key, async (_,_) =>
253+
return await _cache.GetOrSetAsync<ResolutionDetails<double>>(key, async (_, _) =>
253254
{
254255
var resp = await CallApi(flagKey, defaultValue, context).ConfigureAwait(false);
255256
return new ResolutionDetails<double>(flagKey,
@@ -286,7 +287,7 @@ public override async Task<ResolutionDetails<Value>> ResolveStructureValueAsync(
286287
try
287288
{
288289
var key = GenerateCacheKey(flagKey, context);
289-
return await _cache.GetOrSetAsync<ResolutionDetails<Value>>(key, async (_,_) =>
290+
return await _cache.GetOrSetAsync<ResolutionDetails<Value>>(key, async (_, _) =>
290291
{
291292
var resp = await CallApi(flagKey, defaultValue, context).ConfigureAwait(false);
292293
if (resp.Value is JsonElement)
@@ -358,7 +359,7 @@ private async Task<OfrepResponse> CallApi<T>(string flagKey, T defaultValue,
358359

359360
private string GenerateCacheKey(string flagKey, EvaluationContext ctx)
360361
{
361-
return ctx != null ? new OfrepRequest(ctx).AsJsonString() : flagKey;
362+
return ctx != null ? flagKey + ":" + new OfrepRequest(ctx).AsJsonString() : flagKey;
362363
}
363364

364365
/// <summary>

src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProviderOptions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ public class GoFeatureFlagProviderOptions
4747
/// (optional) How long is an entry allowed to be cached in memory
4848
/// Default: 60 seconds
4949
/// </summary>
50-
public TimeSpan? CacheMaxTTL { get; set; }
50+
public TimeSpan CacheMaxTTL { get; set; } = TimeSpan.FromSeconds(60);
5151

5252
/// <summary>
5353
/// (optional) How many entries may be cached
5454
/// Default: 10000
5555
/// </summary>
56-
public int? CacheMaxSize { get; set; }
56+
public int CacheMaxSize { get; set; } = 10000;
5757
}
5858
}

src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
</ItemGroup>
1717

1818
<ItemGroup>
19-
<PackageReference Include="ZiggyCreatures.FusionCache" Version="2.2.0" />
19+
<PackageReference Include="ZiggyCreatures.FusionCache" Version="2.1.0" />
2020
</ItemGroup>
2121

2222
<PropertyGroup>

test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagProviderTest.cs

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Text.Json;
77
using System.Threading.Tasks;
88
using Newtonsoft.Json.Linq;
9+
using NSubstitute;
910
using OpenFeature.Constant;
1011
using OpenFeature.Contrib.Providers.GOFeatureFlag.exception;
1112
using OpenFeature.Contrib.Providers.GOFeatureFlag.models;
@@ -17,14 +18,16 @@ namespace OpenFeature.Contrib.Providers.GOFeatureFlag.Test;
1718

1819
public class GoFeatureFlagProviderTest
1920
{
21+
private static readonly string mediaType = "application/json";
2022
private static readonly string baseUrl = "http://gofeatureflag.org";
2123
private static readonly string prefixEval = baseUrl + "/ofrep/v1/evaluate/flags/";
2224
private readonly EvaluationContext _defaultEvaluationCtx = InitDefaultEvaluationCtx();
23-
private readonly HttpMessageHandler _mockHttp = InitMock();
24-
25-
private static HttpMessageHandler InitMock()
26-
{
27-
const string mediaType = "application/json";
25+
private readonly MockHttpMessageHandler _mockHttp = InitMock();
26+
27+
28+
private static MockHttpMessageHandler InitMock()
29+
{
30+
2831
var mockHttp = new MockHttpMessageHandler();
2932
mockHttp.When($"{prefixEval}fail_500").Respond(HttpStatusCode.InternalServerError);
3033
mockHttp.When($"{prefixEval}api_key_missing").Respond(HttpStatusCode.BadRequest);
@@ -59,8 +62,15 @@ private static HttpMessageHandler InitMock()
5962
mockHttp.When($"{prefixEval}does_not_exists").Respond(mediaType,
6063
"{ \"value\":\"\", \"key\":\"does_not_exists\", \"errorCode\":\"FLAG_NOT_FOUND\", \"variant\":\"defaultSdk\", \"cacheable\":true, \"errorDetails\":\"flag does_not_exists was not found in your configuration\"}");
6164
mockHttp.When($"{prefixEval}integer_with_metadata").Respond(mediaType,
62-
"{ \"value\":100, \"key\":\"integer_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true, \"metadata\":{\"key1\": \"key1\", \"key2\": 1, \"key3\": 1.345, \"key4\": true}}");
65+
"{ \"value\":100, \"key\":\"integer_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true, \"metadata\":{\"key1\": \"key1\", \"key2\": 1, \"key3\": 1.345, \"key4\": true}}");
66+
for (int i = 0; i < 5; i++)
67+
{
68+
mockHttp.When($"{prefixEval}string_key{i}").Respond(mediaType,
69+
$"{{ \"value\":\"C{i}\", \"key\":\"string_key{i}\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}}");
70+
}
71+
6372
return mockHttp;
73+
6474
}
6575

6676
private static EvaluationContext InitDefaultEvaluationCtx()
@@ -300,8 +310,89 @@ public async Task should_throw_an_error_if_we_expect_a_string_and_got_another_ty
300310
Assert.Equal("default", result.Value);
301311
Assert.Equal(ErrorType.TypeMismatch, result.ErrorType);
302312
Assert.Equal(Reason.Error, result.Reason);
303-
}
313+
}
314+
315+
[Fact]
316+
public async Task should_cache_2nd_call()
317+
{
318+
var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
319+
{
320+
Endpoint = baseUrl,
321+
HttpMessageHandler = _mockHttp,
322+
Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
323+
});
324+
var req = _mockHttp.When($"{prefixEval}string_key_cache").Respond(mediaType,
325+
"{ \"value\":\"CC00AA\", \"key\":\"string_key_cache\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}");
326+
Assert.Equal(0, _mockHttp.GetMatchCount(req));
327+
328+
await Api.Instance.SetProviderAsync(g);
329+
var client = Api.Instance.GetClient("test-client");
330+
var result = await client.GetStringDetailsAsync("string_key_cache", "defaultValue", _defaultEvaluationCtx);
331+
Assert.NotNull(result);
332+
Assert.Equal("CC00AA", result.Value);
333+
Assert.Equal(1, _mockHttp.GetMatchCount(req));
334+
335+
var result2 = await client.GetStringDetailsAsync("string_key_cache", "defaultValue", _defaultEvaluationCtx);
336+
Assert.NotNull(result2);
337+
Assert.Equal("CC00AA", result.Value);
338+
Assert.Equal(1, _mockHttp.GetMatchCount(req)); // 2nd lookup should hit cache and not activate http
339+
}
340+
341+
342+
343+
[Fact]
344+
public async Task should_limit_cached_items()
345+
{
346+
var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
347+
{
348+
Endpoint = baseUrl,
349+
HttpMessageHandler = _mockHttp,
350+
Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond),
351+
CacheMaxSize = 2
352+
});
353+
await Api.Instance.SetProviderAsync(g);
354+
var client = Api.Instance.GetClient("test-client");
355+
356+
for (var i = 0; i < 5; i++)
357+
{
358+
var res1 = await client.GetStringDetailsAsync($"string_key{i}", "defaultValue", _defaultEvaluationCtx);
359+
Assert.NotNull(res1);
360+
Assert.Equal($"C{i}", res1.Value);
361+
}
362+
363+
Assert.Equal(2, g._backingCache.Count);
364+
}
365+
366+
[Fact]
367+
public async Task should_not_cache()
368+
{
369+
var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
370+
{
371+
Endpoint = baseUrl,
372+
HttpMessageHandler = _mockHttp,
373+
Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond),
374+
CacheMaxTTL = TimeSpan.FromSeconds(0)
375+
});
376+
var req = _mockHttp.When($"{prefixEval}string_key_cache").Respond(mediaType,
377+
"{ \"value\":\"CC00AA\", \"key\":\"string_key_cache\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}");
378+
379+
380+
Assert.Equal(0, _mockHttp.GetMatchCount(req));
304381

382+
await Api.Instance.SetProviderAsync(g);
383+
var client = Api.Instance.GetClient("test-client");
384+
var result = await client.GetStringDetailsAsync("string_key_cache", "defaultValue", _defaultEvaluationCtx);
385+
Assert.NotNull(result);
386+
Assert.Equal("CC00AA", result.Value);
387+
Assert.Equal(1, _mockHttp.GetMatchCount(req));
388+
389+
var result2 = await client.GetStringDetailsAsync("string_key_cache", "defaultValue", _defaultEvaluationCtx);
390+
Assert.NotNull(result2);
391+
Assert.Equal("CC00AA", result.Value);
392+
Assert.Equal(2, _mockHttp.GetMatchCount(req)); // 2nd lookup should not be cached, but hit http bringing total matches up
393+
}
394+
395+
305396
[Fact]
306397
public async Task should_resolve_a_valid_string_flag_with_TARGETING_MATCH_reason()
307398
{
@@ -387,6 +478,7 @@ public async Task should_use_integer_default_value_if_the_flag_is_disabled()
387478
await Api.Instance.SetProviderAsync(g);
388479
var client = Api.Instance.GetClient("test-client");
389480
var result = await client.GetIntegerDetailsAsync("disabled_integer", 1225, _defaultEvaluationCtx);
481+
390482
Assert.NotNull(result);
391483
Assert.Equal(1225, result.Value);
392484
Assert.Equal(Reason.Disabled, result.Reason);

0 commit comments

Comments
 (0)