J'essaie actuellement de tester l'authentification unitaire pour un nouveau projet WebAPI que j'écris en utilisant OWIN pour m'authentifier et j'ai des problèmes pour l'exécuter dans un contexte de test unitaire.
Ceci est ma méthode de test:
[TestMethod]
public void TestRegister()
{
using (WebApp.Start<Startup>("localhost/myAPI"))
using (AccountController ac = new AccountController()
{
Request = new System.Net.Http.HttpRequestMessage
(HttpMethod.Post, "http://localhost/myAPI/api/Account/Register")
})
{
var result = ac.Register(new Models.RegisterBindingModel()
{
Email = "[email protected]",
Password = "Pass@Word1",
ConfirmPassword = "Pass@Word1"
}).Result;
Assert.IsNotNull(result);
}
}
Je reçois une AggregateException
après avoir obtenu le .Result
avec l'exception interne suivante:
Result Message:
Test method myAPI.Tests.Controllers.AccountControllerTest.TestRegister
threw exception:
System.ArgumentNullException: Value cannot be null.
Parameter name: context
Result StackTrace:
at Microsoft.AspNet.Identity.Owin.OwinContextExtensions
.GetUserManager[TManager](IOwinContext context)
at myAPI.Controllers.AccountController.get_UserManager()
...
J'ai confirmé via le débogage que ma méthode Startup
est appelée, en appelant ConfigurAuth
:
public void ConfigureAuth(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
// Configure the db context and user manager to use a single
// instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>
(ApplicationUserManager.Create);
// Enable the application to use a cookie to store information for
// the signed in user
// and to use a cookie to temporarily store information about a
// user logging in with a third party login provider
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
AllowInsecureHttp = true
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
}
J'ai essayé plusieurs choses, mais rien ne semble fonctionner - je ne peux jamais obtenir un contexte OWIN. Le test échoue sur le code suivant:
// POST api/Account/Register
[AllowAnonymous]
[Route("Register")]
public async Task<IHttpActionResult> Register(RegisterBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var user = new ApplicationUser()
{ UserName = model.Email, Email = model.Email };
IdentityResult result = await UserManager.CreateAsync(user, model.Password);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
Ceci appelle la propriété UserManager
:
public ApplicationUserManager UserManager
{
get
{
return _userManager ?? Request.GetOwinContext()
.GetUserManager<ApplicationUserManager>();
}
private set
{
_userManager = value;
}
}
Il échoue sur:
return _userManager ?? Request.GetOwinContext()
.GetUserManager<ApplicationUserManager>();
avec un NullReferenceException
- Request.GetOwinContext
renvoie null
.
Ma question est donc la suivante: est-ce que je me trompe? Devrais-je simplement tester les réponses JSON? Ou existe-t-il un bon moyen de tester "en interne" l'authentification OWIN?
GetOwinContext appelle context.GetOwinEnvironment (); Qui est
private static IDictionary<string, object> GetOwinEnvironment(this HttpContextBase context)
{
return (IDictionary<string, object>) context.Items[HttpContextItemKeys.OwinEnvironmentKey];
}
et HttpContextItemKeys.OwinEnvironmentKey est une constante "owin.Environment" Si vous ajoutez cela dans les éléments de votre httpcontext, cela fonctionnera.
var request = new HttpRequest("", "http://google.com", "rUrl=http://www.google.com")
{
ContentEncoding = Encoding.UTF8 //UrlDecode needs this to be set
};
var ctx = new HttpContext(request, new HttpResponse(new StringWriter()));
//Session need to be set
var sessionContainer = new HttpSessionStateContainer("id", new SessionStateItemCollection(),
new HttpStaticObjectsCollection(), 10, true,
HttpCookieMode.AutoDetect,
SessionStateMode.InProc, false);
//this adds aspnet session
ctx.Items["AspSession"] = typeof(HttpSessionState).GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance,
null, CallingConventions.Standard,
new[] { typeof(HttpSessionStateContainer) },
null)
.Invoke(new object[] { sessionContainer });
var data = new Dictionary<string, object>()
{
{"a", "b"} // fake whatever you need here.
};
ctx.Items["owin.Environment"] = data;
Pour vous assurer qu'un contexte OWIN est disponible pendant votre test (c'est-à-dire pour corriger l'exception de référence nulle lorsque vous appelez Request.GetOwinContext()
), vous devez installer le package Microsoft.AspNet.WebApi.Owin
NuGet dans votre projet de test. Une fois installé, vous pouvez utiliser la méthode d’extension SetOwinContext
sur la demande.
Exemple:
var controller = new MyController();
controller.Request = new HttpRequestMessage(HttpMethod.Post,
new Uri("api/data/validate", UriKind.Relative)
);
controller.Request.SetOwinContext(new OwinContext());
Cela étant dit, je suis d'accord avec les autres réponses pour votre cas d'utilisation spécifique - fournissez une instance ou une fabrique AppplicationUserManager dans le constructeur. Les étapes SetOwinContext
ci-dessus sont nécessaires si vous devez interagir directement avec le contexte que votre test utilisera.
Vous pouvez simplement passer UserManager dans le constructeur de AccountController afin qu'il ne tente pas de le trouver dans owinContext. Le constructeur par défaut n'est pas convivial aux tests unitaires.
Ce que j'ai tendance à faire, c'est d'injecter AccountController avec une usine de gestion des utilisateurs. De cette façon, vous pouvez facilement échanger une instance du gestionnaire d'utilisateurs utilisée dans test. Votre usine par défaut peut utiliser la demande du constructeur pour continuer à fournir des instances par requête du gestionnaire d'utilisateurs. Votre fabrique de tests renvoie simplement une instance du gestionnaire d'utilisateurs avec lequel vous souhaitez fournir vos tests. J'utilise généralement une instance qui prend une instance tronquée d'un IUserStore, de sorte qu'il n'y a pas de dépendance ferme à un serveur principal utilisé pour stocker les informations d'identité.
Interface d'usine et classe:
public interface IUserManagerFactory<TUser>
where TUser : class, global::Microsoft.AspNet.Identity.IUser<string>
{
UserManager<TUser> Create();
}
public class UserManagerFactory : IUserManagerFactory<AppUser>
{
private HttpRequestMessage request;
public UserManagerFactory(HttpRequestMessage request)
{
if (request == null)
{
throw new ArgumentNullException("request");
}
this.request = request;
}
public UserManager<AppUser, string> Create()
{
return request.GetOwinContext().GetUserManager<UserManager<AppUser>>();
}
}
AccountController:
public AccountController(IUserManagerFactory<AppUser> userManagerFactory)
{
this.userManagerFactory = userManagerFactory;
}
private UserManager<AppUser> userManager;
public UserManager<AppUser> UserManager
{
get
{
if (this.userManager == null)
{
this.userManager = this.userManagerFactory.Create();
}
return this.userManager;
}
}
Usine de test:
public class TestUserManagerFactory : IUserManagerFactory<AppUser>
{
private IUserStore<AppUser> userStore;
public TestUserManagerFactory()
{
this.userStore = new MockUserStore();
}
public UserManager<AppUser> Create()
{
return new UserManager<AppUser>(new MockUserStore());
}
}
var data = new Dictionary<string, object>()
{
{"a", "b"} // fake whatever you need here.
};
ctx.Items["owin.Environment"] = data;
Utilisé ce morceau de code et ajouté à HttpContext au lieu de ctx et le test unitaire a fonctionné comme un charme.
Les réponses ici ont aidé, mais ne m'ont pas tout à fait compris, voici un exemple complet:
var userStore = new Mock<IUserStore<User>>();
var appUserMgrMock = new Mock<ApplicationUserManager>(userStore.Object);
var owin = new OwinContext();
owin.Set(appUserMgrMock.Object);
HttpContext.Current = new HttpContext(new HttpRequest(null, "http://test.com", null), new HttpResponse(null));
HttpContext.Current.Items["owin.Environment"] = owin.Environment;
N'oubliez pas d'installer tous les paquets de nuget requis!