File: Routing\QueryParameterValueSupplierTest.cs
Web Access
Project: src\src\Components\Components\test\Microsoft.AspNetCore.Components.Tests.csproj (Microsoft.AspNetCore.Components.Tests)
// Licensed to the .NET Foundation under one or more agreements.	
// The .NET Foundation licenses this file to you under the MIT license.	
 
namespace Microsoft.AspNetCore.Components.Routing;
 
public class QueryParameterValueSupplierTest
{
    private readonly QueryParameterValueSupplier _supplier = new();
 
    [Fact]
    public void SupportsExpectedValueTypes()
    {
        var query =
            $"BoolVal=true&" +
            $"DateTimeVal=2020-01-02+03:04:05.678-09:00&" +
            $"DecimalVal=-1.234&" +
            $"DoubleVal=-2.345&" +
            $"FloatVal=-3.456&" +
            $"GuidVal=9e7257ad-03aa-42c7-9819-be08b177fef9&" +
            $"IntVal=-54321&" +
            $"LongVal=-99987654321&" +
            $"StringVal=Some+string+%26+more&" +
            $"NullableBoolVal=true&" +
            $"NullableDateTimeVal=2021-01-02+03:04:05.678Z&" +
            $"NullableDecimalVal=1.234&" +
            $"NullableDoubleVal=2.345&" +
            $"NullableFloatVal=3.456&" +
            $"NullableGuidVal=1e7257ad-03aa-42c7-9819-be08b177fef9&" +
            $"NullableIntVal=54321&" +
            $"NullableLongVal=99987654321&";
 
        ReadQuery(query);
 
        AssertKeyValuePair<bool>("BoolVal", true);
        AssertKeyValuePair<DateTime>("DateTimeVal", new DateTimeOffset(2020, 1, 2, 3, 4, 5, 678, TimeSpan.FromHours(-9)).LocalDateTime);
        AssertKeyValuePair<decimal>("DecimalVal", -1.234m);
        AssertKeyValuePair<double>("DoubleVal", -2.345);
        AssertKeyValuePair<float>("FloatVal", -3.456f);
        AssertKeyValuePair<Guid>("GuidVal", new Guid("9e7257ad-03aa-42c7-9819-be08b177fef9"));
        AssertKeyValuePair<int>("IntVal", -54321);
        AssertKeyValuePair<long>("LongVal", -99987654321);
        AssertKeyValuePair<bool?>("NullableBoolVal", true);
        AssertKeyValuePair<DateTime?>("NullableDateTimeVal", new DateTime(2021, 1, 2, 3, 4, 5, 678, DateTimeKind.Utc).ToLocalTime());
        AssertKeyValuePair<decimal?>("NullableDecimalVal", 1.234m);
        AssertKeyValuePair<double?>("NullableDoubleVal", 2.345);
        AssertKeyValuePair<float?>("NullableFloatVal", 3.456f);
        AssertKeyValuePair<Guid?>("NullableGuidVal", new Guid("1e7257ad-03aa-42c7-9819-be08b177fef9"));
        AssertKeyValuePair<int?>("NullableIntVal", 54321);
        AssertKeyValuePair<long?>("NullableLongVal", 99987654321);
        AssertKeyValuePair<string>("StringVal", "Some string & more");
    }
 
