单元测试ASP.Net MVC Authorize属性以验证重定向到登录页面

时间:2009-03-21 11:52:39

标签: asp.net-mvc

这可能只是需要另一双眼睛的情况。我必须遗漏一些东西,但我无法弄清楚为什么这种东西无法测试。我基本上试图通过使用[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);
}

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

我做错了吗?测试在错误的水平?我是否应该在路线级别测试此类事情?

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

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

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

任何帮助表示赞赏...

5 个答案:

答案 0 :(得分:96)

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

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

编辑:如果要验证单元测试中是否存在属性,则需要使用反射来检查控制器方法,如下所示。此示例将验证与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");
    }
}

答案 1 :(得分:25)

那么你可能会在错误的级别进行测试,但它的测试才有意义。我的意思是,如果我使用authorize(Roles =“Superhero”)属性标记一个方法,如果我标记它,我真的不需要测试。我(我想)想要的是测试未经授权的用户没有访问权限以及授权用户的访问权限。

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

// 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");

嗯,这并不容易,我花了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();
}

虽然如果没有这个辅助功能,那就不是很有用了:

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;
}

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

我很好奇为什么在一个据称“可测试”的框架中这么难。

答案 2 :(得分:4)

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

答案 3 :(得分:2)

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

为了确保在调用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 

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

首先从nuget安装软件包(在编写本文时版本为1.4.0-beta4)

  

PM&gt; install-package Xania.AspNet.Simulator -Pre

然后你的测试方法看起来像这样(假设安装了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(); 
}

答案 4 :(得分:0)

对于 .NET Framework,我们使用这个类来验证每个 MVC 和 API 控制器都有 AuthorizeAttribute,并且每个 API 控制器都应该有一个 RoutePrefixAttribute

[TestFixture]
public class TestControllerHasAuthorizeRole
{
    private static IEnumerable<Type> GetChildTypes<T>()
    {
        var types = typeof(Startup).Assembly.GetTypes();
        return types.Where(t => t.IsSubclassOf(typeof(T)) && !t.IsAbstract);
    }

    [Test]
    public void MvcControllersShouldHaveAuthrorizeAttribute()
    {
        var controllers = GetChildTypes<Controller>();
        foreach (var controller in controllers)
        {
            var authorizeAttribute = Attribute.GetCustomAttribute(controller, typeof(System.Web.Mvc.AuthorizeAttribute), true) as System.Web.Mvc.AuthorizeAttribute;
            Assert.IsNotNull(authorizeAttribute, $"MVC-controller {controller.FullName} does not implement AuthorizeAttribute");
        }
    }

    [Test]
    public void ApiControllersShouldHaveAuthorizeAttribute()
    {
        var controllers = GetChildTypes<ApiController>();
        foreach (var controller in controllers)
        {
            var attribute = Attribute.GetCustomAttribute(controller, typeof(System.Web.Http.AuthorizeAttribute), true) as System.Web.Http.AuthorizeAttribute;
            Assert.IsNotNull(attribute, $"API-controller {controller.FullName} does not implement AuthorizeAttribute");
        }
    }

    [Test]
    public void ApiControllersShouldHaveRoutePrefixAttribute()
    {
        var controllers = GetChildTypes<ApiController>();
        foreach (var controller in controllers)
        {
            var attribute = Attribute.GetCustomAttribute(controller, typeof(System.Web.Http.RoutePrefixAttribute), true) as System.Web.Http.RoutePrefixAttribute;
            Assert.IsNotNull(attribute, $"API-controller {controller.FullName} does not implement RoutePrefixAttribute");
            Assert.IsTrue(attribute.Prefix.StartsWith("api/", StringComparison.OrdinalIgnoreCase), $"API-controller {controller.FullName} does not have a route prefix that starts with api/");
        }
    }
}

在 .NET Core 和 .NET 5 中更容易一些<。这里的 MVC 控制器继承自 Controller,而 ControllerBase 又继承自 ControllerBase。 Api 控制器直接从 public class AuthorizeAttributeTest { private static IEnumerable<Type> GetChildTypes<T>() { var types = typeof(Startup).Assembly.GetTypes(); return types.Where(t => t.IsSubclassOf(typeof(T)) && !t.IsAbstract); } [Fact] public void ApiAndMVCControllersShouldHaveAuthorizeAttribute() { var controllers = GetChildTypes<ControllerBase>(); foreach (var controller in controllers) { var attribute = Attribute.GetCustomAttribute(controller, typeof(Microsoft.AspNetCore.Authorization.AuthorizeAttribute), true) as Microsoft.AspNetCore.Authorization.AuthorizeAttribute; Assert.NotNull(attribute); } } } 继承,因此我们可以使用单一方法测试 MVC 和 API 控制器:

In [4]: remaining = (4 - df2.groupby('B').size()).to_dict()