asp.net-mvc 单元测试 ASP.Net MVC 授权属性以验证重定向到登录页面

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/669175/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-07 23:26:08  来源:igfitidea点击:

Unit testing ASP.Net MVC Authorize attribute to verify redirect to login page

asp.net-mvc

提问by RobertTheGrey

This is probably going to turn out to be a case of just needing another pair of eyes. I must be missing something, but I cannot figure out why this kind of thing cannot be tested for. I'm basically trying to ensure that unauthenticated users cannot access the view by marking the controller with the [Authorize] attribute and I'm trying to tests this using the following code:

这可能只是需要另一双眼睛的情况。我一定遗漏了一些东西,但我无法弄清楚为什么不能测试这种东西。我基本上是在尝试通过使用 [Authorize] 属性标记控制器来确保未经身份验证的用户无法访问视图,并且我正在尝试使用以下代码对其进行测试:

[Fact]
public void ShouldRedirectToLoginForUnauthenticatedUsers()
{
    var mockControllerContext = new Mock<ControllerContext>()
                         { DefaultValue = DefaultValue.Mock };
    var controller = new MyAdminController() 
              {ControllerContext = mockControllerContext.Object};
    mockControllerContext.Setup(c =>
               c.HttpContext.Request.IsAuthenticated).Returns(false);
    var result = controller.Index();
    Assert.IsAssignableFrom<RedirectResult>(result);
}

The RedirectResult I'm looking for is some kind of indication that the user is being redirected to the login form, but instead a ViewResult is always returned and when debugging I can see that the Index() method is successfully hit even though the user is not authenticated.

我正在寻找的 RedirectResult 是某种指示用户正在被重定向到登录表单,但总是返回一个 ViewResult 并且在调试时我可以看到 Index() 方法被成功命中,即使用户是未认证。

Am I doing something wrong? Testing at the wrong level? Should I rather be testing at the route level for this kind of thing?

难道我做错了什么?在错误的级别进行测试?对于这种事情,我应该在路由级别进行测试吗?

I know that the [Authorize] attribute is working, because when I spin up the page, the login screen is indeed forced upon me - but how do I verify this in a test?

我知道 [Authorize] 属性正在工作,因为当我启动页面时,登录屏幕确实强加给我 - 但我如何在测试中验证这一点?

The controller and index method are very simple just so that I can verify the behaviour. I've included them for completeness:

控制器和索引方法非常简单,以便我可以验证行为。为了完整起见,我将它们包括在内:

[Authorize]
public class MyAdminController : Controller
{
    public ActionResult Index()
    {
        return View();
    }
}

Any help appreciated...

任何帮助表示赞赏...

回答by Dylan Beattie

You are testing at the wrong level. The [Authorize] attribute ensures that the routingengine will never invoke that method for an unauthorized user - the RedirectResult will actually be coming from the route, not from your controller method.

您在错误的级别进行测试。[Authorize] 属性确保路由引擎永远不会为未经授权的用户调用该方法 - RedirectResult 实际上来自路由,而不是来自您的控制器方法。

Good news is - there's already test coverage for this (as part of the MVC framework source code), so I'd say you don't need to worry about it; just make sure your controller method does the right thing whenit gets called, and trust the framework not to call it in the wrong circumstances.

好消息是 - 已经有测试覆盖(作为 MVC 框架源代码的一部分),所以我想说你不需要担心它;只需确保您的控制器方法被调用做正确的事情,并相信框架不会在错误的情况下调用它。

EDIT: If you want to verify the presence of the attribute in your unit tests, you'll need to use reflection to inspect your controller methods as follows. This example will verify the presence of the Authorize attribute on the ChangePassword POST method in the 'New ASP.NET MVC 2 Project' demo that's installed with MVC2.

编辑:如果要验证单元测试中属性的存在,则需要使用反射来检查控制器方法,如下所示。此示例将验证随 MVC2 一起安装的“新 ASP.NET MVC 2 项目”演示中 ChangePassword POST 方法中是否存在 Authorize 属性。

