Search code examples
c#asp.net-mvcrazorintegration-testing

C# How Do I Logout in An Integration Test for An MVC App?


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", "[email protected]" },
        { "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", "[email protected]" },
        { "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:
enter image description here

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.


Solution

  • 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", "[email protected]" },
                        { "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", "[email protected]" },
                        { "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