In this post I'll describe how you can test protected API endpoints.
I'll use the two most common scenarios: Cookie & JWT Authentication.
I have also created a public repo with the full code. If you want to follow step by step you can also look at the commit history.
Set up the IntegrationTestInitializer
If you've read my introduction to integration testing in ASP.NET Core you'll notice that we've changed the IntegrationTestInitializer
quite a bit, because a lot of work has been put into Microsoft.AspNetCore.Mvc.Testing
to allow for more configuration. By default the TestServer
didn't handle cookies automatically, but when inheriting from WebApplicationFactory<T>
the cookies are handled by default. This also cleans up the code.
Introduction to ASP.NET Core Integration Testing
Kai Oswald ・ Sep 10 '19
> Install-Package Microsoft.AspNetCore.Mvc.Testing
[TestClass]
public abstract class IntegrationTestInitializer : WebApplicationFactory<Startup>
{
protected HttpClient _client;
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing")
.UseStartup<Startup>();
base.ConfigureWebHost(builder);
}
[TestInitialize]
public void Setup()
{
var builder = new WebHostBuilder()
.UseEnvironment("Testing")
.UseStartup<Startup>();
_client = this.CreateClient();
}
}
Note that we've also set a custom environment Testing
for our WebHost.
This will be used so we can decouple our testing code from our production code.
Cookie Authentication
The cookie authentication setup in the API could look similiar to this:
services.AddAuthentication(o => o.DefaultScheme = "Cookies")
.AddCookie("Cookies", o =>
{
o.Cookie.Name = "auth_cookie";
o.Cookie.HttpOnly = true;
o.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = redirectContext =>
{
redirectContext.HttpContext.Response.StatusCode = 401;
return Task.CompletedTask;
}
};
});
This configuration basically sets up our HTTP Only Cookie scheme that returns a 401 on unauthenticated requests (by default a 302
redirect to the login page would be returned).
Then the login Method:
[HttpPost("Login")]
public async Task<IActionResult> Login([FromBody] UserLoginModel user)
{
ClaimsPrincipal claimsPrincipal = null;
// Return a test user when environment is our Test environment
if (_env.EnvironmentName == "Testing")
{
var claimsIdentity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, user.UserName)
}, "Cookies");
claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
}
else
{
// you should validate the userName and password here and load the specified user
// We'll just return a default user to keep things simple...
}
await Request.HttpContext.SignInAsync("Cookies", claimsPrincipal);
return NoContent();
}
Note that we just return a default user when we're in the test environment for now.
In a future post I'll also cover invalid users and a simple Database setup.
Testing the unauthorized client
With the authentication method in tact, you should also test if the protected endpoints behave correctly when the client is unauthorized. We expect a HTTP status code of 401
if an unauthorized client tries to access an authorized endpoint.
[TestMethod]
public async Task GetUsersUnauthorizedShouldReturn401()
{
var response = await _client.GetAsync("api/users");
Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode);
}
Testing the authorized client
In C# the HttpClient
handles cookies by default, so all we need to do now is to make a POST
request to the Login
method providing user credentials before we can access protected endpoints.
private async Task PerformLogin(string userName, string password)
{
var user = new UserLoginModel
{
UserName = userName,
Password = password
};
var res = await _client.PostAsJsonAsync("api/account/login", user);
}
[TestMethod]
public async Task CanGetUsers()
{
List<string> expectedResponse = new List<string> { "Foo", "Bar", "Baz" };
await PerformLogin("Test", "hunter2");
var responseJson = await _client.GetStringAsync("api/users");
List<string> actualResponse = JsonConvert.DeserializeObject<List<string>>(responseJson);
CollectionAssert.AreEqual(expectedResponse, actualResponse);
}
JWT Authentication
To set up JWT Authentication we have to add it as an Authentication Scheme in our Startup.cs
// Configure Authentication
services.AddAuthentication(o => o.DefaultScheme = "Cookies")
.AddCookie("Cookies", o =>
{
// omitted
})
.AddJwtBearer("Token", o =>
{
var key = Encoding.ASCII.GetBytes(appSettings.Secret);
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
I won't go into too much detail how to set up JWT Authentication, but you can look at the GitHub repository containing the full source code. Note that we now have 2 valid Authentication methods active: Cookie & JWT, with Cookies
being the default. So if we want to support both authentication methods we'd have to mark our Controller/Action with both authentication schemes.
[Authorize(AuthenticationSchemes= "Cookies,Token")]
Testing the unauthorized client
Let's write our test with a random token where the authentication should fail.
[TestMethod]
public async Task GetUsersJwtInvalidTokenShouldReturnUnauthorized()
{
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "invalid_token");
var response = await _client.GetAsync("api/users");
Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode);
}
Testing the authorized client
To obtain a valid bearer token, we have to make a POST
request to our token endpoint providing our credentials.
private async Task<string> GetToken(string userName, string password)
{
var user = new UserLoginModel
{
UserName = userName,
Password = password
};
var res = await _client.PostAsJsonAsync("api/account/token", user);
if(!res.IsSuccessStatusCode) return null;
var userModel = await res.Content.ReadAsAsync<User>();
return userModel?.Token;
}
Then we can set the token for the Authorization
header on the test client.
[TestMethod]
public async Task GetUsersJwtInvalidTokenShouldReturnUnauthorized()
{
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "invalid_token");
var response = await _client.GetAsync("api/users");
Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode);
}
Conclusion
Now we can test Cookie and JWT protected API endpoints.
In the next post I'll describe how you can test database access using an in-memory database approach.
Top comments (1)
Hi,
I'd like to mock the cookie that the
awat Request.HttpContext.SignInAsync("Cookies", claimsPrincipal);
gives back, in order to test the endpoints protected with the Authorize attribute. I don't want to call the login endpoint just before every test scenario to have a valid cookie, just want to mock it.
How can I achieve it?
Thank in advance.