[英]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.