File: Controls\ResourceDetailsTests.cs
Web Access
Project: src\tests\Aspire.Dashboard.Components.Tests\Aspire.Dashboard.Components.Tests.csproj (Aspire.Dashboard.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.Collections.Concurrent;
using System.Collections.Immutable;
using System.Globalization;
using Aspire.Dashboard.Components.Controls;
using Aspire.Dashboard.Components.Tests.Shared;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Resources;
using Aspire.Dashboard.Tests.Shared;
using Aspire.Tests.Shared.DashboardModel;
using Bunit;
using Google.Protobuf.WellKnownTypes;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.FluentUI.AspNetCore.Components;
using Xunit;
 
namespace Aspire.Dashboard.Components.Tests.Controls;
 
[UseCulture("en-US")]
public class ResourceDetailsTests : DashboardTestContext
{
    [Fact]
    public async Task ClickMaskAllSwitch_UpdatedResource_MaskChanged()
    {
        // Arrange
        ResourceSetupHelpers.SetupResourceDetails(this);
        var menuProvider = RenderComponent<FluentMenuProvider>();
 
        var resource1 = ModelTestHelpers.CreateResource(
            "app1",
            environment: new List<EnvironmentVariableViewModel>
            {
                new EnvironmentVariableViewModel("envvar1", "value!", fromSpec: true),
                new EnvironmentVariableViewModel("envvar2", "value!", fromSpec: true)
            }.ToImmutableArray());
 
        // Act
        var cut = RenderComponent<ResourceDetails>(builder =>
        {
            builder.Add(p => p.ShowSpecOnlyToggle, true);
            builder.Add(p => p.Resource, resource1);
            builder.Add(p => p.ResourceByName, new ConcurrentDictionary<string, ResourceViewModel>([new KeyValuePair<string, ResourceViewModel> (resource1.Name, resource1)]));
        });
 
        // Assert
        Assert.Collection(cut.Instance.FilteredEnvironmentVariables,
            e =>
            {
                Assert.Equal("envvar1", e.Name);
                Assert.Equal("value!", e.Value);
                Assert.True(e.IsValueMasked);
            },
            e =>
            {
                Assert.Equal("envvar2", e.Name);
                Assert.Equal("value!", e.Value);
                Assert.True(e.IsValueMasked);
            });
 
        var actionsButton = cut.Find(".resource-details-actions");
        await actionsButton.ClickAsync(new MouseEventArgs());
 
        var maskAllSwitch = menuProvider.WaitForElement(".mask-all-switch");
 
        // HACK. Calling OnClick on the element isn't triggering the event correctly. Instead, call OnClick on the menu item model.
        var item = cut.FindComponents<AspireMenu>().SelectMany(m => m.Instance.Items).Single(s => s.Class == maskAllSwitch.Attributes["class"]!.Value);
        Assert.NotNull(item.OnClick);
        await cut.InvokeAsync(item.OnClick);
        cut.Render();
 
        Assert.Collection(cut.Instance.FilteredEnvironmentVariables,
            e =>
            {
                Assert.Equal("envvar1", e.Name);
                Assert.False(e.IsValueMasked);
            },
            e =>
            {
                Assert.Equal("envvar2", e.Name);
                Assert.False(e.IsValueMasked);
            });
 
        var resource2 = ModelTestHelpers.CreateResource(
            "app1",
            environment: new List<EnvironmentVariableViewModel>
            {
                new EnvironmentVariableViewModel("envvar1", "value!", fromSpec: true),
                new EnvironmentVariableViewModel("envvar2", "value!", fromSpec: true),
                new EnvironmentVariableViewModel("envvar3", "value!", fromSpec: true)
            }.ToImmutableArray());
 
        cut.SetParametersAndRender(builder =>
        {
            builder.Add(p => p.Resource, resource2);
        });
 
        Assert.Collection(cut.Instance.FilteredEnvironmentVariables,
            e =>
            {
                Assert.Equal("envvar1", e.Name);
                Assert.False(e.IsValueMasked);
            },
            e =>
            {
                Assert.Equal("envvar2", e.Name);
                Assert.False(e.IsValueMasked);
            },
            e =>
            {
                Assert.Equal("envvar3", e.Name);
                Assert.False(e.IsValueMasked);
            });
    }
 
    [Fact]
    public async Task ClickMaskAllSwitch_NewResource_MaskChanged()
    {
        // Arrange
        ResourceSetupHelpers.SetupResourceDetails(this);
        var menuProvider = RenderComponent<FluentMenuProvider>();
 
        var resource1 = ModelTestHelpers.CreateResource(
            "app1",
            environment: new List<EnvironmentVariableViewModel>
            {
                new EnvironmentVariableViewModel("envvar1", "value!", fromSpec: true),
                new EnvironmentVariableViewModel("envvar2", "value!", fromSpec: true)
            }.ToImmutableArray());
 
        // Act
        var cut = RenderComponent<ResourceDetails>(builder =>
        {
            builder.Add(p => p.ShowSpecOnlyToggle, true);
            builder.Add(p => p.Resource, resource1);
            builder.Add(p => p.ResourceByName, new ConcurrentDictionary<string, ResourceViewModel>([new KeyValuePair<string, ResourceViewModel> (resource1.Name, resource1)]));
        });
 
        // Assert
        Assert.Collection(cut.Instance.FilteredEnvironmentVariables,
            e =>
            {
                Assert.Equal("envvar1", e.Name);
                Assert.Equal("value!", e.Value);
                Assert.True(e.IsValueMasked);
            },
            e =>
            {
                Assert.Equal("envvar2", e.Name);
                Assert.Equal("value!", e.Value);
                Assert.True(e.IsValueMasked);
            });
 
        var actionsButton = cut.Find(".resource-details-actions");
        await actionsButton.ClickAsync(new MouseEventArgs());
 
        var maskAllSwitch = menuProvider.WaitForElement(".mask-all-switch");
 
        // HACK. Calling OnClick on the element isn't triggering the event correctly. Instead, call OnClick on the menu item model.
        var item = cut.FindComponents<AspireMenu>().SelectMany(m => m.Instance.Items).Single(s => s.Class == maskAllSwitch.Attributes["class"]!.Value);
        Assert.NotNull(item.OnClick);
        await cut.InvokeAsync(item.OnClick);
        cut.Render();
 
        Assert.Collection(cut.Instance.FilteredEnvironmentVariables,
            e =>
            {
                Assert.Equal("envvar1", e.Name);
                Assert.False(e.IsValueMasked);
            },
            e =>
            {
                Assert.Equal("envvar2", e.Name);
                Assert.False(e.IsValueMasked);
            });
 
        var resource2 = ModelTestHelpers.CreateResource(
            "app2",
            environment: new List<EnvironmentVariableViewModel>
            {
                new EnvironmentVariableViewModel("envvar1", "value!", fromSpec: true),
                new EnvironmentVariableViewModel("envvar2", "value!", fromSpec: true),
                new EnvironmentVariableViewModel("envvar3", "value!", fromSpec: true)
            }.ToImmutableArray());
 
        cut.SetParametersAndRender(builder =>
        {
            builder.Add(p => p.Resource, resource2);
        });
 
        Assert.Collection(cut.Instance.FilteredEnvironmentVariables,
            e =>
            {
                Assert.Equal("envvar1", e.Name);
                Assert.True(e.IsValueMasked);
            },
            e =>
            {
                Assert.Equal("envvar2", e.Name);
                Assert.True(e.IsValueMasked);
            },
            e =>
            {
                Assert.Equal("envvar3", e.Name);
                Assert.True(e.IsValueMasked);
            });
    }
 
    [Fact]
    public async Task ClickMaskEnvVarSwitch_UpdatedResource_MaskChanged()
    {
        // Arrange
        ResourceSetupHelpers.SetupResourceDetails(this);
 
        var resource1 = ModelTestHelpers.CreateResource(
            "app1",
            environment: new List<EnvironmentVariableViewModel>
            {
                new EnvironmentVariableViewModel("envvar1", "value!", fromSpec: true),
                new EnvironmentVariableViewModel("envvar2", "value!", fromSpec: true)
            }.ToImmutableArray());
 
        // Act
        var cut = RenderComponent<ResourceDetails>(builder =>
        {
            builder.Add(p => p.ShowSpecOnlyToggle, true);
            builder.Add(p => p.Resource, resource1);
            builder.Add(p => p.ResourceByName, new ConcurrentDictionary<string, ResourceViewModel>([new KeyValuePair<string, ResourceViewModel> (resource1.Name, resource1)]));
        });
 
        // Assert
        Assert.Collection(cut.Instance.FilteredEnvironmentVariables,
            e =>
            {
                Assert.Equal("envvar1", e.Name);
                Assert.Equal("value!", e.Value);
                Assert.True(e.IsValueMasked);
            },
            e =>
            {
                Assert.Equal("envvar2", e.Name);
                Assert.Equal("value!", e.Value);
                Assert.True(e.IsValueMasked);
            });
 
        var maskValueButton = cut.Find(".env-var-properties .grid-value-mask-button");
        await maskValueButton.ClickAsync(new MouseEventArgs());
 
        Assert.Collection(cut.Instance.FilteredEnvironmentVariables,
            e =>
            {
                Assert.Equal("envvar1", e.Name);
                Assert.False(e.IsValueMasked);
            },
            e =>
            {
                Assert.Equal("envvar2", e.Name);
                Assert.True(e.IsValueMasked);
            });
 
        var resource2 = ModelTestHelpers.CreateResource(
            "app1",
            environment: new List<EnvironmentVariableViewModel>
            {
                new EnvironmentVariableViewModel("envvar1", "value!", fromSpec: true),
                new EnvironmentVariableViewModel("envvar2", "value!", fromSpec: true),
                new EnvironmentVariableViewModel("envvar3", "value!", fromSpec: true)
            }.ToImmutableArray());
 
        cut.SetParametersAndRender(builder =>
        {
            builder.Add(p => p.Resource, resource2);
        });
 
        Assert.Collection(cut.Instance.FilteredEnvironmentVariables,
            e =>
            {
                Assert.Equal("envvar1", e.Name);
                Assert.False(e.IsValueMasked);
            },
            e =>
            {
                Assert.Equal("envvar2", e.Name);
                Assert.True(e.IsValueMasked);
            },
            e =>
            {
                Assert.Equal("envvar3", e.Name);
                Assert.True(e.IsValueMasked);
            });
    }
 
    [Fact]
    public async Task ClickMaskEnvVarSwitch_NewResource_MaskChanged()
    {
        // Arrange
        ResourceSetupHelpers.SetupResourceDetails(this);
 
        var resource1 = ModelTestHelpers.CreateResource(
            "app1",
            environment: new List<EnvironmentVariableViewModel>
            {
                new EnvironmentVariableViewModel("envvar1", "value!", fromSpec: true),
                new EnvironmentVariableViewModel("envvar2", "value!", fromSpec: true)
            }.ToImmutableArray());
 
        // Act
        var cut = RenderComponent<ResourceDetails>(builder =>
        {
            builder.Add(p => p.ShowSpecOnlyToggle, true);
            builder.Add(p => p.Resource, resource1);
            builder.Add(p => p.ResourceByName, new ConcurrentDictionary<string, ResourceViewModel>([new KeyValuePair<string, ResourceViewModel> (resource1.Name, resource1)]));
        });
 
        // Assert
        Assert.Collection(cut.Instance.FilteredEnvironmentVariables,
            e =>
            {
                Assert.Equal("envvar1", e.Name);
                Assert.Equal("value!", e.Value);
                Assert.True(e.IsValueMasked);
            },
            e =>
            {
                Assert.Equal("envvar2", e.Name);
                Assert.Equal("value!", e.Value);
                Assert.True(e.IsValueMasked);
            });
 
        var maskValueButton = cut.Find(".env-var-properties .grid-value-mask-button");
        await maskValueButton.ClickAsync(new MouseEventArgs());
 
        Assert.Collection(cut.Instance.FilteredEnvironmentVariables,
            e =>
            {
                Assert.Equal("envvar1", e.Name);
                Assert.False(e.IsValueMasked);
            },
            e =>
            {
                Assert.Equal("envvar2", e.Name);
                Assert.True(e.IsValueMasked);
            });
 
        var resource2 = ModelTestHelpers.CreateResource(
            "app2",
            environment: new List<EnvironmentVariableViewModel>
            {
                new EnvironmentVariableViewModel("envvar1", "value!", fromSpec: true),
                new EnvironmentVariableViewModel("envvar2", "value!", fromSpec: true),
                new EnvironmentVariableViewModel("envvar3", "value!", fromSpec: true)
            }.ToImmutableArray());
 
        cut.SetParametersAndRender(builder =>
        {
            builder.Add(p => p.Resource, resource2);
        });
 
        Assert.Collection(cut.Instance.FilteredEnvironmentVariables,
            e =>
            {
                Assert.Equal("envvar1", e.Name);
                Assert.True(e.IsValueMasked);
            },
            e =>
            {
                Assert.Equal("envvar2", e.Name);
                Assert.True(e.IsValueMasked);
            },
            e =>
            {
                Assert.Equal("envvar3", e.Name);
                Assert.True(e.IsValueMasked);
            });
    }
 
    [Fact]
    public void Render_StateDescription_ShowsAsResourceDetailEntry()
    {
        ResourceSetupHelpers.SetupResourceDetails(this);
 
        var resource = ModelTestHelpers.CreateResource(
            resourceName: "app1",
            state: KnownResourceState.Waiting);
 
        var cut = RenderComponent<ResourceDetails>(builder =>
        {
            builder.Add(p => p.Resource, resource);
            builder.Add(p => p.ResourceByName, new ConcurrentDictionary<string, ResourceViewModel>([new KeyValuePair<string, ResourceViewModel>(resource.Name, resource)]));
        });
 
        var resourcePropertyGrid = cut.FindAll(".property-grid")[0];
        Assert.Contains(ControlsStrings.ResourceDetailsStateDescriptionHeader, resourcePropertyGrid.TextContent);
        Assert.Contains(Columns.StateColumnResourceWaiting, resourcePropertyGrid.TextContent);
    }
 
    [Fact]
    public void Render_NotStartedStateDescription_ShowsDescription()
    {
        ResourceSetupHelpers.SetupResourceDetails(this);
 
        var resource = ModelTestHelpers.CreateResource(
            resourceName: "app1",
            state: KnownResourceState.NotStarted);
 
        var cut = RenderComponent<ResourceDetails>(builder =>
        {
            builder.Add(p => p.Resource, resource);
            builder.Add(p => p.ResourceByName, new ConcurrentDictionary<string, ResourceViewModel>([new KeyValuePair<string, ResourceViewModel>(resource.Name, resource)]));
        });
 
        var resourcePropertyGrid = cut.FindAll(".property-grid")[0];
        Assert.Contains(ControlsStrings.ResourceDetailsStateDescriptionHeader, resourcePropertyGrid.TextContent);
        Assert.Contains(Columns.StateColumnResourceNotStarted, resourcePropertyGrid.TextContent);
    }
 
    [Fact]
    public void Render_StateDescription_ShowsWaitingForDependenciesAsResourceDetailEntry()
    {
        ResourceSetupHelpers.SetupResourceDetails(this);
 
        var nginx = ModelTestHelpers.CreateResource(resourceName: "nginx-abcxyz", displayName: "nginx");
        var redis = ModelTestHelpers.CreateResource(resourceName: "redis");
 
        var resource = ModelTestHelpers.CreateResource(
            resourceName: "app1",
            state: KnownResourceState.Waiting,
            properties: new Dictionary<string, ResourcePropertyViewModel>
            {
                [KnownProperties.Resource.WaitingFor] = new(
                    KnownProperties.Resource.WaitingFor,
                    Value.ForList(Value.ForString("nginx-abcxyz"), Value.ForString("redis")),
                    isValueSensitive: false,
                    knownProperty: null,
                    sortOrder: 0,
                    displayName: null,
                    isHighlighted: false)
            });
 
        var cut = RenderComponent<ResourceDetails>(builder =>
        {
            builder.Add(p => p.Resource, resource);
            builder.Add(p => p.ResourceByName, new ConcurrentDictionary<string, ResourceViewModel>([
                new KeyValuePair<string, ResourceViewModel>(resource.Name, resource),
                new KeyValuePair<string, ResourceViewModel>(nginx.Name, nginx),
                new KeyValuePair<string, ResourceViewModel>(redis.Name, redis)
            ]));
        });
 
        var resourcePropertyGrid = cut.FindAll(".property-grid")[0];
        Assert.Contains(ControlsStrings.ResourceDetailsStateDescriptionHeader, resourcePropertyGrid.TextContent);
        Assert.Contains(string.Format(CultureInfo.InvariantCulture, Columns.StateColumnResourceWaitingFor, "nginx, redis"), resourcePropertyGrid.TextContent);
 
        Assert.Empty(resourcePropertyGrid.QuerySelectorAll("fluent-anchor"));
    }
 
    [Fact]
    public void Render_NullState_ShowsUnknownStateInResourceDetails()
    {
        Services.AddSingleton<IDashboardClient>(new TestDashboardClient(isEnabled: true));
        ResourceSetupHelpers.SetupResourceDetails(this);
 
        var properties = new Dictionary<string, ResourcePropertyViewModel>
        {
            [KnownProperties.Resource.State] = new ResourcePropertyViewModel(
                KnownProperties.Resource.State,
                Value.ForNull(),
                isValueSensitive: false,
                knownProperty: new KnownProperty(KnownProperties.Resource.State, _ => Aspire.Dashboard.Resources.Resources.ResourcesDetailsStateProperty),
                sortOrder: 0,
                displayName: null,
                isHighlighted: false)
        };
 
        var resource = ModelTestHelpers.CreateResource(
            resourceName: "app1",
            properties: properties);
 
        var cut = RenderComponent<ResourceDetails>(builder =>
        {
            builder.Add(p => p.Resource, resource);
            builder.Add(p => p.ResourceByName, new ConcurrentDictionary<string, ResourceViewModel>([new KeyValuePair<string, ResourceViewModel>(resource.Name, resource)]));
        });
 
        var resourcePropertyGrid = cut.FindAll(".property-grid")[0];
        Assert.Contains(Aspire.Dashboard.Resources.Resources.ResourcesDetailsStateProperty, resourcePropertyGrid.TextContent);
        Assert.Contains(Columns.UnknownStateLabel, resourcePropertyGrid.TextContent);
    }
 
    [Fact]
    public async Task Render_HighlightedUnknownProperty_ShowsInDefaultResourceDetailsAndPreservesSensitivity()
    {
        ResourceSetupHelpers.SetupResourceDetails(this);
 
        var properties = new Dictionary<string, ResourcePropertyViewModel>
        {
            ["provider.diagnostic.message"] = new(
                "provider.diagnostic.message",
                Value.ForString("The provider reported a recoverable deployment error."),
                isValueSensitive: false,
                knownProperty: null,
                sortOrder: int.MaxValue,
                displayName: "Provider diagnostic",
                isHighlighted: true),
            ["provider.diagnostic.secret"] = new(
                "provider.diagnostic.secret",
                Value.ForString("Sensitive provider diagnostic detail."),
                isValueSensitive: true,
                knownProperty: null,
                sortOrder: int.MaxValue,
                displayName: "Provider secret",
                isHighlighted: true),
            ["provider.diagnostic.hidden"] = new(
                "provider.diagnostic.hidden",
                Value.ForString("This should require show all."),
                isValueSensitive: false,
                knownProperty: null,
                sortOrder: int.MaxValue,
                displayName: null,
                isHighlighted: false)
        };
 
        var resource = ModelTestHelpers.CreateResource(
            resourceName: "redis",
            stateStyle: "error",
            properties: properties);
 
        var cut = RenderComponent<ResourceDetails>(builder =>
        {
            builder.Add(p => p.Resource, resource);
            builder.Add(p => p.ResourceByName, new ConcurrentDictionary<string, ResourceViewModel>([new KeyValuePair<string, ResourceViewModel>(resource.Name, resource)]));
        });
 
        var resourcePropertyGrid = cut.FindAll(".property-grid")[0];
        Assert.Contains("Provider diagnostic", resourcePropertyGrid.TextContent);
        Assert.Contains("The provider reported a recoverable deployment error.", resourcePropertyGrid.TextContent);
        Assert.Contains("Provider secret", resourcePropertyGrid.TextContent);
        Assert.DoesNotContain("Sensitive provider diagnostic detail.", resourcePropertyGrid.TextContent);
        Assert.DoesNotContain("This should require show all.", resourcePropertyGrid.TextContent);
 
        var maskValueButton = cut.Find(".property-grid .grid-value-mask-button");
        await maskValueButton.ClickAsync(new MouseEventArgs());
 
        cut.WaitForAssertion(() =>
        {
            var updatedResourcePropertyGrid = cut.FindAll(".property-grid")[0];
            Assert.Contains("Sensitive provider diagnostic detail.", updatedResourcePropertyGrid.TextContent);
        });
    }
 
    [Fact]
    public void Render_ProducerSuppliedSortOrder_OrdersUnknownHighlightedProperties()
    {
        ResourceSetupHelpers.SetupResourceDetails(this);
 
        var properties = new Dictionary<string, ResourcePropertyViewModel>
        {
            [KnownProperties.Container.Command] = new(
                KnownProperties.Container.Command,
                Value.ForString("redis-server"),
                isValueSensitive: false,
                knownProperty: null,
                displayName: "Container command",
                isHighlighted: true,
                sortOrder: ProducerDefinedDisplaySortOrder(2)),
            [KnownProperties.Container.Image] = new(
                KnownProperties.Container.Image,
                Value.ForString("redis:latest"),
                isValueSensitive: false,
                knownProperty: null,
                displayName: "Container image",
                isHighlighted: true,
                sortOrder: ProducerDefinedDisplaySortOrder(0)),
            [KnownProperties.Container.Id] = new(
                KnownProperties.Container.Id,
                Value.ForString("1234567890abcdef"),
                isValueSensitive: false,
                knownProperty: null,
                displayName: "Container ID",
                isHighlighted: true,
                sortOrder: ProducerDefinedDisplaySortOrder(1)),
            ["provider.diagnostic"] = new(
                "provider.diagnostic",
                Value.ForString("diagnostic"),
                isValueSensitive: false,
                knownProperty: null,
                sortOrder: int.MaxValue,
                displayName: "AAA provider diagnostic",
                isHighlighted: true)
        };
 
        var resource = ModelTestHelpers.CreateResource(
            resourceName: "redis",
            properties: properties);
 
        var cut = RenderComponent<ResourceDetails>(builder =>
        {
            builder.Add(p => p.Resource, resource);
            builder.Add(p => p.ResourceByName, new ConcurrentDictionary<string, ResourceViewModel>([new KeyValuePair<string, ResourceViewModel>(resource.Name, resource)]));
        });
 
        Assert.Collection(cut.Instance.FilteredResourceProperties.Select(p => p.DisplayName),
            name => Assert.Equal("Container image", name),
            name => Assert.Equal("Container ID", name),
            name => Assert.Equal("Container command", name),
            name => Assert.Equal("AAA provider diagnostic", name));
    }
 
    private static int ProducerDefinedDisplaySortOrder(int producerSortOrder)
    {
        return KnownResourcePropertySortOrder.ConnectionString + 1 + producerSortOrder;
    }
 
    [Fact]
    public void FilteredEnvironmentVariables_SortedByName()
    {
        // Arrange
        ResourceSetupHelpers.SetupResourceDetails(this);
 
        var resource = ModelTestHelpers.CreateResource(
            "app1",
            environment: new List<EnvironmentVariableViewModel>
            {
                new EnvironmentVariableViewModel("ZEBRA", "value1", fromSpec: true),
                new EnvironmentVariableViewModel("alpha", "value2", fromSpec: true),
                new EnvironmentVariableViewModel("Beta", "value3", fromSpec: true),
                new EnvironmentVariableViewModel("GAMMA", "value4", fromSpec: true),
                new EnvironmentVariableViewModel("delta", "value5", fromSpec: true)
            }.ToImmutableArray());
 
        // Act
        var cut = RenderComponent<ResourceDetails>(builder =>
        {
            builder.Add(p => p.ShowSpecOnlyToggle, true);
            builder.Add(p => p.Resource, resource);
            builder.Add(p => p.ResourceByName, new ConcurrentDictionary<string, ResourceViewModel>([new KeyValuePair<string, ResourceViewModel>(resource.Name, resource)]));
        });
 
        // Assert - verify environment variables are sorted alphabetically (case-insensitive)
        var envVars = cut.Instance.FilteredEnvironmentVariables.ToList();
        Assert.Equal(5, envVars.Count);
        Assert.Collection(envVars,
            e => Assert.Equal("alpha", e.Name),
            e => Assert.Equal("Beta", e.Name),
            e => Assert.Equal("delta", e.Name),
            e => Assert.Equal("GAMMA", e.Name),
            e => Assert.Equal("ZEBRA", e.Name));
    }
 
    [Fact]
    public void FilteredVolumes_SortedBySource()
    {
        // Arrange
        ResourceSetupHelpers.SetupResourceDetails(this);
 
        var resource = ModelTestHelpers.CreateResource(
            "app1",
            volumes: new List<VolumeViewModel>
            {
                new VolumeViewModel(0, "zebra-volume", "/target1", "bind", false),
                new VolumeViewModel(1, "Alpha-Volume", "/target2", "volume", false),
                new VolumeViewModel(2, "beta-volume", "/target3", "bind", true),
                new VolumeViewModel(3, "GAMMA-Volume", "/target4", "volume", false),
                new VolumeViewModel(4, "delta-volume", "/target5", "bind", false)
            }.ToImmutableArray());
 
        // Act
        var cut = RenderComponent<ResourceDetails>(builder =>
        {
            builder.Add(p => p.ShowSpecOnlyToggle, true);
            builder.Add(p => p.Resource, resource);
            builder.Add(p => p.ResourceByName, new ConcurrentDictionary<string, ResourceViewModel>([new KeyValuePair<string, ResourceViewModel>(resource.Name, resource)]));
        });
 
        // Assert - verify volumes are sorted alphabetically by source (case-insensitive)
        var volumes = cut.Instance.FilteredVolumes.ToList();
        Assert.Equal(5, volumes.Count);
        Assert.Collection(volumes,
            v => Assert.Equal("Alpha-Volume", v.Source),
            v => Assert.Equal("beta-volume", v.Source),
            v => Assert.Equal("delta-volume", v.Source),
            v => Assert.Equal("GAMMA-Volume", v.Source),
            v => Assert.Equal("zebra-volume", v.Source));
    }
 
    [Fact]
    public void FilteredHealthReports_SortedByName()
    {
        // Arrange
        ResourceSetupHelpers.SetupResourceDetails(this);
 
        var resource = ModelTestHelpers.CreateResource(
            "app1",
            healthReports: new List<HealthReportViewModel>
            {
                new HealthReportViewModel("zebra-check", HealthStatus.Healthy, "OK", null),
                new HealthReportViewModel("Alpha-Check", HealthStatus.Healthy, "OK", null),
                new HealthReportViewModel("beta-check", HealthStatus.Degraded, "Warning", null),
                new HealthReportViewModel("GAMMA-Check", HealthStatus.Healthy, "OK", null),
                new HealthReportViewModel("delta-check", HealthStatus.Unhealthy, "Failed", null)
            }.ToImmutableArray());
 
        // Act
        var cut = RenderComponent<ResourceDetails>(builder =>
        {
            builder.Add(p => p.ShowSpecOnlyToggle, true);
            builder.Add(p => p.Resource, resource);
            builder.Add(p => p.ResourceByName, new ConcurrentDictionary<string, ResourceViewModel>([new KeyValuePair<string, ResourceViewModel>(resource.Name, resource)]));
        });
 
        // Assert - verify health reports are sorted alphabetically by name (case-insensitive)
        var healthReports = cut.Instance.FilteredHealthReports.ToList();
        Assert.Equal(5, healthReports.Count);
        Assert.Collection(healthReports,
            h => Assert.Equal("Alpha-Check", h.Name),
            h => Assert.Equal("beta-check", h.Name),
            h => Assert.Equal("delta-check", h.Name),
            h => Assert.Equal("GAMMA-Check", h.Name),
            h => Assert.Equal("zebra-check", h.Name));
    }
 
    [Fact]
    public async Task UnsetParameterResource_RendersValueNotSetButton_AndInvokesSetCommand()
    {
        ResourceSetupHelpers.SetupResourceDetails(this);
 
        var setCommand = new CommandViewModel(
            CommandViewModel.SetParameterCommand,
            CommandViewModelState.Enabled,
            displayName: "Set value",
            displayDescription: "Set the parameter value",
            confirmationMessage: string.Empty,
            argumentInputs: [],
            isHighlighted: false,
            iconName: string.Empty,
            iconVariant: IconVariant.Regular);
 
        var properties = new Dictionary<string, ResourcePropertyViewModel>
        {
            [KnownProperties.Parameter.Value] = CreateParameterValueProperty("Parameter 'p' not found in configuration.")
        };
 
        var resource = CreateParameterResource(
            resourceName: "myparameter",
            state: nameof(KnownResourceState.ValueMissing),
            stateStyle: "warning",
            properties: properties,
            commands: ImmutableArray.Create(setCommand));
 
        CommandViewModel? capturedCommand = null;
        var cut = RenderComponent<ResourceDetails>(builder =>
        {
            builder.Add(p => p.Resource, resource);
            builder.Add(p => p.ResourceByName, new ConcurrentDictionary<string, ResourceViewModel>([new KeyValuePair<string, ResourceViewModel>(resource.Name, resource)]));
            builder.Add(p => p.CommandSelected, EventCallback.Factory.Create<CommandViewModel>(this, c => capturedCommand = c));
            builder.Add(p => p.IsCommandExecuting, (_, _) => false);
        });
 
        var button = cut.Find("button.value-not-set-link");
        Assert.Equal("Value not set", button.TextContent.Trim());
 
        await button.ClickAsync(new MouseEventArgs());
 
        Assert.NotNull(capturedCommand);
        Assert.Equal(CommandViewModel.SetParameterCommand, capturedCommand!.Name);
    }
 
    [Fact]
    public async Task UnsetParameterResource_WhenSetCommandIsExecuting_DisablesValueNotSetButton()
    {
        ResourceSetupHelpers.SetupResourceDetails(this);
 
        var setCommand = new CommandViewModel(
            CommandViewModel.SetParameterCommand,
            CommandViewModelState.Enabled,
            displayName: "Set value",
            displayDescription: "Set the parameter value",
            confirmationMessage: string.Empty,
            argumentInputs: [],
            isHighlighted: false,
            iconName: string.Empty,
            iconVariant: IconVariant.Regular);
 
        var properties = new Dictionary<string, ResourcePropertyViewModel>
        {
            [KnownProperties.Parameter.Value] = CreateParameterValueProperty("Parameter 'p' not found in configuration.")
        };
 
        var resource = CreateParameterResource(
            resourceName: "myparameter",
            state: nameof(KnownResourceState.ValueMissing),
            stateStyle: "warning",
            properties: properties,
            commands: ImmutableArray.Create(setCommand));
 
        CommandViewModel? capturedCommand = null;
        var cut = RenderComponent<ResourceDetails>(builder =>
        {
            builder.Add(p => p.Resource, resource);
            builder.Add(p => p.ResourceByName, new ConcurrentDictionary<string, ResourceViewModel>([new KeyValuePair<string, ResourceViewModel>(resource.Name, resource)]));
            builder.Add(p => p.CommandSelected, EventCallback.Factory.Create<CommandViewModel>(this, c => capturedCommand = c));
            builder.Add(p => p.IsCommandExecuting, (currentResource, command) =>
                currentResource.Name == resource.Name && command.Name == CommandViewModel.SetParameterCommand);
        });
 
        var button = cut.Find("button.value-not-set-link");
        Assert.True(button.HasAttribute("disabled"));
 
        await button.ClickAsync(new MouseEventArgs());
 
        Assert.Null(capturedCommand);
    }
 
    [Fact]
    public void UnsetSecretParameterResource_RendersValueNotSetButton()
    {
        ResourceSetupHelpers.SetupResourceDetails(this);
 
        var setCommand = new CommandViewModel(
            CommandViewModel.SetParameterCommand,
            CommandViewModelState.Enabled,
            displayName: "Set value",
            displayDescription: "Set the parameter value",
            confirmationMessage: string.Empty,
            argumentInputs: [],
            isHighlighted: false,
            iconName: string.Empty,
            iconVariant: IconVariant.Regular);
 
        var properties = new Dictionary<string, ResourcePropertyViewModel>
        {
            [KnownProperties.Parameter.Value] = CreateParameterValueProperty("Parameter 'p' not found in configuration.", isValueSensitive: true)
        };
 
        var resource = CreateParameterResource(
            resourceName: "mysecretparameter",
            state: nameof(KnownResourceState.ValueMissing),
            stateStyle: "warning",
            properties: properties,
            commands: ImmutableArray.Create(setCommand));
 
        var cut = RenderComponent<ResourceDetails>(builder =>
        {
            builder.Add(p => p.Resource, resource);
            builder.Add(p => p.ResourceByName, new ConcurrentDictionary<string, ResourceViewModel>([new KeyValuePair<string, ResourceViewModel>(resource.Name, resource)]));
            builder.Add(p => p.CommandSelected, EventCallback.Factory.Create<CommandViewModel>(this, _ => Task.CompletedTask));
            builder.Add(p => p.IsCommandExecuting, (_, _) => false);
        });
 
        Assert.NotNull(cut.Find("button.value-not-set-link"));
        Assert.Empty(cut.FindAll(".grid-value-mask-button"));
    }
 
    [Fact]
    public void ErrorParameterResource_DisplaysErrorMessage_InsteadOfValueNotSetButton()
    {
        ResourceSetupHelpers.SetupResourceDetails(this);
 
        const string errorMessage = "Failed to initialize parameter from external provider.";
        var properties = new Dictionary<string, ResourcePropertyViewModel>
        {
            [KnownProperties.Parameter.Value] = CreateParameterValueProperty(errorMessage)
        };
 
        var resource = ModelTestHelpers.CreateResource(
            resourceName: "myparameter",
            resourceType: KnownResourceTypes.Parameter,
            stateStyle: "error",
            properties: properties);
 
        var cut = RenderComponent<ResourceDetails>(builder =>
        {
            builder.Add(p => p.Resource, resource);
            builder.Add(p => p.ResourceByName, new ConcurrentDictionary<string, ResourceViewModel>([new KeyValuePair<string, ResourceViewModel>(resource.Name, resource)]));
            builder.Add(p => p.IsCommandExecuting, (_, _) => false);
        });
 
        Assert.Empty(cut.FindAll("button.value-not-set-link"));
        Assert.Contains(errorMessage, cut.FindAll(".grid-value").Select(e => e.TextContent.Trim()));
    }
 
    [Fact]
    public void RunningParameterResource_DoesNotRenderValueNotSetButton()
    {
        ResourceSetupHelpers.SetupResourceDetails(this);
 
        var properties = new Dictionary<string, ResourcePropertyViewModel>
        {
            [KnownProperties.Parameter.Value] = CreateParameterValueProperty("resolved-value")
        };
 
        var resource = ModelTestHelpers.CreateResource(
            resourceName: "myparameter",
            resourceType: KnownResourceTypes.Parameter,
            state: KnownResourceState.Running,
            properties: properties);
 
        var cut = RenderComponent<ResourceDetails>(builder =>
        {
            builder.Add(p => p.Resource, resource);
            builder.Add(p => p.ResourceByName, new ConcurrentDictionary<string, ResourceViewModel>([new KeyValuePair<string, ResourceViewModel>(resource.Name, resource)]));
            builder.Add(p => p.IsCommandExecuting, (_, _) => false);
        });
 
        Assert.Empty(cut.FindAll("button.value-not-set-link"));
    }
 
    private static ResourceViewModel CreateParameterResource(
        string resourceName,
        string? state,
        string? stateStyle,
        Dictionary<string, ResourcePropertyViewModel> properties,
        ImmutableArray<CommandViewModel>? commands = null)
    {
        return new ResourceViewModel
        {
            Name = resourceName,
            ResourceType = KnownResourceTypes.Parameter,
            DisplayName = resourceName,
            Uid = resourceName,
            ReplicaIndex = 0,
            State = state,
            KnownState = state is not null && System.Enum.TryParse<KnownResourceState>(state, out var knownState) ? knownState : null,
            StateStyle = stateStyle,
            CreationTimeStamp = DateTime.UtcNow,
            StartTimeStamp = DateTime.UtcNow,
            StopTimeStamp = DateTime.UtcNow,
            Environment = [],
            Urls = [],
            Volumes = [],
            Relationships = [],
            Properties = properties.ToImmutableDictionary(),
            Commands = commands ?? [],
            HealthReports = [],
        };
    }
 
    private static ResourcePropertyViewModel CreateParameterValueProperty(string value, bool isValueSensitive = false)
    {
        return new(
            KnownProperties.Parameter.Value,
            Value.ForString(value),
            isValueSensitive,
            knownProperty: null,
            displayName: "Value",
            isHighlighted: true,
            sortOrder: ProducerDefinedDisplaySortOrder(0));
    }
 
}