71 votes

Test unitaire ASP.Net MVC Attribut Authorize pour vérifier la redirection vers la page de connexion

Il va probablement s'avérer que j'ai juste besoin d'une autre paire d'yeux. Je dois manquer quelque chose, mais je n'arrive pas à comprendre pourquoi ce genre de chose ne peut pas être testé. J'essaie essentiellement de m'assurer que les utilisateurs non authentifiés ne peuvent pas accéder à la vue en marquant le contrôleur avec l'attribut [Authorize] et j'essaie de tester cela en utilisant le code suivant :

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

Le RedirectResult que je recherche est une sorte d'indication que l'utilisateur est redirigé vers le formulaire de connexion, mais au lieu de cela, un ViewResult est toujours renvoyé et lors du débogage, je peux voir que la méthode Index() est frappée avec succès même si l'utilisateur n'est pas authentifié.

Est-ce que je fais quelque chose de mal ? Je teste au mauvais niveau ? Devrais-je plutôt tester au niveau de la route pour ce genre de choses ?

Je sais que l'attribut [Authorize] fonctionne, car lorsque je lance la page, l'écran de connexion m'est effectivement imposé - mais comment puis-je vérifier cela dans un test ?

Le contrôleur et la méthode d'indexation sont très simples, juste pour que je puisse vérifier le comportement. Je les ai inclus par souci d'exhaustivité :

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

Toute aide est appréciée...

0 votes

Une façon de tester ceci est d'utiliser les tests d'intégration en utilisant le serveur hôte en mémoire fourni par Microsoft.AspNetCore.Mvc.Testing, dans lequel vous pouvez simuler AuthHandler. Les détails peuvent être trouvés ici : docs.microsoft.com/en-us/aspnet/core/test/

113voto

Dylan Beattie Points 23222

Vous testez au mauvais niveau. L'attribut [Authorize] permet de s'assurer que l'utilisateur n'a pas besoin de l'autorisation de l'utilisateur. routage n'invoquera jamais cette méthode pour un utilisateur non autorisé - le RedirectResult proviendra en fait de la route, et non de la méthode de votre contrôleur.

La bonne nouvelle est qu'il existe déjà une couverture de test pour cela (dans le code source du framework MVC), donc je dirais que vous n'avez pas besoin de vous inquiéter à ce sujet ; assurez-vous simplement que la méthode du contrôleur fait la bonne chose. quand il est appelé, et faire confiance au cadre pour ne pas l'appeler dans de mauvaises circonstances.

EDIT : Si vous voulez vérifier la présence de l'attribut dans vos tests unitaires, vous devrez utiliser la réflexion pour inspecter vos méthodes de contrôleur comme suit. Cet exemple vérifiera la présence de l'attribut Authorize sur la méthode POST ChangePassword dans la démo 'New ASP.NET MVC 2 Project' qui est installée avec MVC2.

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

2 votes

Merci Dylan - je pensais que je testais peut-être au mauvais niveau. Je suis heureux avec l'idée de "supposer" que si le contrôleur est touché, l'utilisateur est authentifié. P.S. Etes-vous sûr que c'est testé dans le framework ? Je peux voir quelques tests fournissant des IPrincipal valides, mais aucun ne teste le cas invalide ;-)

3 votes

Euh, non... je n'ai pas encore vérifié ce cas de test moi-même ; je fais confiance à l'équipe MVC pour le faire correctement. C'est ma faute !

6 votes

J'aime la réponse qui explique pourquoi ce n'est pas la bonne approche, mais je ne suis pas convaincu par l'argument "la fonctionnalité est testée dans le framework et fonctionne". J'ai confiance dans le fait que l'attribut fonctionne correctement, c'est le travail du framework, mais j'aimerais quand même pouvoir affirmer quelles méthodes de mes contrôleurs utilisent l'attribut.

28voto

DanielEli Points 1331

