// 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));
}
}