diff --git a/.gitmodules b/.gitmodules index 6940ffbf..b426c70e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "src/OpenFeature.Contrib.Providers.Flagd/schemas"] path = src/OpenFeature.Contrib.Providers.Flagd/schemas - url = git@github.com:open-feature/schemas.git + url = https://github.com/open-feature/schemas.git [submodule "spec"] path = spec url = https://github.com/open-feature/spec.git diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs index 6154982e..5b449a0a 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Text.RegularExpressions; using JsonLogic.Net; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -9,31 +10,24 @@ using OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators; using OpenFeature.Error; using OpenFeature.Model; -using System.Text.RegularExpressions; namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess { - internal class FlagConfiguration { - [JsonProperty("state")] - internal string State { get; set; } - [JsonProperty("defaultVariant")] - internal string DefaultVariant { get; set; } - [JsonProperty("variants")] - internal Dictionary Variants { get; set; } - [JsonProperty("targeting")] - internal object Targeting { get; set; } - [JsonProperty("source")] - internal string Source { get; set; } + [JsonProperty("state")] internal string State { get; set; } + [JsonProperty("defaultVariant")] internal string DefaultVariant { get; set; } + [JsonProperty("variants")] internal Dictionary Variants { get; set; } + [JsonProperty("targeting")] internal object Targeting { get; set; } + [JsonProperty("source")] internal string Source { get; set; } + [JsonProperty("metadata")] internal Dictionary Metadata { get; set; } } internal class FlagSyncData { - [JsonProperty("flags")] - internal Dictionary Flags { get; set; } - [JsonProperty("$evaluators")] - internal Dictionary Evaluators { get; set; } + [JsonProperty("flags")] internal Dictionary Flags { get; set; } + [JsonProperty("$evaluators")] internal Dictionary Evaluators { get; set; } + [JsonProperty("metadata")] internal Dictionary Metadata { get; set; } } internal class FlagConfigurationSync @@ -53,6 +47,7 @@ internal enum FlagConfigurationUpdateType internal class JsonEvaluator { private Dictionary _flags = new Dictionary(); + private Dictionary _flagSetMetadata = new Dictionary(); private string _selector; @@ -88,7 +83,57 @@ internal FlagSyncData Parse(string flagConfigurations) }); } - return JsonConvert.DeserializeObject(transformed); + + var data = JsonConvert.DeserializeObject(transformed); + if (data.Metadata == null) + { + data.Metadata = new Dictionary(); + } + else + { + foreach (var key in new List(data.Metadata.Keys)) + { + var value = data.Metadata[key]; + if (value is long longValue) + { + value = data.Metadata[key] = (int)longValue; + } + + VerifyMetadataValue(key, value); + } + } + + foreach (var flagConfig in data.Flags) + { + if (flagConfig.Value.Metadata == null) + { + continue; + } + + foreach (var key in new List(flagConfig.Value.Metadata.Keys)) + { + var value = flagConfig.Value.Metadata[key]; + if (value is long longValue) + { + value = flagConfig.Value.Metadata[key] = (int)longValue; + } + + VerifyMetadataValue(key, value); + } + } + + return data; + } + + private static void VerifyMetadataValue(string key, object value) + { + if (value is int || value is double || value is string || value is bool) + { + return; + } + + throw new ParseErrorException("Metadata entry for key " + key + " and value " + value + + " is of unknown type"); } internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurations) @@ -99,55 +144,69 @@ internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurat { case FlagConfigurationUpdateType.ALL: _flags = flagConfigsMap.Flags; + _flagSetMetadata = flagConfigsMap.Metadata; + break; case FlagConfigurationUpdateType.ADD: + case FlagConfigurationUpdateType.UPDATE: foreach (var keyAndValue in flagConfigsMap.Flags) { _flags[keyAndValue.Key] = keyAndValue.Value; } - break; - case FlagConfigurationUpdateType.UPDATE: - foreach (var keyAndValue in flagConfigsMap.Flags) + + foreach (var metadata in flagConfigsMap.Metadata) { - _flags[keyAndValue.Key] = keyAndValue.Value; + _flagSetMetadata[metadata.Key] = metadata.Value; } + break; case FlagConfigurationUpdateType.DELETE: foreach (var keyAndValue in flagConfigsMap.Flags) { _flags.Remove(keyAndValue.Key); } - break; + foreach (var keyValuePair in flagConfigsMap.Metadata) + { + _flagSetMetadata.Remove(keyValuePair.Key); + } + + break; } } - public ResolutionDetails ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext context = null) + public ResolutionDetails ResolveBooleanValueAsync(string flagKey, bool defaultValue, + EvaluationContext context = null) { return ResolveValue(flagKey, defaultValue, context); } - public ResolutionDetails ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext context = null) + public ResolutionDetails ResolveStringValueAsync(string flagKey, string defaultValue, + EvaluationContext context = null) { return ResolveValue(flagKey, defaultValue, context); } - public ResolutionDetails ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null) + public ResolutionDetails ResolveIntegerValueAsync(string flagKey, int defaultValue, + EvaluationContext context = null) { return ResolveValue(flagKey, defaultValue, context); } - public ResolutionDetails ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext context = null) + public ResolutionDetails ResolveDoubleValueAsync(string flagKey, double defaultValue, + EvaluationContext context = null) { return ResolveValue(flagKey, defaultValue, context); } - public ResolutionDetails ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext context = null) + public ResolutionDetails ResolveStructureValueAsync(string flagKey, Value defaultValue, + EvaluationContext context = null) { return ResolveValue(flagKey, defaultValue, context); } - private ResolutionDetails ResolveValue(string flagKey, T defaultValue, EvaluationContext context = null) + private ResolutionDetails ResolveValue(string flagKey, T defaultValue, + EvaluationContext context = null) { // check if we find the flag key var reason = Reason.Static; @@ -155,15 +214,30 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, Eva { if ("DISABLED" == flagConfiguration.State) { - throw new FeatureProviderException(ErrorType.FlagNotFound, "FLAG_NOT_FOUND: flag '" + flagKey + "' is disabled"); + throw new FeatureProviderException(ErrorType.FlagNotFound, + "FLAG_NOT_FOUND: flag '" + flagKey + "' is disabled"); + } + + Dictionary combinedMetadata = new Dictionary(_flagSetMetadata); + if (flagConfiguration.Metadata != null) + { + foreach (var metadataEntry in flagConfiguration.Metadata) + { + combinedMetadata[metadataEntry.Key] = metadataEntry.Value; + } } + + var flagMetadata = new ImmutableMetadata(combinedMetadata); var variant = flagConfiguration.DefaultVariant; - if (flagConfiguration.Targeting != null && !String.IsNullOrEmpty(flagConfiguration.Targeting.ToString()) && flagConfiguration.Targeting.ToString() != "{}") + if (flagConfiguration.Targeting != null && + !String.IsNullOrEmpty(flagConfiguration.Targeting.ToString()) && + flagConfiguration.Targeting.ToString() != "{}") { reason = Reason.TargetingMatch; var flagdProperties = new Dictionary(); flagdProperties.Add(FlagdProperties.FlagKeyKey, new Value(flagKey)); - flagdProperties.Add(FlagdProperties.TimestampKey, new Value(DateTimeOffset.UtcNow.ToUnixTimeSeconds())); + flagdProperties.Add(FlagdProperties.TimestampKey, + new Value(DateTimeOffset.UtcNow.ToUnixTimeSeconds())); if (context == null) { @@ -173,7 +247,7 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, Eva var targetingContext = context.AsDictionary().Add( FlagdProperties.FlagdPropertiesKey, new Value(new Structure(flagdProperties)) - ); + ); var targetingString = flagConfiguration.Targeting.ToString(); // Parse json into hierarchical structure @@ -202,32 +276,39 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, Eva { // if variant is null, revert to default reason = Reason.Default; - flagConfiguration.Variants.TryGetValue(flagConfiguration.DefaultVariant, out var defaultVariantValue); + flagConfiguration.Variants.TryGetValue(flagConfiguration.DefaultVariant, + out var defaultVariantValue); if (defaultVariantValue == null) { - throw new FeatureProviderException(ErrorType.ParseError, "PARSE_ERROR: flag '" + flagKey + "' has missing or invalid defaultVariant."); + throw new FeatureProviderException(ErrorType.ParseError, + "PARSE_ERROR: flag '" + flagKey + "' has missing or invalid defaultVariant."); } + var value = ExtractFoundVariant(defaultVariantValue, flagKey); return new ResolutionDetails( - flagKey: flagKey, - value, - reason: reason, - variant: variant - ); + flagKey: flagKey, + value, + reason: reason, + variant: variant, + flagMetadata: flagMetadata + ); } else if (flagConfiguration.Variants.TryGetValue(variant, out var foundVariantValue)) { // if variant can be found, return it - this could be TARGETING_MATCH or STATIC. var value = ExtractFoundVariant(foundVariantValue, flagKey); return new ResolutionDetails( - flagKey: flagKey, - value, - reason: reason, - variant: variant - ); + flagKey: flagKey, + value, + reason: reason, + variant: variant, + flagMetadata: flagMetadata + ); } } - throw new FeatureProviderException(ErrorType.FlagNotFound, "FLAG_NOT_FOUND: flag '" + flagKey + "' not found"); + + throw new FeatureProviderException(ErrorType.FlagNotFound, + "FLAG_NOT_FOUND: flag '" + flagKey + "' not found"); } static T ExtractFoundVariant(object foundVariantValue, string flagKey) @@ -236,6 +317,7 @@ static T ExtractFoundVariant(object foundVariantValue, string flagKey) { foundVariantValue = Convert.ToInt32(foundVariantValue); } + if (typeof(T) == typeof(double)) { foundVariantValue = Convert.ToDouble(foundVariantValue); @@ -244,11 +326,14 @@ static T ExtractFoundVariant(object foundVariantValue, string flagKey) { foundVariantValue = ConvertJObjectToOpenFeatureValue(value); } + if (foundVariantValue is T castValue) { return castValue; } - throw new FeatureProviderException(ErrorType.TypeMismatch, "TYPE_MISMATCH: flag '" + flagKey + "' does not match the expected type"); + + throw new FeatureProviderException(ErrorType.TypeMismatch, + "TYPE_MISMATCH: flag '" + flagKey + "' does not match the expected type"); } static dynamic ConvertToDynamicObject(IImmutableDictionary dictionary) @@ -259,7 +344,9 @@ static dynamic ConvertToDynamicObject(IImmutableDictionary dictio foreach (var kvp in dictionary) { expandoDict.Add(kvp.Key, - kvp.Value.IsStructure ? ConvertToDynamicObject(kvp.Value.AsStructure.AsDictionary()) : kvp.Value.AsObject); + kvp.Value.IsStructure + ? ConvertToDynamicObject(kvp.Value.AsStructure.AsDictionary()) + : kvp.Value.AsObject); } return expandoObject; @@ -302,4 +389,4 @@ static Value ConvertJObjectToOpenFeatureValue(JObject jsonValue) return new Value(new Structure(result)); } } -} +} \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs index 930250a2..9348c16f 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Immutable; using AutoFixture; using OpenFeature.Constant; @@ -10,7 +11,6 @@ namespace OpenFeature.Contrib.Providers.Flagd.Test { public class UnitTestJsonEvaluator { - [Fact] public void TestJsonEvaluatorAddFlagConfig() { @@ -23,7 +23,6 @@ public void TestJsonEvaluatorAddFlagConfig() var result = jsonEvaluator.ResolveBooleanValueAsync("validFlag", false); Assert.True(result.Value); - } [Fact] @@ -40,7 +39,6 @@ public void TestJsonEvaluatorAddStaticStringEvaluation() Assert.Equal("#CC0000", result.Value); Assert.Equal("red", result.Variant); Assert.Equal(Reason.Static, result.Reason); - } [Fact] @@ -57,7 +55,7 @@ public void TestJsonEvaluatorDynamicBoolEvaluation() var builder = EvaluationContext.Builder(); builder - .Set("color", "yellow"); + .Set("color", "yellow"); var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlag", false, builder.Build()); @@ -80,7 +78,8 @@ public void TestJsonEvaluatorDynamicBoolEvaluationUsingFlagdPropertyFlagKey() var builder = EvaluationContext.Builder(); - var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdProperty", false, builder.Build()); + var result = + jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdProperty", false, builder.Build()); Assert.True(result.Value); Assert.Equal("bool1", result.Variant); @@ -101,7 +100,8 @@ public void TestJsonEvaluatorDynamicBoolEvaluationUsingFlagdPropertyTimestamp() var builder = EvaluationContext.Builder(); - var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdPropertyTimestamp", false, builder.Build()); + var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdPropertyTimestamp", false, + builder.Build()); Assert.True(result.Value); Assert.Equal("bool1", result.Variant); @@ -119,7 +119,8 @@ public void TestJsonEvaluatorDynamicBoolEvaluationSharedEvaluator() var builder = EvaluationContext.Builder().Set("email", "test@faas.com"); - var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluator", false, builder.Build()); + var result = + jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluator", false, builder.Build()); Assert.True(result.Value); Assert.Equal("bool1", result.Variant); @@ -137,7 +138,9 @@ public void TestJsonEvaluatorDynamicBoolEvaluationSharedEvaluatorReturningBoolTy var builder = EvaluationContext.Builder().Set("email", "test@faas.com"); - var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluatorReturningBoolType", false, builder.Build()); + var result = + jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluatorReturningBoolType", false, + builder.Build()); Assert.True(result.Value); Assert.Equal("true", result.Variant); @@ -155,7 +158,9 @@ public void TestJsonEvaluatorDynamicBoolEvaluationWithMissingDefaultVariant() var builder = EvaluationContext.Builder(); - Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithMissingDefaultVariant", false, builder.Build())); + Assert.Throws(() => + jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithMissingDefaultVariant", false, + builder.Build())); } [Fact] @@ -169,7 +174,9 @@ public void TestJsonEvaluatorDynamicBoolEvaluationWithUnexpectedVariantType() var builder = EvaluationContext.Builder(); - Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithUnexpectedVariantType", false, builder.Build())); + Assert.Throws(() => + jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithUnexpectedVariantType", false, + builder.Build())); } [Fact] @@ -186,7 +193,7 @@ public void TestJsonEvaluatorDynamicStringEvaluation() var builder = EvaluationContext.Builder(); builder - .Set("color", "yellow"); + .Set("color", "yellow"); var result = jsonEvaluator.ResolveStringValueAsync("targetingStringFlag", "", builder.Build()); @@ -209,7 +216,7 @@ public void TestJsonEvaluatorDynamicFloatEvaluation() var builder = EvaluationContext.Builder(); builder - .Set("color", "yellow"); + .Set("color", "yellow"); var result = jsonEvaluator.ResolveDoubleValueAsync("targetingFloatFlag", 0, builder.Build()); @@ -232,7 +239,7 @@ public void TestJsonEvaluatorDynamicIntEvaluation() var builder = EvaluationContext.Builder(); builder - .Set("color", "yellow"); + .Set("color", "yellow"); var result = jsonEvaluator.ResolveIntegerValueAsync("targetingNumberFlag", 0, builder.Build()); @@ -255,7 +262,7 @@ public void TestJsonEvaluatorDynamicObjectEvaluation() var builder = EvaluationContext.Builder(); builder - .Set("color", "yellow"); + .Set("color", "yellow"); var result = jsonEvaluator.ResolveStructureValueAsync("targetingObjectFlag", null, builder.Build()); @@ -280,7 +287,8 @@ public void TestJsonEvaluatorDisabledBoolEvaluation() builder .Set("color", "yellow"); - Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("disabledFlag", false, builder.Build())); + Assert.Throws(() => + jsonEvaluator.ResolveBooleanValueAsync("disabledFlag", false, builder.Build())); } [Fact] @@ -299,7 +307,8 @@ public void TestJsonEvaluatorFlagNotFoundEvaluation() builder .Set("color", "yellow"); - Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("missingFlag", false, builder.Build())); + Assert.Throws(() => + jsonEvaluator.ResolveBooleanValueAsync("missingFlag", false, builder.Build())); } [Fact] @@ -318,7 +327,80 @@ public void TestJsonEvaluatorWrongTypeEvaluation() builder .Set("color", "yellow"); - Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("staticStringFlag", false, builder.Build())); + Assert.Throws(() => + jsonEvaluator.ResolveBooleanValueAsync("staticStringFlag", false, builder.Build())); + } + + [Fact] + public void TestJsonEvaluatorReturnsFlagMetadata() + { + var fixture = new Fixture(); + + var jsonEvaluator = new JsonEvaluator(fixture.Create()); + + jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.flags); + + var result = jsonEvaluator.ResolveBooleanValueAsync("metadata-flag", false); + Assert.NotNull(result.FlagMetadata); + Assert.Equal("1.0.2", result.FlagMetadata.GetString("string")); + Assert.Equal(2, result.FlagMetadata.GetInt("integer")); + Assert.Equal(true, result.FlagMetadata.GetBool("boolean")); + Assert.Equal(.1, result.FlagMetadata.GetDouble("float")); + } + + [Fact] + public void TestJsonEvaluatorAddsFlagSetMetadataToFlagWithoutMetadata() + { + var fixture = new Fixture(); + + var jsonEvaluator = new JsonEvaluator(fixture.Create()); + + jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.metadataFlags); + + var result = jsonEvaluator.ResolveBooleanValueAsync("without-metadata-flag", false); + Assert.NotNull(result.FlagMetadata); + Assert.Equal("1.0.3", result.FlagMetadata.GetString("string")); + Assert.Equal(3, result.FlagMetadata.GetInt("integer")); + Assert.Equal(false, result.FlagMetadata.GetBool("boolean")); + Assert.Equal(.2, result.FlagMetadata.GetDouble("float")); + } + + [Fact] + public void TestJsonEvaluatorFlagMetadataOverwritesFlagSetMetadata() + { + var fixture = new Fixture(); + + var jsonEvaluator = new JsonEvaluator(fixture.Create()); + + jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.metadataFlags); + + var result = jsonEvaluator.ResolveBooleanValueAsync("metadata-flag", false); + + Assert.NotNull(result.FlagMetadata); + Assert.Equal("1.0.2", result.FlagMetadata.GetString("string")); + Assert.Equal(2, result.FlagMetadata.GetInt("integer")); + Assert.Equal(true, result.FlagMetadata.GetBool("boolean")); + Assert.Equal(.1, result.FlagMetadata.GetDouble("float")); + } + + [Fact] + public void TestJsonEvaluatorThrowsOnInvalidFlagSetMetadata() + { + var fixture = new Fixture(); + + var jsonEvaluator = new JsonEvaluator(fixture.Create()); + + Assert.Throws(() => jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.invalidFlagSetMetadata)); + } + + [Fact] + public void TestJsonEvaluatorThrowsOnInvalidFlagMetadata() + { + var fixture = new Fixture(); + + var jsonEvaluator = new JsonEvaluator(fixture.Create()); + + Assert.Throws(() => jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.invalidFlagMetadata)); } } } diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs index c1db7aaa..de386478 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs @@ -301,10 +301,95 @@ public class Utils ""off"": false }, ""defaultVariant"": ""on"" + }, + ""metadata-flag"": { + ""state"": ""ENABLED"", + ""variants"": { + ""on"": true, + ""off"": false + }, + ""defaultVariant"": ""on"", + ""metadata"": { + ""string"": ""1.0.2"", + ""integer"": 2, + ""boolean"": true, + ""float"": 0.1, + } + } + } +}"; + + public static string metadataFlags = @"{ + ""flags"":{ + ""metadata-flag"": { + ""state"": ""ENABLED"", + ""variants"": { + ""on"": true, + ""off"": false + }, + ""defaultVariant"": ""on"", + ""metadata"":{ + ""string"": ""1.0.2"", + ""integer"": 2, + ""boolean"": true, + ""float"": 0.1, + } + }, + ""without-metadata-flag"": { + ""state"": ""ENABLED"", + ""variants"": { + ""on"": true, + ""off"": false + }, + ""defaultVariant"": ""on"" } + }, + ""metadata"": { + ""string"": ""1.0.3"", + ""integer"": 3, + ""boolean"": false, + ""float"": 0.2, } }"; + public static string invalidFlagSetMetadata = @"{ + ""flags"":{ + ""without-metadata-flag"": { + ""state"": ""ENABLED"", + ""variants"": { + ""on"": true, + ""off"": false + }, + ""defaultVariant"": ""on"" + } + }, + ""metadata"": { + ""string"": {""in"": ""valid""}, + ""integer"": 3, + ""boolean"": false, + ""float"": 0.2, + } +}"; + public static string invalidFlagMetadata = @"{ + ""flags"":{ + ""invalid-metadata-flag"": { + ""state"": ""ENABLED"", + ""variants"": { + ""on"": true, + ""off"": false + }, + ""defaultVariant"": ""on"", + ""metadata"": { + ""string"": ""1.0.2"", + ""integer"": 2, + ""boolean"": true, + ""float"": {""in"": ""valid""}, + } + }, + } +}"; + + /// /// Repeatedly runs the supplied assertion until it doesn't throw, or the timeout is reached. /// @@ -312,11 +397,11 @@ public class Utils /// Timeout in millis (defaults to 1000) /// Poll interval (defaults to 100 /// - public static async Task AssertUntilAsync(Action assertionFunc, int timeoutMillis = 1000, int pollIntervalMillis = 100) + public static async Task AssertUntilAsync(Action assertionFunc, int timeoutMillis = 1000, + int pollIntervalMillis = 100) { using (var cts = CancellationTokenSource.CreateLinkedTokenSource(default(CancellationToken))) { - cts.CancelAfter(timeoutMillis); var exceptions = new List(); @@ -347,6 +432,7 @@ public static async Task AssertUntilAsync(Action assertionFun throw new AggregateException(message, exceptions); } } + throw new AggregateException(message, exceptions); } }