    [Theory]
    [InlineData("")]
    [InlineData("?")]
    [InlineData("?unrelated=123")]
    public void SuppliesNullForValueTypesIfNotSpecified(string query)
    {
        ReadQuery(query);
 
        // Although we could supply default(T) for missing values, there's precedent in the routing	
        // system for supplying null for missing route parameters. The component is then responsible	
        // for interpreting null as a blank value for the parameter, regardless of its type. To keep	
        // the rules aligned, we do the same thing for querystring parameters.	
        AssertKeyValuePair<bool>("BoolVal", null);
        AssertKeyValuePair<DateTime>("DateTimeVal", null);
        AssertKeyValuePair<decimal>("DecimalVal", null);
        AssertKeyValuePair<double>("DoubleVal", null);
        AssertKeyValuePair<float>("FloatVal", null);
        AssertKeyValuePair<Guid>("GuidVal", null);
        AssertKeyValuePair<int>("IntVal", null);
        AssertKeyValuePair<long>("LongVal", null);
        AssertKeyValuePair<bool?>("NullableBoolVal", null);
        AssertKeyValuePair<DateTime?>("NullableDateTimeVal", null);
        AssertKeyValuePair<decimal?>("NullableDecimalVal", null);
        AssertKeyValuePair<double?>("NullableDoubleVal", null);
        AssertKeyValuePair<float?>("NullableFloatVal", null);
        AssertKeyValuePair<Guid?>("NullableGuidVal", null);
        AssertKeyValuePair<int?>("NullableIntVal", null);
        AssertKeyValuePair<long?>("NullableLongVal", null);
        AssertKeyValuePair<string>("StringVal", null);
    }
 
    [Fact]
    public void SupportsExpectedArrayTypes()
    {
        var query =
            $"BoolVals=true&" +
            $"DateTimeVals=2020-01-02+03:04:05.678Z&" +
            $"DecimalVals=-1.234&" +
            $"DoubleVals=-2.345&" +
            $"FloatVals=-3.456&" +
            $"GuidVals=9e7257ad-03aa-42c7-9819-be08b177fef9&" +
            $"IntVals=-54321&" +
            $"LongVals=-99987654321&" +
            $"StringVals=Some+string+%26+more&" +
            $"NullableBoolVals=true&" +
            $"NullableDateTimeVals=2021-01-02+03:04:05.678Z&" +
            $"NullableDecimalVals=1.234&" +
            $"NullableDoubleVals=2.345&" +
            $"NullableFloatVals=3.456&" +
            $"NullableGuidVals=1e7257ad-03aa-42c7-9819-be08b177fef9&" +
            $"NullableIntVals=54321&" +
            $"NullableLongVals=99987654321&";
 
        ReadQuery(query);
 
        AssertKeyValuePair<bool[]>("BoolVals", new[] { true });
        AssertKeyValuePair<DateTime[]>("DateTimeVals", new[] { new DateTime(2020, 1, 2, 3, 4, 5, 678, DateTimeKind.Utc).ToLocalTime() });
        AssertKeyValuePair<decimal[]>("DecimalVals", new[] { -1.234m });
        AssertKeyValuePair<double[]>("DoubleVals", new[] { -2.345 });
        AssertKeyValuePair<float[]>("FloatVals", new[] { -3.456f });
        AssertKeyValuePair<Guid[]>("GuidVals", new[] { new Guid("9e7257ad-03aa-42c7-9819-be08b177fef9") });
        AssertKeyValuePair<int[]>("IntVals", new[] { -54321 });
        AssertKeyValuePair<long[]>("LongVals", new[] { -99987654321 });
        AssertKeyValuePair<bool?[]>("NullableBoolVals", new[] { true });
        AssertKeyValuePair<DateTime?[]>("NullableDateTimeVals", new[] { new DateTime(2021, 1, 2, 3, 4, 5, 678, DateTimeKind.Utc).ToLocalTime() });
        AssertKeyValuePair<decimal?[]>("NullableDecimalVals", new[] { 1.234m });
        AssertKeyValuePair<double?[]>("NullableDoubleVals", new[] { 2.345 });
        AssertKeyValuePair<float?[]>("NullableFloatVals", new[] { 3.456f });
        AssertKeyValuePair<Guid?[]>("NullableGuidVals", new[] { new Guid("1e7257ad-03aa-42c7-9819-be08b177fef9") });
        AssertKeyValuePair<int?[]>("NullableIntVals", new[] { 54321 });
        AssertKeyValuePair<long?[]>("NullableLongVals", new[] { 99987654321 });
        AssertKeyValuePair<string[]>("StringVals", new[] { "Some string & more" });
    }
 
