File: SupplyParameterFromPersistentComponentStateValueProviderTests.cs
Web Access
Project: src\src\Components\Components\test\Microsoft.AspNetCore.Components.Tests.csproj (Microsoft.AspNetCore.Components.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Components.Infrastructure;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
 
namespace Microsoft.AspNetCore.Components;
public class SupplyParameterFromPersistentComponentStateValueProviderTests
{
    [Fact]
    public void CanRestoreState_ForComponentWithProperties()
    {
        // Arrange
        var state = new PersistentComponentState(
            new Dictionary<string, byte[]>(),
            []);
 
        var provider = new SupplyParameterFromPersistentComponentStateValueProvider(state);
        var renderer = new TestRenderer();
        var component = new TestComponent();
        // Update the method call to match the correct signature
        var componentStates = CreateComponentState(renderer, [(component, null)], null);
        var componentState = componentStates.First();
        var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
 
        InitializeState(state,
        new List<(ComponentState, string, string)>
        {
            (componentState, cascadingParameterInfo.PropertyName, "state")
        });
 
        // Act
        var result = provider.GetCurrentValue(componentState, cascadingParameterInfo);
 
        // Assert
        Assert.Equal("state", result);
    }
 
    [Fact]
    public void Subscribe_RegistersPersistenceCallback()
    {
        // Arrange
        var state = new PersistentComponentState(
            new Dictionary<string, byte[]>(),
            []);
        var provider = new SupplyParameterFromPersistentComponentStateValueProvider(state);
        var renderer = new TestRenderer();
        var component = new TestComponent();
        var componentStates = CreateComponentState(renderer, [(component, null)], null);
        var componentState = componentStates.First();
 
        var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
 
        // Act
        provider.Subscribe(componentState, cascadingParameterInfo);
 
        // Assert
        Assert.Single(provider.Subscriptions);
    }
 
    [Fact]
    public void Unsubscribe_RemovesCallbackFromRegisteredCallbacks()
    {
        // Arrange
        var state = new PersistentComponentState(
            new Dictionary<string, byte[]>(),
            []);
        var provider = new SupplyParameterFromPersistentComponentStateValueProvider(state);
        var renderer = new TestRenderer();
        var component = new TestComponent();
        var componentStates = CreateComponentState(renderer, [(component, null)], null);
        var componentState = componentStates.First();
 
        var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
 
        provider.Subscribe(componentState, cascadingParameterInfo);
 
        // Act
        provider.Unsubscribe(componentState, cascadingParameterInfo);
 
        // Assert
        Assert.Empty(provider.Subscriptions);
    }
 
    [Fact]
    public async Task PersistAsync_PersistsStateForSubscribedComponentProperties()
    {
        // Arrange
        var state = new Dictionary<string, byte[]>();
        var store = new TestStore(state);
        var persistenceManager = new ComponentStatePersistenceManager(
            NullLogger<ComponentStatePersistenceManager>.Instance,
            new ServiceCollection().BuildServiceProvider());
 
        var renderer = new TestRenderer();
        var component = new TestComponent { State = "testValue" };
        var componentStates = CreateComponentState(renderer, [(component, null)], null);
        var componentState = componentStates.First();
 
        // Create the provider and subscribe the component
        var provider = new SupplyParameterFromPersistentComponentStateValueProvider(persistenceManager.State);
        var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
        provider.Subscribe(componentState, cascadingParameterInfo);
 
        // Act
        await persistenceManager.PersistStateAsync(store, renderer);
 
        // Assert
        // The key will be a hash computed from the component type and property name
        // We can verify the state was persisted by checking if any entry exists in the store
        Assert.NotEmpty(store.State);
 
        // To verify the actual content, we need to create a new state and restore it
        var newState = new PersistentComponentState(new Dictionary<string, byte[]>(), []);
        newState.InitializeExistingState(store.State);
 
        // The key used for storing the property value is computed by the SupplyParameterFromPersistentComponentStateValueProvider
        var key = SupplyParameterFromPersistentComponentStateValueProvider.ComputeKey(componentState, cascadingParameterInfo.PropertyName);
        Assert.True(newState.TryTakeFromJson<string>(key, out var retrievedValue));
        Assert.Equal("testValue", retrievedValue);
    }
 
    [Fact]
    public async Task PersistAsync_UsesParentComponentType_WhenAvailable()
    {
        // Arrange
        var state = new Dictionary<string, byte[]>();
        var store = new TestStore(state);
        var persistenceManager = new ComponentStatePersistenceManager(
            NullLogger<ComponentStatePersistenceManager>.Instance,
            new ServiceCollection().BuildServiceProvider());
 
        var renderer = new TestRenderer();
        var parentComponent = new ParentComponent();
        var component = new TestComponent { State = "testValue" };
        var componentStates = CreateComponentState(renderer, [(component, null)], parentComponent);
        var componentState = componentStates.First();
 
        // Create the provider and subscribe the component
        var provider = new SupplyParameterFromPersistentComponentStateValueProvider(persistenceManager.State);
        var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
        provider.Subscribe(componentState, cascadingParameterInfo);
 
        // Act
        await persistenceManager.PersistStateAsync(store, renderer);
 
        // Assert
        // The key will be a hash computed from the parent component type, component type, and property name
        Assert.NotEmpty(store.State);
 
        // To verify the actual content, we need to create a new state and restore it
        var newState = new PersistentComponentState(new Dictionary<string, byte[]>(), []);
        newState.InitializeExistingState(store.State);
 
        // The key used for storing the property value is computed by the SupplyParameterFromPersistentComponentStateValueProvider
        var key = SupplyParameterFromPersistentComponentStateValueProvider.ComputeKey(componentState, cascadingParameterInfo.PropertyName);
        Assert.True(newState.TryTakeFromJson<string>(key, out var retrievedValue));
        Assert.Equal("testValue", retrievedValue);
    }
 
    [Fact]
    public async Task PersistAsync_CanPersistMultipleComponentsOfSameType_WhenParentProvidesDifferentKeys()
    {
        // Arrange
        var state = new Dictionary<string, byte[]>();
        var store = new TestStore(state);
        var persistenceManager = new ComponentStatePersistenceManager(
            NullLogger<ComponentStatePersistenceManager>.Instance,
            new ServiceCollection().BuildServiceProvider());
 
        var renderer = new TestRenderer();
        var parentComponent = new ParentComponent();
        var component1 = new TestComponent { State = "testValue1" };
        var component2 = new TestComponent { State = "testValue2" };
        var componentStates = CreateComponentState(renderer, [(component1, 1), (component2, 2)], parentComponent);
        var componentState1 = componentStates.First();
        var componentState2 = componentStates.Last();
 
        // Create the provider and subscribe the component
        var provider = new SupplyParameterFromPersistentComponentStateValueProvider(persistenceManager.State);
        var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
        provider.Subscribe(componentState1, cascadingParameterInfo);
        provider.Subscribe(componentState2, cascadingParameterInfo);
 
        // Act
        await persistenceManager.PersistStateAsync(store, renderer);
 
        // Assert
        // The key will be a hash computed from the parent component type, component type, and property name
        Assert.NotEmpty(store.State);
 
        // To verify the actual content, we need to create a new state and restore it
        var newState = new PersistentComponentState(new Dictionary<string, byte[]>(), []);
        newState.InitializeExistingState(store.State);
 
        // The key used for storing the property value is computed by the SupplyParameterFromPersistentComponentStateValueProvider
        var key1 = SupplyParameterFromPersistentComponentStateValueProvider.ComputeKey(componentState1, cascadingParameterInfo.PropertyName);
        Assert.True(newState.TryTakeFromJson<string>(key1, out var retrievedValue));
        Assert.Equal("testValue1", retrievedValue);
 
        var key2 = SupplyParameterFromPersistentComponentStateValueProvider.ComputeKey(componentState2, cascadingParameterInfo.PropertyName);
        Assert.True(newState.TryTakeFromJson<string>(key2, out retrievedValue));
        Assert.Equal("testValue2", retrievedValue);
    }
 
    public static TheoryData<object, object> ValidKeyTypesData => new TheoryData<object, object>
    {
        { true, false },
        { 'A', 'B' },
        { (sbyte)42, (sbyte)-42 },
        { (byte)240, (byte)15 },
        { (short)12345, (short)-12345 },
        { (ushort)54321, (ushort)12345 },
        { 42, -42 },
        { (uint)3000000000, (uint)1000000000 },
        { 9223372036854775807L, -9223372036854775808L },
        { (ulong)18446744073709551615UL, (ulong)1 },
        { 3.14159f, -3.14159f },
        { Math.PI, -Math.PI },
        { 123456.789m, -123456.789m },
        { new DateTime(2023, 1, 1), new DateTime(2023, 12, 31) },
        { new DateTimeOffset(2023, 1, 1, 0, 0, 0, TimeSpan.FromSeconds(0)), new DateTimeOffset(2023, 12, 31, 0, 0, 0, TimeSpan.FromSeconds(0)) },
        { "key1", "key2" },
        // Include a very long key to validate logic around growing buffers
        { new string('a', 10000), new string('b', 10000) },
        { Guid.NewGuid(), Guid.NewGuid() },
        { new DateOnly(2023, 1, 1), new DateOnly(2023, 12, 31) },
        { new TimeOnly(12, 34, 56), new TimeOnly(23, 45, 56) },
    };
 
    [Theory]
    [MemberData(nameof(ValidKeyTypesData))]
    public async Task PersistAsync_CanPersistMultipleComponentsOfSameType_SupportsDifferentKeyTypes(
        object componentKey1,
        object componentKey2)
    {
        // Arrange
        var state = new Dictionary<string, byte[]>();
        var store = new TestStore(state);
        var persistenceManager = new ComponentStatePersistenceManager(
            NullLogger<ComponentStatePersistenceManager>.Instance,
            new ServiceCollection().BuildServiceProvider());
 
        var renderer = new TestRenderer();
        var parentComponent = new ParentComponent();
        var component1 = new TestComponent { State = "testValue1" };
        var component2 = new TestComponent { State = "testValue2" };
        var componentStates = CreateComponentState(renderer, [(component1, componentKey1), (component2, componentKey2)], parentComponent);
        var componentState1 = componentStates.First();
        var componentState2 = componentStates.Last();
 
        // Create the provider and subscribe the component
        var provider = new SupplyParameterFromPersistentComponentStateValueProvider(persistenceManager.State);
        var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
        provider.Subscribe(componentState1, cascadingParameterInfo);
        provider.Subscribe(componentState2, cascadingParameterInfo);
 
        // Act
        await persistenceManager.PersistStateAsync(store, renderer);
 
        // Assert
        // The key will be a hash computed from the parent component type, component type, and property name
        Assert.NotEmpty(store.State);
 
        // To verify the actual content, we need to create a new state and restore it
        var newState = new PersistentComponentState(new Dictionary<string, byte[]>(), []);
        newState.InitializeExistingState(store.State);
 
        // The key used for storing the property value is computed by the SupplyParameterFromPersistentComponentStateValueProvider
        var key1 = SupplyParameterFromPersistentComponentStateValueProvider.ComputeKey(componentState1, cascadingParameterInfo.PropertyName);
        Assert.True(newState.TryTakeFromJson<string>(key1, out var retrievedValue));
        Assert.Equal("testValue1", retrievedValue);
 
        var key2 = SupplyParameterFromPersistentComponentStateValueProvider.ComputeKey(componentState2, cascadingParameterInfo.PropertyName);
        Assert.True(newState.TryTakeFromJson<string>(key2, out retrievedValue));
        Assert.Equal("testValue2", retrievedValue);
    }
 
    [Fact]
    public async Task PersistenceFails_IfMultipleComponentsOfSameType_TryToPersistDataWithoutParentComponents()
    {
        // Arrange
        var (logger, sink) = CreateTestLogger();
        var state = new Dictionary<string, byte[]>();
        var store = new TestStore(state);
        var persistenceManager = new ComponentStatePersistenceManager(
            logger,
            new ServiceCollection().BuildServiceProvider());
 
        var renderer = new TestRenderer();
        var component1 = new TestComponent { State = "testValue1" };
        var component2 = new TestComponent { State = "testValue2" };
        var componentStates = CreateComponentState(renderer, [(component1, null), (component2, null)], null);
        var componentState1 = componentStates.First();
        var componentState2 = componentStates.Last();
 
        // Create the provider and subscribe the components
        var provider = new SupplyParameterFromPersistentComponentStateValueProvider(persistenceManager.State);
        var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
        provider.Subscribe(componentState1, cascadingParameterInfo);
        provider.Subscribe(componentState2, cascadingParameterInfo);
 
        // Act
        await persistenceManager.PersistStateAsync(store, renderer);
 
        // Assert
        Assert.Empty(store.State);
        Assert.Contains(sink.Writes, w => w is { LogLevel: LogLevel.Error } && w.EventId == new EventId(1000, "PersistenceCallbackError"));
    }
 
    private static (TestLogger<ComponentStatePersistenceManager> logger, TestSink testLoggerSink) CreateTestLogger()
    {
        var testLoggerSink = new TestSink();
        var testLoggerFactory = new TestLoggerFactory(testLoggerSink, enabled: true);
        var logger = new TestLogger<ComponentStatePersistenceManager>(testLoggerFactory);
        return (logger, testLoggerSink);
    }
 
    [Fact]
    public async Task PersistentceFails_IfMultipleComponentsOfSameType_TryToPersistDataWithParentComponentOfSameType()
    {
        // Arrange
        var (logger, sink) = CreateTestLogger();
        var state = new Dictionary<string, byte[]>();
        var store = new TestStore(state);
        var persistenceManager = new ComponentStatePersistenceManager(
            logger,
            new ServiceCollection().BuildServiceProvider());
 
        var renderer = new TestRenderer();
        var parentComponent = new ParentComponent();
        var component1 = new TestComponent { State = "testValue1" };
        var component2 = new TestComponent { State = "testValue2" };
        var componentStates = CreateComponentState(renderer, [(component1, null), (component2, null)], parentComponent);
        var componentState1 = componentStates.First();
        var componentState2 = componentStates.Last();
 
        // Create the provider and subscribe the components
        var provider = new SupplyParameterFromPersistentComponentStateValueProvider(persistenceManager.State);
        var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
        provider.Subscribe(componentState1, cascadingParameterInfo);
        provider.Subscribe(componentState2, cascadingParameterInfo);
 
        // Act
        await persistenceManager.PersistStateAsync(store, renderer);
 
        // Assert
        Assert.Empty(store.State);
        Assert.Contains(sink.Writes, w => w is { LogLevel: LogLevel.Error } && w.EventId == new EventId(1000, "PersistenceCallbackError"));
    }
 
    [Fact]
    public async Task PersistenceFails_MultipleComponentsUseTheSameKey()
    {
        // Arrange
        var (logger, sink) = CreateTestLogger();
        var state = new Dictionary<string, byte[]>();
        var store = new TestStore(state);
        var persistenceManager = new ComponentStatePersistenceManager(
            logger,
            new ServiceCollection().BuildServiceProvider());
 
        var renderer = new TestRenderer();
        var parentComponent = new ParentComponent();
        var component1 = new TestComponent { State = "testValue1" };
        var component2 = new TestComponent { State = "testValue2" };
        var componentStates = CreateComponentState(renderer, [(component1, 1), (component2, 1)], parentComponent);
        var componentState1 = componentStates.First();
        var componentState2 = componentStates.Last();
 
        // Create the provider and subscribe the components
        var provider = new SupplyParameterFromPersistentComponentStateValueProvider(persistenceManager.State);
        var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
        provider.Subscribe(componentState1, cascadingParameterInfo);
        provider.Subscribe(componentState2, cascadingParameterInfo);
 
        // Act
        await persistenceManager.PersistStateAsync(store, renderer);
 
        // Assert
        Assert.Empty(store.State);
        Assert.Contains(sink.Writes, w => w is { LogLevel: LogLevel.Error } && w.EventId == new EventId(1000, "PersistenceCallbackError"));
    }
 
    public static TheoryData<object, object> InvalidKeyTypesData => new TheoryData<object, object>
    {
        { new object(), new object() },
        { new TestComponent(), new TestComponent() }
    };
 
    [Theory]
    [MemberData(nameof(InvalidKeyTypesData))]
    public async Task PersistenceFails_MultipleComponentsUseInvalidKeyTypes(object componentKeyType1, object componentKeyType2)
    {
        // Arrange
        var (logger, sink) = CreateTestLogger();
        var state = new Dictionary<string, byte[]>();
        var store = new TestStore(state);
        var persistenceManager = new ComponentStatePersistenceManager(
            logger,
            new ServiceCollection().BuildServiceProvider());
 
        var renderer = new TestRenderer();
        var parentComponent = new ParentComponent();
        var component1 = new TestComponent { State = "testValue1" };
        var component2 = new TestComponent { State = "testValue2" };
        var componentStates = CreateComponentState(renderer, [(component1, componentKeyType1), (component2, componentKeyType2)], parentComponent);
        var componentState1 = componentStates.First();
        var componentState2 = componentStates.Last();
 
        // Create the provider and subscribe the components
        var provider = new SupplyParameterFromPersistentComponentStateValueProvider(persistenceManager.State);
        var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
        provider.Subscribe(componentState1, cascadingParameterInfo);
        provider.Subscribe(componentState2, cascadingParameterInfo);
 
        // Act
        await persistenceManager.PersistStateAsync(store, renderer);
 
        // Assert
        Assert.Empty(store.State);
        Assert.Contains(sink.Writes, w => w is { LogLevel: LogLevel.Error } && w.EventId == new EventId(1000, "PersistenceCallbackError"));
    }
 
    private static void InitializeState(PersistentComponentState state, List<(ComponentState componentState, string propertyName, string value)> items)
    {
        var dictionary = new Dictionary<string, byte[]>();
        foreach (var item in items)
        {
            var key = SupplyParameterFromPersistentComponentStateValueProvider.ComputeKey(item.componentState, item.propertyName);
            dictionary[key] = JsonSerializer.SerializeToUtf8Bytes(item.value, JsonSerializerOptions.Web);
        }
        state.InitializeExistingState(dictionary);
    }
 
    private static CascadingParameterInfo CreateCascadingParameterInfo(string propertyName, Type propertyType)
    {
        return new CascadingParameterInfo(
            new SupplyParameterFromPersistentComponentStateAttribute(),
            propertyName,
            propertyType);
    }
 
    private static List<ComponentState> CreateComponentState(
        TestRenderer renderer,
        List<(TestComponent, object)> components,
        ParentComponent parentComponent = null)
    {
        var i = 1;
        var parentComponentState = parentComponent != null ? new ComponentState(renderer, i++, parentComponent, null) : null;
        var currentRenderTree = parentComponentState?.CurrentRenderTree;
        var result = new List<ComponentState>();
        foreach (var (component, key) in components)
        {
            var componentState = new ComponentState(renderer, i++, component, parentComponentState);
            if (currentRenderTree != null && key != null)
            {
                currentRenderTree.OpenComponent<TestComponent>(0);
                var frames = currentRenderTree.GetFrames();
                frames.Array[frames.Count - 1].ComponentStateField = componentState;
                if (key != null)
                {
                    currentRenderTree.SetKey(key);
                }
                currentRenderTree.CloseComponent();
            }
 
            result.Add(componentState);
        }
 
        return result;
    }
 
    private class TestRenderer() : Renderer(new ServiceCollection().BuildServiceProvider(), NullLoggerFactory.Instance)
    {
        public override Dispatcher Dispatcher => Dispatcher.CreateDefault();
 
        protected override void HandleException(Exception exception) => throw new NotImplementedException();
        protected override Task UpdateDisplayAsync(in RenderBatch renderBatch) => throw new NotImplementedException();
    }
 
    private class TestComponent : IComponent
    {
        [SupplyParameterFromPersistentComponentState]
        public string State { get; set; }
 
        public void Attach(RenderHandle renderHandle) => throw new NotImplementedException();
        public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException();
    }
 
    private class TestStore(Dictionary<string, byte[]> initialState) : IPersistentComponentStateStore
    {
        public IDictionary<string, byte[]> State { get; set; } = initialState;
 
        public Task<IDictionary<string, byte[]>> GetPersistedStateAsync()
        {
            return Task.FromResult(State);
        }
 
        public Task PersistStateAsync(IReadOnlyDictionary<string, byte[]> state)
        {
            // We copy the data here because it's no longer available after this call completes.
            State = state.ToDictionary(k => k.Key, v => v.Value);
            return Task.CompletedTask;
        }
    }
 
    private class ParentComponent : IComponent
    {
        public void Attach(RenderHandle renderHandle) => throw new NotImplementedException();
        public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException();
    }
}