File: Matching\AcceptsMatcherPolicyTest.cs
Web Access
Project: src\src\Http\Routing\test\UnitTests\Microsoft.AspNetCore.Routing.Tests.csproj (Microsoft.AspNetCore.Routing.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Routing.Patterns;
 
namespace Microsoft.AspNetCore.Routing.Matching;
 
// There are some unit tests here for the IEndpointSelectorPolicy implementation.
// The INodeBuilderPolicy implementation is well-tested by functional tests.
public class AcceptsMatcherPolicyTest
{
    [Fact]
    public void INodeBuilderPolicy_AppliesToEndpoints_EndpointWithoutMetadata_ReturnsFalse()
    {
        // Arrange
        var endpoints = new[] { CreateEndpoint("/", null), };
 
        var policy = (INodeBuilderPolicy)CreatePolicy();
 
        // Act
        var result = policy.AppliesToEndpoints(endpoints);
 
        // Assert
        Assert.False(result);
    }
 
    [Fact]
    public void INodeBuilderPolicy_AppliesToEndpoints_EndpointWithoutContentTypes_ReturnsFalse()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new AcceptsMetadata(Array.Empty<string>())),
            };
 
        var policy = (INodeBuilderPolicy)CreatePolicy();
 
        // Act
        var result = policy.AppliesToEndpoints(endpoints);
 
        // Assert
        Assert.False(result);
    }
 
    [Fact]
    public void INodeBuilderPolicy_AppliesToEndpoints_EndpointHasContentTypes_ReturnsTrue()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new AcceptsMetadata(Array.Empty<string>())),
                CreateEndpoint("/", new AcceptsMetadata(new[] { "application/json", })),
            };
 
        var policy = (INodeBuilderPolicy)CreatePolicy();
 
        // Act
        var result = policy.AppliesToEndpoints(endpoints);
 
        // Assert
        Assert.True(result);
    }
 
    [Fact]
    public void INodeBuilderPolicy_AppliesToEndpoints_WithDynamicMetadata_ReturnsFalse()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new AcceptsMetadata(Array.Empty<string>()), new DynamicEndpointMetadata()),
                CreateEndpoint("/", new AcceptsMetadata(new[] { "application/json", })),
            };
 
        var policy = (INodeBuilderPolicy)CreatePolicy();
 
        // Act
        var result = policy.AppliesToEndpoints(endpoints);
 
        // Assert
        Assert.False(result);
    }
 
    [Fact]
    public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointWithoutMetadata_ReturnsTrue()
    {
        // Arrange
        var endpoints = new[] { CreateEndpoint("/", null, new DynamicEndpointMetadata()), };
 
        var policy = (IEndpointSelectorPolicy)CreatePolicy();
 
        // Act
        var result = policy.AppliesToEndpoints(endpoints);
 
        // Assert
        Assert.True(result);
    }
 
    [Fact]
    public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointWithoutContentTypes_ReturnsTrue()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new AcceptsMetadata(Array.Empty<string>()), new DynamicEndpointMetadata()),
            };
 
        var policy = (IEndpointSelectorPolicy)CreatePolicy();
 
        // Act
        var result = policy.AppliesToEndpoints(endpoints);
 
        // Assert
        Assert.True(result);
    }
 
    [Fact]
    public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointHasContentTypes_ReturnsTrue()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new AcceptsMetadata(Array.Empty<string>()), new DynamicEndpointMetadata()),
                CreateEndpoint("/", new AcceptsMetadata(new[] { "application/json", })),
            };
 
        var policy = (IEndpointSelectorPolicy)CreatePolicy();
 
        // Act
        var result = policy.AppliesToEndpoints(endpoints);
 
        // Assert
        Assert.True(result);
    }
 
    [Fact]
    public void IEndpointSelectorPolicy_AppliesToEndpoints_WithoutDynamicMetadata_ReturnsFalse()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new AcceptsMetadata(Array.Empty<string>())),
                CreateEndpoint("/", new AcceptsMetadata(new[] { "application/json", })),
            };
 
        var policy = (IEndpointSelectorPolicy)CreatePolicy();
 
        // Act
        var result = policy.AppliesToEndpoints(endpoints);
 
        // Assert
        Assert.False(result);
    }
 
    [Fact]
    public void GetEdges_GroupsByContentType()
    {
        // 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 AcceptsMetadata(new[] { "application/json", "application/*+json", })),
                CreateEndpoint("/", new AcceptsMetadata(Array.Empty<string>())),
                CreateEndpoint("/", new AcceptsMetadata(new[] { "application/xml", "application/*+xml", })),
                CreateEndpoint("/", new AcceptsMetadata(new[] { "application/*", })),
                CreateEndpoint("/", new AcceptsMetadata(new[]{ "*/*", })),
            };
 
        var policy = CreatePolicy();
 
        // Act
        var edges = policy.GetEdges(endpoints);
 
        // Assert
        Assert.Collection(
            edges.OrderBy(e => e.State),
            e =>
            {
                Assert.Equal(string.Empty, e.State);
                Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal("*/*", e.State);
                Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal("application/*", e.State);
                Assert.Equal(new[] { endpoints[1], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal("application/*+json", e.State);
                Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal("application/*+xml", e.State);
                Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal("application/json", e.State);
                Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal("application/xml", e.State);
                Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
            });
    }
 
    [Fact] // See explanation in GetEdges for how this case is different
    public void GetEdges_GroupsByContentType_CreatesHttp415Endpoint()
    {
        // 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 AcceptsMetadata(new[] { "application/json", "application/*+json", })),
                CreateEndpoint("/", new AcceptsMetadata(new[] { "application/xml", "application/*+xml", })),
                CreateEndpoint("/", new AcceptsMetadata(new[] { "application/*", })),
            };
 
        var policy = CreatePolicy();
 
        // Act
        var edges = policy.GetEdges(endpoints);
 
        // Assert
        Assert.Collection(
            edges.OrderBy(e => e.State),
            e =>
            {
                Assert.Equal(string.Empty, e.State);
                Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal("*/*", e.State);
                Assert.Equal(AcceptsMatcherPolicy.Http415EndpointDisplayName, Assert.Single(e.Endpoints).DisplayName);
            },
            e =>
            {
                Assert.Equal("application/*", e.State);
                Assert.Equal(new[] { endpoints[2], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal("application/*+json", e.State);
                Assert.Equal(new[] { endpoints[0], endpoints[2], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal("application/*+xml", e.State);
                Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal("application/json", e.State);
                Assert.Equal(new[] { endpoints[0], endpoints[2], }, e.Endpoints.ToArray());
            },
            e =>
            {
                Assert.Equal("application/xml", e.State);
                Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
            });
 
    }
 
    [Theory]
    [InlineData("image/png", 1)]
    [InlineData("application/foo", 2)]
    [InlineData("text/xml", 3)]
    [InlineData("application/product+json", 6)] // application/json will match this
    [InlineData("application/product+xml", 7)] // application/xml will match this
    [InlineData("application/json", 6)]
    [InlineData("application/xml", 7)]
    public void BuildJumpTable_SortsEdgesByPriority(string contentType, int expected)
    {
        // Arrange
        var edges = new PolicyJumpTableEdge[]
        {
                // In reverse order of how they should be processed
                new PolicyJumpTableEdge(string.Empty, 0),
                new PolicyJumpTableEdge("*/*", 1),
                new PolicyJumpTableEdge("application/*", 2),
                new PolicyJumpTableEdge("text/*", 3),
                new PolicyJumpTableEdge("application/*+xml", 4),
                new PolicyJumpTableEdge("application/*+json", 5),
                new PolicyJumpTableEdge("application/json", 6),
                new PolicyJumpTableEdge("application/xml", 7),
        };
 
        var policy = CreatePolicy();
 
        var jumpTable = policy.BuildJumpTable(-1, edges);
 
        var httpContext = new DefaultHttpContext();
        httpContext.Request.ContentType = contentType;
 
        // Act
        var actual = jumpTable.GetDestination(httpContext);
 
        // Assert
        Assert.Equal(expected, actual);
    }
 
    [Fact]
    public async Task ApplyAsync_EndpointWithoutMetadata_MatchWithoutContentType()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", null),
            };
 
        var candidates = CreateCandidateSet(endpoints);
        var httpContext = new DefaultHttpContext();
 
        var policy = CreatePolicy();
 
        // Act
        await policy.ApplyAsync(httpContext, candidates);
 
        // Assert
        Assert.True(candidates.IsValidCandidate(0));
    }
 
    [Fact]
    public async Task ApplyAsync_EndpointAllowsAnyContentType_MatchWithoutContentType()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new AcceptsMetadata(Array.Empty<string>())),
            };
 
        var candidates = CreateCandidateSet(endpoints);
        var httpContext = new DefaultHttpContext();
 
        var policy = CreatePolicy();
 
        // Act
        await policy.ApplyAsync(httpContext, candidates);
 
        // Assert
        Assert.True(candidates.IsValidCandidate(0));
    }
 
    [Fact]
    public async Task ApplyAsync_EndpointHasWildcardContentType_MatchWithoutContentType()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new AcceptsMetadata(new string[] { "*/*" })),
            };
 
        var candidates = CreateCandidateSet(endpoints);
        var httpContext = new DefaultHttpContext();
 
        var policy = CreatePolicy();
 
        // Act
        await policy.ApplyAsync(httpContext, candidates);
 
        // Assert
        Assert.True(candidates.IsValidCandidate(0));
    }
 
    [Fact]
    public async Task ApplyAsync_EndpointWithoutMetadata_MatchWithAnyContentType()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", null),
            };
 
        var candidates = CreateCandidateSet(endpoints);
        var httpContext = new DefaultHttpContext()
        {
            Request =
                {
                    ContentType = "text/plain",
                },
        };
 
        var policy = CreatePolicy();
 
        // Act
        await policy.ApplyAsync(httpContext, candidates);
 
        // Assert
        Assert.True(candidates.IsValidCandidate(0));
    }
 
    [Fact]
    public async Task ApplyAsync_EndpointAllowsAnyContentType_MatchWithAnyContentType()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new AcceptsMetadata(Array.Empty<string>())),
            };
 
        var candidates = CreateCandidateSet(endpoints);
        var httpContext = new DefaultHttpContext()
        {
            Request =
                {
                    ContentType = "text/plain",
                },
        };
 
        var policy = CreatePolicy();
 
        // Act
        await policy.ApplyAsync(httpContext, candidates);
 
        // Assert
        Assert.True(candidates.IsValidCandidate(0));
    }
 
    [Fact]
    public async Task ApplyAsync_EndpointHasWildcardContentType_MatchWithAnyContentType()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new AcceptsMetadata(new string[] { "*/*" })),
            };
 
        var candidates = CreateCandidateSet(endpoints);
        var httpContext = new DefaultHttpContext()
        {
            Request =
                {
                    ContentType = "text/plain",
                },
        };
 
        var policy = CreatePolicy();
 
        // Act
        await policy.ApplyAsync(httpContext, candidates);
 
        // Assert
        Assert.True(candidates.IsValidCandidate(0));
    }
 
    [Fact]
    public async Task ApplyAsync_EndpointHasSubTypeWildcard_MatchWithValidContentType()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new AcceptsMetadata(new string[] { "application/*+json", })),
            };
 
        var candidates = CreateCandidateSet(endpoints);
        var httpContext = new DefaultHttpContext()
        {
            Request =
                {
                    ContentType = "application/project+json",
                },
        };
 
        var policy = CreatePolicy();
 
        // Act
        await policy.ApplyAsync(httpContext, candidates);
 
        // Assert
        Assert.True(candidates.IsValidCandidate(0));
    }
 
    [Fact]
    public async Task ApplyAsync_EndpointHasMultipleContentType_MatchWithValidContentType()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new AcceptsMetadata(new string[] { "text/xml", "application/xml", })),
            };
 
        var candidates = CreateCandidateSet(endpoints);
        var httpContext = new DefaultHttpContext()
        {
            Request =
                {
                    ContentType = "application/xml",
                },
        };
 
        var policy = CreatePolicy();
 
        // Act
        await policy.ApplyAsync(httpContext, candidates);
 
        // Assert
        Assert.True(candidates.IsValidCandidate(0));
    }
 
    [Fact]
    public async Task ApplyAsync_EndpointDoesNotMatch_Returns415()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new AcceptsMetadata(new string[] { "text/xml", "application/xml", })),
            };
 
        var candidates = CreateCandidateSet(endpoints);
        var httpContext = new DefaultHttpContext()
        {
            Request =
                {
                    ContentType = "application/json",
                },
        };
 
        var policy = CreatePolicy();
 
        // Act
        await policy.ApplyAsync(httpContext, candidates);
 
        // Assert
        Assert.False(candidates.IsValidCandidate(0));
        Assert.NotNull(httpContext.GetEndpoint());
    }
 
    [Fact]
    public async Task ApplyAsync_EndpointDoesNotMatch_DoesNotReturns415WithContentTypeObliviousEndpoint()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new AcceptsMetadata(new string[] { "text/xml", "application/xml", })),
                CreateEndpoint("/", null)
            };
 
        var candidates = CreateCandidateSet(endpoints);
        var httpContext = new DefaultHttpContext()
        {
            Request =
                {
                    ContentType = "application/json",
                },
        };
 
        var policy = CreatePolicy();
 
        // Act
        await policy.ApplyAsync(httpContext, candidates);
 
        // Assert
        Assert.False(candidates.IsValidCandidate(0));
        Assert.Null(httpContext.GetEndpoint());
    }
 
    [Fact]
    public async Task ApplyAsync_EndpointDoesNotMatch_DoesNotReturns415WithContentTypeWildcardEndpoint()
    {
        // Arrange
        var endpoints = new[]
        {
                CreateEndpoint("/", new AcceptsMetadata(new string[] { "text/xml", "application/xml", })),
                CreateEndpoint("/", new AcceptsMetadata(new string[] { "*/*", }))
            };
 
        var candidates = CreateCandidateSet(endpoints);
        var httpContext = new DefaultHttpContext()
        {
            Request =
                {
                    ContentType = "application/json",
                },
        };
 
        var policy = CreatePolicy();
 
        // Act
        await policy.ApplyAsync(httpContext, candidates);
 
        // Assert
        Assert.False(candidates.IsValidCandidate(0));
        Assert.True(candidates.IsValidCandidate(1));
        Assert.Null(httpContext.GetEndpoint());
    }
 
    private static RouteEndpoint CreateEndpoint(string template, AcceptsMetadata consumesMetadata, params object[] more)
    {
        var metadata = new List<object>();
        if (consumesMetadata != null)
        {
            metadata.Add(consumesMetadata);
        }
 
        if (more != null)
        {
            metadata.AddRange(more);
        }
 
        return new RouteEndpoint(
            (context) => Task.CompletedTask,
            RoutePatternFactory.Parse(template),
            0,
            new EndpointMetadataCollection(metadata),
            $"test: {template} - {string.Join(", ", consumesMetadata?.ContentTypes ?? Array.Empty<string>())}");
    }
 
    private static CandidateSet CreateCandidateSet(Endpoint[] endpoints)
    {
        return new CandidateSet(endpoints, new RouteValueDictionary[endpoints.Length], new int[endpoints.Length]);
    }
 
    private static AcceptsMatcherPolicy CreatePolicy()
    {
        return new AcceptsMatcherPolicy();
    }
 
    private class DynamicEndpointMetadata : IDynamicEndpointMetadata
    {
        public bool IsDynamic => true;
    }
}