// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Principal;
using System.Text;
using System.Xml.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Authentication.Cookies;

public class CookieTests : SharedAuthenticationTests<CookieAuthenticationOptions>
{
    private readonly MockTimeProvider _timeProvider = new();

    protected override string DefaultScheme => CookieAuthenticationDefaults.AuthenticationScheme;
    protected override Type HandlerType => typeof(CookieAuthenticationHandler);

    protected override void RegisterAuth(AuthenticationBuilder services, Action<CookieAuthenticationOptions> configure)
    {
        services.AddCookie(configure);
    }

    [Fact]
    public async Task NormalRequestPassesThrough()
    {
        using var host = await CreateHost(s => { });
        using var server = host.GetTestServer();
        var response = await server.CreateClient().GetAsync("http://example.com/normal");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }

    [Fact]
    public async Task AjaxLoginRedirectToReturnUrlTurnsInto200WithLocationHeader()
    {
        using var host = await CreateHost(o => o.LoginPath = "/login");
        using var server = host.GetTestServer();
        var transaction = await SendAsync(server, "http://example.com/challenge?X-Requested-With=XMLHttpRequest");
        Assert.Equal(HttpStatusCode.Unauthorized, transaction.Response.StatusCode);
        var responded = transaction.Response.Headers.GetValues("Location");
        Assert.Single(responded);
        Assert.StartsWith("http://example.com/login", responded.Single());
    }

    [Fact]
    public async Task AjaxForbidTurnsInto403WithLocationHeader()
    {
        using var host = await CreateHost(o => o.AccessDeniedPath = "/denied");
        using var server = host.GetTestServer();
        var transaction = await SendAsync(server, "http://example.com/forbid?X-Requested-With=XMLHttpRequest");
        Assert.Equal(HttpStatusCode.Forbidden, transaction.Response.StatusCode);
        var responded = transaction.Response.Headers.GetValues("Location");
        Assert.Single(responded);
        Assert.StartsWith("http://example.com/denied", responded.Single());
    }

    [Fact]
    public async Task AjaxLogoutRedirectToReturnUrlTurnsInto200WithLocationHeader()
    {
        using var host = await CreateHost(o => o.LogoutPath = "/signout");
        using var server = host.GetTestServer();
        var transaction = await SendAsync(server, "http://example.com/signout?X-Requested-With=XMLHttpRequest&ReturnUrl=/");
        Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
        var responded = transaction.Response.Headers.GetValues("Location");
        Assert.Single(responded);
        Assert.StartsWith("/", responded.Single());
    }

    [Fact]
    public async Task AjaxChallengeRedirectTurnsInto200WithLocationHeader()
    {
        using var host = await CreateHost(s => { });
        using var server = host.GetTestServer();
        var transaction = await SendAsync(server, "http://example.com/challenge?X-Requested-With=XMLHttpRequest&ReturnUrl=/");
        Assert.Equal(HttpStatusCode.Unauthorized, transaction.Response.StatusCode);
        var responded = transaction.Response.Headers.GetValues("Location");
        Assert.Single(responded);
        Assert.StartsWith("http://example.com/Account/Login", responded.Single());
    }

    [Fact]
    public async Task ProtectedCustomRequestShouldRedirectToCustomRedirectUri()
    {
        using var host = await CreateHost(s => { });
        using var server = host.GetTestServer();

        var transaction = await SendAsync(server, "http://example.com/protected/CustomRedirect");

        Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
        var location = transaction.Response.Headers.Location;
        Assert.Equal("http://example.com/Account/Login?ReturnUrl=%2FCustomRedirect", location.ToString());
    }

    private static Task SignInAsAlice(HttpContext context)
    {
        var user = new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"));
        user.AddClaim(new Claim("marker", "true"));
        return context.SignInAsync("Cookies",
            new ClaimsPrincipal(user),
            new AuthenticationProperties());
    }