[TestFixture]
public class AccountControllerTests {

    [Test]
    public void Verify_ChangePassword_Method_Is_Decorated_With_Authorize_Attribute() {
        var controller = new AccountController();
        var type = controller.GetType();
        var methodInfo = type.GetMethod("ChangePassword", new Type[] { typeof(ChangePasswordModel) });
        var attributes = methodInfo.GetCustomAttributes(typeof(AuthorizeAttribute), true);
        Assert.IsTrue(attributes.Any(), "No AuthorizeAttribute found on ChangePassword(ChangePasswordModel model) method");
    }
}

回答by DanielEli

Well you might be testing at the wrong level but its the test that makes sense. I mean, if I flag a method with the authorize(Roles="Superhero") attribute, I don't really need a test if I flagged it. What I (think I) want is to test that an unauthorized user doesn't have access and that an authorized user does.

好吧,您可能在错误的级别进行测试,但这是有意义的测试。我的意思是,如果我用 authorize(Roles="Superhero") 属性标记一个方法,如果我标记它,我真的不需要测试。我(认为我)想要的是测试未经授权的用户没有访问权限而授权用户有。

For a unauthorized user a test like this:

对于未经授权的用户,测试如下:

// Arrange
var user = SetupUser(isAuthenticated, roles);
var controller = SetupController(user);

// Act
SomeHelper.Invoke(controller => controller.MyAction());

// Assert
Assert.AreEqual(401,
  controller.ControllerContext.HttpContext.Response.StatusCode, "Status Code");

Well, it's not easy and it took me 10 hours, but here it is. I hope someone can benefit from it or convince me to go into another profession. :) (BTW - I'm using rhino mock)

嗯,这并不容易,我花了 10 个小时,但就是这样。我希望有人可以从中受益或说服我从事其他职业。:)(顺便说一句 - 我正在使用犀牛模拟)

[Test]
public void AuthenticatedNotIsUserRole_Should_RedirectToLogin()
{
    // Arrange
    var mocks = new MockRepository();
    var controller = new FriendsController();
    var httpContext = FakeHttpContext(mocks, true);
    controller.ControllerContext = new ControllerContext
    {
        Controller = controller,
        RequestContext = new RequestContext(httpContext, new RouteData())
    };

    httpContext.User.Expect(u => u.IsInRole("User")).Return(false);
    mocks.ReplayAll();

    // Act
    var result =
        controller.ActionInvoker.InvokeAction(controller.ControllerContext, "Index");
    var statusCode = httpContext.Response.StatusCode;

    // Assert
    Assert.IsTrue(result, "Invoker Result");
    Assert.AreEqual(401, statusCode, "Status Code");
    mocks.VerifyAll();
}

Although, thats not very useful without this helper function:

虽然,如果没有这个辅助函数,那不是很有用:

public static HttpContextBase FakeHttpContext(MockRepository mocks, bool isAuthenticated)
{
    var context = mocks.StrictMock<HttpContextBase>();
    var request = mocks.StrictMock<HttpRequestBase>();
    var response = mocks.StrictMock<HttpResponseBase>();
    var session = mocks.StrictMock<HttpSessionStateBase>();
    var server = mocks.StrictMock<HttpServerUtilityBase>();
    var cachePolicy = mocks.Stub<HttpCachePolicyBase>();
    var user = mocks.StrictMock<IPrincipal>();
    var identity = mocks.StrictMock<IIdentity>();
    var itemDictionary = new Dictionary<object, object>();

    identity.Expect(id => id.IsAuthenticated).Return(isAuthenticated);
    user.Expect(u => u.Identity).Return(identity).Repeat.Any();

    context.Expect(c => c.User).PropertyBehavior();
    context.User = user;
    context.Expect(ctx => ctx.Items).Return(itemDictionary).Repeat.Any();
    context.Expect(ctx => ctx.Request).Return(request).Repeat.Any();
    context.Expect(ctx => ctx.Response).Return(response).Repeat.Any();
    context.Expect(ctx => ctx.Session).Return(session).Repeat.Any();
    context.Expect(ctx => ctx.Server).Return(server).Repeat.Any();

    response.Expect(r => r.Cache).Return(cachePolicy).Repeat.Any();
    response.Expect(r => r.StatusCode).PropertyBehavior();

    return context;
}