Vous testez peut-être au mauvais niveau, mais c'est le test qui a du sens. Je veux dire, si je signale une méthode avec l'attribut authorize(Roles="Superhero"), je n'ai pas vraiment besoin d'un test si je l'ai signalé. Ce que je veux (je pense), c'est tester qu'un utilisateur non autorisé n'a pas accès et qu'un utilisateur autorisé a accès.

Pour un utilisateur non autorisé, un test comme celui-ci :

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

Eh bien, ce n'est pas facile et ça m'a pris 10 heures, mais voilà. J'espère que quelqu'un pourra en profiter ou me convaincre d'exercer une autre profession :) (BTW - j'utilise rhino mock)

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

Cependant, cela n'est pas très utile sans cette fonction d'aide :

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

Cela vous permet donc de confirmer que les utilisateurs qui n'ont pas de rôle n'ont pas d'accès. J'ai essayé d'écrire un test pour confirmer le contraire, mais après deux heures de plus à creuser dans la plomberie de mvc, je vais laisser cela aux testeurs manuels. (J'ai abandonné quand je suis arrivé à la classe VirtualPathProviderViewEngine. WTF ? Je ne veux rien qui ait à voir avec VirtualPath ou Provider ou ViewEngine ou l'union des trois).

Je suis curieux de savoir pourquoi cela est si difficile dans un cadre prétendument "testable".

0 votes

WTF en effet, heureusement si vous vous y tenez vous pouvez trouver un moyen de le contourner et de contourner tous les problèmes qui suivent, comme je l'ai fait. Jetez un coup d'oeil à mon projet github à : github.com/ibrahimbensalah/Xania.AspNet.Simulator/blob/master/

0 votes

Ce poste est presque entièrement le même que le lien référencé dans le post de @Dario. L'avez-vous développé vous-même ?

0 votes

Oui, il a été développé par moi-même et est toujours en cours de développement. Il prend actuellement en charge mvc4 et mvc5 à partir de l'autorisation, des classeurs de modèles, de la validation des demandes, du rendu razor.....

8voto

Dario Quintana Points 179

Si vous voulez tester un Authorize personnalisé, vous pouvez faire comme suit este .

4voto

Adrian Grigore Points 15993

Pourquoi ne pas simplement utiliser la réflexion pour chercher le [Authorize] sur la classe du contrôleur et/ou la méthode d'action que vous testez ? En supposant que le framework s'assure que l'attribut est respecté, ce serait la chose la plus simple à faire.

1 votes

Il y a deux choses distinctes qui sont testées ici. (1) Tester qu'un attribut personnalisé fait ce qu'il est censé faire ; et (2) Qu'un contrôleur/action reste décoré avec l'attribut. Vous répondez à (2) mais je pense que le lien posté par Dario Quintana répond mieux à (1).

0 votes

Dans le monde réel, l'annotation avec l'attribut Authorize n'est pas le seul moyen utilisé pour autoriser les demandes / actions du contrôleur.

3voto

Ibrahim ben Salah Points 643

Je ne suis pas d'accord avec la réponse de Dylan, car "l'utilisateur doit être connecté" n'implique pas que "la méthode du contrôleur est annotée avec AuthorizeAttribute".

pour s'assurer que l'utilisateur doit être connecté lorsque vous appelez la méthode d'action, le cadre ASP.NET MVC fait quelque chose comme ceci (tenez bon, ça va devenir plus simple par la suite)

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 

Il est difficile d'implémenter ce pseudo script dans un code c# fonctionnel. Vraisemblablement, Xania.AspNet.Simulator permet de configurer très simplement un test de ce type et exécute exactement ces étapes sous le couvert. voici un exemple.

installer d'abord le paquet à partir de nuget (version 1.4.0-beta4 au moment de l'écriture)

PM > install-package Xania.AspNet.Simulator -Pre

Alors votre méthode de test pourrait ressembler à ceci (en supposant que NUnit et FluentAssertions sont installés) :

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X