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