I am struggling to simulate the required HttpContext
for my unit tests.
I have abstracted control of the session away from my Mvc controller with a SessionManager
interface and implemented this with a class called CookieSessionManager
. (early development stages).
CookieSessionManager
uses the HttpContext
by using the injected singleton HttpContextAccessor
(in Startup.cs ConfigureServices).
I'm using Cookie Authentication which is setup in Startup.cs with app.UseCookieAuthentication
.
Testing this manually in debug mode works as expected
The MSUnit
tests I have written for my AccountController
class work with a MockSessionManager
class injected.
The real issue I have is with the unit tests I have written for my CookieSessionManager
class. I have tried to setup the HttpContext
as shown below;
[TestClass]
public class CookieSessionManagerTest
{
private IHttpContextAccessor contextAccessor;
private HttpContext context;
private SessionManager sessionManager;
[TestInitialize]
public void Setup_CookieSessionManagerTest()
{
context = new DefaultHttpContext();
contextAccessor = new HttpContextAccessor();
contextAccessor.HttpContext = context;
sessionManager = new CookieSessionManager(contextAccessor);
}
The Error
But the call to sessionManager.Login(CreateValidApplicationUser());
doesn't appear to set the IsAuthenticated
flag and the test CookieSessionManager_Login_ValidUser_Authenticated_isTrue
fails.
[TestMethod]
public void CookieSessionManager_Login_ValidUser_Authenticated_isTrue()
{
sessionManager.Login(CreateValidApplicationUser());
Assert.IsTrue(sessionManager.isAuthenticated());
}
public ApplicationUser CreateValidApplicationUser()
{
ApplicationUser applicationUser = new ApplicationUser();
applicationUser.UserName = "ValidUser";
//applicationUser.Password = "ValidPass";
return applicationUser;
}
Test Name: CookieSessionManager_Login_ValidUser_Authenticated_isTrue
: line 43 Test Outcome: Failed Test Duration: 0:00:00.0433169
Result StackTrace: at ClaimsWebAppTests.Identity.CookieSessionManagerTest.CookieSessionManager_Login_ValidUser_Authenticated_isTrue()
CookieSessionManagerTest.cs:line 46 Result Message: Assert.IsTrue failed.
MY CODE
SessionManager
using ClaimsWebApp.Models;
namespace ClaimsWebApp.Identity
{
public interface SessionManager
{
bool isAuthenticated();
void Login(ApplicationUser applicationUser);
void Logout();
}
}
CookieSessionManager
using ClaimsWebApp.Identity;
using ClaimsWebApp.Models;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Security.Claims;
namespace ClaimsWebApp
{
public class CookieSessionManager : SessionManager
{
private List<ApplicationUser> applicationUsers;
private IHttpContextAccessor ContextAccessor;
private bool IsAuthenticated;
public CookieSessionManager(IHttpContextAccessor contextAccessor)
{
this.IsAuthenticated = false;
this.ContextAccessor = contextAccessor;
IsAuthenticated = ContextAccessor.HttpContext.User.Identity.IsAuthenticated;
applicationUsers = new List<ApplicationUser>();
applicationUsers.Add(new ApplicationUser { UserName = "ValidUser" });
}
public bool isAuthenticated()
{
return IsAuthenticated;
}
public void Login(ApplicationUser applicationUser)
{
if (applicationUsers.Find(m => m.UserName.Equals(applicationUser.UserName)) != null)
{
var identity = new ClaimsIdentity(new[] {
new Claim(ClaimTypes.Name, applicationUser.UserName)
},
"MyCookieMiddlewareInstance");
var principal = new ClaimsPrincipal(identity);
ContextAccessor.HttpContext.Authentication.SignInAsync("MyCookieMiddlewareInstance", principal);
IsAuthenticated = ContextAccessor.HttpContext.User.Identity.IsAuthenticated;
}
else
{
throw new Exception("User not found");
}
}
public void Logout()
{
ContextAccessor.HttpContext.Authentication.SignOutAsync("MyCookieMiddlewareInstance");
IsAuthenticated = ContextAccessor.HttpContext.User.Identity.IsAuthenticated;
}
}
}
Startup.cs
using ClaimsWebApp.Identity;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace ClaimsWebApp
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<SessionManager, CookieSessionManager>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationScheme = "MyCookieMiddlewareInstance",
LoginPath = new PathString("/Account/Unauthorized/"),
AccessDeniedPath = new PathString("/Account/Forbidden/"),
AutomaticAuthenticate = true,
AutomaticChallenge = true
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Account}/{action=Login}/{id?}");
});
}
}
}
CookieSessionManagerTest.cs
using ClaimsWebApp;
using ClaimsWebApp.Identity;
using ClaimsWebApp.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ClaimsWebAppTests.Identity
{
[TestClass]
public class CookieSessionManagerTest
{
private IHttpContextAccessor contextAccessor;
private HttpContext context;
private SessionManager sessionManager;
[TestInitialize]
public void Setup_CookieSessionManagerTest()
{
context = new DefaultHttpContext();
contextAccessor = new HttpContextAccessor();
contextAccessor.HttpContext = context;
sessionManager = new CookieSessionManager(contextAccessor);
}
[TestMethod]
public void CookieSessionManager_Can_Be_Implemented()
{
Assert.IsInstanceOfType(sessionManager, typeof(SessionManager));
}
[TestMethod]
public void CookieSessionManager_Default_Authenticated_isFalse()
{
Assert.IsFalse(sessionManager.isAuthenticated());
}
[TestMethod]
public void CookieSessionManager_Login_ValidUser_Authenticated_isTrue()
{
sessionManager.Login(CreateValidApplicationUser());
Assert.IsTrue(sessionManager.isAuthenticated());
}
public ApplicationUser CreateValidApplicationUser()
{
ApplicationUser applicationUser = new ApplicationUser();
applicationUser.UserName = "ValidUser";
//applicationUser.Password = "ValidPass";
return applicationUser;
}
public ApplicationUser CreateInValidApplicationUser()
{
ApplicationUser applicationUser = new ApplicationUser();
applicationUser.UserName = "InValidUser";
//applicationUser.Password = "ValidPass";
return applicationUser;
}
}
}
Unfortunately, it's pretty much impossible to test with HttpContext
. It's a sealed class that doesn't utilize any interfaces, so you cannot mock it. Usually, your best bet is to abstract away the code that works with HttpContext
, and then test just your other, more application-specific code.
It looks like you've sort of already done this via HttpContextAccessor
, but you're utilizing it incorrectly. First, you're exposing the HttpContext
instance, which pretty much defeats the entire purpose. This class should be able to return something like User.Identity.IsAuthenticated
on its own, like: httpContextAccessor.IsAuthenticated
. Internally, the property would access the private HttpContext
instance and just return the result.
Once you're utilizing it in this way, you can then mock HttpContextAccessor
to simply return what you need for your tests, and you don't have to worry about supplying it with an HttpContext
instance.
Granted, this means there's still some untested code, namely, the accessor methods that work with HttpContext
, but these are generally very straight-forward. For example, the code for IsAuthenticated
would just be something like return httpContext.User.Identity.IsAuthenticated
. The only way you're going to screw that up is if you fat-finger something, but the compiler will warn you about that.
I created this helper functionality for my unit tests, this allowed me to test those specific methods that required portions of the httpRequest.
public static IHttpContextAccessor GetHttpContext(string incomingRequestUrl, string host)
{
var context = new DefaultHttpContext();
context.Request.Path = incomingRequestUrl;
context.Request.Host = new HostString(host);
//Do your thing here...
var obj = new HttpContextAccessor();
obj.HttpContext = context;
return obj;
}
This doesn't directly answer the context of the question but it provides an alternative method to testing and when you start using it makes life so much easier.
There is an integration testing package available for ASP.NET Core and documentation about it can be found here:
https://docs.asp.net/en/latest/testing/integration-testing.html
Enjoy!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With