I have an MVC app for which I am writing Integration tests. I have a process I'm testing that involves the user logging out and then logging in. I can login just fine. The test below succeeds (btw I'm injecting an in memory db with a matching user entry):
[Fact]
public async Task D_LoginTest()
{
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = true
});
var initResponse = await client.GetAsync("/Identity/Account/Login");
var antiForgeryValues = await AntiForgeryTokenExtractor.ExtractAntiForgeryValues(initResponse);
var postRequest = new HttpRequestMessage(HttpMethod.Post, "/Identity/Account/Login");
postRequest.Headers.Add("Cookie", new CookieHeaderValue(AntiForgeryTokenExtractor.AntiForgeryCookieName, antiForgeryValues.cookieValue).ToString());
var formModel = new Dictionary<string, string>
{
{ AntiForgeryTokenExtractor.AntiForgeryFieldName, antiForgeryValues.fieldValue },
{ "Input.Email", "test@example.com" },
{ "Input.Password", "pas3w0!rRd" }
};
postRequest.Content = new FormUrlEncodedContent(formModel);
var response = await client.SendAsync(postRequest);
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
But when I add a logout to this test:
[Fact]
public async Task D_LoginTest()
{
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = true
});
var initResponse = await client.GetAsync("/Identity/Account/Login");
var antiForgeryValues = await AntiForgeryTokenExtractor.ExtractAntiForgeryValues(initResponse);
var postRequest = new HttpRequestMessage(HttpMethod.Post, "/Identity/Account/Login");
postRequest.Headers.Add("Cookie", new CookieHeaderValue(AntiForgeryTokenExtractor.AntiForgeryCookieName, antiForgeryValues.cookieValue).ToString());
var formModel = new Dictionary<string, string>
{
{ AntiForgeryTokenExtractor.AntiForgeryFieldName, antiForgeryValues.fieldValue },
{ "Input.Email", "test@example.com" },
{ "Input.Password", "pas3w0!rRd" }
};
postRequest.Content = new FormUrlEncodedContent(formModel);
var response = await client.SendAsync(postRequest);
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var postRequestLogout = new HttpRequestMessage(HttpMethod.Post, "/Identity/Account/Logout");
postRequestLogout.Headers.Add("Cookie", new CookieHeaderValue(AntiForgeryTokenExtractor.AntiForgeryCookieName, antiForgeryValues.cookieValue).ToString());
var postRequestLougoutForm = new Dictionary<string, string>
{
{ AntiForgeryTokenExtractor.AntiForgeryFieldName, antiForgeryValues.fieldValue },
};
postRequestLogout.Content = new FormUrlEncodedContent(postRequestLougoutForm);
var logoutAnswer = await client.SendAsync(postRequestLogout);
logoutAnswer.EnsureSuccessStatusCode();
Console.WriteLine(logoutAnswer.StatusCode);
Assert.Equal(HttpStatusCode.OK, logoutAnswer.StatusCode);
}
It fails with this error.
Failed amaranth.Tests.AdminControllerTests.D_LoginTest [23 ms]
Error Message:
System.Net.Http.HttpRequestException : Response status code does not indicate success: 400 (Bad Request).
Stack Trace:
at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
at amaranth.Tests.AdminControllerTests.D_LoginTest() in /path/to/project/dir/amaranth.Tests/IntegrationTests/AdminControllerTests.cs:line 308
--- End of stack trace from previous location ---
Also in case it's helpful, this is the Logout.cshtml.cs file:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
namespace amaranth.Areas.Identity.Pages.Account
{
[AllowAnonymous]
public class LogoutModel : PageModel
{
private readonly SignInManager<IdentityUser> _signInManager;
private readonly ILogger<LogoutModel> _logger;
public LogoutModel(SignInManager<IdentityUser> signInManager, ILogger<LogoutModel> logger)
{
_signInManager = signInManager;
_logger = logger;
}
public void OnGet()
{
}
public async Task<IActionResult> OnPost(string returnUrl = null)
{
await _signInManager.SignOutAsync();
_logger.LogInformation("User logged out.");
if (returnUrl != null)
{
return LocalRedirect(returnUrl);
}
else
{
return Page();
}
}
}
}
And here's a picture of the error in my debugger:
What am I doing wrong? How do I logout in an Integration Test? Btw, This is the default Razor Page MVC scaffolded logout if that helps.
I created a minimum reproducible example amaranth-minal-testing-example
that successfully logs out in an integration test. First this is amaranth-minal-testing-example/TestRunProject.Tests/AntiForgeryTokenExtractor.cs
using System.Text.RegularExpressions;
using Microsoft.Net.Http.Headers;
namespace TestRunProject.Tests
{
public static class AntiForgeryTokenExtractor
{
public static string ExtractAntiForgeryToken(string htmlBody)
{
var requestVerificationTokenMatch =
Regex.Match(htmlBody, $@"\<input name=""__RequestVerificationToken"" type=""hidden"" value=""([^""]+)"" \/\>");
if (requestVerificationTokenMatch.Success)
return requestVerificationTokenMatch.Groups[1].Captures[0].Value;
throw new ArgumentException($"Anti forgery token '__RequestVerificationToken' not found in HTML", nameof(htmlBody));
}
}
}
And this is amaranth-minal-testing-example/TestRunProject.Tests/IntegrationTests/AuthTest.cs
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Net.Http.Headers;
using Microsoft.VisualStudio.TestPlatform.TestHost;
namespace TestRunProject.Tests
{
public class AuthTests
{
[Fact]
public async Task DLoginTest()
{
{
var application = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
// ... Configure test services
});
var client = application.CreateClient();
// -- REGISTER --
var registerResponse = await client.GetAsync("/Identity/Account/Register");
registerResponse.EnsureSuccessStatusCode();
string registerResponseContent = await registerResponse.Content.ReadAsStringAsync();
var requestVerificationToken = AntiForgeryTokenExtractor.ExtractAntiForgeryToken(registerResponseContent);
var formModel = new Dictionary<string, string>
{
{ "Input.Email", "test2@example.com" },
{ "Input.Password", "pas3w02!rRd" },
{ "Input.ConfirmPassword", "pas3w02!rRd" },
{ "__RequestVerificationToken", requestVerificationToken },
};
var postRequest2 = new HttpRequestMessage(HttpMethod.Post, "/Identity/Account/Register");
postRequest2.Content = new FormUrlEncodedContent(formModel);
var registerResponse2 = await client.SendAsync(postRequest2);
registerResponse2.EnsureSuccessStatusCode();
// -- LOGOUT --
var logoutRequest = new StringContent("");
logoutRequest.Headers.Add("RequestVerificationToken", requestVerificationToken);
var logoutResponse = await client.PostAsync("/Identity/Account/Logout", logoutRequest);
logoutResponse.EnsureSuccessStatusCode();
// -- LOGIN --
var loginResponse = await client.GetAsync("/Identity/Account/Login");
loginResponse.EnsureSuccessStatusCode();
string loginResponseContent = await registerResponse.Content.ReadAsStringAsync();
requestVerificationToken = AntiForgeryTokenExtractor.ExtractAntiForgeryToken(loginResponseContent);
formModel = new Dictionary<string, string>
{
{ "Input.Email", "test2@example.com" },
{ "Input.Password", "pas3w02!rRd" },
{ "__RequestVerificationToken", requestVerificationToken },
};
var loginRequest = new HttpRequestMessage(HttpMethod.Post, "/Identity/Account/Login");
loginRequest.Content = new FormUrlEncodedContent(formModel);
var loginResponse2 = await client.SendAsync(loginRequest);
loginResponse2.EnsureSuccessStatusCode();
}
}
}
}
This works for a default scaffolded C# ASP.NET core identity razor pages project when TestRunProject.Tests
is placed in the same directory as TestRunProject
(the default scaffolded C# ASP.NET core identity razor pages project).
I have a Github repo for this code here https://github.com/ChristianOConnor/amaranth-minal-testing-example. The above code is from this commit: 0d768c4aa181cb4289de4b17f1eac222323ee469