File: RequireHttpsAttributeTests.cs
Web Access
Project: src\src\Mvc\Mvc.Core\test\Microsoft.AspNetCore.Mvc.Core.Test.csproj (Microsoft.AspNetCore.Mvc.Core.Test)
// 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.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
 
namespace Microsoft.AspNetCore.Mvc;
 
public class RequireHttpsAttributeTests
{
    [Fact]
    public void OnAuthorization_AllowsTheRequestIfItIsHttps()
    {
        // Arrange
        var requestContext = new DefaultHttpContext();
        requestContext.Request.Scheme = "https";
 
        var authContext = CreateAuthorizationContext(requestContext);
        var attr = new RequireHttpsAttribute();
 
        // Act
        attr.OnAuthorization(authContext);
 
        // Assert
        Assert.Null(authContext.Result);
    }
 
    public static TheoryData<string, string, string, string, string> RedirectToHttpEndpointTestData
    {
        get
        {
            // host, pathbase, path, query, expectedRedirectUrl
            return new TheoryData<string, string, string, string, string>
                {
                    { "localhost", null, null, null, "https://localhost" },
                    { "localhost:5000", null, null, null, "https://localhost" },
                    { "localhost", "/pathbase", null, null, "https://localhost/pathbase" },
                    { "localhost", "/pathbase", "/path", null, "https://localhost/pathbase/path" },
                    { "localhost", "/pathbase", "/path", "?foo=bar", "https://localhost/pathbase/path?foo=bar" },
 
                    // Encode some special characters on the URL.
                    { "localhost", "/path?base", null, null, "https://localhost/path%3Fbase" },
                    { "localhost", null, "/pa?th", null, "https://localhost/pa%3Fth" },
 
                    { "localhost", "/", null, "?foo=bar%2Fbaz", "https://localhost/?foo=bar%2Fbaz" },
 
                    // URLs with punycode
                    // 本地主機 is "localhost" in chinese traditional, "xn--tiq21tzznx7c" is the
                    // punycode representation.
                    { "本地主機", "/", null, null, "https://xn--tiq21tzznx7c/" },
                };
        }
    }
 
    [Theory]
    [MemberData(nameof(RedirectToHttpEndpointTestData))]
    public void OnAuthorization_RedirectsToHttpsEndpoint_ForNonHttpsGetRequests(
        string host,
        string pathBase,
        string path,
        string queryString,
        string expectedUrl)
    {
        // Arrange
        var requestContext = new DefaultHttpContext();
        requestContext.RequestServices = CreateServices();
        requestContext.Request.Scheme = "http";
        requestContext.Request.Method = "GET";
        requestContext.Request.Host = HostString.FromUriComponent(host);
 
        if (pathBase != null)
        {
            requestContext.Request.PathBase = new PathString(pathBase);
        }
 
        if (path != null)
        {
            requestContext.Request.Path = new PathString(path);
        }
 
        if (queryString != null)
        {
            requestContext.Request.QueryString = new QueryString(queryString);
        }
 
        var authContext = CreateAuthorizationContext(requestContext);
        var attr = new RequireHttpsAttribute();
 
        // Act
        attr.OnAuthorization(authContext);
 
        // Assert
        Assert.NotNull(authContext.Result);
        var result = Assert.IsType<RedirectResult>(authContext.Result);
 
        Assert.False(result.Permanent);
        Assert.Equal(expectedUrl, result.Url);
    }
 
    [Theory]
    [InlineData("POST")]
    [InlineData("PUT")]
    [InlineData("PATCH")]
    [InlineData("DELETE")]
    public void OnAuthorization_SignalsBadRequestStatusCode_ForNonHttpsAndNonGetRequests(string method)
    {
        // Arrange
        var requestContext = new DefaultHttpContext();
        requestContext.RequestServices = CreateServices();
        requestContext.Request.Scheme = "http";
        requestContext.Request.Method = method;
        var authContext = CreateAuthorizationContext(requestContext);
        var attr = new RequireHttpsAttribute();
 
        // Act
        attr.OnAuthorization(authContext);
 
        // Assert
        Assert.NotNull(authContext.Result);
        var result = Assert.IsType<StatusCodeResult>(authContext.Result);
        Assert.Equal(StatusCodes.Status403Forbidden, result.StatusCode);
    }
 
