Using .NET Core 2.2, I'm trying to test a Razor Page using Identity Framework that starts like this
var user = await _userManager.GetUserAsync(User);
There are lots of solutions online that can be summed up by this generic class: setting up the UserManager on a IUserStore mock, and setting up the User on the PageContext.
public class SetupContext<T> where T : IdentityUser<Guid>, new()
{
public Mock<IUserRoleStore<T>> MockUserStore { get; private set; }
public UserManager<T> SetupUserManager()
{
MockUserStore = new Mock<IUserStore<T>>().As<IUserRoleStore<T>>();
MockUserStore.Setup(x => x.FindByIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(
(string id, CancellationToken token) => new T()
{
Id = Guid.Parse(id),
UserName = "User name"
});
return new UserManager<T>(MockUserStore.Object, null, null, null, null, null, null, null, null);
}
public PageContext SetupPageContext()
{
var displayName = "User name";
var identity = new GenericIdentity(displayName);
var principle = new ClaimsPrincipal(identity);
var httpContext = new DefaultHttpContext()
{
User = principle
};
var modelState = new ModelStateDictionary();
var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(), modelState);
var modelMetadataProvider = new EmptyModelMetadataProvider();
var viewData = new ViewDataDictionary(modelMetadataProvider, modelState);
var pageContext = new PageContext(actionContext)
{
ViewData = viewData
};
return pageContext;
}
}
The problem is that this won't work. GetUserAsync calls GetUserId which calls
principal.FindFirstValue(Options.ClaimsIdentity.UserIdClaimType)
This claim isn't setup, in fact no ID is setup anywhere, thus it can't return the user nor the user id. Then later I'll want to access database data related to UserId for testing.
I've spent quite a bit of time searching and haven't found the solution. It looks like I'm 90% there but what more do I need to set up the test correctly?
The test I'll run will be setup like this
private async Task<FreeTrialModel> SetupModelAsync()
{
_db = new RemoteHealingTechDbContext(InMemoryDbContext<RemoteHealingTechDbContext>.GetTestDbOptions());
await SeedData.Initialize(_db);
var _setup = new SetupContext<ApplicationUser>();
var userManager = _setup.SetupUserManager();
var pageContext = _setup.SetupPageContext();
return new FreeTrialModel(_db, userManager)
{
PageContext = pageContext
};
}
After more digging I finally found the answer here
I must use ClaimsIdentity containing a Claims instead of a GenericIdentity. Replace
var identity = new GenericIdentity(displayName);
with
var identity = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.NameIdentifier, userName, null)
});
Then, the FindByIdAsync method takes the UserName and not the UserId so I changed the declaration to
MockUserStore.Setup(x => x.FindByIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(
(string userName, CancellationToken token) => new T()
{
Id = userId,
UserName = userName
});
Overall solution
/// <summary>
/// Configures the .Net Core Identity Framework for unit testing.
/// </summary>
/// <typeparam name="T">The type of IdentityUser used for the framework.</typeparam>
/// <typeparam name="TKey">The data type of primary keys.</typeparam>
public class SetupContext<T, TKey> where T : IdentityUser<TKey>, new() where TKey : IEquatable<TKey>
{
/// <summary>
/// Returns the generated IUserRoleStore mock.
/// </summary>
public Mock<IUserRoleStore<T>> MockUserStore { get; private set; }
/// <summary>
/// Returns a UserManager based on a IUserRoleStore mock.
/// </summary>
/// <param name="userId">The UserId to set on generated IdentityUser objects.</param>
/// <returns>A new UserManager for unit testing.</returns>
public UserManager<T> SetupUserManager(TKey userId = default(TKey))
{
MockUserStore = new Mock<IUserStore<T>>().As<IUserRoleStore<T>>();
MockUserStore.Setup(x => x.FindByIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(
(string userName, CancellationToken token) => new T()
{
Id = userId,
UserName = userName
});
return new UserManager<T>(MockUserStore.Object, null, null, null, null, null, null, null, null);
}
/// <summary>
/// Returns a PageContext to set on a page, containing a logged in user.
/// </summary>
/// <param name="userName">The user name to set on the ClaimsIdentity.</param>
/// <returns>A new PageContext object.</returns>
public PageContext SetupPageContext(string userName = "User name")
{
var identity = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.NameIdentifier, userName, null)
});
var principle = new ClaimsPrincipal(identity);
var httpContext = new DefaultHttpContext()
{
User = principle
};
var modelState = new ModelStateDictionary();
var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(), modelState);
var modelMetadataProvider = new EmptyModelMetadataProvider();
var viewData = new ViewDataDictionary(modelMetadataProvider, modelState);
var pageContext = new PageContext(actionContext)
{
ViewData = viewData
};
return pageContext;
}
}
Related
I am trying to implement on a Blazor-Server side application a simple login against LDAP server and use cookie to store user claims. I have the MainLayout set to Authorized, if the user is not authenticated it will be re-direct to Login page. I have already tested the LDAP connection and it works properly, the problem is no matter what I do the cookie doesn't get created in the browser. When I run the POST command I see the HttpStatusCode.OK but the cookie it's not created and the browser re-direct again to login page of course.
Can someone please tell me what am I doing wrong? My code:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddControllersWithViews().AddRazorRuntimeCompilation();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
AuthenticationController.cs
[ApiController]
public class AuthenticationController : Controller
{
[HttpPost]
[Route("authentication/login")]
public async Task<ActionResult> Login([FromBody]UserCredentials credentials)
{
string path = "LDAP://serveraddress.xxx";
try
{
using DirectoryEntry entry = new(path, credentials.Username, credentials.Password);
using DirectorySearcher searcher = new(entry);
searcher.Filter = $"(&(objectclass=user)(objectcategory=person)(samaccountname={credentials.Username}))";
var result = searcher.FindOne();
if (result != null)
{
List<Claim> claims = new();
claims.Add(new Claim(ClaimTypes.Name, credentials.Username));
//Get Groups
ResultPropertyCollection fields = result.Properties;
foreach (var group in result.Properties["memberof"])
{
var distinguishedName = new X500DistinguishedName(group.ToString());
var commonNameData = new AsnEncodedData("CN", distinguishedName.RawData);
var commonName = commonNameData.Format(false);
if (!string.IsNullOrEmpty(commonName))
{
claims.Add(new Claim(ClaimTypes.Role, commonName));
}
}
//Get Emails
foreach (var email in result.Properties["mail"])
{
claims.Add(new Claim(ClaimTypes.Email, email.ToString()));
}
ClaimsIdentity claimsIdentity = new(claims, CookieAuthenticationDefaults.AuthenticationScheme);
AuthenticationProperties authProperties = new()
{
AllowRefresh = true,
IssuedUtc = DateTime.Now,
ExpiresUtc = DateTimeOffset.Now.AddDays(1),
IsPersistent = true,
};
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties);
return Ok();
}
else
{
return NotFound("User Not Found!");
}
}
catch (Exception)
{
return NotFound("Login credentials is incorrect!");
}
}
[HttpPost]
[Route("authentication/logout")]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Ok();
}
}
Login.razor
#page "/login"
#page "/login/{ErrorMessage}"
#layout CenteredBlockLayout
#attribute [AllowAnonymous]
<MudPaper Elevation="25" Class="pa-8" Width="100%" MaxWidth="500px">
<MudItem><img src="/images/logo.svg" alt="Logo" style="width:400px; height:50px;" /></MudItem>
<MudText Typo="Typo.h4" GutterBottom="true">Sign In</MudText>
<MudTextField #bind-Value="#Username" T="string" Label="Username"/>
<MudTextField #bind-Value="#Password" T="string" Label="Password"/>
<MudButton OnClick="(() => PerformLoginAsync())">Sign In</MudButton>
</MudPaper>
#if (!string.IsNullOrEmpty(ErrorMessage))
{
<MudAlert Severity="Severity.Error">#ErrorMessage</MudAlert>
}
Login.razor.cs
public partial class Login
{
public string Username { get; set; }
public string Password { get; set; }
[Parameter]
public string ErrorMessage { get; set; }
[Inject]
HttpClient Client { get; set; }
[Inject]
private NavigationManager NavMan { get; set; }
private async Task PerformLoginAsync()
{
if (!string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(Password))
{
UserCredentials cred = new UserCredentials
{
Username = Username,
Password = Password
};
var serialized = JsonConvert.SerializeObject(cred);
var stringContent = new StringContent(serialized, Encoding.UTF8, "application/json");
using var result = await Client.PostAsync($"NavMan.BaseUri}authentication/login", stringContent);
if (result.StatusCode == System.Net.HttpStatusCode.OK)
{
NavMan.NavigateTo("/", true);
}
else
{
ErrorMessage = await result.Content.ReadAsStringAsync();
}
}
}
}
I believe you need to append the cookie to the response. I haven't tested this with your code but it should work something like this:
HttpContext.Response.Cookies.Append("my_cookie", claimsString, new CookieOptions()
{
Domain = "mydomain.com",
SameSite = SameSiteMode.Lax,
Secure = true,
Path = "/",
Expires = DateTime.UtcNow.AddDays(1)
}
(These cookie options are just an example, of course. Tailor them to your specific needs.)
Keep in mind that you'll need to convert your claims to a string so that you can store it as the value in a cookie. In our case we store claims in a JWT, so that's what gets stored in the cookie. Here's how I do it:
public string CreateJWT(HttpContext httpContext, User user)
{
var handler = new JwtSecurityTokenHandler();
var descriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[] {
new Claim(ClaimTypes.GivenName, user.FirstName),
new Claim(ClaimTypes.Surname, user.LastName),
new Claim(ClaimTypes.Name, $"{user.FirstName} {user.LastName}"),
new Claim(ClaimTypes.Email, user.Email),
}),
Expires = DateTime.UtcNow.AddMinutes(Config.AccessExpMins),
Issuer = Config.Issuer,
Audience = Config.Audience,
SigningCredentials = new SigningCredentials(Key, SecurityAlgorithms.RsaSha256)
};
var token = handler.CreateJwtSecurityToken(descriptor);
var accessToken = handler.WriteToken(token);
httpContext.Response.Cookies.Append("my_cookie", accessToken, new CookieOptions()
{
Domain = Config.CookieDomain,
SameSite = SameSiteMode.Lax,
Secure = true,
Path = "/",
Expires = DateTime.UtcNow.AddMinutes(Config.AccessExpMins)
});
return accessToken;
}
As for parsing the JWT, I'm sure there are a number of ways to go about it. The one that worked for me was this one.
I have been reading articles trying to figure this one out. Structured off of this article How to add claims in a mock ClaimsPrincipal. I am still getting a null user from my controller and test fails on a null object of User inside the controller.
BusinessController
[Route("api/[controller]")]
[ApiController]
public class BusinessController : ControllerBase
{
private readonly IGBusinessRepository businessRepository;
private readonly IPersonRepository personRepository;
private readonly IUserClaims userClaims;
public BusinessController(IGBusinessRepository businessRepository,
IPersonRepository personRepository,
IUserClaims userClaims)
{
this.businessRepository = businessRepository;
this.personRepository = personRepository;
this.userClaims = userClaims;
}
// GET api/<BusinessController>/5
[HttpGet("{id}")]
[Authorize]
public async Task<IActionResult> GetBusiness(Guid businessId)
{
var userGuid = userClaims.GetUserGuid(User.Claims);
var ownerId = await personRepository.GetPersonIdByGUID(userGuid);
var business = await businessRepository.GetBusinessById(businessId);
if(business != null && business.OwnerId == businessId)
{
return Ok(business);
}
return BadRequest("Bad business id or your not the owner");
}
UserClaims
public class UserClaims : IUserClaims
{
public string GetUserGuid(IEnumerable<Claim> claims)
{
var claimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
var guidClaim = claims.Where(c => c.Type == claimType).Select(s => s.Value).SingleOrDefault();
return guidClaim;
}
}
TestIdentity
public class TestIdentity : ClaimsIdentity
{
public TestIdentity(params Claim[] claims) : base(claims)
{
}
}
TestPrincipal
public class TestPrincipal : ClaimsPrincipal
{
public TestPrincipal(params Claim[] claims) : base(new TestIdentity(claims))
{
}
}
BusinessControllerTests
public class BusinessControllerTests
{
//Controller
private readonly Mock<IGBusinessRepository> mockBusinessRepository;
private readonly Mock<IPersonRepository> mockPersonRepository;
private readonly Mock<IUserClaims> mockUserClaims;
private BusinessController controller;
//Objects
private Guid id = Guid.NewGuid();
public BusinessControllerTests()
{
mockBusinessRepository = new Mock<IGBusinessRepository>();
mockPersonRepository = new Mock<IPersonRepository>();
mockUserClaims = new Mock<IUserClaims>();
controller = new BusinessController(mockBusinessRepository.Object, mockPersonRepository.Object, mockUserClaims.Object);
}
[Fact]
public async Task GetBussiness_NotBusinessOwner_ReturnsBadRequest()
{
//Arrange
var userGuidString = Guid.NewGuid().ToString();
var ownerId = Guid.NewGuid();
var userClaim = new TestPrincipal(new Claim("name", "user#domain.com"));
Thread.CurrentPrincipal = userClaim;
//mockUserClaims.Setup(repo => repo.GetUserGuid(userClaim)).Returns(userGuidString);
mockPersonRepository.Setup(repo => repo.GetPersonIdByGUID(userGuidString));
mockBusinessRepository.Setup(repo => repo.GetBusinessById(id)).ReturnsAsync(business);
//Act
var result = await controller.GetBusiness(id);
//Assert
Assert.IsType<BadRequestResult>(result);
}
private GobiezBusiness business = new GobiezBusiness()
{
Id = new MongoDB.Bson.ObjectId(),
BusinessId = Guid.NewGuid(),
Name = "Test",
Email = "Test#helpme.com",
Address = "123 A street",
State = "WA",
ZipCode = "12345",
PhoneNumber = "123-456-7890",
OwnerId = Guid.NewGuid()
};
}
The controller was not arranged correctly to be able to access the principal
// ... omitted for brevity
var userClaim = new TestPrincipal(new Claim("name", "user#domain.com"));
var httpContext = new DefaultHttpContext() {
User = userClaim;
};
//Controller needs a controller context to access HttpContext
var controllerContext = new ControllerContext() {
HttpContext = httpContext
};
//assign context to controller
BusinessController controller = new BusinessController(
mockBusinessRepository.Object,
mockPersonRepository.Object,
mockUserClaims.Object)
{
ControllerContext = controllerContext
};
//Act
var result = await controller.GetBusiness(id);
// ... omitted for brevity
The controller should also be created within the scope of the test being executed.
This line in the subject under test
//...
var userGuid = userClaims.GetUserGuid(User.Claims);
//...
Should now return the created TestPrincipal arranged in the test
I am using testing my Identity actions in my .NET Core web application but keep running into problems. I recently found a way to create a mocked usermanager without running into problems with its parameters, but then a new error came to me that I can't find any solutions too: "System.TypeLoadException: Could not load type 'Microsoft.EntityFrameworkCore.Query.Internal.IAsyncQueryProvider'"
Here is my relevant code:
Setting up the mock usermanager:
var _userManager = new Mock<FakeUserManager>();
UserIdentity user1 = new UserIdentity() { Id = UserId1, UserName = "test#gmail.com", Score = 5 };
UserIdentity user2 = new UserIdentity() { Id = UserId2, UserName = "pragim#gmail.com", Score = 1 };
UserIdentity user3 = new UserIdentity() { Id = UserId3, UserName = "ajax#gmail.com", Score = 0 };
UserIdentity user4 = new UserIdentity() { Id = UserId4, UserName = "pim#gmail.com", Score = 4 };
List<UserIdentity> users = new List<UserIdentity>() { user1, user2, user3, user4 };
var mock = users.AsQueryable().BuildMock();
_userManager.Setup(x => x.Users).Returns(mock.Object);
var identityRepository = new IdentityRepository(_userManager.Object, null, null);
_identityService = new IdentityService(identityRepository);
FakeUserManager.cs:
public class FakeUserManager : UserManager<UserIdentity>
{
public FakeUserManager()
: base(new Mock<IUserStore<UserIdentity>>().Object,
new Mock<IOptions<IdentityOptions>>().Object,
new Mock<IPasswordHasher<UserIdentity>>().Object,
new IUserValidator<UserIdentity>[0],
new IPasswordValidator<UserIdentity>[0],
new Mock<ILookupNormalizer>().Object,
new Mock<IdentityErrorDescriber>().Object,
new Mock<IServiceProvider>().Object,
new Mock<ILogger<UserManager<UserIdentity>>>().Object)
{ }
}
Test method:
[TestMethod()]
public async Task GetUserAsyncTest()
{
//Arrange
//Act
var user = await _identityService.GetUserAsync(UserId4);
//Assert
Assert.AreEqual("pim#gmail.com", user.UserName);
}
Does anyone know a bypass / solution to my problem?
UPDATE: Stack trace:
EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, Expression expression, CancellationToken cancellationToken)
EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, CancellationToken cancellationToken)
EntityFrameworkQueryableExtensions.SingleOrDefaultAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
IdentityRepository.GetUserAsync(Nullable`1 userId) line 55
IdentityService.GetUserAsync(Nullable`1 id) line 172
IdentityServiceTests.GetUserAsyncTest() line 102
ThreadOperations.ExecuteWithAbortSafety(Action action)
and the method GetUserAsync in the repository:
public async Task<UserIdentity> GetUserAsync(Guid? userId)
{
return await _userManager.Users.Where(x => x.Id.Equals(userId)).SingleOrDefaultAsync();
}
Though I am pretty late but after struggling with this error thought should post my bit too.
In my case, a package MockQueryable.Moq(https://www.nuget.org/packages/MockQueryable.Moq/) was being used, upgrading which solved this problem.
The _users sequence needs a provider that implements IAsyncQueryProvider. Couple of ways you can do it, the following is a mocked implementation.
Firstly, some scaffolding based on your OP:
public class UserIdentity : IdentityUser<Guid>
{
public int Score { get; set; }
}
public class IdentityRepository
{
readonly UserManager<UserIdentity> _userManager;
public IdentityRepository(UserManager<UserIdentity> userManager)
{
_userManager = userManager;
}
public async Task<UserIdentity> GetUserAsync(Guid? userId)
{
return await _userManager.Users.SingleOrDefaultAsync(x => x.Id.Equals(userId));
}
}
Create your mocked IAsyncQueryProvider and add it to your own IQueryable<UserIdentity>:
var user1 = new UserIdentity() { Id = Guid.NewGuid(), UserName = "foo#bar.com", Score = 1 };
var user2 = new UserIdentity() { Id = Guid.NewGuid(), UserName = "bar#baz.com", Score = 2 };
var user3 = new UserIdentity() { Id = Guid.NewGuid(), UserName = "tony stark", Score = 3000 };
var dataSource = new List<UserIdentity> { user1, user2, user3 }.AsQueryable();
var providerMock = new Mock<IAsyncQueryProvider>();
providerMock.Setup(x => x.ExecuteAsync<Task<UserIdentity>>(It.IsAny<Expression>(), It.IsAny<CancellationToken>()))
.Returns((Expression providedExpression, CancellationToken providedCancellationToken) => Task.FromResult(dataSource.Provider.Execute<UserIdentity>(providedExpression)));
var usersMock = new Mock<IQueryable<UserIdentity>>();
usersMock.Setup(x => x.ElementType).Returns(dataSource.ElementType);
usersMock.Setup(x => x.Expression).Returns(dataSource.Expression);
usersMock.Setup(x => x.Provider).Returns(providerMock.Object);
...
userManagerMock.Setup(x => x.Users).Returns(() => usersMock.Object);
You need a way to set the IQueryable<T> provider, the above is just one way you can do it.
If we pull this all together into a working LINQPad sample:
void Main()
{
var user1 = new UserIdentity() { Id = Guid.NewGuid(), UserName = "foo#bar.com", Score = 1 };
var user2 = new UserIdentity() { Id = Guid.NewGuid(), UserName = "bar#baz.com", Score = 2 };
var user3 = new UserIdentity() { Id = Guid.NewGuid(), UserName = "tony stark", Score = 3000 };
var dataSource = new List<UserIdentity> { user1, user2, user3 }.AsQueryable();
var providerMock = new Mock<IAsyncQueryProvider>();
providerMock.Setup(x => x.ExecuteAsync<Task<UserIdentity>>(It.IsAny<Expression>(), It.IsAny<CancellationToken>()))
.Returns((Expression providedExpression, CancellationToken providedCancellationToken) => Task.FromResult(dataSource.Provider.Execute<UserIdentity>(providedExpression)));
var usersMock = new Mock<IQueryable<UserIdentity>>();
usersMock.Setup(x => x.ElementType).Returns(dataSource.ElementType);
usersMock.Setup(x => x.Expression).Returns(dataSource.Expression);
usersMock.Setup(x => x.Provider).Returns(providerMock.Object);
var userManagerMock = new Mock<UserManager<UserIdentity>>
(new Mock<IUserStore<UserIdentity>>().Object,
new Mock<IOptions<IdentityOptions>>().Object,
new Mock<IPasswordHasher<UserIdentity>>().Object,
new IUserValidator<UserIdentity>[0],
new IPasswordValidator<UserIdentity>[0],
new Mock<ILookupNormalizer>().Object,
new Mock<IdentityErrorDescriber>().Object,
new Mock<IServiceProvider>().Object,
new Mock<ILogger<UserManager<UserIdentity>>>().Object);
userManagerMock.Setup(x => x.Users).Returns(() => usersMock.Object);
var identityRepository = new IdentityRepository(userManagerMock.Object);
var result = identityRepository.GetUserAsync(user2.Id).Result;
Console.WriteLine(result);
}
public class UserIdentity : IdentityUser<Guid>
{
public int Score { get; set; }
}
public class IdentityRepository
{
readonly UserManager<UserIdentity> _userManager;
public IdentityRepository(UserManager<UserIdentity> userManager)
{
_userManager = userManager;
}
public async Task<UserIdentity> GetUserAsync(Guid? userId)
{
return await _userManager.Users.SingleOrDefaultAsync(x => x.Id.Equals(userId));
}
}
we get the desired result:
I've left out your IdentityService in the above as essentially it's a detail to the answer; you just need to get a working, mocked UserManager. For my mock libraries, in particular EntityFrameworkCore.Testing, I'd use a concrete IAsyncQueryProvider rather than a mock; follow the link for an example should you want to go down that path.
I have an ASP.NET MVC Core application that I am writing unit tests for. One of the action methods uses User name for some functionality:
SettingsViewModel svm = _context.MySettings(User.Identity.Name);
which obviously fails in the unit test. I looked around and all suggestions are from .NET 4.5 to mock HttpContext. I am sure there is a better way to do that. I tried to inject IPrincipal, but it threw an error; and I even tried this (out of desperation, I suppose):
public IActionResult Index(IPrincipal principal = null) {
IPrincipal user = principal ?? User;
SettingsViewModel svm = _context.MySettings(user.Identity.Name);
return View(svm);
}
but this threw an error as well.
Couldn't find anything in the docs either...
The controller’s User is accessed through the HttpContext of the controller. The latter is stored within the ControllerContext.
The easiest way to set the user is by assigning a different HttpContext with a constructed user. We can use DefaultHttpContext for this purpose, that way we don’t have to mock everything. Then we just use that HttpContext within a controller context and pass that to the controller instance:
var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, "example name"),
new Claim(ClaimTypes.NameIdentifier, "1"),
new Claim("custom-claim", "example claim value"),
}, "mock"));
var controller = new SomeController(dependencies…);
controller.ControllerContext = new ControllerContext()
{
HttpContext = new DefaultHttpContext() { User = user }
};
When creating your own ClaimsIdentity, make sure to pass an explicit authenticationType to the constructor. This makes sure that IsAuthenticated will work correctly (in case you use that in your code to determine whether a user is authenticated).
In previous versions you could have set User directly on the controller, which made for some very easy unit tests.
If you look at the source code for ControllerBase you will notice that the User is extracted from HttpContext.
/// <summary>
/// Gets the <see cref="ClaimsPrincipal"/> for user associated with the executing action.
/// </summary>
public ClaimsPrincipal User => HttpContext?.User;
and the controller accesses the HttpContext via ControllerContext
/// <summary>
/// Gets the <see cref="Http.HttpContext"/> for the executing action.
/// </summary>
public HttpContext HttpContext => ControllerContext.HttpContext;
You will notice that these two are read only properties. The good news is that ControllerContext property allows for setting it's value so that will be your way in.
So the target is to get at that object. In Core HttpContext is abstract so it is a lot easier to mock.
Assuming a controller like
public class MyController : Controller {
IMyContext _context;
public MyController(IMyContext context) {
_context = context;
}
public IActionResult Index() {
SettingsViewModel svm = _context.MySettings(User.Identity.Name);
return View(svm);
}
//...other code removed for brevity
}
Using Moq, a test could look like this
public void Given_User_Index_Should_Return_ViewResult_With_Model() {
//Arrange
var username = "FakeUserName";
var identity = new GenericIdentity(username, "");
var mockPrincipal = new Mock<ClaimsPrincipal>();
mockPrincipal.Setup(x => x.Identity).Returns(identity);
mockPrincipal.Setup(x => x.IsInRole(It.IsAny<string>())).Returns(true);
var mockHttpContext = new Mock<HttpContext>();
mockHttpContext.Setup(m => m.User).Returns(mockPrincipal.Object);
var model = new SettingsViewModel() {
//...other code removed for brevity
};
var mockContext = new Mock<IMyContext>();
mockContext.Setup(m => m.MySettings(username)).Returns(model);
var controller = new MyController(mockContext.Object) {
ControllerContext = new ControllerContext {
HttpContext = mockHttpContext.Object
}
};
//Act
var viewResult = controller.Index() as ViewResult;
//Assert
Assert.IsNotNull(viewResult);
Assert.IsNotNull(viewResult.Model);
Assert.AreEqual(model, viewResult.Model);
}
There is also the possibility to use the existing classes, and mock only when needed.
var user = new Mock<ClaimsPrincipal>();
_controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext
{
User = user.Object
}
};
In my case, I needed to make use of Request.HttpContext.User.Identity.IsAuthenticated, Request.HttpContext.User.Identity.Name and some business logic sitting outside of the controller. I was able to use a combination of Nkosi's, Calin's and Poke's answer for this:
var identity = new Mock<IIdentity>();
identity.SetupGet(i => i.IsAuthenticated).Returns(true);
identity.SetupGet(i => i.Name).Returns("FakeUserName");
var mockPrincipal = new Mock<ClaimsPrincipal>();
mockPrincipal.Setup(x => x.Identity).Returns(identity.Object);
var mockAuthHandler = new Mock<ICustomAuthorizationHandler>();
mockAuthHandler.Setup(x => x.CustomAuth(It.IsAny<ClaimsPrincipal>(), ...)).Returns(true).Verifiable();
var controller = new MyController(...);
var mockHttpContext = new Mock<HttpContext>();
mockHttpContext.Setup(m => m.User).Returns(mockPrincipal.Object);
controller.ControllerContext = new ControllerContext();
controller.ControllerContext.HttpContext = new DefaultHttpContext()
{
User = mockPrincipal.Object
};
var result = controller.Get() as OkObjectResult;
//Assert results
mockAuthHandler.Verify();
I want to hit my Controllers directly and just use DI like AutoFac. To do this I first registering ContextController.
var identity = new GenericIdentity("Test User");
var httpContext = new DefaultHttpContext()
{
User = new GenericPrincipal(identity, null)
};
var context = new ControllerContext { HttpContext = httpContext};
builder.RegisterInstance(context);
Next I enable property injection when I register the Controllers.
builder.RegisterAssemblyTypes(assembly)
.Where(t => t.Name.EndsWith("Controller")).PropertiesAutowired();
Then User.Identity.Name is populated, and I do not need to do anything special when calling a method on my Controller.
public async Task<ActionResult<IEnumerable<Employee>>> Get()
{
var requestedBy = User.Identity?.Name;
..................
I would look to implement an Abstract Factory Pattern.
Create an interface for a factory specifically for providing user names.
Then provide concrete classes, one which provides User.Identity.Name, and one that provides some other hard coded value that works for your tests.
You can then use the appropriate concrete class depending on production versus test code. Perhaps looking to pass the factory in as a parameter, or switching to the correct factory based on some configuration value.
interface IUserNameFactory
{
string BuildUserName();
}
class ProductionFactory : IUserNameFactory
{
public BuildUserName() { return User.Identity.Name; }
}
class MockFactory : IUserNameFactory
{
public BuildUserName() { return "James"; }
}
IUserNameFactory factory;
if(inProductionMode)
{
factory = new ProductionFactory();
}
else
{
factory = new MockFactory();
}
SettingsViewModel svm = _context.MySettings(factory.BuildUserName());
I got a brownfield .net 4.8 project that I needed to convert to .net 5.0 and I wanted to keep as much of the original code as possible, including the unit-/integration tests. The test for Controllers relied on the Context a lot so I created this Extension method to enable setting tokens, claims and headers:
public static void AddContextMock(
this ControllerBase controller,
IEnumerable<(string key, string value)> claims = null,
IEnumerable<(string key, string value)> tokens = null,
IEnumerable<(string key, string value)> headers = null)
{
HttpContext mockContext = new DefaultHttpContext();
if(claims != null)
{
mockContext.User = SetupClaims(claims);
}
if(tokens != null)
{
mockContext.RequestServices = SetupTokens(tokens);
}
if(headers != null)
{
SetupHeaders(mockContext, headers);
}
controller.ControllerContext = new ControllerContext()
{
HttpContext = mockContext
};
}
private static void SetupHeaders(HttpContext mockContext, IEnumerable<(string key, string value)> headers)
{
foreach(var header in headers)
{
mockContext.Request.Headers.Add(header.key, header.value);
}
}
private static ClaimsPrincipal SetupClaims(IEnumerable<(string key, string value)> claimValues)
{
var claims = claimValues.Select(c => new Claim(c.key, c.value));
return new ClaimsPrincipal(new ClaimsIdentity(claims, "mock"));
}
private static IServiceProvider SetupTokens(IEnumerable<(string key, string value)> tokenValues)
{
var mockServiceProvider = new Mock<IServiceProvider>();
var authenticationServiceMock = new Mock<IAuthenticationService>();
var authResult = AuthenticateResult.Success(
new AuthenticationTicket(new ClaimsPrincipal(), null));
var tokens = tokenValues.Select(t => new AuthenticationToken { Name = t.key, Value = t.value });
authResult.Properties.StoreTokens(tokens);
authenticationServiceMock
.Setup(x => x.AuthenticateAsync(It.IsAny<HttpContext>(), null))
.ReturnsAsync(authResult);
mockServiceProvider.Setup(_ => _.GetService(typeof(IAuthenticationService))).Returns(authenticationServiceMock.Object);
return mockServiceProvider.Object;
}
This uses Moq but can be adapted to other mocking frameworks. The authentication type is hardcoded to "mock" since I rely on default authentication but this could be supplied as well.
It is used as such:
_controllerUnderTest.AddContextMock(
claims: new[]
{
(ClaimTypes.Name, "UserName"),
(ClaimTypes.MobilePhone, "1234"),
},
tokens: new[]
{
("access_token", "accessTokenValue")
},
headers: new[]
{
("header", "headerValue")
});
If you're using Razor pages and want to override the claims:
[SetUp]
public void Setup()
{
var user = new ClaimsPrincipal(new ClaimsIdentity(
new Claim[] {
new("dateofbirth", "2000-10-10"),
new("surname", "Smith") },
"mock"));
_razorModel = new RazorModel()
{
PageContext = new PageContext
{
HttpContext = new DefaultHttpContext() { User = user }
}
};
}
I'm trying to get a unit test working for a service that is injecting items into the IHttpRequest.Items, using a request filter:
this.RequestFilters.Add((req, res, dto) =>
{
// simplified for readability...
var repo = container.Resolve<IClientRepository>();
var apiKey = req.Headers["ApiKey"];
// lookup account code from api key
var accountcode = repo.GetByApiKey(apiKey);
req.Items.Add("AccountCode", accountCode);
});
My service uses that dictionary item:
public class UserService : AppServiceBase
{
public IUserServiceGateway UserServiceGateway { get; set; }
public object Any(UserRequest request)
{
var accountCode = base.Request.Items["AccountCode"].ToString();
var user = UserServiceGateway.GetUserByUsername(request.Name);
return new UserResponse { User = user };
}
}
My test needs be somehow to mock the request, and insert that account code item:
[Test]
public void ValidUsernameReturnUser()
{
// arrange
var gateway = new Mock<IUserServiceGateway>();
gateway.Setup(s => s.GetUserByUsername(It.IsAny<string>()))
.Returns(new UserAccountDTO { Forename = "John", Surname = "Doe" });
var service = new UserService {
UserServiceGateway = gateway.Object,
RequestContext = new MockRequestContext(),
//Request = has no setter
};
// request is this case is null
base.Request.Items.Add("AccountCode", "DEF456");
// act
var response = (UserResponse)service.Any(new UserRequest { Name = "test" });
// assert
Assert.That(response.Result, Is.Not.Null);
}
The service itself accepts a mocked RequestContext, but not a Request. The test therefore fails. Is there a better way to do this?
I think this should do it.
[Test]
public void ValidUsernameReturnUser()
{
// arrange
var mockRequestContext = new MockRequestContext();
//add items to Request
mockRequestContext.Get<IHttpRequest>().Items.Add("AccountCode", "DEF456");
var gateway = new Mock<IUserServiceGateway>();
gateway.Setup(s => s.GetUserByUsername(It.IsAny<string>()))
.Returns(new UserAccountDTO { Forename = "John", Surname = "Doe" });
var service = new UserService {
UserServiceGateway = gateway.Object,
RequestContext = new MockRequestContext(),
};
// act
var response = (UserResponse)service.Any(new UserRequest { Name = "test" });
// assert
Assert.That(response.Result, Is.Not.Null);
}