diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider/EvalContextConverter.cs b/src/LaunchDarkly.OpenFeature.ServerProvider/EvalContextConverter.cs index 210dc7a..b361cab 100644 --- a/src/LaunchDarkly.OpenFeature.ServerProvider/EvalContextConverter.cs +++ b/src/LaunchDarkly.OpenFeature.ServerProvider/EvalContextConverter.cs @@ -33,6 +33,14 @@ public EvalContextConverter(Logger log) private static string InvalidTypeMessage(string attribute, string type) => $"The attribute '{attribute}' " + $"must be of type {type}"; + /// + /// Get the value as a string, or null if it is not a string or is empty. + /// + /// The value to inspect + /// The non-empty string value, or null + private static string NonEmptyString(Value value) => + value != null && value.IsString && value.AsString.Length != 0 ? value.AsString : null; + /// /// Extract a string value and log an error if the value was not a string. /// @@ -196,10 +204,17 @@ private Context BuildSingleLdContext(IImmutableDictionary attribu { // targetingKey is in the specification, so it takes precedence. attributes.TryGetValue("key", out var keyAttr); - attributes.TryGetValue("targetingKey", out var targetingKey); - var finalKey = (targetingKey ?? keyAttr)?.AsString; + attributes.TryGetValue("targetingKey", out var targetingKeyAttr); + var targetingKey = NonEmptyString(targetingKeyAttr); + var keyFromAttr = NonEmptyString(keyAttr); + var finalKey = targetingKey ?? keyFromAttr; + + if (keyAttr != null && !keyAttr.IsNull && !keyAttr.IsString) + { + _log.Warn("A non-string 'key' attribute was provided."); + } - if (keyAttr != null && targetingKey != null) + if (keyFromAttr != null && targetingKey != null) { _log.Warn("The EvaluationContext contained both a 'targetingKey' and a 'key' attribute. The 'key'" + " attribute will be discarded."); diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/EvalContextConverterTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/EvalContextConverterTests.cs index 01b8478..fdc2025 100644 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/EvalContextConverterTests.cs +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/EvalContextConverterTests.cs @@ -179,6 +179,46 @@ public void ItUsesTheTargetingKeyInFavorOfKey() Assert.Equal("targeting-key", _converter.ToLdContext(evaluationContext).Key); } + [Fact] + public void ItIgnoresANonStringKey() + { + var evaluationContext = EvaluationContext.Builder() + .Set("key", 42) + .Build(); + + Assert.False(_converter.ToLdContext(evaluationContext).Valid); + Assert.True(_logCapture.HasMessageWithText(LogLevel.Warn, + "A non-string 'key' attribute was provided.")); + Assert.True(_logCapture.HasMessageWithText(LogLevel.Error, + "The EvaluationContext must contain either a 'targetingKey' or a 'key' and the type" + + " must be a string.")); + } + + [Fact] + public void ItUsesTheKeyAttributeWhenTheTargetingKeyIsEmpty() + { + var evaluationContext = EvaluationContext.Builder() + .Set("targetingKey", "") + .Set("key", "the-key") + .Build(); + + Assert.Equal("the-key", _converter.ToLdContext(evaluationContext).Key); + Assert.Empty(_logCapture.GetMessages()); + } + + [Fact] + public void ItLogsAnErrorWhenTheOnlyKeyIsEmpty() + { + var evaluationContext = EvaluationContext.Builder() + .Set("targetingKey", "") + .Build(); + + Assert.False(_converter.ToLdContext(evaluationContext).Valid); + Assert.True(_logCapture.HasMessageWithText(LogLevel.Error, + "The EvaluationContext must contain either a 'targetingKey' or a 'key' and the type" + + " must be a string.")); + } + [Fact] public void ItCanBuildASingleContext() {