File: Model\ResourceStateViewModelTests.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;
using Aspire.Dashboard.Resources;
using Aspire.Tests.Shared.DashboardModel;
using Google.Protobuf.WellKnownTypes;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.FluentUI.AspNetCore.Components;
using Xunit;
using Enum = System.Enum;
 
namespace Aspire.Dashboard.Tests.Model;
 
public class ResourceStateViewModelTests
{
    [Theory]
    // Resource is no longer running
    [InlineData(
        /* state */ "Container", KnownResourceState.Exited, null, null,null,
        /* expected output */ $"Localized:{nameof(Columns.StateColumnResourceExited)}:Container", "RecordStop", Color.Info, "Exited")]
    [InlineData(
        /* state */ "Container", KnownResourceState.Exited, 3, null, null,
        /* expected output */ $"Localized:{nameof(Columns.StateColumnResourceExitedUnexpectedly)}:Container+3", "ErrorCircle", Color.Error, "Exited")]
    [InlineData(
        /* state */ "Container", KnownResourceState.Exited, 0, null, null,
        /* expected output */ $"Localized:{nameof(Columns.StateColumnResourceExited)}:Container", "RecordStop", Color.Info, "Exited")]
    [InlineData(
        /* state */ "Container", KnownResourceState.Finished, 0, null, null,
        /* expected output */ $"Localized:{nameof(Columns.StateColumnResourceExited)}:Container", "RecordStop", Color.Info, "Finished")]
    [InlineData(
        /* state */ "CustomResource", KnownResourceState.Finished, null, null, null,
        /* expected output */ $"Localized:{nameof(Columns.StateColumnResourceExited)}:CustomResource", "RecordStop", Color.Info, "Finished")]
    // Resource failed to start - should use dedicated message, not "no longer running"
    [InlineData(
        /* state */ "Container", KnownResourceState.FailedToStart, null, null, null,
        /* expected output */ $"Localized:{nameof(Columns.StateColumnResourceFailedToStart)}:Container", "Warning", Color.Warning, "Failed to start")]
    [InlineData(
        /* state */ "CustomResource", KnownResourceState.FailedToStart, null, null, null,
        /* expected output */ $"Localized:{nameof(Columns.StateColumnResourceFailedToStart)}:CustomResource", "Warning", Color.Warning, "Failed to start")]
    [InlineData(
        /* state */ "Container", KnownResourceState.Unknown, null, null, null,
        /* expected output */ "Unknown", "CircleHint", Color.Info, "Unknown")]
    // Health checks
    [InlineData(
        /* state */ "Container", KnownResourceState.Running, null, "Healthy", null,
        /* expected output */ "Running", "CheckmarkCircle", Color.Success, "Running")]
    [InlineData(
        /* state */ "Container", KnownResourceState.Running, null, "", null,
        /* expected output */ $"Localized:{nameof(Columns.RunningAndUnhealthyResourceStateToolTip)}", "CheckmarkCircleWarning", Color.Warning, "Running (Unhealthy)")]
    [InlineData(
        /* state */ "Container", KnownResourceState.Running, null, "Unhealthy", null,
        /* expected output */ $"Localized:{nameof(Columns.RunningAndUnhealthyResourceStateToolTip)}", "CheckmarkCircleWarning", Color.Warning, "Running (Unhealthy)")]
    [InlineData(
        /* state */ "Container", KnownResourceState.Running, null, "Healthy", "warning",
        /* expected output */ "Running", "Warning", Color.Warning, "Running")]
    [InlineData(
        /* state */ "Container", KnownResourceState.Running, null, "Healthy", "NOT_A_VALID_STATE_STYLE",
        /* expected output */ "Running", "Circle", Color.Default, "Running")]
    [InlineData(
        /* state */ "Container", KnownResourceState.Running, null, null, "info",
        /* expected output */ "Running", "Info", Color.Info, "Running")]
    [InlineData(
        /* state */ "Container", KnownResourceState.RuntimeUnhealthy, null, null, null,
        /* expected output */ $"Localized:{nameof(Columns.StateColumnResourceContainerRuntimeUnhealthy)}", "Warning", Color.Warning, "Runtime unhealthy")]
    public void ResourceViewModel_ReturnsCorrectIconAndTooltip(
        string resourceType,
        KnownResourceState state,
        int? exitCode,
        string? healthStatusString,
        string? stateStyle,
        string? expectedTooltip,
        string expectedIconName,
        Color expectedColor,
        string expectedText)
    {
        // Arrange
        HealthStatus? healthStatus = string.IsNullOrEmpty(healthStatusString) ? null : Enum.Parse<HealthStatus>(healthStatusString);
        var propertiesDictionary = new Dictionary<string, ResourcePropertyViewModel>();
        if (exitCode is not null)
        {
            propertiesDictionary.TryAdd(KnownProperties.Resource.ExitCode, new ResourcePropertyViewModel(KnownProperties.Resource.ExitCode, Value.ForNumber((double)exitCode), false, null, 0, displayName: null, isHighlighted: false));
        }
 
        var resource = ModelTestHelpers.CreateResource(
            state: state,
            reportHealthStatus: healthStatus,
            createNullHealthReport: healthStatusString == "",
            stateStyle: stateStyle,
            resourceType: resourceType,
            properties: propertiesDictionary);
 
        if (exitCode is not null)
        {
            resource.Properties.TryAdd(KnownProperties.Resource.ExitCode, new ResourcePropertyViewModel(KnownProperties.Resource.ExitCode, Value.ForNumber((double)exitCode), false, null, 0, displayName: null, isHighlighted: false));
        }
 
        var localizer = new TestStringLocalizer<Columns>();
 
        // Act
        var tooltip = ResourceStateViewModel.GetResourceStateTooltip(resource, localizer);
        var vm = ResourceStateViewModel.GetStateViewModel(resource, localizer);
 
        // Assert
        Assert.Equal(expectedTooltip, tooltip);
 
        Assert.Equal(expectedIconName, vm.Icon.Name);
        Assert.Equal(expectedColor, vm.Color);
        Assert.Equal(expectedText, vm.Text);
    }
 
