File: TempData\CookieTempDataProviderTest.cs
Web Access
Project: src\src\Components\Endpoints\test\Microsoft.AspNetCore.Components.Endpoints.Tests.csproj (Microsoft.AspNetCore.Components.Endpoints.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
 
namespace Microsoft.AspNetCore.Components.Endpoints;
 
public class CookieTempDataProviderTest
{
    private readonly CookieTempDataProvider _cookieTempDataProvider;
 
    internal TempData CreateTempData()
    {
        return new TempData(() => new Dictionary<string, object>());
    }
 
    public CookieTempDataProviderTest()
    {
        _cookieTempDataProvider = new CookieTempDataProvider(
            new EphemeralDataProtectionProvider(),
            Options.Create<RazorComponentsServiceOptions>(new()),
            new JsonTempDataSerializer(),
            NullLogger<CookieTempDataProvider>.Instance);
    }
 
    [Fact]
    public void Load_ReturnsEmptyTempData_WhenNoCookieExists()
    {
        var httpContext = CreateHttpContext();
        var tempData = _cookieTempDataProvider.LoadTempData(httpContext);
 
        Assert.NotNull(tempData);
        Assert.Empty(tempData);
    }
 
    [Fact]
    public void Load_ReturnsEmptyTempData_AndClearsCookie_WhenDataIsInvalid()
    {
        var httpContext = CreateHttpContext();
        httpContext.Request.Headers["Cookie"] = ".AspNetCore.Components.TempData=not-valid-base64!!!";
 
        var tempData = _cookieTempDataProvider.LoadTempData(httpContext);
 
        Assert.NotNull(tempData);
        Assert.Empty(tempData);
        // Cookie should be deleted when invalid
        var cookieFeature = httpContext.Features.Get<TestResponseCookiesFeature>();
        Assert.NotNull(cookieFeature);
        Assert.Contains(".AspNetCore.Components.TempData", cookieFeature.DeletedCookies);
    }
 
    [Fact]
    public void Save_DeletesCookie_WhenNoDataToSave()
    {
        var httpContext = CreateHttpContext();
        var tempData = CreateTempData();
 
        _cookieTempDataProvider.SaveTempData(httpContext, tempData.Save());
 
        var cookieFeature = httpContext.Features.Get<TestResponseCookiesFeature>();
        Assert.NotNull(cookieFeature);
        Assert.Contains(".AspNetCore.Components.TempData", cookieFeature.DeletedCookies);
    }
 
    [Fact]
    public void Save_SetsCookie_WhenDataExists()
    {
        var httpContext = CreateHttpContext();
        var tempData = CreateTempData();
        tempData["Key1"] = "Value1";
 
        _cookieTempDataProvider.SaveTempData(httpContext, tempData.Save());
 
        var cookieFeature = httpContext.Features.Get<TestResponseCookiesFeature>();
        Assert.NotNull(cookieFeature);
        Assert.Contains(".AspNetCore.Components.TempData", cookieFeature.SetCookies.Keys);
    }
 
    [Fact]
    public void Save_ThrowsForUnsupportedType()
    {
        var httpContext = CreateHttpContext();
        var tempData = CreateTempData();
        tempData["Key"] = new object();
 
        Assert.Throws<InvalidOperationException>(() => _cookieTempDataProvider.SaveTempData(httpContext, tempData.Save()));
    }
 
    [Fact]
    public void RoundTrip_SaveAndLoad_WorksCorrectly()
    {
        var httpContext = CreateHttpContext();
        var tempData = CreateTempData();
        tempData["StringKey"] = "StringValue";
        tempData["IntKey"] = 42;
 
        _cookieTempDataProvider.SaveTempData(httpContext, tempData.Save());
        SimulateCookieRoundTrip(httpContext);
        var loadedTempData = _cookieTempDataProvider.LoadTempData(httpContext);
 
        Assert.Equal("StringValue", loadedTempData["StringKey"]);
        Assert.Equal(42, Assert.IsType<int>(loadedTempData["IntKey"]));
    }
 
    private static DefaultHttpContext CreateHttpContext()
    {
        var services = new ServiceCollection()
            .AddSingleton<IDataProtectionProvider, PassThroughDataProtectionProvider>()
            .BuildServiceProvider();
 
        var httpContext = new DefaultHttpContext
        {
            RequestServices = services
        };
        httpContext.Request.Scheme = "https";
        httpContext.Request.Host = new HostString("localhost");
 
        var cookieFeature = new TestResponseCookiesFeature();
        httpContext.Features.Set(cookieFeature);
        httpContext.Features.Set<IResponseCookiesFeature>(cookieFeature);
 
        return httpContext;
    }
 
    private static void SimulateCookieRoundTrip(HttpContext httpContext)
    {
        var cookieFeature = httpContext.Features.Get<TestResponseCookiesFeature>();
        if (cookieFeature != null && cookieFeature.SetCookies.TryGetValue(".AspNetCore.Components.TempData", out var cookieValue))
        {
            httpContext.Request.Headers["Cookie"] = $".AspNetCore.Components.TempData={cookieValue}";
        }
    }
 
    private class PassThroughDataProtectionProvider : IDataProtectionProvider
    {
        public IDataProtector CreateProtector(string purpose) => new PassThroughDataProtector();
 
        private class PassThroughDataProtector : IDataProtector
        {
            public IDataProtector CreateProtector(string purpose) => this;
            public byte[] Protect(byte[] plaintext) => plaintext;
            public byte[] Unprotect(byte[] protectedData) => protectedData;
        }
    }
 
    private class TestResponseCookiesFeature : IResponseCookiesFeature
    {
        public Dictionary<string, string> SetCookies { get; } = new();
        public HashSet<string> DeletedCookies { get; } = new();
 
        public IResponseCookies Cookies => new TestResponseCookies(this);
 
        private class TestResponseCookies : IResponseCookies
        {
            private readonly TestResponseCookiesFeature _feature;
 
            public TestResponseCookies(TestResponseCookiesFeature feature)
            {
                _feature = feature;
            }
 
            public void Append(string key, string value) => Append(key, value, new CookieOptions());
 
            public void Append(string key, string value, CookieOptions options)
            {
                // ChunkingCookieManager deletes by appending with expired date (UnixEpoch)
                if (options.Expires.HasValue && options.Expires.Value <= DateTimeOffset.UnixEpoch)
                {
                    _feature.DeletedCookies.Add(key);
                }
                else
                {
                    _feature.SetCookies[key] = value;
                }
            }
 
            public void Delete(string key) => Delete(key, new CookieOptions());
 
            public void Delete(string key, CookieOptions options)
            {
                _feature.DeletedCookies.Add(key);
            }
        }
    }
}