    [Theory]
    [InlineData("")]
    [InlineData("?")]
    [InlineData("?unrelated=123")]
    public void SuppliesEmptyArrayForArrayTypesIfNotSpecified(string query)
    {
        ReadQuery(query);
 
        AssertKeyValuePair<bool[]>("BoolVals", Array.Empty<bool>());
        AssertKeyValuePair<DateTime[]>("DateTimeVals", Array.Empty<DateTime>());
        AssertKeyValuePair<decimal[]>("DecimalVals", Array.Empty<decimal>());
        AssertKeyValuePair<double[]>("DoubleVals", Array.Empty<double>());
        AssertKeyValuePair<float[]>("FloatVals", Array.Empty<float>());
        AssertKeyValuePair<Guid[]>("GuidVals", Array.Empty<Guid>());
        AssertKeyValuePair<int[]>("IntVals", Array.Empty<int>());
        AssertKeyValuePair<long[]>("LongVals", Array.Empty<long>());
        AssertKeyValuePair<bool?[]>("NullableBoolVals", Array.Empty<bool?>());
        AssertKeyValuePair<DateTime?[]>("NullableDateTimeVals", Array.Empty<DateTime?>());
        AssertKeyValuePair<decimal?[]>("NullableDecimalVals", Array.Empty<decimal?>());
        AssertKeyValuePair<double?[]>("NullableDoubleVals", Array.Empty<double?>());
        AssertKeyValuePair<float?[]>("NullableFloatVals", Array.Empty<float?>());
        AssertKeyValuePair<Guid?[]>("NullableGuidVals", Array.Empty<Guid?>());
        AssertKeyValuePair<int?[]>("NullableIntVals", Array.Empty<int?>());
        AssertKeyValuePair<long?[]>("NullableLongVals", Array.Empty<long?>());
        AssertKeyValuePair<string[]>("StringVals", Array.Empty<string>());
    }
 
    [Theory]
    [InlineData("BoolVal", "abc", typeof(bool))]
    [InlineData("DateTimeVal", "2020-02-31", typeof(DateTime))]
    [InlineData("DecimalVal", "1.2.3", typeof(decimal))]
    [InlineData("DoubleVal", "1x", typeof(double))]
    [InlineData("FloatVal", "1e1000", typeof(float))]
    [InlineData("GuidVal", "123456-789-0", typeof(Guid))]
    [InlineData("IntVal", "5000000000", typeof(int))]
    [InlineData("LongVal", "this+is+a+long+value", typeof(long))]
    [InlineData("NullableBoolVal", "abc", typeof(bool?))]
    [InlineData("NullableDateTimeVal", "2020-02-31", typeof(DateTime?))]
    [InlineData("NullableDecimalVal", "1.2.3", typeof(decimal?))]
    [InlineData("NullableDoubleVal", "1x", typeof(double?))]
    [InlineData("NullableFloatVal", "1e1000", typeof(float?))]
    [InlineData("NullableGuidVal", "123456-789-0", typeof(Guid?))]
    [InlineData("NullableIntVal", "5000000000", typeof(int?))]
    [InlineData("NullableLongVal", "this+is+a+long+value", typeof(long?))]
    public void RejectsUnparseableValues(string key, string value, Type targetType)
    {
        ReadQuery($"?{key}={value}");
 
        var ex = Assert.Throws<InvalidOperationException>(() => _supplier.GetQueryParameterValue(targetType, key));
        Assert.Equal($"Cannot parse the value '{value.Replace('+', ' ')}' as type '{targetType}' for '{key}'.", ex.Message);
    }
 
