繁体   English   中英

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

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

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

[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] 属性正在工作,因为当我打开页面时,登录屏幕确实是强加给我的 - 但是我如何在测试中验证这一点?

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

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

任何帮助表示赞赏...

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

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

编辑:如果您想在单元测试中验证属性的存在,您需要使用反射来检查您的 controller 方法,如下所示。 此示例将验证随 MVC2 一起安装的“New 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");
    }
}

好吧,您可能在错误的级别进行测试,但这是有意义的测试。 我的意思是,如果我用 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 个小时,但就是这样。 我希望有人能从中受益或说服我 go 进入另一个职业。 :)(顺便说一句 - 我正在使用犀牛模拟)

[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();
}

虽然,如果没有这个助手 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;
}

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

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

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

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

为了确保在调用操作方法时“用户必须登录”,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 安装 package(撰写本文时版本为 1.4.0-beta4)

PM > 安装包 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(); 
}

For .NET Framework we use this class to verify that every MVC and API Controller have AuthorizeAttribute and that every API Controller should have a 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 继承自Controller ,后者又继承自ControllerBase Api Controller 直接从ControllerBase继承,因此我们可以使用单一方法测试 MVC 和 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);
        }
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM