File: Circuits\DefaultInMemoryCircuitPersistenceProviderTest.cs
Web Access
Project: src\src\Components\Server\test\Microsoft.AspNetCore.Components.Server.Tests.csproj (Microsoft.AspNetCore.Components.Server.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.Components.Server.Circuits;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
 
namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits;
 
public class DefaultInMemoryCircuitPersistenceProviderTest
{
    [Fact]
    public async Task PersistCircuitAsync_StoresCircuitState()
    {
        // Arrange
        var clock = new TestSystemClock();
        var circuitId = TestCircuitIdFactory.CreateTestFactory().CreateCircuitId();
        var persistedState = new PersistedCircuitState();
        var provider = CreateProvider(clock);
 
        // Act
        await provider.PersistCircuitAsync(circuitId, persistedState);
 
        // Assert
        var result = await provider.RestoreCircuitAsync(circuitId);
        Assert.Same(persistedState, result);
    }
 
    [Fact]
    public async Task RestoreCircuitAsync_ReturnsNull_WhenCircuitDoesNotExist()
    {
        // Arrange
        var clock = new TestSystemClock();
        var circuitId = TestCircuitIdFactory.CreateTestFactory().CreateCircuitId();
        var provider = CreateProvider(clock);
 
        // Act
        var result = await provider.RestoreCircuitAsync(circuitId);
 
        // Assert
        Assert.Null(result);
    }
 
    [Fact]
    public async Task RestoreCircuitAsync_RemovesCircuitFromCache()
    {
        // Arrange
        var clock = new TestSystemClock();
        var circuitId = TestCircuitIdFactory.CreateTestFactory().CreateCircuitId();
        var persistedState = new PersistedCircuitState();
        var provider = CreateProvider(clock);
 
        await provider.PersistCircuitAsync(circuitId, persistedState);
 
        // Act
        var firstResult = await provider.RestoreCircuitAsync(circuitId);
        var secondResult = await provider.RestoreCircuitAsync(circuitId);
 
        // Assert
        Assert.Same(persistedState, firstResult);
        Assert.Null(secondResult); // Second attempt should return null as the entry should be removed
    }
 
    [Fact]
    public async Task CircuitStateIsEvictedAfterConfiguredTimeout()
    {
        // Arrange
        var clock = new TestSystemClock();
        var circuitOptions = new CircuitOptions
        {
            PersistedCircuitInMemoryRetentionPeriod = TimeSpan.FromSeconds(2)
        };
        var circuitId = TestCircuitIdFactory.CreateTestFactory().CreateCircuitId();
        var persistedState = new PersistedCircuitState();
        var provider = CreateProvider(clock, circuitOptions);
        var postEvictionCallback = provider.PostEvictionCallback.EvictionCallback;
        var callbackRan = new TaskCompletionSource();
        provider.PostEvictionCallback = new PostEvictionCallbackRegistration
        {
            EvictionCallback = (key, value, reason, state) =>
            {
                callbackRan.SetResult();
                postEvictionCallback(key, value, reason, state);
            }
        };
 
        await provider.PersistCircuitAsync(circuitId, persistedState);
 
        // Act - advance the clock past the retention period
        clock.UtcNow = clock.UtcNow.AddSeconds(2);
 
        // Allow time for the timer to fire and the eviction to occur
        await callbackRan.Task;
 
        // Assert
        var result = await provider.RestoreCircuitAsync(circuitId);
        Assert.Null(result);
    }
 
    [Fact]
    public async Task CircuitStatesAreLimitedByConfiguredCapacity()
    {
        // Arrange
        var clock = new TestSystemClock();
        var circuitOptions = new CircuitOptions
        {
            PersistedCircuitInMemoryMaxRetained = 2 // Only allow 2 circuits to be stored
        };
        var provider = CreateProvider(clock, circuitOptions);
        var factory = TestCircuitIdFactory.CreateTestFactory();
 
        var evictedKeys = new List<string>();
        var evictionTcs = new TaskCompletionSource();
        var postEvictionCallback = provider.PostEvictionCallback.EvictionCallback;
        provider.PostEvictionCallback = new PostEvictionCallbackRegistration
        {
            EvictionCallback = (key, value, reason, state) =>
            {
                evictedKeys.Add((string)key);
                evictionTcs.TrySetResult();
                postEvictionCallback(key, value, reason, state);
            }
        };
 
        var circuitId1 = factory.CreateCircuitId();
        var circuitId2 = factory.CreateCircuitId();
        var circuitId3 = factory.CreateCircuitId();
        var circuitIds = new Dictionary<string, CircuitId>
        {
            [circuitId1.Secret] = circuitId1,
            [circuitId2.Secret] = circuitId2,
            [circuitId3.Secret] = circuitId3
        };
 
        var state1 = new PersistedCircuitState();
        var state2 = new PersistedCircuitState();
        var state3 = new PersistedCircuitState();
 
        // Act - persist 3 circuits when capacity is 2
        await provider.PersistCircuitAsync(circuitId1, state1);
        await provider.PersistCircuitAsync(circuitId2, state2);
        await provider.PersistCircuitAsync(circuitId3, state3);
 
        // Wait for eviction to occur
        await evictionTcs.Task;
 
        // Assert
        var evicted = Assert.Single(evictedKeys);
        var evictedId = circuitIds[evicted];
 
        circuitIds.Remove(evicted);
 
        var evictedResults = await provider.RestoreCircuitAsync(evictedId);
        Assert.Null(evictedResults);
 
        var nonEvictedResults = await Task.WhenAll(circuitIds.Select(ne => provider.RestoreCircuitAsync(ne.Value)));
 
        Assert.Collection(nonEvictedResults,
            Assert.NotNull,
            Assert.NotNull);
    }
 
    private static DefaultInMemoryCircuitPersistenceProvider CreateProvider(
        ISystemClock clock,
        CircuitOptions options = null)
    {
        return new DefaultInMemoryCircuitPersistenceProvider(
            clock,
            NullLogger<ICircuitPersistenceProvider>.Instance,
            Options.Create(options ?? new CircuitOptions()));
    }
 
    private class TestSystemClock : ISystemClock
    {
        public TestSystemClock()
        {
            UtcNow = new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero);
        }
 
        public DateTimeOffset UtcNow { get; set; }
    }
}