    [Theory]
    [InlineData("BoolVals", "true", "abc", typeof(bool))]
    [InlineData("DateTimeVals", "2020-02-28", "2020-02-31", typeof(DateTime))]
    [InlineData("DecimalVals", "1.23", "1.2.3", typeof(decimal))]
    [InlineData("DoubleVals", "1", "1x", typeof(double))]
    [InlineData("FloatVals", "1000", "1e1000", typeof(float))]
    [InlineData("GuidVals", "9e7257ad-03aa-42c7-9819-be08b177fef9", "123456-789-0", typeof(Guid))]
    [InlineData("IntVals", "5000000", "5000000000", typeof(int))]
    [InlineData("LongVals", "-1234", "this+is+a+long+value", typeof(long))]
    [InlineData("NullableBoolVals", "true", "abc", typeof(bool?))]
    [InlineData("NullableDateTimeVals", "2020-02-28", "2020-02-31", typeof(DateTime?))]
    [InlineData("NullableDecimalVals", "1.23", "1.2.3", typeof(decimal?))]
    [InlineData("NullableDoubleVals", "1", "1x", typeof(double?))]
    [InlineData("NullableFloatVals", "1000", "1e1000", typeof(float?))]
    [InlineData("NullableGuidVals", "9e7257ad-03aa-42c7-9819-be08b177fef9", "123456-789-0", typeof(Guid?))]
    [InlineData("NullableIntVals", "5000000", "5000000000", typeof(int?))]
    [InlineData("NullableLongVals", "-1234", "this+is+a+long+value", typeof(long?))]
    public void RejectsUnparseableArrayEntries(string key, string validValue, string invalidValue, Type targetType)
    {
        ReadQuery($"?{key}={validValue}&{key}={invalidValue}");
 
        var ex = Assert.Throws<InvalidOperationException>(() => _supplier.GetQueryParameterValue(targetType.MakeArrayType(), key));
        Assert.Equal($"Cannot parse the value '{invalidValue.Replace('+', ' ')}' as type '{targetType}' for '{key}'.", ex.Message);
    }
 
    [Theory]
    [InlineData("BoolVal", typeof(bool))]
    [InlineData("DateTimeVal", typeof(DateTime))]
    [InlineData("DecimalVal", typeof(decimal))]
    [InlineData("DoubleVal", typeof(double))]
    [InlineData("FloatVal", typeof(float))]
    [InlineData("GuidVal", typeof(Guid))]
    [InlineData("IntVal", typeof(int))]
    [InlineData("LongVal", typeof(long))]
    public void RejectsBlankValuesWhenNotNullable(string key, Type targetType)
    {
        ReadQuery($"?StringVal=somevalue&{key}=");
 
        var ex = Assert.Throws<InvalidOperationException>(() => _supplier.GetQueryParameterValue(targetType, key));
        Assert.Equal($"Cannot parse the value '' as type '{targetType}' for '{key}'.", ex.Message);
    }
 
    [Fact]
    public void AcceptsBlankValuesWhenNullable()
    {
        var query =
            $"NullableBoolVal=&" +
            $"NullableDateTimeVal=&" +
            $"NullableDecimalVal=&" +
            $"NullableDoubleVal=&" +
            $"NullableFloatVal=&" +
            $"NullableGuidVal=&" +
            $"NullableIntVal=&" +
            $"NullableLongVal=&";
 
        ReadQuery(query);
 
        AssertKeyValuePair<bool?>("NullableBoolVal", null);
        AssertKeyValuePair<DateTime?>("NullableDateTimeVal", null);
        AssertKeyValuePair<decimal?>("NullableDecimalVal", null);
        AssertKeyValuePair<double?>("NullableDoubleVal", null);
        AssertKeyValuePair<float?>("NullableFloatVal", null);
        AssertKeyValuePair<Guid?>("NullableGuidVal", null);
        AssertKeyValuePair<int?>("NullableIntVal", null);
        AssertKeyValuePair<long?>("NullableLongVal", null);
    }
 
    [Theory]
    [InlineData("")]
    [InlineData("=")]
    public void EmptyStringValuesAreSuppliedAsEmptyString(string queryPart)
    {
        ReadQuery($"?StringVal{queryPart}");
 
        Assert.Equal(string.Empty, _supplier.GetQueryParameterValue(typeof(string), "StringVal"));
    }
 