So that gets you confirmation that users not in a role don't have access. I tried writing a test to confirm the opposite, but after two more hours of digging through mvc plumbing I will leave it to manual testers. (I bailed when I got to the VirtualPathProviderViewEngine class. WTF? I don't want anything to do a VirtualPath or a Provider or ViewEngine much the union of the three!)

这样您就可以确认不在角色中的用户无权访问。我尝试编写一个测试来确认相反的情况,但是经过两个小时的 mvc 管道挖掘后,我将把它留给手动测试人员。(当我进入 VirtualPathProviderViewEngine 类时,我放弃了。WTF?我不想做任何事情来做 VirtualPath 或 Provider 或 ViewEngine,这三者的结合!)

I am curious as to why this is so hard in an allegedly "testable" framework.

我很好奇为什么这在所谓的“可测试”框架中如此困难。

回答by Adrian Grigore

Why not just use reflection to look for the [Authorize]attribute on the controller class and / or the action method you are testing? Assuming the framework does make sure the Attribute is honored, this would be the easiest thing to do.

为什么不直接使用反射来查找[Authorize]控制器类和/或您正在测试的操作方法上的属性?假设框架确实确保 Attribute 受到尊重,这将是最容易做的事情。

回答by Ibrahim ben Salah

I don't agree with Dylan's answer, because 'user must be logged in' does not imply that 'controller method is annotated with AuthorizeAttribute'

我不同意 Dylan 的回答,因为“用户必须登录”并不意味着“控制器方法用 AuthorizeAttribute 注释”

to ensure 'user must be logged in' when you call the action method, the ASP.NET MVC framework does something like this (just hold on, it will get simpler eventually)

为了确保在调用 action 方法时“用户必须登录”,ASP.NET MVC 框架会做这样的事情(坚持下去,最终会变得更简单)

let $filters = All associated filter attributes which implement
               IAuthorizationFilter

let $invoker = instance of type ControllerActionInvoker
let $ctrlCtx = instance or mock of type ControllerContext
let $actionDesc = instance or mock of type ActionDescriptor
let $authzCtx = $invoker.InvokeAuthorizationFilters($ctrlCtx, $filters, $actionDesc);

then controller action is authorized when $authzCtx.Result is not null 

It is hard to implement this pseudo script in a working c# code. Likely, Xania.AspNet.Simulatormakes it really simple to setup a test like this and performs exactly these step under the cover. here is an example.

很难在工作的 c# 代码中实现这个伪脚本。很可能,Xania.AspNet.Simulator使设置这样的测试变得非常简单,并在幕后完全执行这些步骤。这是一个例子。

first install the package from nuget (version 1.4.0-beta4 at the time of writing)

首先从 nuget 安装包(撰写本文时为 1.4.0-beta4 版)

PM > install-package Xania.AspNet.Simulator -Pre

PM > 安装包 Xania.AspNet.Simulator -Pre

Then your test method could look like this (assuming NUnit and FluentAssertions are installed):

那么您的测试方法可能如下所示(假设安装了 NUnit 和 FluentAssertions):

[Test]
public void AnonymousUserIsNotAuthorized()
{
  // arrange
  var action = new ProfileController().Action(c => c.Index());
  // act
  var result = action.GetAuthorizationResult();
  // assert
  result.Should().NotBeNull(); 
}

[Test]
public void LoggedInUserIsAuthorized()
{
  // arrange
  var action = new ProfileController().Action(c => c.Index())
     // simulate authenticated user
     .Authenticate("user1", new []{"role1"});
  // act
  var result = action.GetAuthorizationResult();
  // assert
  result.Should().BeNull(); 
}