512 lines
18 KiB
C#
512 lines
18 KiB
C#
// 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 System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Routing.Matchers;
|
|
using Microsoft.AspNetCore.Routing.Patterns;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Logging.Testing;
|
|
using Moq;
|
|
using Xunit;
|
|
|
|
namespace Microsoft.AspNetCore.Routing.EndpointConstraints
|
|
{
|
|
public class EndpointConstraintEndpointSelectorTest
|
|
{
|
|
[Fact]
|
|
public async Task SelectBestCandidate_MultipleEndpoints_BestMatchSelected()
|
|
{
|
|
// Arrange
|
|
var defaultEndpoint = CreateEndpoint("No constraint endpoint");
|
|
|
|
var postEndpoint = CreateEndpoint(
|
|
"POST constraint endpoint",
|
|
new HttpMethodEndpointConstraint(new[] { "POST" }));
|
|
|
|
var endpoints = new[]
|
|
{
|
|
defaultEndpoint,
|
|
postEndpoint
|
|
};
|
|
|
|
var selector = CreateSelector(endpoints);
|
|
|
|
var httpContext = new DefaultHttpContext();
|
|
httpContext.Request.Method = "POST";
|
|
|
|
var feature = new EndpointFeature();
|
|
|
|
// Act
|
|
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
|
|
|
|
// Assert
|
|
Assert.Same(postEndpoint, feature.Endpoint);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SelectBestCandidate_MultipleEndpoints_AmbiguousMatchExceptionThrown()
|
|
{
|
|
// Arrange
|
|
var expectedMessage =
|
|
"The request matched multiple endpoints. Matches: " + Environment.NewLine +
|
|
Environment.NewLine +
|
|
"Ambiguous1" + Environment.NewLine +
|
|
"Ambiguous2";
|
|
|
|
var defaultEndpoint1 = CreateEndpoint("Ambiguous1");
|
|
var defaultEndpoint2 = CreateEndpoint("Ambiguous2");
|
|
|
|
var endpoints = new[]
|
|
{
|
|
defaultEndpoint1,
|
|
defaultEndpoint2
|
|
};
|
|
|
|
var selector = CreateSelector(endpoints);
|
|
|
|
var httpContext = new DefaultHttpContext();
|
|
httpContext.Request.Method = "POST";
|
|
|
|
var feature = new EndpointFeature();
|
|
|
|
// Act
|
|
var ex = await Assert.ThrowsAnyAsync<AmbiguousMatchException>(() =>
|
|
{
|
|
return selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
|
|
});
|
|
|
|
// Assert
|
|
Assert.Equal(expectedMessage, ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SelectBestCandidate_AmbiguousEndpoints_LogIsCorrect()
|
|
{
|
|
// Arrange
|
|
var sink = new TestSink();
|
|
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
|
|
|
var endpoints = new[]
|
|
{
|
|
CreateEndpoint("A1"),
|
|
CreateEndpoint("A2"),
|
|
};
|
|
|
|
var selector = CreateSelector(endpoints, loggerFactory);
|
|
|
|
var httpContext = CreateHttpContext("POST");
|
|
var feature = new EndpointFeature();
|
|
|
|
var names = string.Join(", ", endpoints.Select(action => action.DisplayName));
|
|
var expectedMessage =
|
|
$"Request matched multiple endpoints for request path '/test'. " +
|
|
$"Matching endpoints: {names}";
|
|
|
|
// Act
|
|
await Assert.ThrowsAsync<AmbiguousMatchException>(() =>
|
|
{
|
|
return selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
|
|
});
|
|
|
|
// Assert
|
|
Assert.Empty(sink.Scopes);
|
|
var write = Assert.Single(sink.Writes);
|
|
Assert.Equal(expectedMessage, write.State?.ToString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SelectBestCandidate_PrefersEndpointWithConstraints()
|
|
{
|
|
// Arrange
|
|
var endpointWithConstraint = CreateEndpoint(
|
|
"Has constraint",
|
|
new HttpMethodEndpointConstraint(new string[] { "POST" }));
|
|
|
|
var endpointWithoutConstraints = CreateEndpoint("No constraint");
|
|
|
|
var endpoints = new[] { endpointWithConstraint, endpointWithoutConstraints };
|
|
|
|
var selector = CreateSelector(endpoints);
|
|
var httpContext = CreateHttpContext("POST");
|
|
var feature = new EndpointFeature();
|
|
|
|
// Act
|
|
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
|
|
|
|
// Assert
|
|
Assert.Same(endpointWithConstraint, endpointWithConstraint);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SelectBestCandidate_ConstraintsRejectAll()
|
|
{
|
|
// Arrange
|
|
var endpoint1 = CreateEndpoint(
|
|
"action1",
|
|
new BooleanConstraint() { Pass = false, });
|
|
|
|
var endpoint2 = CreateEndpoint(
|
|
"action2",
|
|
new BooleanConstraint() { Pass = false, });
|
|
|
|
var endpoints = new[] { endpoint1, endpoint2 };
|
|
|
|
var selector = CreateSelector(endpoints);
|
|
var httpContext = CreateHttpContext("POST");
|
|
var feature = new EndpointFeature();
|
|
|
|
// Act
|
|
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
|
|
|
|
// Assert
|
|
Assert.Null(feature.Endpoint);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SelectBestCandidate_ConstraintsRejectAll_DifferentStages()
|
|
{
|
|
// Arrange
|
|
var endpoint1 = CreateEndpoint(
|
|
"action1",
|
|
new BooleanConstraint() { Pass = false, Order = 0 },
|
|
new BooleanConstraint() { Pass = true, Order = 1 });
|
|
|
|
var endpoint2 = CreateEndpoint(
|
|
"action2",
|
|
new BooleanConstraint() { Pass = true, Order = 0 },
|
|
new BooleanConstraint() { Pass = false, Order = 1 });
|
|
|
|
var endpoints = new[] { endpoint1, endpoint2 };
|
|
|
|
var selector = CreateSelector(endpoints);
|
|
var httpContext = CreateHttpContext("POST");
|
|
var feature = new EndpointFeature();
|
|
|
|
// Act
|
|
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
|
|
|
|
// Assert
|
|
Assert.Null(feature.Endpoint);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SelectBestCandidate_EndpointConstraintFactory()
|
|
{
|
|
// Arrange
|
|
var endpointWithConstraints = CreateEndpoint(
|
|
"actionWithConstraints",
|
|
new ConstraintFactory()
|
|
{
|
|
Constraint = new BooleanConstraint() { Pass = true },
|
|
});
|
|
|
|
var actionWithoutConstraints = CreateEndpoint("actionWithoutConstraints");
|
|
|
|
var endpoints = new[] { endpointWithConstraints, actionWithoutConstraints };
|
|
|
|
var selector = CreateSelector(endpoints);
|
|
var httpContext = CreateHttpContext("POST");
|
|
var feature = new EndpointFeature();
|
|
|
|
// Act
|
|
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
|
|
|
|
// Assert
|
|
Assert.Same(endpointWithConstraints, feature.Endpoint);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SelectBestCandidate_MultipleCallsNoConstraint_ReturnsEndpoint()
|
|
{
|
|
// Arrange
|
|
var noConstraint = CreateEndpoint("noConstraint");
|
|
|
|
var endpoints = new[] { noConstraint };
|
|
|
|
var selector = CreateSelector(endpoints);
|
|
var httpContext = CreateHttpContext("POST");
|
|
var feature = new EndpointFeature();
|
|
|
|
// Act
|
|
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
|
|
var endpoint1 = feature.Endpoint;
|
|
|
|
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
|
|
var endpoint2 = feature.Endpoint;
|
|
|
|
// Assert
|
|
Assert.Same(endpoint1, noConstraint);
|
|
Assert.Same(endpoint2, noConstraint);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SelectBestCandidate_MultipleCallsNonConstraintMetadata_ReturnsEndpoint()
|
|
{
|
|
// Arrange
|
|
var noConstraint = CreateEndpoint("noConstraint", new object());
|
|
|
|
var endpoints = new[] { noConstraint };
|
|
|
|
var selector = CreateSelector(endpoints);
|
|
var httpContext = CreateHttpContext("POST");
|
|
var feature = new EndpointFeature();
|
|
|
|
// Act
|
|
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
|
|
var endpoint1 = feature.Endpoint;
|
|
|
|
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
|
|
var endpoint2 = feature.Endpoint;
|
|
|
|
// Assert
|
|
Assert.Same(endpoint1, noConstraint);
|
|
Assert.Same(endpoint2, noConstraint);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SelectBestCandidate_EndpointConstraintFactory_ReturnsNull()
|
|
{
|
|
// Arrange
|
|
var nullConstraint = CreateEndpoint("nullConstraint", new ConstraintFactory());
|
|
|
|
var endpoints = new[] { nullConstraint };
|
|
|
|
var selector = CreateSelector(endpoints);
|
|
var httpContext = CreateHttpContext("POST");
|
|
var feature = new EndpointFeature();
|
|
|
|
// Act
|
|
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
|
|
var endpoint1 = feature.Endpoint;
|
|
|
|
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
|
|
var endpoint2 = feature.Endpoint;
|
|
|
|
// Assert
|
|
Assert.Same(endpoint1, nullConstraint);
|
|
Assert.Same(endpoint2, nullConstraint);
|
|
}
|
|
|
|
// There's a custom constraint provider registered that only understands BooleanConstraintMarker
|
|
[Fact]
|
|
public async Task SelectBestCandidate_CustomProvider()
|
|
{
|
|
// Arrange
|
|
var endpointWithConstraints = CreateEndpoint(
|
|
"actionWithConstraints",
|
|
new BooleanConstraintMarker() { Pass = true });
|
|
|
|
var endpointWithoutConstraints = CreateEndpoint("actionWithoutConstraints");
|
|
|
|
var endpoints = new[] { endpointWithConstraints, endpointWithoutConstraints, };
|
|
|
|
var selector = CreateSelector(endpoints);
|
|
var httpContext = CreateHttpContext("POST");
|
|
var feature = new EndpointFeature();
|
|
|
|
// Act
|
|
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
|
|
|
|
// Assert
|
|
Assert.Same(endpointWithConstraints, feature.Endpoint);
|
|
}
|
|
|
|
// Due to ordering of stages, the first action will be better.
|
|
[Fact]
|
|
public async Task SelectBestCandidate_ConstraintsInOrder()
|
|
{
|
|
// Arrange
|
|
var best = CreateEndpoint("best", new BooleanConstraint() { Pass = true, Order = 0, });
|
|
|
|
var worst = CreateEndpoint("worst", new BooleanConstraint() { Pass = true, Order = 1, });
|
|
|
|
var endpoints = new[] { best, worst };
|
|
|
|
var selector = CreateSelector(endpoints);
|
|
var httpContext = CreateHttpContext("POST");
|
|
var feature = new EndpointFeature();
|
|
|
|
// Act
|
|
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
|
|
|
|
// Assert
|
|
Assert.Same(best, feature.Endpoint);
|
|
}
|
|
|
|
// Due to ordering of stages, the first action will be better.
|
|
[Fact]
|
|
public async Task SelectBestCandidate_ConstraintsInOrder_MultipleStages()
|
|
{
|
|
// Arrange
|
|
var best = CreateEndpoint(
|
|
"best",
|
|
new BooleanConstraint() { Pass = true, Order = 0, },
|
|
new BooleanConstraint() { Pass = true, Order = 1, },
|
|
new BooleanConstraint() { Pass = true, Order = 2, });
|
|
|
|
var worst = CreateEndpoint(
|
|
"worst",
|
|
new BooleanConstraint() { Pass = true, Order = 0, },
|
|
new BooleanConstraint() { Pass = true, Order = 1, },
|
|
new BooleanConstraint() { Pass = true, Order = 3, });
|
|
|
|
var endpoints = new[] { best, worst };
|
|
|
|
var selector = CreateSelector(endpoints);
|
|
var httpContext = CreateHttpContext("POST");
|
|
var feature = new EndpointFeature();
|
|
|
|
// Act
|
|
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
|
|
|
|
// Assert
|
|
Assert.Same(best, feature.Endpoint);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SelectBestCandidate_Fallback_ToEndpointWithoutConstraints()
|
|
{
|
|
// Arrange
|
|
var nomatch1 = CreateEndpoint(
|
|
"nomatch1",
|
|
new BooleanConstraint() { Pass = true, Order = 0, },
|
|
new BooleanConstraint() { Pass = true, Order = 1, },
|
|
new BooleanConstraint() { Pass = false, Order = 2, });
|
|
|
|
var nomatch2 = CreateEndpoint(
|
|
"nomatch2",
|
|
new BooleanConstraint() { Pass = true, Order = 0, },
|
|
new BooleanConstraint() { Pass = true, Order = 1, },
|
|
new BooleanConstraint() { Pass = false, Order = 3, });
|
|
|
|
var best = CreateEndpoint("best");
|
|
|
|
var endpoints = new[] { best, nomatch1, nomatch2 };
|
|
|
|
var selector = CreateSelector(endpoints);
|
|
var httpContext = CreateHttpContext("POST");
|
|
var feature = new EndpointFeature();
|
|
|
|
// Act
|
|
await selector.SelectAsync(httpContext, feature, CreateCandidateSet(endpoints));
|
|
|
|
// Assert
|
|
Assert.Same(best, feature.Endpoint);
|
|
}
|
|
|
|
private static MatcherEndpoint CreateEndpoint(string displayName, params object[] metadata)
|
|
{
|
|
return new MatcherEndpoint(
|
|
MatcherEndpoint.EmptyInvoker,
|
|
RoutePatternFactory.Parse("/"),
|
|
new RouteValueDictionary(),
|
|
0,
|
|
new EndpointMetadataCollection(metadata),
|
|
displayName);
|
|
}
|
|
|
|
private static CandidateSet CreateCandidateSet(MatcherEndpoint[] endpoints)
|
|
{
|
|
var scores = new int[endpoints.Length];
|
|
return new CandidateSet(endpoints, scores);
|
|
}
|
|
|
|
private static EndpointSelector CreateSelector(IReadOnlyList<Endpoint> actions, ILoggerFactory loggerFactory = null)
|
|
{
|
|
loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
|
|
|
|
var endpointDataSource = new CompositeEndpointDataSource(new[] { new DefaultEndpointDataSource(actions) });
|
|
|
|
var actionConstraintProviders = new IEndpointConstraintProvider[] {
|
|
new DefaultEndpointConstraintProvider(),
|
|
new BooleanConstraintProvider(),
|
|
};
|
|
|
|
return new EndpointConstraintEndpointSelector(
|
|
endpointDataSource,
|
|
GetEndpointConstraintCache(actionConstraintProviders),
|
|
loggerFactory);
|
|
}
|
|
|
|
private static HttpContext CreateHttpContext(string httpMethod)
|
|
{
|
|
var serviceProvider = new ServiceCollection().BuildServiceProvider();
|
|
|
|
var httpContext = new Mock<HttpContext>(MockBehavior.Strict);
|
|
|
|
var request = new Mock<HttpRequest>(MockBehavior.Strict);
|
|
request.SetupGet(r => r.Method).Returns(httpMethod);
|
|
request.SetupGet(r => r.Path).Returns(new PathString("/test"));
|
|
request.SetupGet(r => r.Headers).Returns(new HeaderDictionary());
|
|
httpContext.SetupGet(c => c.Request).Returns(request.Object);
|
|
httpContext.SetupGet(c => c.RequestServices).Returns(serviceProvider);
|
|
|
|
return httpContext.Object;
|
|
}
|
|
|
|
private static EndpointConstraintCache GetEndpointConstraintCache(IEndpointConstraintProvider[] actionConstraintProviders = null)
|
|
{
|
|
return new EndpointConstraintCache(
|
|
new CompositeEndpointDataSource(Array.Empty<EndpointDataSource>()),
|
|
actionConstraintProviders.AsEnumerable() ?? new List<IEndpointConstraintProvider>());
|
|
}
|
|
|
|
private class BooleanConstraint : IEndpointConstraint
|
|
{
|
|
public bool Pass { get; set; }
|
|
|
|
public int Order { get; set; }
|
|
|
|
public bool Accept(EndpointConstraintContext context)
|
|
{
|
|
return Pass;
|
|
}
|
|
}
|
|
|
|
private class ConstraintFactory : IEndpointConstraintFactory
|
|
{
|
|
public IEndpointConstraint Constraint { get; set; }
|
|
|
|
public bool IsReusable => true;
|
|
|
|
public IEndpointConstraint CreateInstance(IServiceProvider services)
|
|
{
|
|
return Constraint;
|
|
}
|
|
}
|
|
|
|
private class BooleanConstraintMarker : IEndpointConstraintMetadata
|
|
{
|
|
public bool Pass { get; set; }
|
|
}
|
|
|
|
private class BooleanConstraintProvider : IEndpointConstraintProvider
|
|
{
|
|
public int Order { get; set; }
|
|
|
|
public void OnProvidersExecuting(EndpointConstraintProviderContext context)
|
|
{
|
|
foreach (var item in context.Results)
|
|
{
|
|
if (item.Metadata is BooleanConstraintMarker marker)
|
|
{
|
|
Assert.Null(item.Constraint);
|
|
item.Constraint = new BooleanConstraint() { Pass = marker.Pass };
|
|
}
|
|
}
|
|
}
|
|
|
|
public void OnProvidersExecuted(EndpointConstraintProviderContext context)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|