    [Fact]
    public void EmptyStringArrayValuesAreSuppliedAsEmptyStrings()
    {
        var query = $"?StringVals=a&" +
            $"StringVals&" +
            $"StringVals=&" +
            $"StringVals=b";
 
        ReadQuery(query);
 
        Assert.Equal(new[] { "a", string.Empty, string.Empty, "b" }, _supplier.GetQueryParameterValue(typeof(string[]), "StringVals"));
    }
 
    [Theory]
    [InlineData("BoolVals", typeof(bool))]
    [InlineData("DateTimeVals", typeof(DateTime))]
    [InlineData("DecimalVals", typeof(decimal))]
    [InlineData("DoubleVals", typeof(double))]
    [InlineData("FloatVals", typeof(float))]
    [InlineData("GuidVals", typeof(Guid))]
    [InlineData("IntVals", typeof(int))]
    [InlineData("LongVals", typeof(long))]
    public void RejectsBlankArrayEntriesWhenNotNullable(string key, Type targetType)
    {
        ReadQuery($"?StringVal=somevalue&{key}=");
 
        var ex = Assert.Throws<InvalidOperationException>(
            () => _supplier.GetQueryParameterValue(targetType, key));
        Assert.Equal($"Cannot parse the value '' as type '{targetType}' for '{key}'.", ex.Message);
    }
 
    [Fact]
    public void AcceptsBlankArrayEntriesWhenNullable()
    {
        var query =
            $"NullableBoolVals=&" +
            $"NullableDateTimeVals=&" +
            $"NullableDecimalVals=&" +
            $"NullableDoubleVals=&" +
            $"NullableFloatVals=&" +
            $"NullableGuidVals=&" +
            $"NullableIntVals=&" +
            $"NullableLongVals=&";
 
        ReadQuery(query);
 
        AssertKeyValuePair<bool?[]>("NullableBoolVals", new bool?[] { null });
        AssertKeyValuePair<DateTime?[]>("NullableDateTimeVals", new DateTime?[] { null });
        AssertKeyValuePair<decimal?[]>("NullableDecimalVals", new decimal?[] { null });
        AssertKeyValuePair<double?[]>("NullableDoubleVals", new double?[] { null });
        AssertKeyValuePair<float?[]>("NullableFloatVals", new float?[] { null });
        AssertKeyValuePair<Guid?[]>("NullableGuidVals", new Guid?[] { null });
        AssertKeyValuePair<int?[]>("NullableIntVals", new int?[] { null });
        AssertKeyValuePair<long?[]>("NullableLongVals", new long?[] { null });
    }
 
    [Fact]
    public void DecodesKeysAndValues()
    {
        var nameThatLooksEncoded = "name+that+looks+%5Bencoded%5D";
        var encodedName = Uri.EscapeDataString(nameThatLooksEncoded);
        var query = $"?{encodedName}=Some+%5Bencoded%5D+value";
 
        ReadQuery(query);
 
        AssertKeyValuePair<string>(nameThatLooksEncoded, "Some [encoded] value");
    }
 
    [Fact]
    public void MatchesKeysCaseInsensitively()
    {
        ReadQuery($"?KEYONE=1&KEYTWO=2");
 
        AssertKeyValuePair<int>("KeyOne", 1);
        AssertKeyValuePair<int>("KeyTwo", 2);
    }
 
    [Fact]
    public void MatchesKeysWithNonAsciiChars()
    {
        ReadQuery($"?Имя_моей_собственности=first&خاصية_أخرى=second");
 
        AssertKeyValuePair<string>("خاصية_أخرى", "second");
        AssertKeyValuePair<string>("Имя_моей_собственности", "first");
    }
 
    private void ReadQuery(string query)
    {
        _supplier.ReadParametersFromQuery(query.AsMemory());
    }
 
    private void AssertKeyValuePair<T>(string key, object expectedValue)
    {
        var actualValue = _supplier.GetQueryParameterValue(typeof(T), key);
        Assert.Equal(expectedValue, actualValue);
    }
}