簡體   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