6
6
using System . Text . Json ;
7
7
using System . Threading . Tasks ;
8
8
using Newtonsoft . Json . Linq ;
9
+ using NSubstitute ;
9
10
using OpenFeature . Constant ;
10
11
using OpenFeature . Contrib . Providers . GOFeatureFlag . exception ;
11
12
using OpenFeature . Contrib . Providers . GOFeatureFlag . models ;
@@ -17,14 +18,16 @@ namespace OpenFeature.Contrib.Providers.GOFeatureFlag.Test;
17
18
18
19
public class GoFeatureFlagProviderTest
19
20
{
21
+ private static readonly string mediaType = "application/json" ;
20
22
private static readonly string baseUrl = "http://gofeatureflag.org" ;
21
23
private static readonly string prefixEval = baseUrl + "/ofrep/v1/evaluate/flags/" ;
22
24
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
+
28
31
var mockHttp = new MockHttpMessageHandler ( ) ;
29
32
mockHttp . When ( $ "{ prefixEval } fail_500") . Respond ( HttpStatusCode . InternalServerError ) ;
30
33
mockHttp . When ( $ "{ prefixEval } api_key_missing") . Respond ( HttpStatusCode . BadRequest ) ;
@@ -59,8 +62,15 @@ private static HttpMessageHandler InitMock()
59
62
mockHttp . When ( $ "{ prefixEval } does_not_exists") . Respond ( mediaType ,
60
63
"{ \" value\" :\" \" , \" key\" :\" does_not_exists\" , \" errorCode\" :\" FLAG_NOT_FOUND\" , \" variant\" :\" defaultSdk\" , \" cacheable\" :true, \" errorDetails\" :\" flag does_not_exists was not found in your configuration\" }" ) ;
61
64
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
+
63
72
return mockHttp ;
73
+
64
74
}
65
75
66
76
private static EvaluationContext InitDefaultEvaluationCtx ( )
@@ -300,8 +310,89 @@ public async Task should_throw_an_error_if_we_expect_a_string_and_got_another_ty
300
310
Assert . Equal ( "default" , result . Value ) ;
301
311
Assert . Equal ( ErrorType . TypeMismatch , result . ErrorType ) ;
302
312
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 ) ) ;
304
381
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
+
305
396
[ Fact ]
306
397
public async Task should_resolve_a_valid_string_flag_with_TARGETING_MATCH_reason ( )
307
398
{
@@ -387,6 +478,7 @@ public async Task should_use_integer_default_value_if_the_flag_is_disabled()
387
478
await Api . Instance . SetProviderAsync ( g ) ;
388
479
var client = Api . Instance . GetClient ( "test-client" ) ;
389
480
var result = await client . GetIntegerDetailsAsync ( "disabled_integer" , 1225 , _defaultEvaluationCtx ) ;
481
+
390
482
Assert . NotNull ( result ) ;
391
483
Assert . Equal ( 1225 , result . Value ) ;
392
484
Assert . Equal ( Reason . Disabled , result . Reason ) ;
0 commit comments