I have a class that takes an AuthenticationStateProvider in the constructor. I'd like to write unit test and mock this.
I'd like to be able to set what user is returned from the call GetAuthenticationStateAsync.
const string userId = "123";
const string userName = "John Doe";
const string email = "john.doe#test.com";
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, userId),
new Claim(ClaimTypes.Name, userName),
new Claim(ClaimTypes.Email, email),
};
var identity = new Mock<ClaimsIdentity>(claims);
var principal = new Mock<ClaimsPrincipal>(identity.Object);
var mockOfAuthenticationStateProvider = new Mock<AuthenticationStateProvider>();
var mockOfAuthState = new Mock<AuthenticationState>();
mockOfAuthenticationStateProvider.Setup(p =>
p.GetAuthenticationStateAsync()).Returns(Task.FromResult(mockOfAuthState.Object));
I get this errormessage:
Testfunction threw exception: System.ArgumentException: Can not
instantiate proxy of class:
Microsoft.AspNetCore.Components.Authorization.AuthenticationState.
Could not find a parameterless constructor. (Parameter
'constructorArguments') ---> System.MissingMethodException:
Constructor on type 'Castle.Proxies.AuthenticationStateProxy' not
found.
AuthenticationStateProvider is abstract, so if you can't mock it you can create an implementation of it, like this:
public class FakeAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly ClaimsPrincipal _principal;
public FakeAuthenticationStateProvider(ClaimsPrincipal principal)
{
_principal = principal;
}
// This static method isn't really necessary. You could call the
// constructor directly. I just like how it makes it more clear
// what the fake is doing within the test.
public static FakeAuthenticationStateProvider ForPrincipal(ClaimsPrincipal principal)
{
return new FakeAuthenticationStateProvider(principal);
}
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
return Task.FromResult(new AuthenticationState(_principal));
}
}
You can set it up like this:
const string userId = "123";
const string userName = "John Doe";
const string email = "john.doe#test.com";
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, userId),
new Claim(ClaimTypes.Name, userName),
new Claim(ClaimTypes.Email, email),
};
// These don't need to be mocks. If they are the test likely
// won't behave correctly.
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var authenticationStateProvider =
FakeAuthenticationStateProvider.ForPrincipal(principal);
and then pass it to any other class that depends on AuthenticationStateProvider.
When we create a fake implementation instead of a mock it's reusable and also easier to set up, so it keeps the test a little bit smaller.
For example, if the reason you're setting this up is so that the mock/fake returns a user with certain claims, you could have the fake just take a set of claims in its constructor. Then the constructor does the rest of the work, making your test even smaller.
For example,
public class FakeAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly ClaimsPrincipal _principal;
public FakeAuthenticationStateProvider(ClaimsPrincipal principal)
{
_principal = principal;
}
public static FakeAuthenticationStateProvider ForPrincipal(ClaimsPrincipal principal)
{
return new FakeAuthenticationStateProvider(principal);
}
public static FakeAuthenticationStateProvider ThatReturnsClaims(params Claim[] claims)
{
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
return new FakeAuthenticationStateProvider(principal);
}
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
return Task.FromResult(new AuthenticationState(_principal));
}
}
Now your test can have less clutter:
var claims = new []
{
new Claim(ClaimTypes.NameIdentifier, "123"),
new Claim(ClaimTypes.Name, "John Doe"),
new Claim(ClaimTypes.Email, "john.doe#test.com"),
};
var authenticationStateProvider = FakeAuthenticationStateProvider.ThatReturnsClaims(claims);
I usually end up with a folder called "Fakes" in my test project for them.
Related
I have an API set up that allows placing orders, looking up product info, reporting, etc. Each API key has specific permissions on which controllers/methods they can or can't access, as well as fields that should be omitted. Unfortunately right now I have this hardcoded in a dictionary class and would like to instead pull these permissions from a database.
The problem is I don't want to call the database to lookup permissions every time a method is called to avoid a performance hit. Is there a way to POST these settings/permissions any time there's a change (using an admin page) and have the API "remember" them in memory in some sort of dictionary? Also when restarting the API I'm guessing these are cleared so I would need a way to pull this information when the API initializes. Not sure what the best way to design this is, any suggestions are helpful thanks.
can't you just use standard roles based authorization?
this is what I followed when I set mine up https://weblog.west-wind.com/posts/2021/Mar/09/Role-based-JWT-Tokens-in-ASPNET-Core
[Authorize(Roles = "admin")]
[HttpPost]
public async Task<IActionResult> Post(RoomDelegate roomDelegate) =>
HandleResult(await Mediator.Send(new Post.Command { RoomDelegate = roomDelegate }));
store your roles in the tokens claims.
public class TokenService
{
private readonly IConfiguration _config;
private readonly UserManager<AppUser> _userManager;
public TokenService(IConfiguration config, UserManager<AppUser> userManager)
{
_config = config;
_userManager = userManager;
}
public IConfiguration Config { get; }
public async Task<string> CreateToken(AppUser user)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName ),
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Email, user.Email),
};
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["TokenKey"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);
var tokenDescription = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(10),
SigningCredentials = creds
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescription);
return tokenHandler.WriteToken(token);
}
public RefreshToken GenerateRefreshToken()
{
var randomNumber = new byte[32];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
return new RefreshToken { Token = Convert.ToBase64String(randomNumber) };
}
}
I'm trying to add Azure MSAL Authentication to an existing Blazor WASM application that already handles authentication with JWT.
But if I register a custom AuthenticationStateProvider I use, blazor throw at runtime:
Unhandled exception rendering component: Specified cast is not valid.
System.InvalidCastException: Specified cast is not valid.
Looking through sources, It seems it throws because an MSAL class expects that IServiceProvider returns an IRemoteAuthenticationService<TRemoteAuthenticationState> as AuthenticationStateProvider see WebAssemblyAuthenticationServiceCollectionExtensions at line 65
That's the AuthenticationStateProvider implementation I use:
public class ApiAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly HttpClient _httpClient;
private readonly ILocalStorageService _localStorage;
public ApiAuthenticationStateProvider(AuthenticationHttpClient authHttpClient, ILocalStorageService localStorage)
{
_httpClient = authHttpClient.HttpClient;
_localStorage = localStorage;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var savedToken = await _localStorage.GetItemAsync<string>("authToken");
if (string.IsNullOrWhiteSpace(savedToken))
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", savedToken);
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(savedToken), "jwt")));
}
public void MarkUserAsAuthenticated(string email)
{
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, email) }, "apiauth"));
var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
NotifyAuthenticationStateChanged(authState);
}
public void MarkUserAsLoggedOut()
{
var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
var authState = Task.FromResult(new AuthenticationState(anonymousUser));
NotifyAuthenticationStateChanged(authState);
}
private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
var claims = new List<Claim>();
var payload = jwt.Split('.')[1];
var jsonBytes = ParseBase64WithoutPadding(payload);
var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
keyValuePairs.TryGetValue(ClaimTypes.Role, out object roles);
if (roles != null)
{
if (roles.ToString().Trim().StartsWith("["))
{
var parsedRoles = JsonSerializer.Deserialize<string[]>(roles.ToString());
foreach (var parsedRole in parsedRoles)
{
claims.Add(new Claim(ClaimTypes.Role, parsedRole));
}
}
else
{
claims.Add(new Claim(ClaimTypes.Role, roles.ToString()));
}
keyValuePairs.Remove(ClaimTypes.Role);
}
claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())));
return claims;
}
private byte[] ParseBase64WithoutPadding(string base64)
{
switch (base64.Length % 4)
{
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}
return Convert.FromBase64String(base64);
}
}
Which I register as follows: builder.Services.AddScoped<AuthenticationStateProvider, ApiAuthenticationStateProvider>();
Is there a way to solve the issue ? I've looked at this other SO question but I'm not been able to solve.
While using AuthenticationStateProvider one of the appropriate System.Security.Claims.AuthenticationTypes values must be added as a parameter along with claimsIdentity.
i.e; To you blazor app you will have to add authenticationType parameter value with ClaimsIdentity so your code will be changed to:
like
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(claims, "Needs Auth Type Here"));
example:
public class ApiAuthenticationStateProvider : AuthenticationStateProvider
{
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
....
var claims = new[] { new Claim(ClaimTypes.Name, "xyz#afs.com") };
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(claims, AuthenticationTypes.Password));
var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
...
return Task.FromResult(authState);
}
}
Note the AuthenticationTypes.Password parameter in the above code for
ClaimsIdentity. Check to change it in all places wherever
ClaimsIdentity is constructed.
If needed check the auth type of user
if (User.Identity.AuthenticationType == AuthenticationTypes.Password) {
// ...
}
So that you can be able to login successfully.
Or try using RemoteAuthenticationService as said in https://github.com/dotnet/aspnetcore/issues
Reference: .net core - Custom AuthenticationStateProvider Authentication Failing - Stack Overflow
I have a controller that checks the claims to get the current user's username, role, like so:
var identity = ClaimsPrincipal.Current.Identities.First();
bUsername = identity.Claims.First(c => c.Type == "Username").Value;
role = identity.Claims.First(c => c.Type == "Role").Value;
These details are set when the user first logs in. When I try to test this method, if falls over at the lines of code above because there was no login. So I tried creating a principal in the TestInitialization so it would always be available for the controllers asking for the username in the above way:
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name, "Test Name"),
new Claim(ClaimTypes.NameIdentifier, "test"),
new Claim("Username", "test"),
new Claim("Role","ADMIN")
};
var identity = new ClaimsIdentity(claims, "TestAuthType");
var claimsPrincipal = new ClaimsPrincipal(identity);
I breakpointed the code, and it hits this code before it hits the above code, so it seems like it is creating the principal before checking for the username, however it still falls over on the username check, it's still not finding anything.
Can anybody advise me on what I'm doing wrong?
So, based on the advice of #DavidG, I ended up abstracting the claim stuff out of the controller. Posting this here for anyone else that needs help in the future. I created the following classes:
ICurrentUser
public interface ICurrentUser
{
User GetUserDetails();
}
CurrentUser
public class CurrentUser : ICurrentUser
{
public User GetUserDetails()
{
var user = new User();
try
{
var identity = ClaimsPrincipal.Current.Identities.First();
user.Username = identity.Claims.First(c => c.Type == "Username").Value;
user.Role = identity.Claims.First(c => c.Type == "Role").Value;
user.LoggedIn = identity.Claims.First(c => c.Type == "LoggedIn").Value;
return user;
}
catch (Exception)
{
return user;
}
}
}
Then I added it to my constructor like so:
public readonly ICurrentUser currentUser;
public readonly IUnitOfWork unitOfWork;
public HomeController(IUnitOfWork unitOfWork, ICurrentUser currentUser)
{
this.unitOfWork = unitOfWork;
this.currentUser = currentUser;
}
Then I use Ninject to inject it:
kernel.Bind<ICurrentUser>().To<CurrentUser>().InRequestScope();
Now it is loosely coupled to my controller, allowing me to mock it, like so:
[TestClass]
public class HomeControllerTest
{
private Mock<IUnitOfWork> _unitOfWorkMock;
private Mock<ICurrentUser> _currentUserMock;
private HomeController _objController;
[TestInitialize]
public void Initialize()
{
_unitOfWorkMock = new Mock<IUnitOfWork>();
_currentUserMock = new Mock<ICurrentUser>();
_currentUserMock.Setup(x => x.GetUserDetails()).Returns(new User { Username = "TEST", Role = "ADMIN", LoggedIn = "Y" });
_objController = new HomeController(_unitOfWorkMock.Object, _currentUserMock.Object);
}
}
I am trying to run unit tests for a successfull login by verifying that the AuthenticationManager SignIn method has been called once Here's the error I am getting:
Message: Test method Portfolio.UnitTest.WebUI.Controllers.LoginControllerTests.Login_Verified_Authenticates_System_User threw exception:
Moq.MockException:
Expected invocation on the mock once, but was 0 times: m => m.SignIn(AuthenticationProperties, [ClaimsIdentity])
Configured setups:
m => m.SignIn(AuthenticationProperties, [ClaimsIdentity])
Performed invocations:
IAuthenticationManager.SignIn(AuthenticationProperties, [ClaimsIdentity])
Even the error message seems to be contradicting itself.
My controller class/method:
public class LoginController : ProjectBaseController
{
private IAuthenticationManager _authenticationManager;
public IAuthenticationManager AuthenticationManager
{
get
{
return _authenticationManager ?? HttpContext.GetOwinContext().Authentication;
}
set
{
_authenticationManager = value;
}
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Index(LoginViewModel accountUser)
{
if (!ModelState.IsValid)
return View(accountUser);
var systemUser = _authenticationService.Verify(accountUser.Email, accountUser.Password);
if (systemUser == null)
{
ModelState.AddModelError("", "Invalid email address or password");
return View(accountUser);
}
var identity = new ClaimsIdentity
(
new[]
{
new Claim(ClaimTypes.Name, systemUser.FullName),
new Claim(ClaimTypes.Email, systemUser.Email)
},
DefaultAuthenticationTypes.ApplicationCookie,
ClaimTypes.Name,
ClaimTypes.Role
);
// Set roles for authorization attributes used throughout dashboard
foreach(var role in systemUser.Roles)
{
identity.AddClaim(new Claim(ClaimTypes.Role, role.Name));
}
AuthenticationManager.SignIn
(
new AuthenticationProperties
{
IsPersistent = true
},
identity
);
return Redirect(accountUser.ReturnUrl);
}
LoginControllerTests:
[TestClass]
public class LoginControllerTests
{
private readonly Mock<IAuthenticationManager> _mockAuthenticationManager;
private readonly Mock<IAuthenticationService> _mockAuthenticationService;
public LoginControllerTests()
{
_mockAuthenticationManager = new Mock<IAuthenticationManager>();
_mockAuthenticationService = new Mock<IAuthenticationService>();
}
private LoginController GetLoginControllerInstance()
{
var controller = new LoginController(_mockAuthenticationService.Object);
controller.AuthenticationManager = _mockAuthenticationManager.Object;
return controller;
}
[TestMethod]
public void Login_Verified_Authenticates_System_User()
{
// Arrange
var viewModel = new LoginViewModel("/")
{
Email = "email#test.co.uk",
Password = "password-test"
};
var systemUser = new SystemUser()
{
Id = new Random().Next(),
FirstName = "Joe",
LastName = "Bloggs",
Email = viewModel.Email,
Password = viewModel.Password,
Roles = new List<Role>()
};
var identity = new ClaimsIdentity
(
new[]
{
new Claim(ClaimTypes.Name, systemUser.FullName),
new Claim(ClaimTypes.Email, systemUser.Email)
},
DefaultAuthenticationTypes.ApplicationCookie,
ClaimTypes.Name,
ClaimTypes.Role
);
var authenticationProperty = new AuthenticationProperties
{
IsPersistent = true
};
var controller = this.GetLoginControllerInstance();
_mockAuthenticationService.Setup(m => m.Verify(viewModel.Email, viewModel.Password))
.Returns(systemUser);
_mockAuthenticationManager.Setup(m => m.SignIn(authenticationProperty, identity));
// Act
var result = controller.Index(viewModel);
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(controller.ModelState.IsValid);
_mockAuthenticationService.Verify(m => m.Verify(viewModel.Email, viewModel.Password), Times.Once);
_mockAuthenticationManager.Verify(m => m.SignIn(authenticationProperty, identity), Times.Once);
}
}
}
Running in deubg the method gets called within the controller just fine but Moq appears to be ignoring it.
Any ideas?
Nkosi said it in a comment to your question:
In the expression
m => m.SignIn(authenticationProperty, identity)
the two arguments authenticationProperty and identity must be "equal" to the two arguments actually passed in (by the System-Under-Test). So does Equals return true when you compare?
You can use It.IsAny<AuthenticationProperties>() or It.Is((AuthenticationProperties x) => x.IsPersistent) or similar, if needed. And analogous for the ClaimsIdentity. Then Moq will no longer require Equals equality.
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 }
}
};
}