From ccaf982aeb65510c5869f6bc185e1ee189c502a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Tue, 17 Mar 2020 05:26:21 +0100 Subject: [PATCH] Update OpenIddictMessage.AddParameter() to throw an exception when the parameter already exists and introduce new APIs --- .../Primitives/OpenIddictConverter.cs | 62 ++-------- .../Primitives/OpenIddictMessage.cs | 102 ++++++++-------- .../Primitives/OpenIddictParameter.cs | 111 +++++++++++------- .../Primitives/OpenIddictRequest.cs | 26 ++-- .../Primitives/OpenIddictResponse.cs | 26 ++-- .../OpenIddictServerHandlers.Introspection.cs | 12 +- .../Primitives/OpenIddictConverterTests.cs | 76 +++++------- .../Primitives/OpenIddictMessageTests.cs | 108 +++++++++++++---- .../Primitives/OpenIddictParameterTests.cs | 53 ++++++++- ...ictServerIntegrationTests.Introspection.cs | 2 +- 10 files changed, 339 insertions(+), 239 deletions(-) diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictConverter.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictConverter.cs index 9963caf6..3b778084 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictConverter.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictConverter.cs @@ -28,7 +28,9 @@ namespace OpenIddict.Abstractions throw new ArgumentNullException(nameof(type)); } - return typeof(OpenIddictMessage).IsAssignableFrom(type); + return type == typeof(OpenIddictMessage) || + type == typeof(OpenIddictRequest) || + type == typeof(OpenIddictResponse); } /// @@ -45,41 +47,12 @@ namespace OpenIddict.Abstractions throw new ArgumentNullException(nameof(type)); } - // Note: OpenIddict primitives are always represented as JSON objects. - var payload = JsonSerializer.Deserialize(ref reader, options); - if (payload.ValueKind != JsonValueKind.Object) - { - throw new JsonException("An error occurred while reading the JSON payload."); - } + using var document = JsonDocument.ParseValue(ref reader); - OpenIddictMessage message; - - if (type == typeof(OpenIddictMessage)) - { - message = new OpenIddictMessage(); - } - - else if (type == typeof(OpenIddictRequest)) - { - message = new OpenIddictRequest(); - } - - else if (type == typeof(OpenIddictResponse)) - { - message = new OpenIddictResponse(); - } - - else - { - throw new ArgumentException("The specified type is not supported.", nameof(type)); - } - - foreach (var parameter in payload.EnumerateObject()) - { - message.AddParameter(parameter.Name, parameter.Value); - } - - return message; + return type == typeof(OpenIddictMessage) ? new OpenIddictMessage(document.RootElement.Clone()) : + type == typeof(OpenIddictRequest) ? (OpenIddictMessage) new OpenIddictRequest(document.RootElement.Clone()) : + type == typeof(OpenIddictResponse) ? new OpenIddictResponse(document.RootElement.Clone()) : + throw new ArgumentException("The specified type is not supported.", nameof(type)); } /// @@ -100,24 +73,7 @@ namespace OpenIddict.Abstractions throw new ArgumentNullException(nameof(value)); } - writer.WriteStartObject(); - - foreach (var parameter in value.GetParameters()) - { - writer.WritePropertyName(parameter.Key); - - var token = (JsonElement?) parameter.Value; - if (token == null) - { - writer.WriteNullValue(); - - continue; - } - - token.Value.WriteTo(writer); - } - - writer.WriteEndObject(); + value.WriteTo(writer); } } } diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs index e75c6701..3b92fc66 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; @@ -32,27 +33,31 @@ namespace OpenIddict.Abstractions /// /// Initializes a new OpenIddict message. /// - public OpenIddictMessage() { } + public OpenIddictMessage() + { + } /// /// Initializes a new OpenIddict message. /// /// The message parameters. - public OpenIddictMessage([NotNull] IEnumerable> parameters) + public OpenIddictMessage(JsonElement parameters) { - if (parameters == null) + if (parameters.ValueKind != JsonValueKind.Object) { - throw new ArgumentNullException(nameof(parameters)); + throw new ArgumentException("The specified JSON element is not an object.", nameof(parameters)); } - foreach (var parameter in parameters) + foreach (var parameter in parameters.EnumerateObject()) { - if (string.IsNullOrEmpty(parameter.Key)) + // While generally discouraged, JSON objects can contain multiple properties with + // the same name. In this case, the last occurrence replaces the previous ones. + if (HasParameter(parameter.Name)) { - continue; + RemoveParameter(parameter.Name); } - AddParameter(parameter.Key, parameter.Value); + AddParameter(parameter.Name, parameter.Value); } } @@ -69,11 +74,6 @@ namespace OpenIddict.Abstractions foreach (var parameter in parameters) { - if (string.IsNullOrEmpty(parameter.Key)) - { - continue; - } - AddParameter(parameter.Key, parameter.Value); } } @@ -89,14 +89,9 @@ namespace OpenIddict.Abstractions throw new ArgumentNullException(nameof(parameters)); } - foreach (var parameter in parameters) + foreach (var parameter in parameters.GroupBy(parameter => parameter.Key)) { - if (string.IsNullOrEmpty(parameter.Key)) - { - continue; - } - - AddParameter(parameter.Key, parameter.Value); + AddParameter(parameter.Key, parameter.Select(parameter => parameter.Value).ToArray()); } } @@ -113,11 +108,6 @@ namespace OpenIddict.Abstractions foreach (var parameter in parameters) { - if (string.IsNullOrEmpty(parameter.Key)) - { - continue; - } - // Note: the core OAuth 2.0 specification requires that request parameters // not be present more than once but derived specifications like the // token exchange RFC deliberately allow specifying multiple resource @@ -126,8 +116,8 @@ namespace OpenIddict.Abstractions { null => default, 0 => default, - 1 => new OpenIddictParameter(parameter.Value[0]), - _ => new OpenIddictParameter(parameter.Value) + 1 => parameter.Value[0], + _ => parameter.Value }); } } @@ -145,11 +135,6 @@ namespace OpenIddict.Abstractions foreach (var parameter in parameters) { - if (string.IsNullOrEmpty(parameter.Key)) - { - continue; - } - // Note: the core OAuth 2.0 specification requires that request parameters // not be present more than once but derived specifications like the // token exchange RFC deliberately allow specifying multiple resource @@ -157,8 +142,8 @@ namespace OpenIddict.Abstractions AddParameter(parameter.Key, parameter.Value.Count switch { 0 => default, - 1 => new OpenIddictParameter(parameter.Value[0]), - _ => new OpenIddictParameter(parameter.Value.ToArray()) + 1 => parameter.Value[0], + _ => parameter.Value.ToArray() }); } } @@ -186,8 +171,7 @@ namespace OpenIddict.Abstractions = new Dictionary(StringComparer.Ordinal); /// - /// Adds a parameter. Note: if a parameter with the - /// same name was already added, this method has no effect. + /// Adds a parameter. Note: an exception is thrown if a parameter with the same name was already added. /// /// The parameter name. /// The parameter value. @@ -199,11 +183,13 @@ namespace OpenIddict.Abstractions throw new ArgumentException("The parameter name cannot be null or empty.", nameof(name)); } - if (!Parameters.ContainsKey(name)) + if (Parameters.ContainsKey(name)) { - Parameters.Add(name, value); + throw new ArgumentException("A parameter with the same name already exists.", nameof(name)); } + Parameters.Add(name, value); + return this; } @@ -279,8 +265,7 @@ namespace OpenIddict.Abstractions throw new ArgumentException("The parameter name cannot be null or empty.", nameof(name)); } - // If the parameter value is null or empty, - // remove the corresponding entry from the collection. + // If the parameter value is null or empty, remove the corresponding entry from the collection. if (value == null || OpenIddictParameter.IsNullOrEmpty(value.GetValueOrDefault())) { Parameters.Remove(name); @@ -343,22 +328,11 @@ namespace OpenIddict.Abstractions case OpenIddictConstants.Parameters.Password: case OpenIddictConstants.Parameters.RefreshToken: case OpenIddictConstants.Parameters.Token: - { - writer.WriteStringValue("[removed for security reasons]"); - + writer.WriteStringValue("[redacted]"); continue; - } - } - - var token = (JsonElement?) parameter.Value; - if (token == null) - { - writer.WriteNullValue(); - - continue; } - token.Value.WriteTo(writer); + parameter.Value.WriteTo(writer); } writer.WriteEndObject(); @@ -366,5 +340,27 @@ namespace OpenIddict.Abstractions return Encoding.UTF8.GetString(stream.ToArray()); } + + /// + /// Writes the message to the specified JSON writer. + /// + /// The UTF-8 JSON writer. + public void WriteTo(Utf8JsonWriter writer) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + + foreach (var parameter in Parameters) + { + writer.WritePropertyName(parameter.Key); + parameter.Value.WriteTo(writer); + } + + writer.WriteEndObject(); + } } } diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs index a2645edc..121bcd67 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs @@ -22,57 +22,43 @@ namespace OpenIddict.Abstractions public readonly struct OpenIddictParameter : IEquatable { /// - /// Initializes a new OpenID Connect - /// parameter using the specified value. + /// Initializes a new OpenID Connect parameter using the specified value. /// /// The parameter value. public OpenIddictParameter(bool value) => Value = value; /// - /// Initializes a new OpenID Connect - /// parameter using the specified value. + /// Initializes a new OpenID Connect parameter using the specified value. /// /// The parameter value. public OpenIddictParameter(bool? value) => Value = value; /// - /// Initializes a new OpenID Connect - /// parameter using the specified value. + /// Initializes a new OpenID Connect parameter using the specified value. /// /// The parameter value. public OpenIddictParameter(JsonElement value) => Value = value; /// - /// Initializes a new OpenID Connect - /// parameter using the specified value. - /// - /// The parameter value. - public OpenIddictParameter(JsonElement? value) => Value = value; - - /// - /// Initializes a new OpenID Connect - /// parameter using the specified value. + /// Initializes a new OpenID Connect parameter using the specified value. /// /// The parameter value. public OpenIddictParameter(long value) => Value = value; /// - /// Initializes a new OpenID Connect - /// parameter using the specified value. + /// Initializes a new OpenID Connect parameter using the specified value. /// /// The parameter value. public OpenIddictParameter(long? value) => Value = value; /// - /// Initializes a new OpenID Connect - /// parameter using the specified value. + /// Initializes a new OpenID Connect parameter using the specified value. /// /// The parameter value. public OpenIddictParameter(string value) => Value = value; /// - /// Initializes a new OpenID Connect - /// parameter using the specified value. + /// Initializes a new OpenID Connect parameter using the specified value. /// /// The parameter value. public OpenIddictParameter(string[] value) => Value = value; @@ -448,6 +434,56 @@ namespace OpenIddict.Abstractions return false; } + /// + /// Writes the parameter value to the specified JSON writer. + /// + /// The UTF-8 JSON writer. + public void WriteTo(Utf8JsonWriter writer) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + switch (Value) + { + // Note: undefined JsonElement values are assimilated to null values. + case null: + case JsonElement value when value.ValueKind == JsonValueKind.Undefined: + writer.WriteNullValue(); + break; + + case bool value: + writer.WriteBooleanValue(value); + break; + + case long value: + writer.WriteNumberValue(value); + break; + + case string value: + writer.WriteStringValue(value); + break; + + case string[] value: + writer.WriteStartArray(); + + for (var index = 0; index < value.Length; index++) + { + writer.WriteStringValue(value[index]); + } + + writer.WriteEndArray(); + break; + + case JsonElement value: + value.WriteTo(writer); + break; + + default: throw new InvalidOperationException("The type of the parameter value is not supported."); + } + } + /// /// Determines whether two instances are equal. /// @@ -510,40 +546,31 @@ namespace OpenIddict.Abstractions /// The parameter to convert. /// The converted value. public static explicit operator JsonElement(OpenIddictParameter? parameter) - => ((JsonElement?) parameter).GetValueOrDefault(); - - /// - /// Converts an instance to a nullale . - /// - /// The parameter to convert. - /// The converted value. - public static explicit operator JsonElement?(OpenIddictParameter? parameter) { return parameter?.Value switch { - // When the parameter is a null value, return null. - null => null, - - // When the parameter is a JsonElement representing null, return null. - JsonElement value when value.ValueKind == JsonValueKind.Undefined => null, - JsonElement value when value.ValueKind == JsonValueKind.Null => null, + // When the parameter is a null value, return default. + null => default, // When the parameter is already a JsonElement, return it as-is. JsonElement value => value, - // When the parameter is a string, try to derialize it to get a JsonElement instance. - string value => DeserializeElement(value) ?? DeserializeElement(JsonSerializer.Serialize(value, new JsonSerializerOptions - { - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - WriteIndented = false - })), + // When the parameter is a string starting with '{' or '[' (which would correspond + // to a JSON object or array), try to deserialize it to get a JsonElement instance. + string value when value.Length != 0 && (value[0] == '{' || value[0] == '[') => + DeserializeElement(value) ?? + DeserializeElement(JsonSerializer.Serialize(value, new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + WriteIndented = false + })) ?? default, // Otherwise, serialize it to get a JsonElement instance. var value => DeserializeElement(JsonSerializer.Serialize(value, new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, WriteIndented = false - })) + })) ?? default }; static JsonElement? DeserializeElement(string value) diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs index f2e2cd2a..e9222a8c 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs @@ -29,42 +29,54 @@ namespace OpenIddict.Abstractions /// Initializes a new OpenIddict request. /// public OpenIddictRequest() - : base() { } + : base() + { + } /// /// Initializes a new OpenIddict request. /// /// The request parameters. - public OpenIddictRequest([NotNull] IEnumerable> parameters) - : base(parameters) { } + public OpenIddictRequest(JsonElement parameters) + : base(parameters) + { + } /// /// Initializes a new OpenIddict request. /// /// The request parameters. public OpenIddictRequest([NotNull] IEnumerable> parameters) - : base(parameters) { } + : base(parameters) + { + } /// /// Initializes a new OpenIddict request. /// /// The request parameters. public OpenIddictRequest([NotNull] IEnumerable> parameters) - : base(parameters) { } + : base(parameters) + { + } /// /// Initializes a new OpenIddict request. /// /// The request parameters. public OpenIddictRequest([NotNull] IEnumerable> parameters) - : base(parameters) { } + : base(parameters) + { + } /// /// Initializes a new OpenIddict request. /// /// The request parameters. public OpenIddictRequest([NotNull] IEnumerable> parameters) - : base(parameters) { } + : base(parameters) + { + } /// /// Gets or sets the "access_token" parameter. diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs index 92849f6a..7ce1daff 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs @@ -29,42 +29,54 @@ namespace OpenIddict.Abstractions /// Initializes a new OpenIddict response. /// public OpenIddictResponse() - : base() { } + : base() + { + } /// /// Initializes a new OpenIddict response. /// /// The response parameters. - public OpenIddictResponse([NotNull] IEnumerable> parameters) - : base(parameters) { } + public OpenIddictResponse(JsonElement parameters) + : base(parameters) + { + } /// /// Initializes a new OpenIddict response. /// /// The response parameters. public OpenIddictResponse([NotNull] IEnumerable> parameters) - : base(parameters) { } + : base(parameters) + { + } /// /// Initializes a new OpenIddict response. /// /// The response parameters. public OpenIddictResponse([NotNull] IEnumerable> parameters) - : base(parameters) { } + : base(parameters) + { + } /// /// Initializes a new OpenIddict response. /// /// The response parameters. public OpenIddictResponse([NotNull] IEnumerable> parameters) - : base(parameters) { } + : base(parameters) + { + } /// /// Initializes a new OpenIddict response. /// /// The response parameters. public OpenIddictResponse([NotNull] IEnumerable> parameters) - : base(parameters) { } + : base(parameters) + { + } /// /// Gets or sets the "access_token" parameter. diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs index 6bff04c6..cc696796 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs @@ -1168,7 +1168,7 @@ namespace OpenIddict.Server // When multiple claims share the same type, retrieve the underlying // JSON values and add everything to a new unique JSON array. - _ => JsonSerializer.Deserialize(JsonSerializer.Serialize( + _ => DeserializeElement(JsonSerializer.Serialize( claims.Select(claim => ConvertToParameter(claim).Value), new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, @@ -1185,11 +1185,17 @@ namespace OpenIddict.Server ClaimValueTypes.Integer32 => int.Parse(claim.Value, CultureInfo.InvariantCulture), ClaimValueTypes.Integer64 => long.Parse(claim.Value, CultureInfo.InvariantCulture), - JsonClaimValueTypes.Json => JsonSerializer.Deserialize(claim.Value), - JsonClaimValueTypes.JsonArray => JsonSerializer.Deserialize(claim.Value), + JsonClaimValueTypes.Json => DeserializeElement(claim.Value), + JsonClaimValueTypes.JsonArray => DeserializeElement(claim.Value), _ => new OpenIddictParameter(claim.Value) }; + + static JsonElement DeserializeElement(string value) + { + using var document = JsonDocument.Parse(value); + return document.RootElement.Clone(); + } } } diff --git a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictConverterTests.cs b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictConverterTests.cs index 890142c8..ecee7a3d 100644 --- a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictConverterTests.cs +++ b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictConverterTests.cs @@ -6,7 +6,6 @@ using System; using System.IO; -using System.Linq; using System.Text; using System.Text.Json; using Xunit; @@ -56,31 +55,15 @@ namespace OpenIddict.Abstractions.Tests.Primitives var converter = new OpenIddictConverter(); // Act and assert - var exception = Assert.Throws(() => + var exception = Assert.Throws(delegate { var reader = new Utf8JsonReader(); - converter.Read(ref reader, type: null, options: null); + return converter.Read(ref reader, type: null, options: null); }); Assert.Equal("type", exception.ParamName); } - [Fact] - public void Read_ThrowsAnExceptionForUnexpectedJsonToken() - { - // Arrange - var converter = new OpenIddictConverter(); - - // Act and assert - var exception = Assert.Throws(() => - { - var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes("[0,1,2,3]")); - converter.Read(ref reader, type: typeof(OpenIddictRequest), options: null); - }); - - Assert.Equal("An error occurred while reading the JSON payload.", exception.Message); - } - [Theory] [InlineData(typeof(OpenIddictMessage[]))] [InlineData(typeof(OpenIddictRequest[]))] @@ -97,10 +80,10 @@ namespace OpenIddict.Abstractions.Tests.Primitives var converter = new OpenIddictConverter(); // Act and assert - var exception = Assert.Throws(() => + var exception = Assert.Throws(delegate { var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(@"{""name"":""value""}")); - converter.Read(ref reader, type, options: null); + return converter.Read(ref reader, type, options: null); }); Assert.StartsWith("The specified type is not supported.", exception.Message); @@ -118,11 +101,11 @@ namespace OpenIddict.Abstractions.Tests.Primitives var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(@"{""name"":""value""}")); // Act - var result = converter.Read(ref reader, type, options: null); + var message = converter.Read(ref reader, type, options: null); // Assert - Assert.IsType(type, result); - Assert.Equal("value", (string) result.GetParameter("name")); + Assert.IsType(type, message); + Assert.Equal("value", (string) message.GetParameter("name")); } [Fact] @@ -134,20 +117,20 @@ namespace OpenIddict.Abstractions.Tests.Primitives @"{""string"":null,""bool"":null,""long"":null,""array"":null,""object"":null}")); // Act - var result = converter.Read(ref reader, typeof(OpenIddictMessage), options: null); + var message = converter.Read(ref reader, typeof(OpenIddictMessage), options: null); // Assert - Assert.Equal(5, result.GetParameters().Count()); - Assert.NotNull(result.GetParameter("string")); - Assert.NotNull(result.GetParameter("bool")); - Assert.NotNull(result.GetParameter("long")); - Assert.NotNull(result.GetParameter("array")); - Assert.NotNull(result.GetParameter("object")); - Assert.Null((string) result.GetParameter("string")); - Assert.Null((bool?) result.GetParameter("bool")); - Assert.Null((long?) result.GetParameter("long")); - Assert.Null((JsonElement?) result.GetParameter("array")); - Assert.Null((JsonElement?) result.GetParameter("object")); + Assert.Equal(5, message.Count); + Assert.NotNull(message.GetParameter("string")); + Assert.NotNull(message.GetParameter("bool")); + Assert.NotNull(message.GetParameter("long")); + Assert.NotNull(message.GetParameter("array")); + Assert.NotNull(message.GetParameter("object")); + Assert.Null((string) message.GetParameter("string")); + Assert.Null((bool?) message.GetParameter("bool")); + Assert.Null((long?) message.GetParameter("long")); + Assert.Equal(JsonValueKind.Null, ((JsonElement) message.GetParameter("array")).ValueKind); + Assert.Equal(JsonValueKind.Null, ((JsonElement) message.GetParameter("object")).ValueKind); } [Fact] @@ -158,16 +141,16 @@ namespace OpenIddict.Abstractions.Tests.Primitives var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(@"{""string"":"""",""array"":[],""object"":{}}")); // Act - var result = converter.Read(ref reader, typeof(OpenIddictMessage), options: null); + var message = converter.Read(ref reader, typeof(OpenIddictMessage), options: null); // Assert - Assert.Equal(3, result.GetParameters().Count()); - Assert.NotNull(result.GetParameter("string")); - Assert.NotNull(result.GetParameter("array")); - Assert.NotNull(result.GetParameter("object")); - Assert.Empty((string) result.GetParameter("string")); - Assert.NotNull((JsonElement?) result.GetParameter("array")); - Assert.NotNull((JsonElement?) result.GetParameter("object")); + Assert.Equal(3, message.Count); + Assert.NotNull(message.GetParameter("string")); + Assert.NotNull(message.GetParameter("array")); + Assert.NotNull(message.GetParameter("object")); + Assert.Empty((string) message.GetParameter("string")); + Assert.NotNull((JsonElement?) message.GetParameter("array")); + Assert.NotNull((JsonElement?) message.GetParameter("object")); } [Fact] @@ -232,8 +215,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives message.AddParameter("string", new OpenIddictParameter((string) null)); message.AddParameter("bool", new OpenIddictParameter((bool?) null)); message.AddParameter("long", new OpenIddictParameter((long?) null)); - message.AddParameter("array", new OpenIddictParameter((JsonElement?) null)); - message.AddParameter("object", new OpenIddictParameter((JsonElement?) null)); + message.AddParameter("node", new OpenIddictParameter(default(JsonElement))); // Act converter.Write(writer, value: message, options: null); @@ -241,7 +223,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives // Assert writer.Flush(); stream.Seek(0L, SeekOrigin.Begin); - Assert.Equal(@"{""string"":null,""bool"":null,""long"":null,""array"":null,""object"":null}", reader.ReadToEnd()); + Assert.Equal(@"{""string"":null,""bool"":null,""long"":null,""node"":null}", reader.ReadToEnd()); } [Fact] diff --git a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictMessageTests.cs b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictMessageTests.cs index 11e98dc8..6c190ebc 100644 --- a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictMessageTests.cs +++ b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictMessageTests.cs @@ -6,7 +6,8 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.IO; +using System.Text; using System.Text.Encodings.Web; using System.Text.Json; using Xunit; @@ -15,31 +16,65 @@ namespace OpenIddict.Abstractions.Tests.Primitives { public class OpenIddictMessageTests { + [Theory] + [InlineData(null)] + [InlineData("")] + public void Constructor_ThrowsAnExceptionForNullOrEmptyParameterNames(string name) + { + // Arrange, act and assert + var exception = Assert.Throws(delegate + { + return new OpenIddictMessage(new[] + { + new KeyValuePair(name, "Fabrikam") + }); + }); + + Assert.Equal("name", exception.ParamName); + Assert.StartsWith("The parameter name cannot be null or empty.", exception.Message); + } + [Fact] - public void Constructor_ImportsParameters() + public void Constructor_ThrowsAnExceptionForInvalidJsonElement() { - // Arrange and act - var message = new OpenIddictMessage(new[] + // Arrange, act and assert + var exception = Assert.Throws(delegate { - new KeyValuePair("parameter", 42) + return new OpenIddictMessage(JsonSerializer.Deserialize("[0,1,2,3]")); }); - // Assert - Assert.Equal(42, (long) message.GetParameter("parameter")); + Assert.Equal("parameters", exception.ParamName); + Assert.StartsWith("The specified JSON element is not an object.", exception.Message); + } + + [Fact] + public void Constructor_ThrowsAnExceptionForDuplicateParameters() + { + // Arrange, act and assert + var exception = Assert.Throws(delegate + { + return new OpenIddictMessage(new[] + { + new KeyValuePair("parameter", "Fabrikam"), + new KeyValuePair("parameter", "Contoso") + }); + }); + + Assert.Equal("name", exception.ParamName); + Assert.StartsWith("A parameter with the same name already exists.", exception.Message); } [Fact] - public void Constructor_IgnoresNamelessParameters() + public void Constructor_ImportsParameters() { // Arrange and act var message = new OpenIddictMessage(new[] { - new KeyValuePair(null, new OpenIddictParameter()), - new KeyValuePair(string.Empty, new OpenIddictParameter()) + new KeyValuePair("parameter", 42) }); // Assert - Assert.Empty(message.GetParameters()); + Assert.Equal(42, (long) message.GetParameter("parameter")); } [Fact] @@ -53,22 +88,22 @@ namespace OpenIddict.Abstractions.Tests.Primitives }); // Assert - Assert.Equal(2, message.GetParameters().Count()); + Assert.Equal(2, message.Count); } [Fact] - public void Constructor_IgnoresDuplicateParameters() + public void Constructor_AllowsDuplicateParameters() { // Arrange and act var message = new OpenIddictMessage(new[] { - new KeyValuePair("parameter", "Fabrikam"), - new KeyValuePair("parameter", "Contoso") + new KeyValuePair("parameter", "Fabrikam"), + new KeyValuePair("parameter", "Contoso") }); // Assert - Assert.Single(message.GetParameters()); - Assert.Equal("Fabrikam", message.GetParameter("parameter")); + Assert.Equal(1, message.Count); + Assert.Equal(new[] { "Fabrikam", "Contoso" }, (string[]) message.GetParameter("parameter")); } [Fact] @@ -81,7 +116,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives }); // Assert - Assert.Single(message.GetParameters()); + Assert.Equal(1, message.Count); Assert.Equal(new[] { "Fabrikam", "Contoso" }, (string[]) message.GetParameter("parameter")); } @@ -95,7 +130,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives }); // Assert - Assert.Single(message.GetParameters()); + Assert.Equal(1, message.Count); Assert.Equal("Fabrikam", message.GetParameter("parameter")?.Value); } @@ -444,7 +479,40 @@ namespace OpenIddict.Abstractions.Tests.Primitives // Act and assert var element = JsonSerializer.Deserialize(message.ToString()); Assert.DoesNotContain("secret value", message.ToString()); - Assert.Equal("[removed for security reasons]", element.GetProperty(parameter).GetString()); + Assert.Equal("[redacted]", element.GetProperty(parameter).GetString()); + } + + [Fact] + public void WriteTo_ThrowsAnExceptionForNullWriter() + { + // Arrange + var message = new OpenIddictMessage(); + + // Act and assert + var exception = Assert.Throws(() => message.WriteTo(writer: null)); + Assert.Equal("writer", exception.ParamName); + } + + [Fact] + public void WriteTo_WritesUtf8JsonRepresentation() + { + // Arrange + var message = new OpenIddictMessage + { + ["redirect_uris"] = new[] { "https://abc.org/callback" }, + ["client_name"] = "My Example Client" + }; + + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream); + + // Act + message.WriteTo(writer); + writer.Flush(); + + // Assert + Assert.Equal(@"{""redirect_uris"":[""https://abc.org/callback""],""client_name"":""My Example Client""}", + Encoding.UTF8.GetString(stream.ToArray())); } } } diff --git a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictParameterTests.cs b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictParameterTests.cs index fedd2c1e..8be37d37 100644 --- a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictParameterTests.cs +++ b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictParameterTests.cs @@ -6,7 +6,10 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Text; +using System.Text.Encodings.Web; using System.Text.Json; using Xunit; @@ -111,13 +114,13 @@ namespace OpenIddict.Abstractions.Tests.Primitives } [Fact] - public void Equals_SupportsNullJsonValues() + public void Equals_SupportsUndefinedJsonValues() { // Arrange var parameter = new OpenIddictParameter(42); // Act and assert - Assert.False(parameter.Equals(new OpenIddictParameter((JsonElement?) null))); + Assert.False(parameter.Equals(new OpenIddictParameter(default(JsonElement)))); } [Fact] @@ -424,7 +427,13 @@ namespace OpenIddict.Abstractions.Tests.Primitives Assert.True(OpenIddictParameter.IsNullOrEmpty(new OpenIddictParameter((long?) null))); Assert.True(OpenIddictParameter.IsNullOrEmpty(new OpenIddictParameter((string) null))); Assert.True(OpenIddictParameter.IsNullOrEmpty(new OpenIddictParameter((string[]) null))); - Assert.True(OpenIddictParameter.IsNullOrEmpty(new OpenIddictParameter((JsonElement?) null))); + } + + [Fact] + public void IsNullOrEmpty_ReturnsTrueForUndefinedValues() + { + // Arrange, act and assert + Assert.True(OpenIddictParameter.IsNullOrEmpty(new OpenIddictParameter(default(JsonElement)))); } [Fact] @@ -567,6 +576,38 @@ namespace OpenIddict.Abstractions.Tests.Primitives Assert.Null(value.Value); } + [Fact] + public void WriteTo_ThrowsAnExceptionForNullWriter() + { + // Arrange + var parameter = new OpenIddictParameter(); + + // Act and assert + var exception = Assert.Throws(() => parameter.WriteTo(writer: null)); + Assert.Equal("writer", exception.ParamName); + } + + [Fact] + public void WriteTo_WritesUtf8JsonRepresentation() + { + // Arrange + var parameter = new OpenIddictParameter(JsonSerializer.Deserialize(@"{ + ""redirect_uris"": [""https://abc.org/callback""], + ""client_name"":""My Example Client"" +}")); + + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream); + + // Act + parameter.WriteTo(writer); + writer.Flush(); + + // Assert + Assert.Equal(@"{""redirect_uris"":[""https://abc.org/callback""],""client_name"":""My Example Client""}", + Encoding.UTF8.GetString(stream.ToArray())); + } + [Fact] public void BoolConverter_CanCreateParameterFromBooleanValue() { @@ -655,15 +696,15 @@ namespace OpenIddict.Abstractions.Tests.Primitives public void JsonElementConverter_ReturnsDefaultValueForNullValues() { // Arrange, act and assert - Assert.Null((JsonElement?) new OpenIddictParameter()); - Assert.Null((JsonElement?) (OpenIddictParameter?) null); + Assert.Equal(JsonValueKind.Undefined, ((JsonElement) new OpenIddictParameter()).ValueKind); + Assert.Equal(JsonValueKind.Undefined, ((JsonElement) (OpenIddictParameter?) null).ValueKind); } [Fact] public void JsonElementConverter_ReturnsDefaultValueForUnsupportedJsonValues() { // Arrange, act and assert - Assert.Null((JsonElement?) new OpenIddictParameter(new JsonElement())); + Assert.Equal(JsonValueKind.Undefined, ((JsonElement) new OpenIddictParameter(new JsonElement())).ValueKind); } [Fact] diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs index 8fe454e6..bd306eee 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs @@ -718,7 +718,7 @@ namespace OpenIddict.Server.FunctionalTests }); // Assert - Assert.Equal(11, response.GetParameters().Count()); + Assert.Equal(11, response.Count); Assert.True((bool) response[Claims.Active]); Assert.Equal("66B65AED-4033-4E9C-B975-A8CA7FB6FA79", (string) response[Claims.JwtId]); Assert.Equal(TokenTypes.Bearer, (string) response[Claims.TokenType]);