pocheptsov
pocheptsov

Reputation: 1956

Access Cookies inside unit test in AspNet.TestHost.TestServer context on ASP.NET 5 / MVC 6

There is no easy way to get an access to a CookieContainer in response object running integration tests with AspNet.TestHost.TestServer. Cookies have to be set by the controller action. What is the best way to achieve that?

            var client = TestServer.Create(app =>
            {
                app.UseMvc(routes => 
                       routes.MapRoute("default", "{controller}/{action}/{id?}"));
                app.UseIdentity();
            }).CreateClient();

            var request = new HttpRequestMessage(HttpMethod.Get, "account/login");
            var response = await client.SendAsync(request);

            // how to get an access to cookie container ?????????
            // response.Cookies prop doesn't exist
            Assert.NotEmpty(response.Cookies["auth"]);

Solution that I see is to extend instance of the TestServer, return instance of a class CustomClientHandler : ClientHandler and override the whole process of sending a request in that handler, but it needs literally to change all logic except relatively small code of the TestServer.

Any better suggestion how to implement an access to Cookies in a response?

Upvotes: 12

Views: 5208

Answers (4)

Martin
Martin

Reputation: 1048

As an addition to @Oleh's response, you can achieve the same without reflection on newer frameworks like .NET 4.6.1+ / .NET Core

public class TestHttpClientHandler : DelegatingHandler
{
    [NotNull]
    private readonly CookieContainer cookies = new CookieContainer();

    public TestHttpClientHandler([NotNull] HttpMessageHandler innerHandler)
        : base(innerHandler) { }

    [NotNull, ItemNotNull]
    protected override async Task<HttpResponseMessage> SendAsync([NotNull] HttpRequestMessage request, CancellationToken ct)
    {
        Uri requestUri = request.RequestUri;
        request.Headers.Add(HeaderNames.Cookie, this.cookies.GetCookieHeader(requestUri));

        HttpResponseMessage response = await base.SendAsync(request, ct);

        if (response.Headers.TryGetValues(HeaderNames.SetCookie, out IEnumerable<string> setCookieHeaders))
        {
            foreach (SetCookieHeaderValue cookieHeader in SetCookieHeaderValue.ParseList(setCookieHeaders.ToList()))
            {
                Cookie cookie = new Cookie(cookieHeader.Name.Value, cookieHeader.Value.Value, cookieHeader.Path.Value);
                if (cookieHeader.Expires.HasValue)
                {
                    cookie.Expires = cookieHeader.Expires.Value.DateTime;
                }
                this.cookies.Add(requestUri, cookie);
            }
        }

        return response;
    }
}

Upvotes: 7

Oleh Nechytailo
Oleh Nechytailo

Reputation: 2195

I've implemented a custom HttpMessageHandler that tracks cookies.

It uses reflection to invoke the actual handler and just reads/sets cookie headers.

class TestMessageHandler : HttpMessageHandler
{
    delegate Task<HttpResponseMessage> HandlerSendAsync(HttpRequestMessage message, CancellationToken token);

    private readonly HandlerSendAsync nextDelegate;
    private readonly CookieContainer cookies = new System.Net.CookieContainer();

    public TestMessageHandler(HttpMessageHandler next)
    {
        if(next == null) throw new ArgumentNullException(nameof(next));

        nextDelegate = (HandlerSendAsync)
                            next.GetType()
                                .GetTypeInfo()
                                .GetMethod("SendAsync", BindingFlags.NonPublic | BindingFlags.Instance)
                                .CreateDelegate(typeof(HandlerSendAsync), next);
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("Cookie", cookies.GetCookieHeader(request.RequestUri));

        var resp = await nextDelegate(request, cancellationToken).ConfigureAwait(false);

        if (resp.Headers.TryGetValues("Set-Cookie", out var newCookies))
        {
            foreach (var item in SetCookieHeaderValue.ParseList(newCookies.ToList()))
            {
                cookies.Add(request.RequestUri, new Cookie(item.Name, item.Value, item.Path));
            }
        }

        return resp;
    }
}

And then you create your HttpClient like this:

var httpClient = new HttpClient(
                     new TestMessageHandler(
                         server.CreateHandler()));

TestMessageHandler now takes care of tracking cookies.

Upvotes: 5

user2410689
user2410689

Reputation:

The proper way, using minimal code getting cookies in Asp.Net Core Functional Tests is as follows, (I leave out init code for setting up WebApplicationFactory, which is known stuff)

The given examples above, require either reflection (Since I think MS made a design bug on not exposing the default handlers) or require cookie parsing, which is annoying in 2023.

private (HttpClient, CookieContainerHandler) GetHttpClient()
{        
    CookieContainerHandler cookieContainerHandler = new();

    var client = _factory.WithWebHostBuilder(builder =>
     {
         builder.ConfigureTestServices(services =>
         {
             services.AddAuthentication(defaultScheme: "YourSchema")
                 .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                     "TestAzure", options => { });
         });
     }).CreateDefaultClient(cookieContainerHandler);
    
    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "YourSchema");
    return (client, cookieContainerHandler);
}

[Fact] 
public async Task MyUnitTest()  
{
    // Arrange
    var (client, cookieHandler) = GetHttpClient();

    // Act PUT/GET/POST etc
    var response = await client.PutAsync("youruri", null);
    
    var sessionCookie = cookieHandler.Container.GetAllCookies().FirstOrDefault(f => f.Name == "yourcookie"); // note this ignores cookie domain policy
 }

Upvotes: 2

Matt
Matt

Reputation: 13339

For a dotnet core integration test approach like the one described in the docs here, you can get cookies with the following code:

public class CookieTests : IClassFixture<WebApplicationFactory<Startup>>
{
    private readonly WebApplicationFactory<Startup> _factory;

    public CookieTests(WebApplicationFactory<Startup> factory)
    {
        _factory = factory;
    }

    [Fact]
    public async Task GetPage_ShouldSetCookie_CookieSet()
    {
        using (var client = _factory.CreateClient())
        {
            var response = await client.GetAsync("/cookie_setting_url");
            response.EnsureSuccessStatusCode();

            //or other assertions
            Assert.True(response.Headers.TryGetValues(HeaderNames.SetCookie, out IEnumerable<string> cookies));
        }
    }
}

Upvotes: 2

Related Questions