Add CORS support to HttpMethodMatcherPolicy
Also removes HttpMethodEndpointConstraint, since it's been fully replaced.
This commit is contained in:
parent
c68c5befc7
commit
54e5370e8f
|
|
@ -13,7 +13,6 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
|
||||
private BarebonesMatcher _baseline;
|
||||
private Matcher _dfa;
|
||||
private Matcher _tree;
|
||||
|
||||
private int[] _samples;
|
||||
private EndpointFeature _feature;
|
||||
|
|
@ -31,7 +30,6 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
|
||||
_baseline = (BarebonesMatcher)SetupMatcher(new BarebonesMatcherBuilder());
|
||||
_dfa = SetupMatcher(CreateDfaMatcherBuilder());
|
||||
_tree = SetupMatcher(new TreeRouterMatcherBuilder());
|
||||
|
||||
_feature = new EndpointFeature();
|
||||
}
|
||||
|
|
@ -61,22 +59,5 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
Validate(httpContext, Endpoints[sample], feature.Endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = SampleCount)]
|
||||
public async Task LegacyTreeRouter()
|
||||
{
|
||||
var feature = _feature;
|
||||
for (var i = 0; i < SampleCount; i++)
|
||||
{
|
||||
var sample = _samples[i];
|
||||
var httpContext = Requests[sample];
|
||||
|
||||
// This is required to make the legacy router implementation work with global routing.
|
||||
httpContext.Features.Set<IEndpointFeature>(feature);
|
||||
|
||||
await _tree.MatchAsync(httpContext, feature);
|
||||
Validate(httpContext, Endpoints[sample], feature.Endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
var metadata = new List<object>();
|
||||
if (httpMethod != null)
|
||||
{
|
||||
metadata.Add(new HttpMethodEndpointConstraint(new string[] { httpMethod, }));
|
||||
metadata.Add(new HttpMethodMetadata(new string[] { httpMethod, }));
|
||||
}
|
||||
|
||||
return new MatcherEndpoint(
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
{
|
||||
private BarebonesMatcher _baseline;
|
||||
private Matcher _dfa;
|
||||
private Matcher _tree;
|
||||
|
||||
private EndpointFeature _feature;
|
||||
|
||||
|
|
@ -25,7 +24,6 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
|
||||
_baseline = (BarebonesMatcher)SetupMatcher(new BarebonesMatcherBuilder());
|
||||
_dfa = SetupMatcher(CreateDfaMatcherBuilder());
|
||||
_tree = SetupMatcher(new TreeRouterMatcherBuilder());
|
||||
|
||||
_feature = new EndpointFeature();
|
||||
}
|
||||
|
|
@ -53,21 +51,5 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
Validate(httpContext, Endpoints[i], feature.Endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = EndpointCount)]
|
||||
public async Task LegacyTreeRouter()
|
||||
{
|
||||
var feature = _feature;
|
||||
for (var i = 0; i < EndpointCount; i++)
|
||||
{
|
||||
var httpContext = Requests[i];
|
||||
|
||||
// This is required to make the legacy router implementation work with global routing.
|
||||
httpContext.Features.Set<IEndpointFeature>(feature);
|
||||
|
||||
await _tree.MatchAsync(httpContext, feature);
|
||||
Validate(httpContext, Endpoints[i], feature.Endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ using System.Text;
|
|||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
||||
using Microsoft.AspNetCore.Routing.Matchers;
|
||||
using Microsoft.AspNetCore.Routing.Metadata;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
|
|
@ -134,15 +135,16 @@ namespace Microsoft.AspNetCore.Routing
|
|||
sb.Append(", Order:");
|
||||
sb.Append(matcherEndpoint.Order);
|
||||
|
||||
var httpEndpointConstraints = matcherEndpoint.Metadata.GetOrderedMetadata<IEndpointConstraintMetadata>()
|
||||
.OfType<HttpMethodEndpointConstraint>();
|
||||
foreach (var constraint in httpEndpointConstraints)
|
||||
var httpMethodMetadata = matcherEndpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
|
||||
if (httpMethodMetadata != null)
|
||||
{
|
||||
sb.Append(", Http Methods: ");
|
||||
sb.Append(string.Join(", ", constraint.HttpMethods));
|
||||
sb.Append(", Constraint Order:");
|
||||
sb.Append(constraint.Order);
|
||||
foreach (var httpMethod in httpMethodMetadata.HttpMethods)
|
||||
{
|
||||
sb.Append(", Http Methods: ");
|
||||
sb.Append(string.Join(", ", httpMethod));
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
//
|
||||
services.TryAddSingleton<EndpointSelector, EndpointConstraintEndpointSelector>();
|
||||
services.TryAddSingleton<EndpointConstraintCache>();
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodEndpointSelectorPolicy>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodMatcherPolicy>());
|
||||
|
||||
// Will be cached by the EndpointSelector
|
||||
services.TryAddEnumerable(
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints
|
|||
|
||||
IReadOnlyList<string> IHttpMethodMetadata.HttpMethods => _httpMethods;
|
||||
|
||||
bool IHttpMethodMetadata.AcceptCorsPreflight => false;
|
||||
|
||||
public virtual bool Accept(EndpointConstraintContext context)
|
||||
{
|
||||
if (context == null)
|
||||
|
|
|
|||
|
|
@ -8,11 +8,18 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing.Metadata;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
public sealed class HttpMethodEndpointSelectorPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy
|
||||
public sealed class HttpMethodMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy
|
||||
{
|
||||
// Used in tests
|
||||
internal static readonly string OriginHeader = "Origin";
|
||||
internal static readonly string AccessControlRequestMethod = "Access-Control-Request-Method";
|
||||
internal static readonly string PreflightHttpMethod = "OPTIONS";
|
||||
|
||||
// Used in tests
|
||||
internal const string Http405EndpointDisplayName = "405 HTTP Method Not Supported";
|
||||
|
||||
|
|
@ -47,38 +54,94 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
{
|
||||
// The algorithm here is designed to be preserve the order of the endpoints
|
||||
// while also being relatively simple. Preserving order is important.
|
||||
var allHttpMethods = endpoints
|
||||
.SelectMany(e => GetHttpMethods(e))
|
||||
.Distinct()
|
||||
.OrderBy(m => m); // Sort for testability
|
||||
|
||||
var dictionary = new Dictionary<string, List<Endpoint>>();
|
||||
foreach (var httpMethod in allHttpMethods)
|
||||
{
|
||||
dictionary.Add(httpMethod, new List<Endpoint>());
|
||||
}
|
||||
|
||||
dictionary.Add(AnyMethod, new List<Endpoint>());
|
||||
|
||||
// First, build a dictionary of all possible HTTP method/CORS combinations
|
||||
// that exist in this list of endpoints.
|
||||
//
|
||||
// For now we're just building up the set of keys. We don't add any endpoints
|
||||
// to lists now because we don't want ordering problems.
|
||||
var allHttpMethods = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var edges = new Dictionary<EdgeKey, List<Endpoint>>();
|
||||
for (var i = 0; i < endpoints.Count; i++)
|
||||
{
|
||||
var endpoint = endpoints[i];
|
||||
var (httpMethods, acceptCorsPreFlight) = GetHttpMethods(endpoint);
|
||||
|
||||
var httpMethods = GetHttpMethods(endpoint);
|
||||
// If the action doesn't list HTTP methods then it supports all methods.
|
||||
// In this phase we use a sentinel value to represent the *other* HTTP method
|
||||
// a state that represents any HTTP method that doesn't have a match.
|
||||
if (httpMethods.Count == 0)
|
||||
{
|
||||
// This endpoint suports all HTTP methods.
|
||||
foreach (var kvp in dictionary)
|
||||
{
|
||||
kvp.Value.Add(endpoint);
|
||||
}
|
||||
|
||||
continue;
|
||||
httpMethods = new[] { AnyMethod, };
|
||||
}
|
||||
|
||||
for (var j = 0; j < httpMethods.Count; j++)
|
||||
{
|
||||
dictionary[httpMethods[j]].Add(endpoint);
|
||||
// An endpoint that allows CORS reqests will match both CORS and non-CORS
|
||||
// so we model it as both.
|
||||
var httpMethod = httpMethods[j];
|
||||
var key = new EdgeKey(httpMethod, acceptCorsPreFlight);
|
||||
if (!edges.ContainsKey(key))
|
||||
{
|
||||
edges.Add(key, new List<Endpoint>());
|
||||
}
|
||||
|
||||
// An endpoint that allows CORS reqests will match both CORS and non-CORS
|
||||
// so we model it as both.
|
||||
if (acceptCorsPreFlight)
|
||||
{
|
||||
key = new EdgeKey(httpMethod, false);
|
||||
if (!edges.ContainsKey(key))
|
||||
{
|
||||
edges.Add(key, new List<Endpoint>());
|
||||
}
|
||||
}
|
||||
|
||||
// Also if it's not the *any* method key, then track it.
|
||||
if (!string.Equals(AnyMethod, httpMethod, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
allHttpMethods.Add(httpMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now in a second loop, add endpoints to these lists. We've enumerated all of
|
||||
// the states, so we want to see which states this endpoint matches.
|
||||
for (var i = 0; i < endpoints.Count; i++)
|
||||
{
|
||||
var endpoint = endpoints[i];
|
||||
var (httpMethods, acceptCorsPreFlight) = GetHttpMethods(endpoint);
|
||||
|
||||
if (httpMethods.Count == 0)
|
||||
{
|
||||
// OK this means that this endpoint matches *all* HTTP methods.
|
||||
// So, loop and add it to all states.
|
||||
foreach (var kvp in edges)
|
||||
{
|
||||
if (acceptCorsPreFlight || !kvp.Key.IsCorsPreflightRequest)
|
||||
{
|
||||
kvp.Value.Add(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// OK this endpoint matches specific methods.
|
||||
for (var j = 0; j < httpMethods.Count; j++)
|
||||
{
|
||||
var httpMethod = httpMethods[j];
|
||||
var key = new EdgeKey(httpMethod, acceptCorsPreFlight);
|
||||
|
||||
edges[key].Add(endpoint);
|
||||
|
||||
// An endpoint that allows CORS reqests will match both CORS and non-CORS
|
||||
// so we model it as both.
|
||||
if (acceptCorsPreFlight)
|
||||
{
|
||||
key = new EdgeKey(httpMethod, false);
|
||||
edges[key].Add(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -95,42 +158,67 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
//
|
||||
// This will make 405 much more likely in API-focused applications, and somewhat
|
||||
// unlikely in a traditional MVC application. That's good.
|
||||
if (dictionary[AnyMethod].Count == 0)
|
||||
//
|
||||
// We don't bother returning a 405 when the CORS preflight method doesn't exist.
|
||||
// The developer calling the API will see it as a CORS error, which is fine because
|
||||
// there isn't an endpoint to check for a CORS policy.
|
||||
if (!edges.TryGetValue(new EdgeKey(AnyMethod, false), out var matches))
|
||||
{
|
||||
dictionary[AnyMethod].Add(CreateRejectionEndpoint(allHttpMethods));
|
||||
// Methods sorted for testability.
|
||||
var endpoint = CreateRejectionEndpoint(allHttpMethods.OrderBy(m => m));
|
||||
matches = new List<Endpoint>() { endpoint, };
|
||||
edges[new EdgeKey(AnyMethod, false)] = matches;
|
||||
}
|
||||
|
||||
var edges = new List<PolicyNodeEdge>();
|
||||
foreach (var kvp in dictionary)
|
||||
{
|
||||
edges.Add(new PolicyNodeEdge(kvp.Key, kvp.Value));
|
||||
}
|
||||
return edges
|
||||
.Select(kvp => new PolicyNodeEdge(kvp.Key, kvp.Value))
|
||||
.ToArray();
|
||||
|
||||
return edges;
|
||||
|
||||
IReadOnlyList<string> GetHttpMethods(Endpoint e)
|
||||
(IReadOnlyList<string> httpMethods, bool acceptCorsPreflight) GetHttpMethods(Endpoint e)
|
||||
{
|
||||
return e.Metadata.GetMetadata<IHttpMethodMetadata>()?.HttpMethods ?? Array.Empty<string>();
|
||||
var metadata = e.Metadata.GetMetadata<IHttpMethodMetadata>();
|
||||
return metadata == null ? (Array.Empty<string>(), false) : (metadata.HttpMethods, metadata.AcceptCorsPreflight);
|
||||
}
|
||||
}
|
||||
|
||||
public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJumpTableEdge> edges)
|
||||
{
|
||||
var dictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
var destinations = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
var corsPreflightDestinations = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
for (var i = 0; i < edges.Count; i++)
|
||||
{
|
||||
// We create this data, so it's safe to cast it to a string.
|
||||
dictionary.Add((string)edges[i].State, edges[i].Destination);
|
||||
// We create this data, so it's safe to cast it.
|
||||
var key = (EdgeKey)edges[i].State;
|
||||
if (key.IsCorsPreflightRequest)
|
||||
{
|
||||
corsPreflightDestinations.Add(key.HttpMethod, edges[i].Destination);
|
||||
}
|
||||
else
|
||||
{
|
||||
destinations.Add(key.HttpMethod, edges[i].Destination);
|
||||
}
|
||||
}
|
||||
|
||||
if (dictionary.TryGetValue(AnyMethod, out var matchesAnyVerb))
|
||||
int corsPreflightExitDestination = exitDestination;
|
||||
if (corsPreflightDestinations.TryGetValue(AnyMethod, out var matchesAnyVerb))
|
||||
{
|
||||
// If we have endpoints that match any HTTP method, use that as the exit.
|
||||
corsPreflightExitDestination = matchesAnyVerb;
|
||||
corsPreflightDestinations.Remove(AnyMethod);
|
||||
}
|
||||
|
||||
if (destinations.TryGetValue(AnyMethod, out matchesAnyVerb))
|
||||
{
|
||||
// If we have endpoints that match any HTTP method, use that as the exit.
|
||||
exitDestination = matchesAnyVerb;
|
||||
dictionary.Remove(AnyMethod);
|
||||
destinations.Remove(AnyMethod);
|
||||
}
|
||||
|
||||
return new DictionaryPolicyJumpTable(exitDestination, dictionary);
|
||||
return new HttpMethodPolicyJumpTable(
|
||||
exitDestination,
|
||||
destinations,
|
||||
corsPreflightExitDestination,
|
||||
corsPreflightDestinations);
|
||||
}
|
||||
|
||||
private Endpoint CreateRejectionEndpoint(IEnumerable<string> httpMethods)
|
||||
|
|
@ -150,21 +238,46 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
Http405EndpointDisplayName);
|
||||
}
|
||||
|
||||
private class DictionaryPolicyJumpTable : PolicyJumpTable
|
||||
private class HttpMethodPolicyJumpTable : PolicyJumpTable
|
||||
{
|
||||
private readonly int _exitDestination;
|
||||
private readonly Dictionary<string, int> _destinations;
|
||||
private readonly int _corsPreflightExitDestination;
|
||||
private readonly Dictionary<string, int> _corsPreflightDestinations;
|
||||
|
||||
public DictionaryPolicyJumpTable(int exitDestination, Dictionary<string, int> destinations)
|
||||
private readonly bool _supportsCorsPreflight;
|
||||
|
||||
public HttpMethodPolicyJumpTable(
|
||||
int exitDestination,
|
||||
Dictionary<string, int> destinations,
|
||||
int corsPreflightExitDestination,
|
||||
Dictionary<string, int> corsPreflightDestinations)
|
||||
{
|
||||
_exitDestination = exitDestination;
|
||||
_destinations = destinations;
|
||||
_corsPreflightExitDestination = corsPreflightExitDestination;
|
||||
_corsPreflightDestinations = corsPreflightDestinations;
|
||||
|
||||
_supportsCorsPreflight = _corsPreflightDestinations.Count > 0;
|
||||
}
|
||||
|
||||
public override int GetDestination(HttpContext httpContext)
|
||||
{
|
||||
int destination;
|
||||
|
||||
var httpMethod = httpContext.Request.Method;
|
||||
return _destinations.TryGetValue(httpMethod, out var destination) ? destination : _exitDestination;
|
||||
if (_supportsCorsPreflight &&
|
||||
string.Equals(httpMethod, PreflightHttpMethod, StringComparison.OrdinalIgnoreCase) &&
|
||||
httpContext.Request.Headers.ContainsKey(OriginHeader) &&
|
||||
httpContext.Request.Headers.TryGetValue(AccessControlRequestMethod, out var accessControlRequestMethod) &&
|
||||
!StringValues.IsNullOrEmpty(accessControlRequestMethod))
|
||||
{
|
||||
return _corsPreflightDestinations.TryGetValue(accessControlRequestMethod, out destination)
|
||||
? destination
|
||||
: _corsPreflightExitDestination;
|
||||
}
|
||||
|
||||
return _destinations.TryGetValue(httpMethod, out destination) ? destination : _exitDestination;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -178,5 +291,58 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
y?.HttpMethods.Count > 0 ? y : null);
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct EdgeKey : IEquatable<EdgeKey>, IComparable<EdgeKey>, IComparable
|
||||
{
|
||||
// Note that in contrast with the metadata, the edge represents a possible state change
|
||||
// rather than a list of what's allowed. We represent CORS and non-CORS requests as separate
|
||||
// states.
|
||||
public readonly bool IsCorsPreflightRequest;
|
||||
public readonly string HttpMethod;
|
||||
|
||||
public EdgeKey(string httpMethod, bool isCorsPreflightRequest)
|
||||
{
|
||||
HttpMethod = httpMethod;
|
||||
IsCorsPreflightRequest = isCorsPreflightRequest;
|
||||
}
|
||||
|
||||
// These are comparable so they can be sorted in tests.
|
||||
public int CompareTo(EdgeKey other)
|
||||
{
|
||||
var compare = HttpMethod.CompareTo(other.HttpMethod);
|
||||
if (compare != 0)
|
||||
{
|
||||
return compare;
|
||||
}
|
||||
|
||||
return IsCorsPreflightRequest.CompareTo(other.IsCorsPreflightRequest);
|
||||
}
|
||||
|
||||
public int CompareTo(object obj)
|
||||
{
|
||||
return CompareTo((EdgeKey)obj);
|
||||
}
|
||||
|
||||
public bool Equals(EdgeKey other)
|
||||
{
|
||||
return
|
||||
IsCorsPreflightRequest == other.IsCorsPreflightRequest &&
|
||||
string.Equals(HttpMethod, other.HttpMethod, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var other = obj as EdgeKey?;
|
||||
return other == null ? false : Equals(other.Value);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hash = new HashCodeCombiner();
|
||||
hash.Add(IsCorsPreflightRequest);
|
||||
hash.Add(HttpMethod, StringComparer.Ordinal);
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,20 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Metadata
|
||||
{
|
||||
[DebuggerDisplay("{DebuggerToString,nq}")]
|
||||
public sealed class HttpMethodMetadata : IHttpMethodMetadata
|
||||
{
|
||||
public HttpMethodMetadata(IEnumerable<string> httpMethods)
|
||||
: this(httpMethods, acceptCorsPreflight: false)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpMethodMetadata(IEnumerable<string> httpMethods, bool acceptCorsPreflight)
|
||||
{
|
||||
if (httpMethods == null)
|
||||
{
|
||||
|
|
@ -17,8 +24,16 @@ namespace Microsoft.AspNetCore.Routing.Metadata
|
|||
}
|
||||
|
||||
HttpMethods = httpMethods.ToArray();
|
||||
AcceptCorsPreflight = acceptCorsPreflight;
|
||||
}
|
||||
|
||||
public bool AcceptCorsPreflight { get; }
|
||||
|
||||
public IReadOnlyList<string> HttpMethods { get; }
|
||||
|
||||
private string DebuggerToString()
|
||||
{
|
||||
return $"HttpMethods: {string.Join(",", HttpMethods)} - Cors: {AcceptCorsPreflight}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ namespace Microsoft.AspNetCore.Routing.Metadata
|
|||
{
|
||||
public interface IHttpMethodMetadata
|
||||
{
|
||||
bool AcceptCorsPreflight { get; }
|
||||
|
||||
IReadOnlyList<string> HttpMethods { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
|
|
|||
|
|
@ -1,91 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
||||
using Microsoft.AspNetCore.Routing.TestObjects;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Internal
|
||||
{
|
||||
public class HttpMethodEndpointConstraintTest
|
||||
{
|
||||
public static TheoryData AcceptCaseInsensitiveData =
|
||||
new TheoryData<IEnumerable<string>, string>
|
||||
{
|
||||
{ new string[] { "get", "Get", "GET", "GEt"}, "gEt" },
|
||||
{ new string[] { "POST", "PoSt", "GEt"}, "GET" },
|
||||
{ new string[] { "get" }, "get" },
|
||||
{ new string[] { "post" }, "POST" },
|
||||
{ new string[] { "gEt" }, "get" },
|
||||
{ new string[] { "get", "PoST" }, "pOSt" }
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AcceptCaseInsensitiveData))]
|
||||
public void HttpMethodEndpointConstraint_IgnoresPreflightRequests(IEnumerable<string> httpMethods, string accessControlMethod)
|
||||
{
|
||||
// Arrange
|
||||
var constraint = new HttpMethodEndpointConstraint(httpMethods);
|
||||
var context = CreateEndpointConstraintContext(constraint);
|
||||
context.HttpContext = CreateHttpContext("oPtIoNs", accessControlMethod);
|
||||
|
||||
// Act
|
||||
var result = constraint.Accept(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(result, "Request should have been rejected.");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AcceptCaseInsensitiveData))]
|
||||
public void HttpMethodEndpointConstraint_Accept_CaseInsensitive(IEnumerable<string> httpMethods, string expectedMethod)
|
||||
{
|
||||
// Arrange
|
||||
var constraint = new HttpMethodEndpointConstraint(httpMethods);
|
||||
var context = CreateEndpointConstraintContext(constraint);
|
||||
context.HttpContext = CreateHttpContext(expectedMethod);
|
||||
|
||||
// Act
|
||||
var result = constraint.Accept(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(result, "Request should have been accepted.");
|
||||
}
|
||||
|
||||
private static EndpointConstraintContext CreateEndpointConstraintContext(HttpMethodEndpointConstraint constraint)
|
||||
{
|
||||
var context = new EndpointConstraintContext();
|
||||
|
||||
var endpointSelectorCandidate = new EndpointSelectorCandidate(
|
||||
new TestEndpoint(EndpointMetadataCollection.Empty, string.Empty),
|
||||
0,
|
||||
new RouteValueDictionary(),
|
||||
new List<IEndpointConstraint> { constraint });
|
||||
|
||||
context.Candidates = new List<EndpointSelectorCandidate> { endpointSelectorCandidate };
|
||||
context.CurrentCandidate = context.Candidates[0];
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private static HttpContext CreateHttpContext(string requestedMethod, string accessControlMethod = null)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
|
||||
httpContext.Request.Method = requestedMethod;
|
||||
|
||||
if (accessControlMethod != null)
|
||||
{
|
||||
httpContext.Request.Headers.Add("Origin", StringValues.Empty);
|
||||
httpContext.Request.Headers.Add("Access-Control-Request-Method", accessControlMethod);
|
||||
}
|
||||
|
||||
return httpContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,9 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
|
|
@ -26,13 +26,19 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
template);
|
||||
}
|
||||
|
||||
private Matcher CreateDfaMatcher(EndpointDataSource dataSource)
|
||||
private Matcher CreateDfaMatcher(EndpointDataSource dataSource, EndpointSelector endpointSelector = null)
|
||||
{
|
||||
var services = new ServiceCollection()
|
||||
var serviceCollection = new ServiceCollection()
|
||||
.AddLogging()
|
||||
.AddOptions()
|
||||
.AddRouting()
|
||||
.BuildServiceProvider();
|
||||
.AddRouting();
|
||||
|
||||
if (endpointSelector != null)
|
||||
{
|
||||
serviceCollection.AddSingleton<EndpointSelector>(endpointSelector);
|
||||
}
|
||||
|
||||
var services = serviceCollection.BuildServiceProvider();
|
||||
|
||||
var factory = services.GetRequiredService<MatcherFactory>();
|
||||
return Assert.IsType<DataSourceDependentMatcher>(factory.CreateMatcher(dataSource));
|
||||
|
|
@ -115,22 +121,39 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
public async Task MatchAsync_MultipleMatches_EndpointSelectorCalled()
|
||||
{
|
||||
// Arrange
|
||||
var endpointWithoutConstraint = CreateEndpoint("/Teams", 0);
|
||||
var endpointWithConstraint = CreateEndpoint(
|
||||
"/Teams",
|
||||
0,
|
||||
metadata: new EndpointMetadataCollection(new object[] { new HttpMethodEndpointConstraint(new[] { "POST" }) }));
|
||||
var endpoint1 = CreateEndpoint("/Teams", 0);
|
||||
var endpoint2 = CreateEndpoint("/Teams", 1);
|
||||
|
||||
var endpointSelector = new Mock<EndpointSelector>();
|
||||
endpointSelector
|
||||
.Setup(s => s.SelectAsync(It.IsAny<HttpContext>(), It.IsAny<IEndpointFeature>(), It.IsAny<CandidateSet>()))
|
||||
.Callback<HttpContext, IEndpointFeature, CandidateSet>((c, f, cs) =>
|
||||
{
|
||||
Assert.Equal(2, cs.Count);
|
||||
|
||||
Assert.Same(endpoint1, cs[0].Endpoint);
|
||||
Assert.True(cs[0].IsValidCandidate);
|
||||
Assert.Equal(0, cs[0].Score);
|
||||
Assert.Empty(cs[0].Values);
|
||||
|
||||
Assert.Same(endpoint2, cs[1].Endpoint);
|
||||
Assert.True(cs[1].IsValidCandidate);
|
||||
Assert.Equal(1, cs[1].Score);
|
||||
Assert.Empty(cs[1].Values);
|
||||
|
||||
f.Endpoint = endpoint2;
|
||||
})
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var endpointDataSource = new DefaultEndpointDataSource(new List<Endpoint>
|
||||
{
|
||||
endpointWithoutConstraint,
|
||||
endpointWithConstraint
|
||||
endpoint1,
|
||||
endpoint2
|
||||
});
|
||||
|
||||
var matcher = CreateDfaMatcher(endpointDataSource);
|
||||
var matcher = CreateDfaMatcher(endpointDataSource, endpointSelector.Object);
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Method = "POST";
|
||||
httpContext.Request.Path = "/Teams";
|
||||
|
||||
var endpointFeature = new EndpointFeature();
|
||||
|
|
@ -139,7 +162,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
await matcher.MatchAsync(httpContext, endpointFeature);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(endpointWithConstraint, endpointFeature.Endpoint);
|
||||
Assert.Equal(endpoint2, endpointFeature.Endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Routing.Metadata;
|
|||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
using static Microsoft.AspNetCore.Routing.Matchers.HttpMethodMatcherPolicy;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
|
|
@ -31,6 +32,56 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
MatcherAssert.AssertMatch(feature, endpoint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Match_HttpMethod_CORS()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }, acceptCorsPreflight: true);
|
||||
|
||||
var matcher = CreateMatcher(endpoint);
|
||||
var (httpContext, feature) = CreateContext("/hello", "GET");
|
||||
|
||||
// Act
|
||||
await matcher.MatchAsync(httpContext, feature);
|
||||
|
||||
// Assert
|
||||
MatcherAssert.AssertMatch(feature, endpoint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Match_HttpMethod_CORS_Preflight()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }, acceptCorsPreflight: true);
|
||||
|
||||
var matcher = CreateMatcher(endpoint);
|
||||
var (httpContext, feature) = CreateContext("/hello", "GET", corsPreflight: true);
|
||||
|
||||
// Act
|
||||
await matcher.MatchAsync(httpContext, feature);
|
||||
|
||||
// Assert
|
||||
MatcherAssert.AssertMatch(feature, endpoint);
|
||||
}
|
||||
|
||||
|
||||
[Fact] // Nothing here supports OPTIONS, so it goes to a 405.
|
||||
public async Task NotMatch_HttpMethod_CORS_Preflight()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }, acceptCorsPreflight: false);
|
||||
|
||||
var matcher = CreateMatcher(endpoint);
|
||||
var (httpContext, feature) = CreateContext("/hello", "GET", corsPreflight: true);
|
||||
|
||||
// Act
|
||||
await matcher.MatchAsync(httpContext, feature);
|
||||
|
||||
// Assert
|
||||
Assert.NotSame(endpoint, feature.Endpoint);
|
||||
Assert.Same(HttpMethodMatcherPolicy.Http405EndpointDisplayName, feature.Endpoint.DisplayName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Match_HttpMethod_CaseInsensitive()
|
||||
{
|
||||
|
|
@ -47,6 +98,22 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
MatcherAssert.AssertMatch(feature, endpoint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Match_HttpMethod_CaseInsensitive_CORS_Preflight()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GeT", }, acceptCorsPreflight: true);
|
||||
|
||||
var matcher = CreateMatcher(endpoint);
|
||||
var (httpContext, feature) = CreateContext("/hello", "GET", corsPreflight: true);
|
||||
|
||||
// Act
|
||||
await matcher.MatchAsync(httpContext, feature);
|
||||
|
||||
// Assert
|
||||
MatcherAssert.AssertMatch(feature, endpoint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Match_NoMetadata_MatchesAnyHttpMethod()
|
||||
{
|
||||
|
|
@ -63,6 +130,38 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
MatcherAssert.AssertMatch(feature, endpoint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Match_NoMetadata_MatchesAnyHttpMethod_CORS_Preflight()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = CreateEndpoint("/hello", acceptCorsPreflight: true);
|
||||
|
||||
var matcher = CreateMatcher(endpoint);
|
||||
var (httpContext, feature) = CreateContext("/hello", "GET", corsPreflight: true);
|
||||
|
||||
// Act
|
||||
await matcher.MatchAsync(httpContext, feature);
|
||||
|
||||
// Assert
|
||||
MatcherAssert.AssertMatch(feature, endpoint);
|
||||
}
|
||||
|
||||
[Fact] // This matches because the endpoint accepts OPTIONS
|
||||
public async Task Match_NoMetadata_MatchesAnyHttpMethod_CORS_Preflight_DoesNotSupportPreflight()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = CreateEndpoint("/hello", acceptCorsPreflight: false);
|
||||
|
||||
var matcher = CreateMatcher(endpoint);
|
||||
var (httpContext, feature) = CreateContext("/hello", "GET", corsPreflight: true);
|
||||
|
||||
// Act
|
||||
await matcher.MatchAsync(httpContext, feature);
|
||||
|
||||
// Assert
|
||||
MatcherAssert.AssertMatch(feature, endpoint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Match_EmptyMethodList_MatchesAnyHttpMethod()
|
||||
{
|
||||
|
|
@ -96,7 +195,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
Assert.NotSame(endpoint1, feature.Endpoint);
|
||||
Assert.NotSame(endpoint2, feature.Endpoint);
|
||||
|
||||
Assert.Same(HttpMethodEndpointSelectorPolicy.Http405EndpointDisplayName, feature.Endpoint.DisplayName);
|
||||
Assert.Same(HttpMethodMatcherPolicy.Http405EndpointDisplayName, feature.Endpoint.DisplayName);
|
||||
|
||||
// Invoke the endpoint
|
||||
await feature.Invoker((c) => Task.CompletedTask)(httpContext);
|
||||
|
|
@ -104,6 +203,23 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
Assert.Equal("DELETE, GET, PUT", httpContext.Response.Headers["Allow"]);
|
||||
}
|
||||
|
||||
[Fact] // When all of the candidates handles specific verbs, use a 405 endpoint
|
||||
public async Task NotMatch_HttpMethod_CORS_DoesNotReturn405()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint1 = CreateEndpoint("/hello", httpMethods: new string[] { "GET", "PUT" }, acceptCorsPreflight: true);
|
||||
var endpoint2 = CreateEndpoint("/hello", httpMethods: new string[] { "DELETE" });
|
||||
|
||||
var matcher = CreateMatcher(endpoint1, endpoint2);
|
||||
var (httpContext, feature) = CreateContext("/hello", "POST", corsPreflight: true);
|
||||
|
||||
// Act
|
||||
await matcher.MatchAsync(httpContext, feature);
|
||||
|
||||
// Assert
|
||||
MatcherAssert.AssertNotMatch(feature);
|
||||
}
|
||||
|
||||
[Fact] // When one of the candidates handles all verbs, dont use a 405 endpoint
|
||||
public async Task NotMatch_HttpMethod_WithAllMethodEndpoint_DoesNotReturn405()
|
||||
{
|
||||
|
|
@ -189,12 +305,21 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
return builder.Build();
|
||||
}
|
||||
|
||||
internal static (HttpContext httpContext, IEndpointFeature feature) CreateContext(string path, string httpMethod)
|
||||
internal static (HttpContext httpContext, IEndpointFeature feature) CreateContext(
|
||||
string path,
|
||||
string httpMethod,
|
||||
bool corsPreflight = false)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Method = httpMethod;
|
||||
httpContext.Request.Method = corsPreflight ? PreflightHttpMethod : httpMethod;
|
||||
httpContext.Request.Path = path;
|
||||
|
||||
if (corsPreflight)
|
||||
{
|
||||
httpContext.Request.Headers[OriginHeader] = "example.com";
|
||||
httpContext.Request.Headers[AccessControlRequestMethod] = httpMethod;
|
||||
}
|
||||
|
||||
var feature = new EndpointFeature();
|
||||
httpContext.Features.Set<IEndpointFeature>(feature);
|
||||
|
||||
|
|
@ -205,12 +330,13 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
object defaults = null,
|
||||
object constraints = null,
|
||||
int order = 0,
|
||||
string[] httpMethods = null)
|
||||
string[] httpMethods = null,
|
||||
bool acceptCorsPreflight = false)
|
||||
{
|
||||
var metadata = new List<object>();
|
||||
if (httpMethods != null)
|
||||
{
|
||||
metadata.Add(new HttpMethodMetadata(httpMethods));
|
||||
metadata.Add(new HttpMethodMetadata(httpMethods ?? Array.Empty<string>(), acceptCorsPreflight));
|
||||
}
|
||||
|
||||
var displayName = "endpoint: " + template + " " + string.Join(", ", httpMethods ?? new[] { "(any)" });
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Routing.Metadata;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
using Xunit;
|
||||
using static Microsoft.AspNetCore.Routing.Matchers.HttpMethodMatcherPolicy;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
|
|
@ -32,7 +32,10 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
public void AppliesToNode_EndpointWithoutHttpMethods_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var endpoints = new[] { CreateEndpoint("/", Array.Empty<string>()), };
|
||||
var endpoints = new[]
|
||||
{
|
||||
CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
|
||||
};
|
||||
|
||||
var policy = CreatePolicy();
|
||||
|
||||
|
|
@ -47,7 +50,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
public void AppliesToNode_EndpointHasHttpMethods_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var endpoints = new[] { CreateEndpoint("/", Array.Empty<string>()), CreateEndpoint("/", new[] { "GET", })};
|
||||
var endpoints = new[]
|
||||
{
|
||||
CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
|
||||
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
|
||||
};
|
||||
|
||||
var policy = CreatePolicy();
|
||||
|
||||
|
|
@ -66,11 +73,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
{
|
||||
// These are arrange in an order that we won't actually see in a product scenario. It's done
|
||||
// this way so we can verify that ordering is preserved by GetEdges.
|
||||
CreateEndpoint("/", new[] { "GET", }),
|
||||
CreateEndpoint("/", Array.Empty<string>()),
|
||||
CreateEndpoint("/", new[] { "GET", "PUT", "POST" }),
|
||||
CreateEndpoint("/", new[] { "PUT", "POST" }),
|
||||
CreateEndpoint("/", Array.Empty<string>()),
|
||||
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
|
||||
CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
|
||||
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", "PUT", "POST" })),
|
||||
CreateEndpoint("/", new HttpMethodMetadata(new[] { "PUT", "POST" })),
|
||||
CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
|
||||
};
|
||||
|
||||
var policy = CreatePolicy();
|
||||
|
|
@ -83,26 +90,91 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
edges.OrderBy(e => e.State),
|
||||
e =>
|
||||
{
|
||||
Assert.Equal(HttpMethodEndpointSelectorPolicy.AnyMethod, e.State);
|
||||
Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State);
|
||||
Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal("GET", e.State);
|
||||
Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State);
|
||||
Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], endpoints[4], }, e.Endpoints.ToArray());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal("POST", e.State);
|
||||
Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State);
|
||||
Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal("PUT", e.State);
|
||||
Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State);
|
||||
Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEdges_GroupsByHttpMethod_Cors()
|
||||
{
|
||||
// Arrange
|
||||
var endpoints = new[]
|
||||
{
|
||||
// These are arrange in an order that we won't actually see in a product scenario. It's done
|
||||
// this way so we can verify that ordering is preserved by GetEdges.
|
||||
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
|
||||
CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
|
||||
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", "PUT", "POST" }, acceptCorsPreflight: true)),
|
||||
CreateEndpoint("/", new HttpMethodMetadata(new[] { "PUT", "POST" })),
|
||||
CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>(), acceptCorsPreflight: true)),
|
||||
};
|
||||
|
||||
var policy = CreatePolicy();
|
||||
|
||||
// Act
|
||||
var edges = policy.GetEdges(endpoints);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
edges.OrderBy(e => e.State),
|
||||
e =>
|
||||
{
|
||||
Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State);
|
||||
Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: true), e.State);
|
||||
Assert.Equal(new[] { endpoints[4], }, e.Endpoints.ToArray());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State);
|
||||
Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], endpoints[4], }, e.Endpoints.ToArray());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: true), e.State);
|
||||
Assert.Equal(new[] { endpoints[2], endpoints[4], }, e.Endpoints.ToArray());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State);
|
||||
Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: true), e.State);
|
||||
Assert.Equal(new[] { endpoints[2], endpoints[4], }, e.Endpoints.ToArray());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State);
|
||||
Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: true), e.State);
|
||||
Assert.Equal(new[] { endpoints[2], endpoints[4], }, e.Endpoints.ToArray());
|
||||
});
|
||||
}
|
||||
|
||||
[Fact] // See explanation in GetEdges for how this case is different
|
||||
public void GetEdges_GroupsByHttpMethod_CreatesHttp405Endpoint()
|
||||
{
|
||||
|
|
@ -111,9 +183,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
{
|
||||
// These are arrange in an order that we won't actually see in a product scenario. It's done
|
||||
// this way so we can verify that ordering is preserved by GetEdges.
|
||||
CreateEndpoint("/", new[] { "GET", }),
|
||||
CreateEndpoint("/", new[] { "GET", "PUT", "POST" }),
|
||||
CreateEndpoint("/", new[] { "PUT", "POST" }),
|
||||
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
|
||||
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", "PUT", "POST" })),
|
||||
CreateEndpoint("/", new HttpMethodMetadata(new[] { "PUT", "POST" })),
|
||||
};
|
||||
|
||||
var policy = CreatePolicy();
|
||||
|
|
@ -126,32 +198,91 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
edges.OrderBy(e => e.State),
|
||||
e =>
|
||||
{
|
||||
Assert.Equal(HttpMethodEndpointSelectorPolicy.AnyMethod, e.State);
|
||||
Assert.Equal(HttpMethodEndpointSelectorPolicy.Http405EndpointDisplayName, e.Endpoints.Single().DisplayName);
|
||||
Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State);
|
||||
Assert.Equal(Http405EndpointDisplayName, e.Endpoints.Single().DisplayName);
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal("GET", e.State);
|
||||
Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State);
|
||||
Assert.Equal(new[] { endpoints[0], endpoints[1], }, e.Endpoints.ToArray());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal("POST", e.State);
|
||||
Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State);
|
||||
Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal("PUT", e.State);
|
||||
Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State);
|
||||
Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
[Fact] // See explanation in GetEdges for how this case is different
|
||||
public void GetEdges_GroupsByHttpMethod_CreatesHttp405Endpoint_CORS()
|
||||
{
|
||||
// Arrange
|
||||
var endpoints = new[]
|
||||
{
|
||||
// These are arrange in an order that we won't actually see in a product scenario. It's done
|
||||
// this way so we can verify that ordering is preserved by GetEdges.
|
||||
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
|
||||
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", "PUT", "POST" }, acceptCorsPreflight: true)),
|
||||
CreateEndpoint("/", new HttpMethodMetadata(new[] { "PUT", "POST" })),
|
||||
};
|
||||
|
||||
var policy = CreatePolicy();
|
||||
|
||||
// Act
|
||||
var edges = policy.GetEdges(endpoints);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
edges.OrderBy(e => e.State),
|
||||
e =>
|
||||
{
|
||||
Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State);
|
||||
Assert.Equal(Http405EndpointDisplayName, e.Endpoints.Single().DisplayName);
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State);
|
||||
Assert.Equal(new[] { endpoints[0], endpoints[1], }, e.Endpoints.ToArray());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: true), e.State);
|
||||
Assert.Equal(new[] { endpoints[1], }, e.Endpoints.ToArray());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State);
|
||||
Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: true), e.State);
|
||||
Assert.Equal(new[] { endpoints[1], }, e.Endpoints.ToArray());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State);
|
||||
Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: true), e.State);
|
||||
Assert.Equal(new[] { endpoints[1], }, e.Endpoints.ToArray());
|
||||
});
|
||||
}
|
||||
|
||||
private static MatcherEndpoint CreateEndpoint(string template, string[] httpMethods)
|
||||
private static MatcherEndpoint CreateEndpoint(string template, HttpMethodMetadata httpMethodMetadata)
|
||||
{
|
||||
var metadata = new List<object>();
|
||||
if (httpMethods != null)
|
||||
if (httpMethodMetadata != null)
|
||||
{
|
||||
metadata.Add(new HttpMethodMetadata(httpMethods));
|
||||
metadata.Add(httpMethodMetadata);
|
||||
}
|
||||
|
||||
return new MatcherEndpoint(
|
||||
|
|
@ -163,9 +294,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
$"test: {template}");
|
||||
}
|
||||
|
||||
private static HttpMethodEndpointSelectorPolicy CreatePolicy()
|
||||
private static HttpMethodMatcherPolicy CreatePolicy()
|
||||
{
|
||||
return new HttpMethodEndpointSelectorPolicy();
|
||||
return new HttpMethodMatcherPolicy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue