File: PersistComponentStateTagHelperTest.cs
Web Access
Project: src\src\Mvc\Mvc.TagHelpers\test\Microsoft.AspNetCore.Mvc.TagHelpers.Test.csproj (Microsoft.AspNetCore.Mvc.TagHelpers.Test)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Endpoints;
using Microsoft.AspNetCore.Components.Infrastructure;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using RenderMode = Microsoft.AspNetCore.Components.Web.RenderMode;
 
namespace Microsoft.AspNetCore.Mvc.TagHelpers;
 
public class PersistComponentStateTagHelperTest
{
    private static readonly IDataProtectionProvider _ephemeralProvider =
        new EphemeralDataProtectionProvider();
    private static readonly IDataProtector _protector =
        _ephemeralProvider.CreateProtector("Microsoft.AspNetCore.Components.Server.State");
 
    [Fact]
    public async Task ExecuteAsync_DoesNotPersistDataWhenNoPrerenderHappened()
    {
        // Arrange
        var tagHelper = new PersistComponentStateTagHelper
        {
            ViewContext = GetViewContext()
        };
 
        var context = GetTagHelperContext();
        var output = GetTagHelperOutput();
 
        // Act
        await tagHelper.ProcessAsync(context, output);
 
        // Assert
        var content = HtmlContentUtilities.HtmlContentToString(output.Content);
        Assert.Empty(content);
        Assert.Null(output.TagName);
    }
 
    [Fact]
    public async Task ExecuteAsync_DoesNotRenderWebAssemblyStateWhenStateWasNotPersisted()
    {
        // Arrange
        var tagHelper = new PersistComponentStateTagHelper
        {
            ViewContext = GetViewContext(),
            PersistenceMode = PersistenceMode.WebAssembly
        };
 
        var context = GetTagHelperContext();
        var output = GetTagHelperOutput();
 
        // Act
        await tagHelper.ProcessAsync(context, output);
 
        // Assert
        var content = HtmlContentUtilities.HtmlContentToString(output.Content);
        Assert.Empty(content);
        Assert.Null(output.TagName);
    }
 
    [Fact]
    public async Task ExecuteAsync_RendersWebAssemblyStateExplicitly()
    {
        // Arrange
        var tagHelper = new PersistComponentStateTagHelper
        {
            ViewContext = GetViewContext(),
            PersistenceMode = PersistenceMode.WebAssembly
        };
 
        var context = GetTagHelperContext();
        var output = GetTagHelperOutput();
        var manager = tagHelper.ViewContext.HttpContext.RequestServices.GetRequiredService<ComponentStatePersistenceManager>();
 
        // Act
        manager.State.RegisterOnPersisting(() =>
        {
            manager.State.PersistAsJson("state", "state value");
            return Task.CompletedTask;
        }, RenderMode.InteractiveWebAssembly);
        await tagHelper.ProcessAsync(context, output);
 
        // Assert
        var content = HtmlContentUtilities.HtmlContentToString(output.Content);
        Assert.Null(output.TagName);
        var message = content["<!--Blazor-WebAssembly-Component-State:".Length..^"-->".Length];
        Assert.True(message.Length > 0);
    }
 
    [Fact]
    public async Task ExecuteAsync_RendersWebAssemblyStateImplicitlyWhenAWebAssemblyComponentWasPrerendered()
    {
        // Arrange
        var tagHelper = new PersistComponentStateTagHelper
        {
            ViewContext = GetViewContext()
        };
 
        EndpointHtmlRenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext.HttpContext, RenderMode.InteractiveWebAssembly);
 
        var context = GetTagHelperContext();
        var output = GetTagHelperOutput();
        var manager = tagHelper.ViewContext.HttpContext.RequestServices.GetRequiredService<ComponentStatePersistenceManager>();
 
        // Act
        manager.State.RegisterOnPersisting(() =>
        {
            manager.State.PersistAsJson("state", "state value");
            return Task.CompletedTask;
        }, RenderMode.InteractiveWebAssembly);
        await tagHelper.ProcessAsync(context, output);
 