    [Fact]
    public void WaitingResourceTooltipIncludesWaitingForDependenciesWhenPresent()
    {
        var resource = ModelTestHelpers.CreateResource(
            state: KnownResourceState.Waiting,
            properties: new Dictionary<string, ResourcePropertyViewModel>
            {
                [KnownProperties.Resource.WaitingFor] = new(
                    KnownProperties.Resource.WaitingFor,
                    Value.ForList(Value.ForString("nginx"), Value.ForString("redis")),
                    isValueSensitive: false,
                    knownProperty: null,
                    sortOrder: 0,
                    displayName: null,
                    isHighlighted: false)
            });
 
        var localizer = new TestStringLocalizer<Columns>();
 
        var tooltip = ResourceStateViewModel.GetResourceStateTooltip(resource, localizer);
 
        Assert.Equal($"Localized:{nameof(Columns.StateColumnResourceWaitingFor)}:nginx, redis", tooltip);
    }
 
    [Fact]
    public void WaitingResourceTooltipUsesDisplayNamesForNonReplicaDependencies()
    {
        var dependency = ModelTestHelpers.CreateResource(
            resourceName: "messaging-abcxyz",
            displayName: "messaging");
 
        var resource = ModelTestHelpers.CreateResource(
            state: KnownResourceState.Waiting,
            properties: new Dictionary<string, ResourcePropertyViewModel>
            {
                [KnownProperties.Resource.WaitingFor] = new(
                    KnownProperties.Resource.WaitingFor,
                    Value.ForList(Value.ForString("messaging-abcxyz")),
                    isValueSensitive: false,
                    knownProperty: null,
                    sortOrder: 0,
                    displayName: null,
                    isHighlighted: false)
            });
 
        var localizer = new TestStringLocalizer<Columns>();
 
        var tooltip = ResourceStateViewModel.GetResourceStateTooltip(resource, localizer, [resource, dependency]);
 
        Assert.Equal($"Localized:{nameof(Columns.StateColumnResourceWaitingFor)}:messaging", tooltip);
    }
 
    [Fact]
    public void WaitingResourceTooltipUsesUniqueNamesForReplicaDependencies()
    {
        var firstDependency = ModelTestHelpers.CreateResource(
            resourceName: "messaging-abcxyz",
            displayName: "messaging");
        var secondDependency = ModelTestHelpers.CreateResource(
            resourceName: "messaging-defuvw",
            displayName: "messaging");
 
        var resource = ModelTestHelpers.CreateResource(
            state: KnownResourceState.Waiting,
            properties: new Dictionary<string, ResourcePropertyViewModel>
            {
                [KnownProperties.Resource.WaitingFor] = new(
                    KnownProperties.Resource.WaitingFor,
                    Value.ForList(Value.ForString("messaging-abcxyz")),
                    isValueSensitive: false,
                    knownProperty: null,
                    sortOrder: 0,
                    displayName: null,
                    isHighlighted: false)
            });
 
        var localizer = new TestStringLocalizer<Columns>();
 
        var tooltip = ResourceStateViewModel.GetResourceStateTooltip(resource, localizer, [resource, firstDependency, secondDependency]);
 
        Assert.Equal($"Localized:{nameof(Columns.StateColumnResourceWaitingFor)}:messaging-abcxyz", tooltip);
    }
 
    [Fact]
    public void TryGetResolvedWaitingForDependenciesDoesNotMaterializeAllResources()
    {
        var dependency = ModelTestHelpers.CreateResource(
            resourceName: "messaging-abcxyz",
            displayName: "messaging");
 
        var resource = ModelTestHelpers.CreateResource(
            state: KnownResourceState.Waiting,
            properties: new Dictionary<string, ResourcePropertyViewModel>
            {
                [KnownProperties.Resource.WaitingFor] = new(
                    KnownProperties.Resource.WaitingFor,
                    Value.ForList(Value.ForString("messaging-abcxyz")),
                    isValueSensitive: false,
                    knownProperty: null,
                    sortOrder: 0,
                    displayName: null,
                    isHighlighted: false)
            });
 
        var resources = new CopyToThrowingResourceCollection(resource, dependency);
 
        var result = resource.TryGetResolvedWaitingForDependencies(resources, out var dependencies);
 
        Assert.True(result);
        Assert.Equal(["messaging"], dependencies);
    }
 
    private sealed class CopyToThrowingResourceCollection(params ResourceViewModel[] resources) : ICollection<ResourceViewModel>, IReadOnlyCollection<ResourceViewModel>
    {
        public int Count => resources.Length;
 
        public bool IsReadOnly => true;
 
        public void Add(ResourceViewModel item) => throw new NotSupportedException();
 
        public void Clear() => throw new NotSupportedException();
 
        public bool Contains(ResourceViewModel item) => resources.Contains(item);
 
        public void CopyTo(ResourceViewModel[] array, int arrayIndex) => throw new InvalidOperationException("The resources collection should not be copied.");
 
        public IEnumerator<ResourceViewModel> GetEnumerator() => resources.AsEnumerable().GetEnumerator();
 
        public bool Remove(ResourceViewModel item) => throw new NotSupportedException();
 
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
    }
}