    [Fact]
    public void HandleNonHttpsRequestExtensibility()
    {
        // Arrange
        var requestContext = new DefaultHttpContext();
        requestContext.RequestServices = CreateServices();
        requestContext.Request.Scheme = "http";
 
        var authContext = CreateAuthorizationContext(requestContext);
        var attr = new CustomRequireHttpsAttribute();
 
        // Act
        attr.OnAuthorization(authContext);
 
        // Assert
        var result = Assert.IsType<StatusCodeResult>(authContext.Result);
        Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode);
    }
 
    [Theory]
    [InlineData("http://localhost", null, "https://localhost/")]
    [InlineData("http://localhost:5000", null, "https://localhost/")]
    [InlineData("http://[2001:db8:a0b:12f0::1]", null, "https://[2001:db8:a0b:12f0::1]/")]
    [InlineData("http://[2001:db8:a0b:12f0::1]:5000", null, "https://[2001:db8:a0b:12f0::1]/")]
    [InlineData("http://localhost:5000/path", null, "https://localhost/path")]
    [InlineData("http://localhost:5000/path?foo=bar", null, "https://localhost/path?foo=bar")]
    [InlineData("http://本地主機:5000", null, "https://xn--tiq21tzznx7c/")]
    [InlineData("http://localhost", 44380, "https://localhost:44380/")]
    [InlineData("http://localhost:5000", 44380, "https://localhost:44380/")]
    [InlineData("http://[2001:db8:a0b:12f0::1]", 44380, "https://[2001:db8:a0b:12f0::1]:44380/")]
    [InlineData("http://[2001:db8:a0b:12f0::1]:5000", 44380, "https://[2001:db8:a0b:12f0::1]:44380/")]
    [InlineData("http://localhost:5000/path", 44380, "https://localhost:44380/path")]
    [InlineData("http://localhost:5000/path?foo=bar", 44380, "https://localhost:44380/path?foo=bar")]
    [InlineData("http://本地主機:5000", 44380, "https://xn--tiq21tzznx7c:44380/")]
    public void OnAuthorization_RedirectsToHttpsEndpoint_ForCustomSslPort(
        string url,
        int? sslPort,
        string expectedUrl)
    {
        // Arrange
        var options = Options.Create(new MvcOptions());
        var uri = new Uri(url);
 
        var requestContext = new DefaultHttpContext();
        requestContext.RequestServices = CreateServices(sslPort);
        requestContext.Request.Scheme = "http";
        requestContext.Request.Method = "GET";
        requestContext.Request.Host = HostString.FromUriComponent(uri);
        requestContext.Request.Path = PathString.FromUriComponent(uri);
        requestContext.Request.QueryString = QueryString.FromUriComponent(uri);
 
        var authContext = CreateAuthorizationContext(requestContext);
        var attr = new RequireHttpsAttribute();
 
        // Act
        attr.OnAuthorization(authContext);
 
        // Assert
        Assert.NotNull(authContext.Result);
        var result = Assert.IsType<RedirectResult>(authContext.Result);
 
        Assert.Equal(expectedUrl, result.Url);
    }
 
    [Theory]
    [InlineData(null, true)]
    [InlineData(null, false)]
    [InlineData(true, false)]
    [InlineData(false, true)]
    public void OnAuthorization_RedirectsToHttpsEndpoint_WithSpecifiedStatusCodeAndRequireHttpsPermanentOption(bool? permanent, bool requireHttpsPermanent)
    {
        var requestContext = new DefaultHttpContext();
        requestContext.RequestServices = CreateServices(null, requireHttpsPermanent);
        requestContext.Request.Scheme = "http";
        requestContext.Request.Method = "GET";
 
        var authContext = CreateAuthorizationContext(requestContext);
        var attr = new RequireHttpsAttribute();
        if (permanent.HasValue)
        {
            attr.Permanent = permanent.Value;
        };
 
        // Act
        attr.OnAuthorization(authContext);
 
        // Assert
        var result = Assert.IsType<RedirectResult>(authContext.Result);
        Assert.Equal(permanent ?? requireHttpsPermanent, result.Permanent);
    }
 
    private class CustomRequireHttpsAttribute : RequireHttpsAttribute
    {
        protected override void HandleNonHttpsRequest(AuthorizationFilterContext filterContext)
        {
            filterContext.Result = new StatusCodeResult(StatusCodes.Status404NotFound);
        }
    }
 
    private static AuthorizationFilterContext CreateAuthorizationContext(HttpContext ctx)
    {
        var actionContext = new ActionContext(ctx, new RouteData(), new ActionDescriptor());
        return new AuthorizationFilterContext(actionContext, new IFilterMetadata[0]);
    }
 
    private static IServiceProvider CreateServices(int? sslPort = null, bool requireHttpsPermanent = false)
    {
        var options = Options.Create(new MvcOptions());
        options.Value.SslPort = sslPort;
        options.Value.RequireHttpsPermanent = requireHttpsPermanent;
 
        var services = new ServiceCollection();
        services.AddSingleton<IOptions<MvcOptions>>(options);
 
        return services.BuildServiceProvider();
    }
}