    private static Task SignInAsWrong(HttpContext context)
    {
        return context.SignInAsync("Oops",
            new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))),
            new AuthenticationProperties());
    }

    private Task SignOutAsWrong(HttpContext context)
    {
        return context.SignOutAsync("Oops");
    }

    [Fact]
    public async Task SignInCausesDefaultCookieToBeCreated()
    {
        using var host = await CreateHostWithServices(s => s.AddAuthentication().AddCookie(o =>
        {
            o.LoginPath = new PathString("/login");
            o.Cookie.Name = "TestCookie";
        }), SignInAsAlice);

        using var server = host.GetTestServer();
        var transaction = await SendAsync(server, "http://example.com/testpath");

        var setCookie = transaction.SetCookie;
        Assert.StartsWith("TestCookie=", setCookie);
        Assert.Contains("; path=/", setCookie);
        Assert.Contains("; httponly", setCookie);
        Assert.Contains("; samesite=", setCookie);
        Assert.DoesNotContain("; expires=", setCookie);
        Assert.DoesNotContain("; domain=", setCookie);
        Assert.DoesNotContain("; secure", setCookie);
        Assert.True(transaction.Response.Headers.CacheControl.NoCache);
        Assert.True(transaction.Response.Headers.CacheControl.NoStore);
        Assert.Equal("no-cache", transaction.Response.Headers.Pragma.ToString());
    }

    private class TestTicketStore : ITicketStore
    {
        private const string KeyPrefix = "AuthSessionStore-";
        public readonly Dictionary<string, AuthenticationTicket> Store = new Dictionary<string, AuthenticationTicket>();

        public async Task<string> StoreAsync(AuthenticationTicket ticket)
        {
            var guid = Guid.NewGuid();
            var key = KeyPrefix + guid.ToString();
            await RenewAsync(key, ticket);
            return key;
        }

        public Task RenewAsync(string key, AuthenticationTicket ticket)
        {
            Store[key] = ticket;

            return Task.FromResult(0);
        }

        public Task<AuthenticationTicket> RetrieveAsync(string key)
        {
            AuthenticationTicket ticket;
            Store.TryGetValue(key, out ticket);
            return Task.FromResult(ticket);
        }

        public Task RemoveAsync(string key)
        {
            Store.Remove(key);
            return Task.FromResult(0);
        }
    }

    [Fact]
    public async Task SignInWithTicketStoreWorks()
    {
        var sessionStore = new TestTicketStore();
        using var host = await CreateHostWithServices(s =>
        {
            s.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(o =>
            {
                o.TimeProvider = _timeProvider;
                o.SessionStore = sessionStore;
            });
        }, SignInAsAlice);

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);

        // Make sure we have one key as the session id
        var key1 = Assert.Single(sessionStore.Store.Keys);
        Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));

        // Make sure the session is expired
        _timeProvider.Advance(TimeSpan.FromDays(60));

        // Verify that a new session is generated with a new key
        var transaction3 = await SendAsync(server, "http://example.com/signinalice", transaction1.CookieNameValue);

        var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue);

        var key2 = Assert.Single(sessionStore.Store.Keys);
        Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name));
        Assert.NotEqual(key1, key2);
    }

    [Fact]
    public async Task SessionStoreRemovesExpired()
    {
        var sessionStore = new TestTicketStore();
        using var host = await CreateHostWithServices(s =>
        {
            s.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(o =>
            {
                o.TimeProvider = _timeProvider;
                o.SessionStore = sessionStore;
            });
        }, SignInAsAlice);

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);

        // Make sure we have one key as the session id
        var key1 = Assert.Single(sessionStore.Store.Keys);
        Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));

        // Make sure the session is expired
        _timeProvider.Advance(TimeSpan.FromDays(60));

        // Verify that a new session is generated with a new key
        var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);

        Assert.Empty(sessionStore.Store.Keys);
        Assert.Null(FindClaimValue(transaction3, ClaimTypes.Name));
    }

    [Fact]
    public async Task CustomAuthSchemeEncodesCookieName()
    {
        var schemeName = "With spaces and 界";
        using var host = await CreateHostWithServices(s => s.AddAuthentication(schemeName).AddCookie(schemeName, o =>
        {
            o.LoginPath = new PathString("/login");
        }), context =>
        {
            var user = new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"));
            user.AddClaim(new Claim("marker", "true"));
            return context.SignInAsync(schemeName,
                new ClaimsPrincipal(user),
                new AuthenticationProperties());
        });

        using var server = host.GetTestServer();
        var transaction = await SendAsync(server, "http://example.com/testpath");

        var setCookie = transaction.SetCookie;
        Assert.StartsWith(".AspNetCore.With%20spaces%20and%20%E7%95%8C=", setCookie);
        Assert.Contains("; path=/", setCookie);
        Assert.Contains("; httponly", setCookie);
        Assert.Contains("; samesite=", setCookie);
        Assert.DoesNotContain("; expires=", setCookie);
        Assert.DoesNotContain("; domain=", setCookie);
        Assert.DoesNotContain("; secure", setCookie);
        Assert.True(transaction.Response.Headers.CacheControl.NoCache);
        Assert.True(transaction.Response.Headers.CacheControl.NoStore);
        Assert.Equal("no-cache", transaction.Response.Headers.Pragma.ToString());
    }

    [Fact]
    public void SettingCookieExpirationOptionThrows()
    {
        var services = new ServiceCollection();
        services.AddAuthentication().AddCookie(o =>
        {
            o.Cookie.Expiration = TimeSpan.FromDays(10);
        });
        var options = services.BuildServiceProvider().GetRequiredService<IOptionsMonitor<CookieAuthenticationOptions>>();
        Assert.Throws<OptionsValidationException>(() => options.Get(CookieAuthenticationDefaults.AuthenticationScheme));
    }

    [Fact]
    public async Task SignInWrongAuthTypeThrows()
    {
        using var host = await CreateHost(o =>
        {
            o.LoginPath = new PathString("/login");
            o.Cookie.Name = "TestCookie";
        }, SignInAsWrong);
        using var server = host.GetTestServer();

        await Assert.ThrowsAsync<InvalidOperationException>(async () => await SendAsync(server, "http://example.com/testpath"));
    }

    [Fact]
    public async Task SignOutWrongAuthTypeThrows()
    {
        using var host = await CreateHost(o =>
        {
            o.LoginPath = new PathString("/login");
            o.Cookie.Name = "TestCookie";
        }, SignOutAsWrong);

        using var server = host.GetTestServer();
        await Assert.ThrowsAsync<InvalidOperationException>(async () => await SendAsync(server, "http://example.com/testpath"));
    }

    [Theory]
    [InlineData(CookieSecurePolicy.Always, "http://example.com/testpath", true)]
    [InlineData(CookieSecurePolicy.Always, "https://example.com/testpath", true)]
    [InlineData(CookieSecurePolicy.None, "http://example.com/testpath", false)]
    [InlineData(CookieSecurePolicy.None, "https://example.com/testpath", false)]
    [InlineData(CookieSecurePolicy.SameAsRequest, "http://example.com/testpath", false)]
    [InlineData(CookieSecurePolicy.SameAsRequest, "https://example.com/testpath", true)]
    public async Task SecureSignInCausesSecureOnlyCookieByDefault(
        CookieSecurePolicy cookieSecurePolicy,
        string requestUri,
        bool shouldBeSecureOnly)
    {
        using var host = await CreateHost(o =>
        {
            o.LoginPath = new PathString("/login");
            o.Cookie.Name = "TestCookie";
            o.Cookie.SecurePolicy = cookieSecurePolicy;
        }, SignInAsAlice);

        using var server = host.GetTestServer();
        var transaction = await SendAsync(server, requestUri);
        var setCookie = transaction.SetCookie;

        if (shouldBeSecureOnly)
        {
            Assert.Contains("; secure", setCookie);
        }
        else
        {
            Assert.DoesNotContain("; secure", setCookie);
        }
    }

    [Fact]
    public async Task CookieOptionsAlterSetCookieHeader()
    {
        using var host = await CreateHost(o =>
        {
            o.Cookie.Name = "TestCookie";
            o.Cookie.Path = "/foo";
            o.Cookie.Domain = "another.com";
            o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
            o.Cookie.SameSite = SameSiteMode.None;
            o.Cookie.HttpOnly = true;
            o.Cookie.Extensions.Add("extension0");
            o.Cookie.Extensions.Add("extension1=value1");
        }, SignInAsAlice, baseAddress: new Uri("http://example.com/base"));

        using var server1 = host.GetTestServer();
        var transaction1 = await SendAsync(server1, "http://example.com/base/testpath");

        var setCookie1 = transaction1.SetCookie;

        Assert.Contains("TestCookie=", setCookie1);
        Assert.Contains(" path=/foo", setCookie1);
        Assert.Contains(" domain=another.com", setCookie1);
        Assert.Contains(" secure", setCookie1);
        Assert.Contains(" samesite=none", setCookie1);
        Assert.Contains(" httponly", setCookie1);
        Assert.Contains(" extension0", setCookie1);
        Assert.Contains(" extension1=value1", setCookie1);

        using var host2 = await CreateHost(o =>
        {
            o.Cookie.Name = "SecondCookie";
            o.Cookie.SecurePolicy = CookieSecurePolicy.None;
            o.Cookie.SameSite = SameSiteMode.Strict;
            o.Cookie.HttpOnly = false;
        }, SignInAsAlice, baseAddress: new Uri("http://example.com/base"));

        using var server2 = host2.GetTestServer();
        var transaction2 = await SendAsync(server2, "http://example.com/base/testpath");

        var setCookie2 = transaction2.SetCookie;

        Assert.Contains("SecondCookie=", setCookie2);
        Assert.Contains(" path=/base", setCookie2);
        Assert.Contains(" samesite=strict", setCookie2);
        Assert.DoesNotContain(" domain=", setCookie2);
        Assert.DoesNotContain(" secure", setCookie2);
        Assert.DoesNotContain(" httponly", setCookie2);
        Assert.DoesNotContain(" extension", setCookie2);
    }

    [Fact]
    public async Task CookieContainsIdentity()
    {
        using var host = await CreateHost(o => { }, SignInAsAlice);
        using var server = host.GetTestServer();

        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);

        Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
    }

    [Fact]
    public async Task CookieAppliesClaimsTransform()
    {
        using var host = await CreateHost(o => { },
        SignInAsAlice,
        baseAddress: null,
        claimsTransform: true);

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);

        Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
        Assert.Equal("yup", FindClaimValue(transaction2, "xform"));
        Assert.Null(FindClaimValue(transaction2, "sync"));
    }

    [Fact]
    public async Task CookieStopsWorkingAfterExpiration()
    {
        using var host = await CreateHost(o =>
        {
            o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
            o.SlidingExpiration = false;
        }, SignInAsAlice);

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);

        _timeProvider.Advance(TimeSpan.FromMinutes(7));

        var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);

        _timeProvider.Advance(TimeSpan.FromMinutes(7));

        var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);

        Assert.Null(transaction2.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
        Assert.Null(transaction3.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name));
        Assert.Null(transaction4.SetCookie);
        Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name));
    }

    [Fact]
    public async Task CookieExpirationCanBeOverridenInSignin()
    {
        using var host = await CreateHost(o =>
        {
            o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
            o.SlidingExpiration = false;
        },
        context =>
            context.SignInAsync("Cookies",
                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))),
                new AuthenticationProperties() { ExpiresUtc = _timeProvider.GetUtcNow().Add(TimeSpan.FromMinutes(5)) }));

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);

        _timeProvider.Advance(TimeSpan.FromMinutes(3));

        var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);

        _timeProvider.Advance(TimeSpan.FromMinutes(3));

        var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);

        Assert.Null(transaction2.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
        Assert.Null(transaction3.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name));
        Assert.Null(transaction4.SetCookie);
        Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name));
    }

    [Fact]
    public async Task ExpiredCookieWithValidatorStillExpired()
    {
        using var host = await CreateHost(o =>
        {
            o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
            o.Events = new CookieAuthenticationEvents
            {
                OnValidatePrincipal = ctx =>
                {
                    ctx.ShouldRenew = true;
                    return Task.FromResult(0);
                }
            };
        },
        context =>
            context.SignInAsync("Cookies",
                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        _timeProvider.Advance(TimeSpan.FromMinutes(11));

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.Null(transaction2.SetCookie);
        Assert.Null(FindClaimValue(transaction2, ClaimTypes.Name));
    }

    [Fact]
    public async Task CookieCanBeRejectedAndSignedOutByValidator()
    {
        using var host = await CreateHost(o =>
        {
            o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
            o.SlidingExpiration = false;
            o.Events = new CookieAuthenticationEvents
            {
                OnValidatePrincipal = ctx =>
                {
                    ctx.RejectPrincipal();
                    ctx.HttpContext.SignOutAsync("Cookies");
                    return Task.FromResult(0);
                }
            };
        },
        context =>
            context.SignInAsync("Cookies",
                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.Contains(".AspNetCore.Cookies=; expires=", transaction2.SetCookie);
        Assert.Null(FindClaimValue(transaction2, ClaimTypes.Name));
    }

    [Fact]
    public async Task CookieNotRenewedAfterSignOut()
    {
        using var host = await CreateHost(o =>
        {
            o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
            o.SlidingExpiration = false;
            o.Events = new CookieAuthenticationEvents
            {
                OnValidatePrincipal = ctx =>
                {
                    ctx.ShouldRenew = true;
                    return Task.FromResult(0);
                }
            };
        },
        context =>
            context.SignInAsync("Cookies",
                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        // renews on every request
        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.NotNull(transaction2.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));

        var transaction3 = await server.SendAsync("http://example.com/normal", transaction1.CookieNameValue);
        Assert.NotNull(transaction3.SetCookie[0]);

        // signout wins over renew
        var transaction4 = await server.SendAsync("http://example.com/signout", transaction3.SetCookie[0]);
        Assert.Single(transaction4.SetCookie);
        Assert.Contains(".AspNetCore.Cookies=; expires=", transaction4.SetCookie[0]);
    }

    [Fact]
    public async Task CookieCanBeRenewedByValidator()
    {
        using var host = await CreateHost(o =>
        {
            o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
            o.SlidingExpiration = false;
            o.Events = new CookieAuthenticationEvents
            {
                OnValidatePrincipal = ctx =>
                {
                    ctx.ShouldRenew = true;
                    return Task.FromResult(0);
                }
            };
        },
        context =>
            context.SignInAsync("Cookies",
                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.NotNull(transaction2.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(5));

        var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue);
        Assert.NotNull(transaction3.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(6));

        var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.Null(transaction4.SetCookie);
        Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(5));

        var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue);
        Assert.Null(transaction5.SetCookie);
        Assert.Null(FindClaimValue(transaction5, ClaimTypes.Name));
    }

    [Fact]
    public async Task CookieCanBeReplacedByValidator()
    {
        using var host = await CreateHost(o =>
        {
            o.Events = new CookieAuthenticationEvents
            {
                OnValidatePrincipal = ctx =>
                {
                    ctx.ShouldRenew = true;
                    ctx.ReplacePrincipal(new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice2", "Cookies2"))));
                    return Task.FromResult(0);
                }
            };
        },
        context =>
            context.SignInAsync("Cookies",
                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.NotNull(transaction2.SetCookie);
        Assert.Equal("Alice2", FindClaimValue(transaction2, ClaimTypes.Name));
    }

    [Fact]
    public async Task CookieCanBeUpdatedByValidatorDuringRefresh()
    {
        var replace = false;
        using var host = await CreateHost(o =>
        {
            o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
            o.Events = new CookieAuthenticationEvents
            {
                OnValidatePrincipal = ctx =>
                {
                    if (replace)
                    {
                        ctx.ShouldRenew = true;
                        ctx.ReplacePrincipal(new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice2", "Cookies2"))));
                        ctx.Properties.Items["updated"] = "yes";
                    }
                    return Task.FromResult(0);
                }
            };
        },
        context =>
            context.SignInAsync("Cookies",
                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));

        var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
        Assert.Null(FindPropertiesValue(transaction3, "updated"));

        replace = true;

        var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.NotNull(transaction4.SetCookie);
        Assert.Equal("Alice2", FindClaimValue(transaction4, ClaimTypes.Name));
        Assert.Equal("yes", FindPropertiesValue(transaction4, "updated"));

        replace = false;

        var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue);
        Assert.Equal("Alice2", FindClaimValue(transaction5, ClaimTypes.Name));
        Assert.Equal("yes", FindPropertiesValue(transaction4, "updated"));
    }

    [Fact]
    public async Task CookieCanBeRenewedByValidatorWithSlidingExpiry()
    {
        using var host = await CreateHost(o =>
        {
            o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
            o.Events = new CookieAuthenticationEvents
            {
                OnValidatePrincipal = ctx =>
                {
                    ctx.ShouldRenew = true;
                    return Task.FromResult(0);
                }
            };
        },
        context =>
            context.SignInAsync("Cookies",
                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.NotNull(transaction2.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(5));

        var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue);
        Assert.NotNull(transaction3.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(6));

        var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue);
        Assert.NotNull(transaction4.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(11));

        var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue);
        Assert.Null(transaction5.SetCookie);
        Assert.Null(FindClaimValue(transaction5, ClaimTypes.Name));
    }

    [Fact]
    public async Task CookieCanBeRenewedByValidatorWithModifiedProperties()
    {
        using var host = await CreateHost(o =>
        {
            o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
            o.Events = new CookieAuthenticationEvents
            {
                OnValidatePrincipal = ctx =>
                {
                    ctx.ShouldRenew = true;
                    var id = ctx.Principal.Identities.First();
                    var claim = id.FindFirst("counter");
                    if (claim == null)
                    {
                        id.AddClaim(new Claim("counter", "1"));
                    }
                    else
                    {
                        id.RemoveClaim(claim);
                        id.AddClaim(new Claim("counter", claim.Value + "1"));
                    }
                    return Task.FromResult(0);
                }
            };
        },
        context =>
            context.SignInAsync("Cookies",
                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.NotNull(transaction2.SetCookie);
        Assert.Equal("1", FindClaimValue(transaction2, "counter"));

        _timeProvider.Advance(TimeSpan.FromMinutes(5));

        var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue);
        Assert.NotNull(transaction3.SetCookie);
        Assert.Equal("11", FindClaimValue(transaction3, "counter"));

        _timeProvider.Advance(TimeSpan.FromMinutes(6));

        var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue);
        Assert.NotNull(transaction4.SetCookie);
        Assert.Equal("111", FindClaimValue(transaction4, "counter"));

        _timeProvider.Advance(TimeSpan.FromMinutes(11));

        var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue);
        Assert.Null(transaction5.SetCookie);
        Assert.Null(FindClaimValue(transaction5, "counter"));
    }

    [Fact]
    public async Task CookieCanBeRenewedByValidatorWithModifiedLifetime()
    {
        using var host = await CreateHost(o =>
        {
            o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
            o.Events = new CookieAuthenticationEvents
            {
                OnValidatePrincipal = ctx =>
                {
                    ctx.ShouldRenew = true;
                    var id = ctx.Principal.Identities.First();
                    var claim = id.FindFirst("counter");
                    if (claim == null)
                    {
                        id.AddClaim(new Claim("counter", "1"));
                    }
                    else
                    {
                        id.RemoveClaim(claim);
                        id.AddClaim(new Claim("counter", claim.Value + "1"));
                    }
                    // Causes the expiry time to not be extended because the lifetime is
                    // calculated relative to the issue time.
                    ctx.Properties.IssuedUtc = _timeProvider.GetUtcNow();
                    return Task.FromResult(0);
                }
            };
        },
        context =>
            context.SignInAsync("Cookies",
                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.NotNull(transaction2.SetCookie);
        Assert.Equal("1", FindClaimValue(transaction2, "counter"));

        _timeProvider.Advance(TimeSpan.FromMinutes(1));

        var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue);
        Assert.NotNull(transaction3.SetCookie);
        Assert.Equal("11", FindClaimValue(transaction3, "counter"));

        _timeProvider.Advance(TimeSpan.FromMinutes(1));

        var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue);
        Assert.NotNull(transaction4.SetCookie);
        Assert.Equal("111", FindClaimValue(transaction4, "counter"));

        _timeProvider.Advance(TimeSpan.FromMinutes(9));

        var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue);
        Assert.Null(transaction5.SetCookie);
        Assert.Null(FindClaimValue(transaction5, "counter"));
    }

    [Fact]
    public async Task CookieValidatorOnlyCalledOnce()
    {
        using var host = await CreateHost(o =>
        {
            o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
            o.SlidingExpiration = false;
            o.Events = new CookieAuthenticationEvents
            {
                OnValidatePrincipal = ctx =>
                {
                    ctx.ShouldRenew = true;
                    return Task.FromResult(0);
                }
            };
        },
        context =>
            context.SignInAsync("Cookies",
                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.NotNull(transaction2.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(5));

        var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue);
        Assert.NotNull(transaction3.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(6));

        var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.Null(transaction4.SetCookie);
        Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(5));

        var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue);
        Assert.Null(transaction5.SetCookie);
        Assert.Null(FindClaimValue(transaction5, ClaimTypes.Name));
    }

    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task ShouldRenewUpdatesIssuedExpiredUtc(bool sliding)
    {
        DateTimeOffset? lastValidateIssuedDate = null;
        DateTimeOffset? lastExpiresDate = null;
        using var host = await CreateHost(o =>
        {
            o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
            o.SlidingExpiration = sliding;
            o.Events = new CookieAuthenticationEvents
            {
                OnValidatePrincipal = ctx =>
                {
                    lastValidateIssuedDate = ctx.Properties.IssuedUtc;
                    lastExpiresDate = ctx.Properties.ExpiresUtc;
                    ctx.ShouldRenew = true;
                    return Task.FromResult(0);
                }
            };
        },
        context =>
            context.SignInAsync("Cookies",
                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.NotNull(transaction2.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));

        Assert.NotNull(lastValidateIssuedDate);
        Assert.NotNull(lastExpiresDate);

        var firstIssueDate = lastValidateIssuedDate;
        var firstExpiresDate = lastExpiresDate;

        _timeProvider.Advance(TimeSpan.FromMinutes(1));

        var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue);
        Assert.NotNull(transaction3.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(2));

        var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue);
        Assert.NotNull(transaction4.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name));

        Assert.NotEqual(lastValidateIssuedDate, firstIssueDate);
        Assert.NotEqual(firstExpiresDate, lastExpiresDate);
    }

    [Fact]
    public async Task CookieExpirationCanBeOverridenInEvent()
    {
        using var host = await CreateHost(o =>
        {
            o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
            o.SlidingExpiration = false;
            o.Events = new CookieAuthenticationEvents()
            {
                OnSigningIn = context =>
                {
                    context.Properties.ExpiresUtc = _timeProvider.GetUtcNow().Add(TimeSpan.FromMinutes(5));
                    return Task.FromResult(0);
                }
            };
        },
        SignInAsAlice);

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.Null(transaction2.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(3));

        var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.Null(transaction3.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(3));

        var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.Null(transaction4.SetCookie);
        Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name));
    }

    [Fact]
    public async Task CookieIsRenewedWithSlidingExpiration()
    {
        using var host = await CreateHost(o =>
        {
            o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
            o.SlidingExpiration = true;
        },
        SignInAsAlice);

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.Null(transaction2.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(4));

        var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.Null(transaction3.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(4));

        // transaction4 should arrive with a new SetCookie value
        var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.NotNull(transaction4.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(4));

        var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue);
        Assert.Null(transaction5.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction5, ClaimTypes.Name));
    }

    [Fact]
    public async Task CookieIsRenewedWithSlidingExpirationWithoutTransformations()
    {
        using var host = await CreateHost(o =>
        {
            o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
            o.SlidingExpiration = true;
            o.Events.OnValidatePrincipal = c =>
            {
                // https://github.com/aspnet/Security/issues/1607
                // On sliding refresh the transformed principal should not be serialized into the cookie, only the original principal.
                Assert.Single(c.Principal.Identities);
                Assert.True(c.Principal.Identities.First().HasClaim("marker", "true"));
                return Task.CompletedTask;
            };
        },
        SignInAsAlice,
        claimsTransform: true);

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.Null(transaction2.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(4));

        var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.Null(transaction3.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(4));

        // transaction4 should arrive with a new SetCookie value
        var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
        Assert.NotNull(transaction4.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(4));

        var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue);
        Assert.Null(transaction5.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction5, ClaimTypes.Name));
    }

    [Fact]
    public async Task CookieIsRenewedWithSlidingExpirationEvent()
    {
        using var host = await CreateHost(o =>
        {
            o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
            o.SlidingExpiration = true;
            o.Events = new CookieAuthenticationEvents()
            {
                OnCheckSlidingExpiration = context =>
                {
                    var expectRenew = string.Equals("1", context.Request.Query["expectrenew"]);
                    var renew = string.Equals("1", context.Request.Query["renew"]);
                    Assert.Equal(expectRenew, context.ShouldRenew);
                    context.ShouldRenew = renew;
                    return Task.CompletedTask;
                }
            };
        },
        SignInAsAlice);

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/me/Cookies?expectrenew=0&renew=0", transaction1.CookieNameValue);
        Assert.Null(transaction2.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(4));

        var transaction3 = await SendAsync(server, "http://example.com/me/Cookies?expectrenew=0&renew=0", transaction1.CookieNameValue);
        Assert.Null(transaction3.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name));

        _timeProvider.Advance(TimeSpan.FromMinutes(4));

        // A renewal is now expected, but we've suppressed it
        var transaction4 = await SendAsync(server, "http://example.com/me/Cookies?expectrenew=1&renew=0", transaction1.CookieNameValue);
        Assert.Null(transaction4.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name));

        // Allow the default renewal to happen
        var transaction5 = await SendAsync(server, "http://example.com/me/Cookies?expectrenew=1&renew=1", transaction1.CookieNameValue);
        Assert.NotNull(transaction5.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction5, ClaimTypes.Name));

        // Force a renewal on an un-expired new cookie
        var transaction6 = await SendAsync(server, "http://example.com/me/Cookies?expectrenew=0&renew=1", transaction5.CookieNameValue);
        Assert.NotNull(transaction5.SetCookie);
        Assert.Equal("Alice", FindClaimValue(transaction6, ClaimTypes.Name));
    }

    [Fact]
    public async Task CookieUsesPathBaseByDefault()
    {
        using var host = await CreateHost(o => { },
        context =>
        {
            Assert.Equal(new PathString("/base"), context.Request.PathBase);
            return context.SignInAsync("Cookies",
                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))));
        },
        new Uri("http://example.com/base"));

        using var server = host.GetTestServer();
        var transaction1 = await SendAsync(server, "http://example.com/base/testpath");
        Assert.Contains("path=/base", transaction1.SetCookie);
    }

    [Fact]
    public async Task CookieChallengeRedirectsToLoginWithoutCookie()
    {
        using var host = await CreateHost(o => { }, SignInAsAlice);

        var url = "http://example.com/challenge";
        using var server = host.GetTestServer();
        var transaction = await SendAsync(server, url);

        Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
        var location = transaction.Response.Headers.Location;
        Assert.Equal("/Account/Login", location.LocalPath);
    }

    [Fact]
    public async Task CookieForbidRedirectsWithoutCookie()
    {
        using var host = await CreateHost(o => { }, SignInAsAlice);

        var url = "http://example.com/forbid";
        using var server = host.GetTestServer();
        var transaction = await SendAsync(server, url);

        Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
        var location = transaction.Response.Headers.Location;
        Assert.Equal("/Account/AccessDenied", location.LocalPath);
    }

    [Fact]
    public async Task CookieChallengeRedirectsWithLoginPath()
    {
        using var host = await CreateHost(o =>
        {
            o.LoginPath = new PathString("/page");
        });
        using var server = host.GetTestServer();

        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/challenge", transaction1.CookieNameValue);

        Assert.Equal(HttpStatusCode.Redirect, transaction2.Response.StatusCode);
    }

    [Fact]
    public async Task CookieChallengeWithUnauthorizedRedirectsToLoginIfNotAuthenticated()
    {
        using var host = await CreateHost(o =>
        {
            o.LoginPath = new PathString("/page");
        });
        using var server = host.GetTestServer();

        var transaction1 = await SendAsync(server, "http://example.com/testpath");

        var transaction2 = await SendAsync(server, "http://example.com/unauthorized", transaction1.CookieNameValue);

        Assert.Equal(HttpStatusCode.Redirect, transaction2.Response.StatusCode);
    }

    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task MapWillAffectChallengeOnlyWithUseAuth(bool useAuth)
    {
        using var host = new HostBuilder()
            .ConfigureWebHost(builder =>
                builder.UseTestServer()
                    .Configure(app =>
                    {
                        if (useAuth)
                        {
                            app.UseAuthentication();
                        }
                        app.Map("/login", signoutApp => signoutApp.Run(context => context.ChallengeAsync("Cookies", new AuthenticationProperties() { RedirectUri = "/" })));
                    })
                    .ConfigureServices(s => s.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/page"))))
            .Build();
        await host.StartAsync();
        using var server = host.GetTestServer();

        var transaction = await server.SendAsync("http://example.com/login");

        Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);

        var location = transaction.Response.Headers.Location;
        if (useAuth)
        {
            Assert.Equal("/page", location.LocalPath);
        }
        else
        {
            Assert.Equal("/login/page", location.LocalPath);
        }
        Assert.Equal("?ReturnUrl=%2F", location.Query);
    }

    [ConditionalFact(Skip = "Revisit, exception no longer thrown")]
    public async Task ChallengeDoesNotSet401OnUnauthorized()
    {
        using var host = new HostBuilder()
            .ConfigureWebHost(builder =>
                builder.UseTestServer()
                .Configure(app =>
                {
                    app.UseAuthentication();
                    app.Run(async context =>
                    {
                        await Assert.ThrowsAsync<InvalidOperationException>(() => context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme));
                    });
                })
                .ConfigureServices(services => services.AddAuthentication().AddCookie()))
            .Build();

        await host.StartAsync();
        using var server = host.GetTestServer();

        var transaction = await server.SendAsync("http://example.com");
        Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
    }

    [Fact]
    public async Task CanConfigureDefaultCookieInstance()
    {
        using var host = new HostBuilder()
            .ConfigureWebHost(builder =>
                builder.UseTestServer()
                    .Configure(app =>
                    {
                        app.UseAuthentication();
                        app.Run(context => context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity("whatever"))));
                    })
                    .ConfigureServices(services =>
                    {
                        services.AddAuthentication().AddCookie();
                        services.Configure<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme,
                            o => o.Cookie.Name = "One");
                    }))
            .Build();

        await host.StartAsync();
        using var server = host.GetTestServer();

        var transaction = await server.SendAsync("http://example.com");

        Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
        Assert.StartsWith("One=", transaction.SetCookie[0]);
    }

    [Fact]
    public async Task CanConfigureNamedCookieInstance()
    {
        using var host = new HostBuilder()
            .ConfigureWebHost(builder =>
                builder.UseTestServer()
                    .Configure(app =>
                    {
                        app.UseAuthentication();
                        app.Run(context => context.SignInAsync("Cookie1", new ClaimsPrincipal(new ClaimsIdentity("whatever"))));
                    })
                    .ConfigureServices(services =>
                    {
                        services.AddAuthentication().AddCookie("Cookie1");
                        services.Configure<CookieAuthenticationOptions>("Cookie1",
                            o => o.Cookie.Name = "One");
                    }))
            .Build();

        await host.StartAsync();
        using var server = host.GetTestServer();

        var transaction = await server.SendAsync("http://example.com");

        Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
        Assert.StartsWith("One=", transaction.SetCookie[0]);
    }

    [Fact]
    public async Task MapWithSignInOnlyRedirectToReturnUrlOnLoginPath()
    {
        using var host = new HostBuilder()
            .ConfigureWebHost(builder =>
                builder.UseTestServer()
                    .Configure(app =>
                    {
                        app.UseAuthentication();
                        app.Map("/notlogin", signoutApp => signoutApp.Run(context => context.SignInAsync("Cookies",
                            new ClaimsPrincipal(new ClaimsIdentity("whatever")))));
                    })
                    .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/login"))))
            .Build();

        await host.StartAsync();
        using var server = host.GetTestServer();

        var transaction = await server.SendAsync("http://example.com/notlogin?ReturnUrl=%2Fpage");
        Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
        Assert.NotNull(transaction.SetCookie);
    }

    [Fact]
    public async Task MapWillNotAffectSignInRedirectToReturnUrl()
    {
        using var host = new HostBuilder()
            .ConfigureWebHost(builder =>
                builder.UseTestServer()
                    .Configure(app =>
                    {
                        app.UseAuthentication();
                        app.Map("/login", signoutApp => signoutApp.Run(context => context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity("whatever")))));
                    })
                    .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/login"))))
            .Build();

        await host.StartAsync();
        using var server = host.GetTestServer();

        var transaction = await server.SendAsync("http://example.com/login?ReturnUrl=%2Fpage");

        Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
        Assert.NotNull(transaction.SetCookie);

        var location = transaction.Response.Headers.Location;
        Assert.Equal("/page", location.OriginalString);
    }

    [Fact]
    public async Task MapWithSignOutOnlyRedirectToReturnUrlOnLogoutPath()
    {
        using var host = new HostBuilder()
            .ConfigureWebHost(builder =>
                builder.UseTestServer()
                    .Configure(app =>
                    {
                        app.UseAuthentication();
                        app.Map("/notlogout", signoutApp => signoutApp.Run(context => context.SignOutAsync("Cookies")));
                    })
                    .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LogoutPath = new PathString("/logout"))))
            .Build();

        await host.StartAsync();
        using var server = host.GetTestServer();

        var transaction = await server.SendAsync("http://example.com/notlogout?ReturnUrl=%2Fpage");
        Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
        Assert.Contains(".AspNetCore.Cookies=; expires=", transaction.SetCookie[0]);
    }

    [Fact]
    public async Task MapWillNotAffectSignOutRedirectToReturnUrl()
    {
        using var host = new HostBuilder()
            .ConfigureWebHost(builder =>
                builder.UseTestServer()
                    .Configure(app =>
                    {
                        app.UseAuthentication();
                        app.Map("/logout", signoutApp => signoutApp.Run(context => context.SignOutAsync("Cookies")));
                    })
                    .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LogoutPath = new PathString("/logout"))))
            .Build();

        await host.StartAsync();
        using var server = host.GetTestServer();

        var transaction = await server.SendAsync("http://example.com/logout?ReturnUrl=%2Fpage");

        Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
        Assert.Contains(".AspNetCore.Cookies=; expires=", transaction.SetCookie[0]);

        var location = transaction.Response.Headers.Location;
        Assert.Equal("/page", location.OriginalString);
    }

    [Fact]
    public async Task MapWillNotAffectAccessDenied()
    {
        using var host = new HostBuilder()
            .ConfigureWebHost(builder =>
                builder.UseTestServer()
                    .Configure(app =>
                    {
                        app.UseAuthentication();
                        app.Map("/forbid", signoutApp => signoutApp.Run(context => context.ForbidAsync("Cookies")));
                    })
                    .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.AccessDeniedPath = new PathString("/denied"))))
            .Build();

        await host.StartAsync();
        using var server = host.GetTestServer();
        var transaction = await server.SendAsync("http://example.com/forbid");

        Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);

        var location = transaction.Response.Headers.Location;
        Assert.Equal("/denied", location.LocalPath);
    }

    [Fact]
    public async Task NestedMapWillNotAffectLogin()
    {
        using var host = new HostBuilder()
            .ConfigureWebHost(builder =>
                builder.UseTestServer()
                    .Configure(app =>
                        app.Map("/base", map =>
                        {
                            map.UseAuthentication();
                            map.Map("/login", signoutApp => signoutApp.Run(context => context.ChallengeAsync("Cookies", new AuthenticationProperties() { RedirectUri = "/" })));
                        }))
                    .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/page"))))
            .Build();

        await host.StartAsync();
        using var server = host.GetTestServer();
        var transaction = await server.SendAsync("http://example.com/base/login");

        Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);

        var location = transaction.Response.Headers.Location;
        Assert.Equal("/base/page", location.LocalPath);
        Assert.Equal("?ReturnUrl=%2F", location.Query);
    }

    [Theory]
    [InlineData("/redirect_test", "/loginpath")]
    [InlineData("/redirect_test", "/testpath")]
    [InlineData("http://example.com/redirect_to", "/loginpath")]
    [InlineData("http://example.com/redirect_to", "/testpath")]
    public async Task RedirectUriIsHonoredAfterSignin(string redirectUrl, string loginPath)
    {
        using var host = await CreateHost(o =>
        {
            o.LoginPath = loginPath;
            o.Cookie.Name = "TestCookie";
        },
        async context =>
            await context.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme))),
                new AuthenticationProperties { RedirectUri = redirectUrl })
        );
        using var server = host.GetTestServer();
        var transaction = await SendAsync(server, "http://example.com/testpath");

        Assert.NotEmpty(transaction.SetCookie);
        Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
        Assert.Equal(redirectUrl, transaction.Response.Headers.Location.ToString());
    }

    [Fact]
    public async Task RedirectUriInQueryIsIgnoredAfterSigninForUnrecognizedEndpoints()
    {
        using var host = await CreateHost(o =>
        {
            o.LoginPath = "/loginpath";
            o.ReturnUrlParameter = "return";
            o.Cookie.Name = "TestCookie";
        },
        async context =>
        {
            await context.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme))));
        });
        using var server = host.GetTestServer();
        var transaction = await SendAsync(server, "http://example.com/testpath?return=%2Fret_path_2");

        Assert.NotEmpty(transaction.SetCookie);
        Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
    }

    [Fact]
    public async Task RedirectUriInQueryIsHonoredAfterSignin()
    {
        using var host = await CreateHost(o =>
        {
            o.LoginPath = "/testpath";
            o.ReturnUrlParameter = "return";
            o.Cookie.Name = "TestCookie";
        },
        async context =>
        {
            await context.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme))));
        });
        using var server = host.GetTestServer();
        var transaction = await SendAsync(server, "http://example.com/testpath?return=%2Fret_path_2");

        Assert.NotEmpty(transaction.SetCookie);
        Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
        Assert.Equal("/ret_path_2", transaction.Response.Headers.Location.ToString());
    }

    [Fact]
    public async Task AbsoluteRedirectUriInQueryStringIsRejected()
    {
        using var host = await CreateHost(o =>
        {
            o.LoginPath = "/testpath";
            o.ReturnUrlParameter = "return";
            o.Cookie.Name = "TestCookie";
        },
        async context =>
        {
            await context.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme))));
        });
        using var server = host.GetTestServer();
        var transaction = await SendAsync(server, "http://example.com/testpath?return=http%3A%2F%2Fexample.com%2Fredirect_to");

        Assert.NotEmpty(transaction.SetCookie);
        Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
    }

    [Fact]
    public async Task EnsurePrecedenceOfRedirectUriAfterSignin()
    {
        using var host = await CreateHost(o =>
        {
            o.LoginPath = "/testpath";
            o.ReturnUrlParameter = "return";
            o.Cookie.Name = "TestCookie";
        },
        async context =>
        {
            await context.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme))),
                new AuthenticationProperties { RedirectUri = "/redirect_test" });
        });
        using var server = host.GetTestServer();
        var transaction = await SendAsync(server, "http://example.com/testpath?return=%2Fret_path_2");

        Assert.NotEmpty(transaction.SetCookie);
        Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
        Assert.Equal("/redirect_test", transaction.Response.Headers.Location.ToString());
    }

    [Fact]
    public async Task NestedMapWillNotAffectAccessDenied()
    {
        using var host = new HostBuilder()
            .ConfigureWebHost(builder =>
                builder.UseTestServer()
                    .Configure(app =>
                        app.Map("/base", map =>
                        {
                            map.UseAuthentication();
                            map.Map("/forbid", signoutApp => signoutApp.Run(context => context.ForbidAsync("Cookies")));
                        }))
                        .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.AccessDeniedPath = new PathString("/denied"))))
            .Build();
        await host.StartAsync();
        using var server = host.GetTestServer();
        var transaction = await server.SendAsync("http://example.com/base/forbid");

        Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);

        var location = transaction.Response.Headers.Location;
        Assert.Equal("/base/denied", location.LocalPath);
    }

    [Fact]
    public async Task CanSpecifyAndShareDataProtector()
    {
        var dp = new NoOpDataProtector();
        using var host1 = new HostBuilder()
            .ConfigureWebHost(builder =>
                builder.UseTestServer()
                    .Configure(app =>
                    {
                        app.UseAuthentication();
                        app.Run((context) =>
                            context.SignInAsync("Cookies",
                                            new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))),
                                            new AuthenticationProperties()));
                    })
                    .ConfigureServices(services => services.AddAuthentication().AddCookie(o =>
                    {
                        o.TicketDataFormat = new TicketDataFormat(dp);
                        o.Cookie.Name = "Cookie";
                    })))
            .Build();
        await host1.StartAsync();
        using var server1 = host1.GetTestServer();

        var transaction = await SendAsync(server1, "http://example.com/stuff");
        Assert.NotNull(transaction.SetCookie);

        using var host2 = new HostBuilder()
            .ConfigureWebHost(builder =>
                builder.UseTestServer()
                .Configure(app =>
                {
                    app.UseAuthentication();
                    app.Run(async (context) =>
                    {
                        var result = await context.AuthenticateAsync("Cookies");
                        await DescribeAsync(context.Response, result);
                    });
                })
                .ConfigureServices(services => services.AddAuthentication().AddCookie("Cookies", o =>
                {
                    o.Cookie.Name = "Cookie";
                    o.TicketDataFormat = new TicketDataFormat(dp);
                })))
            .Build();
        await host2.StartAsync();
        using var server2 = host2.GetTestServer();
        var transaction2 = await SendAsync(server2, "http://example.com/stuff", transaction.CookieNameValue);
        Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
    }

    // Issue: https://github.com/aspnet/Security/issues/949
    [Fact]
    public async Task NullExpiresUtcPropertyIsGuarded()
    {
        using var host = new HostBuilder()
            .ConfigureWebHost(builder =>
                builder.UseTestServer()
                .ConfigureServices(services => services.AddAuthentication().AddCookie(o =>
                {
                    o.Events = new CookieAuthenticationEvents
                    {
                        OnValidatePrincipal = context =>
                        {
                            context.Properties.ExpiresUtc = null;
                            context.ShouldRenew = true;
                            return Task.FromResult(0);
                        }
                    };
                }))
                .Configure(app =>
                {
                    app.UseAuthentication();

                    app.Run(async context =>
                    {
                        if (context.Request.Path == "/signin")
                        {
                            await context.SignInAsync(
                                CookieAuthenticationDefaults.AuthenticationScheme,
                                new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))));
                        }
                        else
                        {
                            await context.Response.WriteAsync("ha+1");
                        }
                    });
                }))
            .Build();

        await host.StartAsync();
        using var server = host.GetTestServer();

        var cookie = (await server.SendAsync("http://www.example.com/signin")).SetCookie.FirstOrDefault();
        Assert.NotNull(cookie);

        var transaction = await server.SendAsync("http://www.example.com/", cookie);
        Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
    }

    private class NoOpDataProtector : IDataProtector
    {
        public IDataProtector CreateProtector(string purpose)
        {
            return this;
        }

        public byte[] Protect(byte[] plaintext)
        {
            return plaintext;
        }

        public byte[] Unprotect(byte[] protectedData)
        {
            return protectedData;
        }
    }

    private static string FindClaimValue(Transaction transaction, string claimType)
    {
        var claim = transaction.ResponseElement.Elements("claim").SingleOrDefault(elt => elt.Attribute("type").Value == claimType);
        if (claim == null)
        {
            return null;
        }
        return claim.Attribute("value").Value;
    }

    private static string FindPropertiesValue(Transaction transaction, string key)
    {
        var property = transaction.ResponseElement.Elements("extra").SingleOrDefault(elt => elt.Attribute("type").Value == key);
        if (property == null)
        {
            return null;
        }
        return property.Attribute("value").Value;
    }

    private class ClaimsTransformer : IClaimsTransformation
    {
        public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal p)
        {
            var firstId = p.Identities.First();
            if (firstId.HasClaim("marker", "true"))
            {
                firstId.RemoveClaim(firstId.FindFirst("marker"));
            }
            // TransformAsync could be called twice on one request if you have a default scheme and also
            // call AuthenticateAsync.
            if (!p.Identities.Any(i => i.AuthenticationType == "xform"))
            {
                var id = new ClaimsIdentity("xform");
                id.AddClaim(new Claim("xform", "yup"));
                p.AddIdentity(id);
            }
            return Task.FromResult(p);
        }
    }

    private Task<IHost> CreateHost(Action<CookieAuthenticationOptions> configureOptions, Func<HttpContext, Task> testpath = null, Uri baseAddress = null, bool claimsTransform = false)
        => CreateHostWithServices(s =>
        {
            s.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(o =>
            {
                o.TimeProvider = _timeProvider;
                configureOptions(o);
            });
            if (claimsTransform)
            {
                s.AddSingleton<IClaimsTransformation, ClaimsTransformer>();
            }
        }, testpath, baseAddress);

    private static async Task<IHost> CreateHostWithServices(Action<IServiceCollection> configureServices, Func<HttpContext, Task> testpath = null, Uri baseAddress = null)
    {
        var host = new HostBuilder()
            .ConfigureWebHost(builder =>
                builder.UseTestServer()
                    .Configure(app =>
                    {
                        app.UseAuthentication();
                        app.Use(async (context, next) =>
                        {
                            var req = context.Request;
                            var res = context.Response;
                            PathString remainder;
                            if (req.Path == new PathString("/normal"))
                            {
                                res.StatusCode = 200;
                            }
                            else if (req.Path == new PathString("/forbid")) // Simulate forbidden
                            {
                                await context.ForbidAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                            }
                            else if (req.Path == new PathString("/challenge"))
                            {
                                await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                            }
                            else if (req.Path == new PathString("/signinalice"))
                            {
                                await SignInAsAlice(context);
                            }
                            else if (req.Path == new PathString("/signout"))
                            {
                                await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                            }
                            else if (req.Path == new PathString("/unauthorized"))
                            {
                                await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties());
                            }
                            else if (req.Path == new PathString("/protected/CustomRedirect"))
                            {
                                await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties() { RedirectUri = "/CustomRedirect" });
                            }
                            else if (req.Path == new PathString("/me"))
                            {
                                await DescribeAsync(res, AuthenticateResult.Success(new AuthenticationTicket(context.User, new AuthenticationProperties(), CookieAuthenticationDefaults.AuthenticationScheme)));
                            }
                            else if (req.Path.StartsWithSegments(new PathString("/me"), out remainder))
                            {
                                var ticket = await context.AuthenticateAsync(remainder.Value.Substring(1));
                                await DescribeAsync(res, ticket);
                            }
                            else if (req.Path == new PathString("/testpath") && testpath != null)
                            {
                                await testpath(context);
                            }
                            else if (req.Path == new PathString("/checkforerrors"))
                            {
                                var result = await context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); // this used to be "Automatic"
                                if (result.Failure != null)
                                {
                                    throw new Exception("Failed to authenticate", result.Failure);
                                }
                                return;
                            }
                            else
                            {
                                await next(context);
                            }
                        });
                    })
                    .ConfigureServices(configureServices))
            .Build();

        await host.StartAsync();

        var server = host.GetTestServer();
        server.BaseAddress = baseAddress;
        return host;
    }

    private static Task DescribeAsync(HttpResponse res, AuthenticateResult result)
    {
        res.StatusCode = 200;
        res.ContentType = "text/xml";
        var xml = new XElement("xml");
        if (result?.Ticket?.Principal != null)
        {
            xml.Add(result.Ticket.Principal.Claims.Select(claim => new XElement("claim", new XAttribute("type", claim.Type), new XAttribute("value", claim.Value))));
        }
        if (result?.Ticket?.Properties != null)
        {
            xml.Add(result.Ticket.Properties.Items.Select(extra => new XElement("extra", new XAttribute("type", extra.Key), new XAttribute("value", extra.Value))));
        }
        var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString());
        return res.Body.WriteAsync(xmlBytes, 0, xmlBytes.Length);
    }

    private static async Task<Transaction> SendAsync(TestServer server, string uri, string cookieHeader = null)
    {
        var request = new HttpRequestMessage(HttpMethod.Get, uri);
        if (!string.IsNullOrEmpty(cookieHeader))
        {
            request.Headers.Add("Cookie", cookieHeader);
        }
        var transaction = new Transaction
        {
            Request = request,
            Response = await server.CreateClient().SendAsync(request),
        };
        if (transaction.Response.Headers.Contains("Set-Cookie"))
        {
            transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").SingleOrDefault();
        }
        if (!string.IsNullOrEmpty(transaction.SetCookie))
        {
            transaction.CookieNameValue = transaction.SetCookie.Split(new[] { ';' }, 2).First();
        }
        transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync();

        if (transaction.Response.Content != null &&
            transaction.Response.Content.Headers.ContentType != null &&
            transaction.Response.Content.Headers.ContentType.MediaType == "text/xml")
        {
            transaction.ResponseElement = XElement.Parse(transaction.ResponseText);
        }
        return transaction;
    }

    private class Transaction
    {
        public HttpRequestMessage Request { get; set; }
        public HttpResponseMessage Response { get; set; }

        public string SetCookie { get; set; }
        public string CookieNameValue { get; set; }

        public string ResponseText { get; set; }
        public XElement ResponseElement { get; set; }
    }
}
