| File: Shared\InMemoryBrowserLocalStorage.cs | Web Access |
| Project: src\tests\Aspire.Dashboard.Tests\Aspire.Dashboard.Tests.csproj (Aspire.Dashboard.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Dashboard.Model.BrowserStorage; using Aspire.Dashboard.Serialization; using Aspire.Tests.Shared; using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.JSInterop; namespace Aspire.Dashboard.Tests.Shared; /// <summary> /// The dashboard's <see cref="LocalBrowserStorage"/> over an in-memory browser <c>localStorage</c>, so tests cover the /// JSON the dashboard stores, including its source-generated serialization, like a browser does. /// </summary> internal sealed class InMemoryBrowserLocalStorage { public InMemoryBrowserLocalStorage() { var jsRuntime = new TestJSRuntime { OnInvoke = HandleLocalStorageCall }; var circuitOptions = new CircuitOptions(); #pragma warning disable ASPNETCORE9004 // Native AOT resolver composition is experimental in .NET 11. // Matches the resolvers the dashboard registers, so a stored type missing from the context fails here too. circuitOptions.JsonTypeInfoResolvers.Add(DashboardJsonSerializerContext.Default); #pragma warning restore ASPNETCORE9004 Storage = new DelayedReadLocalStorage(new LocalBrowserStorage( jsRuntime, new ProtectedLocalStorage(jsRuntime, new EphemeralDataProtectionProvider()), NullLogger<LocalBrowserStorage>.Instance, Options.Create(circuitOptions)), this); } /// <summary> /// Gets the items in local storage, as JSON keyed by storage key. /// </summary> public Dictionary<string, string> Items { get; } = new(StringComparer.Ordinal); /// <summary> /// Gets or sets a value indicating whether reading local storage fails, as when the circuit is disconnected. /// </summary> public bool FailReads { get; set; } public ILocalStorage Storage { get; } /// <summary> /// Gets or sets a function awaited after a read of local storage and before its result is returned, to simulate /// a response that arrives after later calls have completed. /// </summary> public Func<Task>? OnAfterReadAsync { get; set; } /// <summary> /// Waits until a key has a value, for saves that are retried after a delay. /// </summary> public async Task WaitForItemAsync(string key, string expectedJson) { using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(); while (true) { lock (Items) { if (Items.TryGetValue(key, out var json) && json == expectedJson) { return; } } await Task.Delay(TimeSpan.FromMilliseconds(50), cts.Token); } } private object? HandleLocalStorageCall((string Identifier, object?[]? Args) call) { var key = (string)call.Args![0]!; lock (Items) { switch (call.Identifier) { case "localStorage.getItem": if (FailReads) { throw new JSException("Local storage is unavailable."); } return Items.GetValueOrDefault(key); case "localStorage.setItem": Items[key] = (string)call.Args[1]!; return null; default: throw new NotSupportedException($"Unexpected JavaScript call '{call.Identifier}'."); } } } private sealed class DelayedReadLocalStorage(ILocalStorage inner, InMemoryBrowserLocalStorage owner) : ILocalStorage { public async Task<StorageResult<TValue>> GetUnprotectedAsync<TValue>(string key) { var result = await inner.GetUnprotectedAsync<TValue>(key); if (owner.OnAfterReadAsync is { } afterRead) { await afterRead(); } return result; } public Task SetUnprotectedAsync<TValue>(string key, TValue value) => inner.SetUnprotectedAsync(key, value); public Task<StorageResult<string>> GetRawUnprotectedAsync(string key) => inner.GetRawUnprotectedAsync(key); public Task SetRawUnprotectedAsync(string key, string value) => inner.SetRawUnprotectedAsync(key, value); public Task<StorageResult<TValue>> GetAsync<TValue>(string key) => inner.GetAsync<TValue>(key); public Task SetAsync<TValue>(string key, TValue value) => inner.SetAsync(key, value); } }