        // Assert
        var content = HtmlContentUtilities.HtmlContentToString(output.Content);
        Assert.Null(output.TagName);
        var message = content["<!--Blazor-WebAssembly-Component-State:".Length..^"-->".Length];
        Assert.True(message.Length > 0);
    }
 
    [Fact]
    public async Task ExecuteAsync_RendersServerStateExplicitly()
    {
        // Arrange
        var tagHelper = new PersistComponentStateTagHelper
        {
            ViewContext = GetViewContext(),
            PersistenceMode = PersistenceMode.Server
        };
 
        var context = GetTagHelperContext();
        var output = GetTagHelperOutput();
        var manager = tagHelper.ViewContext.HttpContext.RequestServices.GetRequiredService<ComponentStatePersistenceManager>();
 
        // Act
        manager.State.RegisterOnPersisting(() =>
        {
            manager.State.PersistAsJson("state", "state value");
            return Task.CompletedTask;
        }, RenderMode.InteractiveServer);
 
        await tagHelper.ProcessAsync(context, output);
 
        // Assert
        var content = HtmlContentUtilities.HtmlContentToString(output.Content);
        Assert.NotEmpty(content);
        var payload = content["<!--Blazor-Server-Component-State:".Length..^"-->".Length];
        var message = _protector.Unprotect(payload);
        Assert.True(message.Length > 0);
    }
 
    [Fact]
    public async Task ExecuteAsync_DoesNotRenderServerStateWhenStateWasNotPersisted()
    {
        // Arrange
        var tagHelper = new PersistComponentStateTagHelper
        {
            ViewContext = GetViewContext(),
            PersistenceMode = PersistenceMode.Server
        };
 
        var context = GetTagHelperContext();
        var output = GetTagHelperOutput();
 
        // Act
        await tagHelper.ProcessAsync(context, output);
 
        // Assert
        var content = HtmlContentUtilities.HtmlContentToString(output.Content);
        Assert.Empty(content);
    }
 
    [Fact]
    public async Task ExecuteAsync_RendersServerStateImplicitlyWhenAServerComponentWasPrerendered()
    {
        // Arrange
        var tagHelper = new PersistComponentStateTagHelper
        {
            ViewContext = GetViewContext()
        };
 
        EndpointHtmlRenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext.HttpContext, Components.Web.RenderMode.InteractiveServer);
 
        var context = GetTagHelperContext();
        var output = GetTagHelperOutput();
        var manager = tagHelper.ViewContext.HttpContext.RequestServices.GetRequiredService<ComponentStatePersistenceManager>();
 
        // Act
        manager.State.RegisterOnPersisting(() =>
        {
            manager.State.PersistAsJson("state", "state value");
            return Task.CompletedTask;
        }, RenderMode.InteractiveServer);
 
        await tagHelper.ProcessAsync(context, output);
 
        // Assert
        var content = HtmlContentUtilities.HtmlContentToString(output.Content);
        Assert.NotEmpty(content);
        var payload = content["<!--Blazor-Server-Component-State:".Length..^"-->".Length];
        var message = _protector.Unprotect(payload);
        Assert.True(message.Length > 0);
    }
 
    [Fact]
    public async Task ExecuteAsync_ThrowsIfItCantInferThePersistMode()
    {
        // Arrange
        var tagHelper = new PersistComponentStateTagHelper
        {
            ViewContext = GetViewContext()
        };
 
        EndpointHtmlRenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext.HttpContext, Components.Web.RenderMode.InteractiveServer);
        EndpointHtmlRenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext.HttpContext, Components.Web.RenderMode.InteractiveWebAssembly);
 
        var context = GetTagHelperContext();
        var output = GetTagHelperOutput();
 
        // Act & Assert
        await Assert.ThrowsAsync<InvalidOperationException>(() => tagHelper.ProcessAsync(context, output));
    }
 
    private static TagHelperContext GetTagHelperContext()
    {
        return new TagHelperContext(
            "persist-component-state",
            new TagHelperAttributeList(),
            new Dictionary<object, object>(),
            Guid.NewGuid().ToString("N"));
    }
 
    private static TagHelperOutput GetTagHelperOutput()
    {
        return new TagHelperOutput(
            "persist-component-state",
            new TagHelperAttributeList(),
            (_, __) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
    }
 
    private ViewContext GetViewContext()
    {
        var httpContext = new DefaultHttpContext
        {
            RequestServices = new ServiceCollection()
                .AddScoped(_ => Mock.Of<IDataProtectionProvider>(
                    x => x.CreateProtector(It.IsAny<string>()) == _protector))
                .AddLogging()
                .AddScoped<ComponentStatePersistenceManager>()
                .AddScoped<IComponentPrerenderer, EndpointHtmlRenderer>()
                .BuildServiceProvider(),
        };
 
        return new ViewContext { HttpContext = httpContext };
    }
}