在ASP.NET Core MVC API控制器上单元测试AuthorizeAttribute

27

我有一个使用ASP.NET Core MVC的API,其中控制器需要进行单元测试。

控制器:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;

namespace TransitApi.Api.Controllers
{
    [Route("api/foo")]
    public class FooController : Controller
    {
        private IFooRepository FooRepository { get; }

        public FooController(IFooRepository fooRepository)
        {
            FooRepository = fooRepository;
        }

        [HttpGet]
        [Authorize("scopes:getfoos")]
        public async Task<IActionResult> GetAsync()
        {
            var foos = await FooRepository.GetAsync();
            return Json(foos);
        }
    }
}

我必须能够对AuthorizeAttribute的有效性进行单元测试是至关重要的。我们的代码库中存在属性缺失和错误范围的问题。虽然这个答案正是我所需要的,但由于Microsoft.AspNetCore.Mvc.Controller中没有ActionInvoker方法,因此我无法采用这种方式。

单元测试:

[Fact]
public void GetAsync_InvalidScope_ReturnsUnauthorizedResult()
{
    // Arrange
    var fooRepository = new StubFooRepository();
    var controller = new FooController(fooRepository)
    {
        ControllerContext = new ControllerContext
        {
            HttpContext = new FakeHttpContext()
            // User unfortunately not available in HttpContext
            //,User = new User() { Scopes = "none" }
        }
    };

    // Act
    var result = controller.GetAsync().Result;

    // Assert
    Assert.IsType<UnauthorizedResult>(result);
}

如何对我的控制器方法进行单元测试,以验证没有正确权限的用户被拒绝访问?

目前我只测试了AuthorizeAttribute的存在,但这样做远远不够:

    [Fact]
    public void GetAsync_Analysis_HasAuthorizeAttribute()
    {
        // Arrange
        var fooRepository = new StubFooRepository();
        var controller = new FooController(fooRepository)
        {
            ControllerContext = new ControllerContext
            {
                HttpContext = new FakeHttpContext()
            }
        };

        // Act
        var type = controller.GetType();
        var methodInfo = type.GetMethod("GetAsync", new Type[] { });
        var attributes = methodInfo.GetCustomAttributes(typeof(AuthorizeAttribute), true);

        // Assert
        Assert.True(attributes.Any());
    }

2
这将需要使用内存测试服务器进行集成测试。 - Nkosi
2
为什么仅检查AuthorizeAttribute是否存在还不够?AuthorizeAttribute既是属性也是一个IAuthorizationFilter,属性部分并不能执行任何操作,它只是元数据。MVC的单元测试保证了如果有此属性,则将其注册为当前请求的授权过滤器并运行逻辑。如果您使用AuthorizeAttribute的子类,那么测试其逻辑是有意义的,但是由于您没有使用子类,因此您需要检查的唯一内容是属性的存在以及其属性(用户)的配置。 - NightOwl888
能够对接收特定HTTP响应进行单元测试的想法很有吸引力。例如,为了确保没有人意外更改了授权范围。但我承认这可能必须作为集成测试来完成。所以有一个答案可以接受,您能否像@Nkosi建议的那样推荐一种优雅的方法来扫描所有控制器操作以进行授权?我宁愿不必为每个操作创建单独的测试... - 08Dc91wk
1
@Ivan - 如果您需要所有方法但只授权少数方法,则可以在启动时全局注册 AuthorizeAttribute,然后使用 AllowAnonymous 来覆盖行为。这样,它们默认是被锁定的,您就不必担心稍后更改会遗漏一个。或者,您可以创建自己的自定义 IAuthorizationFilter 全局注册,管理整个应用程序的安全性(甚至可能是您自己的属性来执行某些操作),这可以作为与控制器和操作分开测试的单独部分。 - NightOwl888
1
用户具有单独访问方法的范围。例如,具有“scope:bar”的用户可以获取“bar”,但不能获取“foo”,反之亦然,而具有“scope:all”的用户可以访问两者。这部分是测试这些属性非常重要的原因。 - 08Dc91wk
显示剩余2条评论
3个回答

16

由于该属性在处理请求管道时由框架进行评估,因此需要使用内存中的测试服务器进行集成测试。

ASP.NET Core 中的集成测试

集成测试可确保应用程序组件在组合在一起时正常运行。ASP.NET Core 支持使用单元测试框架和内置的测试 Web 主机进行集成测试,无需进行网络开销处理请求。

[Fact]
public async Task GetAsync_InvalidScope_ReturnsUnauthorizedResult() {
    // Arrange
    var server = new TestServer(new WebHostBuilder().UseStartup<Startup>());
    var client = server.CreateClient();
    var url = "api/foo";
    var expected = HttpStatusCode.Unauthorized;

    // Act
    var response = await client.GetAsync(url);

    // Assert
    Assert.AreEqual(expected, response.StatusCode);
}

如果你不想让测试影响实际的生产环境,你还可以专门创建一个测试启动项,用存根/模拟替换任何依赖项中的DI。


2
谢谢 - 我们已经将我们的集成测试设置为Postman集合。我们将使用这些来测试范围。 - 08Dc91wk

9
你可以配置测试服务器添加一个匿名过滤器中间件来实现此功能:
private HttpClient CreatControllerClient()
{
        return _factory.WithWebHostBuilder(builder
            => builder.ConfigureTestServices(services =>
            {
                // allow anonymous access to bypass authorization
                services.AddMvc(opt => opt.Filters.Add(new AllowAnonymousFilter()));
            })).CreateClient();
}

0

首先移除IAuthorizationHandler

var authorizationDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IAuthorizationHandler));
                if (authorizationDescriptor != null)
                    services.Remove(authorizationDescriptor);

然后添加

services.AddScoped<IAuthorizationHandler, TestAllowAnonymous>();


public class TestAllowAnonymous : IAuthorizationHandler
        {
            public Task HandleAsync(AuthorizationHandlerContext context)
            {
                foreach (IAuthorizationRequirement requirement in context.PendingRequirements.ToList())
                    context.Succeed(requirement); //Simply pass all requirementsreturn Task.CompletedTask;
                return Task.CompletedTask;
            }


        }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接