using EntKube.Clusters.Domain;
using EntKube.Clusters.Features.AdoptCluster;
using EntKube.Clusters.Features.AdoptCluster.ConfigureComponent;
using EntKube.Clusters.Features.AdoptCluster.DeployComponent;
using EntKube.Clusters.Infrastructure;
using EntKube.SharedKernel.Domain;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
namespace EntKube.Clusters.Tests.Features;
///
/// Verifies that adoption scans, deployments, and configuration changes
/// persist component state on the cluster aggregate. This ensures we don't
/// need to re-scan the cluster every time the UI loads — detected components
/// are stored and updated incrementally.
///
public class ComponentPersistenceTests
{
private readonly InMemoryClusterRepository repository;
public ComponentPersistenceTests()
{
repository = new InMemoryClusterRepository();
}
[Fact]
public async Task AdoptCluster_PersistsDetectedComponentsOnCluster()
{
// Arrange — A connected cluster with two components: one installed, one missing.
KubernetesCluster cluster = KubernetesCluster.Register(
"prod-cluster", "https://k8s.prod:6443", "kubeconfig-data", Guid.NewGuid(), "prod-ctx", Guid.NewGuid());
cluster.MarkConnected();
await repository.AddAsync(cluster);
Mock kyvernoCheck = new();
kyvernoCheck.Setup(c => c.ComponentName).Returns("kyverno");
kyvernoCheck.Setup(c => c.CheckAsync(cluster, It.IsAny()))
.ReturnsAsync(new ComponentCheckResult("kyverno", ComponentStatus.Installed,
new List { "Pod running" }, new List(),
new DiscoveredConfiguration("3.3.4", "kyverno", "kyverno",
new Dictionary { ["replicas"] = "3" })));
Mock certManagerCheck = new();
certManagerCheck.Setup(c => c.ComponentName).Returns("cert-manager");
certManagerCheck.Setup(c => c.CheckAsync(cluster, It.IsAny()))
.ReturnsAsync(new ComponentCheckResult("cert-manager", ComponentStatus.NotInstalled,
new List(), new List { "No pods found" }));
List checks = new() { kyvernoCheck.Object, certManagerCheck.Object };
AdoptClusterHandler handler = new(repository, checks, NullLogger.Instance);
// Act — Run the adoption scan.
Result result = await handler.HandleAsync(cluster.Id);
// Assert — Components should now be persisted on the cluster.
result.IsSuccess.Should().BeTrue();
KubernetesCluster? persisted = await repository.GetByIdAsync(cluster.Id);
persisted!.Components.Should().HaveCount(2);
ClusterComponent kyverno = persisted.Components.First(c => c.ComponentName == "kyverno");
kyverno.Status.Should().Be(ComponentStatus.Installed);
kyverno.Version.Should().Be("3.3.4");
kyverno.Configuration["replicas"].Should().Be("3");
ClusterComponent certManager = persisted.Components.First(c => c.ComponentName == "cert-manager");
certManager.Status.Should().Be(ComponentStatus.NotInstalled);
}
[Fact]
public async Task ConfigureComponent_UpdatesPersistedConfiguration()
{
// Arrange — A cluster with monitoring already detected and persisted.
KubernetesCluster cluster = KubernetesCluster.Register(
"prod-cluster", "https://k8s.prod:6443", "kubeconfig-data", Guid.NewGuid(), "ctx", Guid.NewGuid());
cluster.MarkConnected();
cluster.UpdateComponents(new List
{
new("monitoring", ComponentStatus.Installed, new List(), new List(),
new DiscoveredConfiguration("45.0.0", "monitoring", "kube-prometheus-stack",
new Dictionary { ["retention"] = "30d" }))
});
await repository.AddAsync(cluster);
Mock monitoringInstaller = new();
monitoringInstaller.Setup(i => i.ComponentName).Returns("monitoring");
monitoringInstaller
.Setup(i => i.ConfigureAsync(cluster, It.IsAny(), It.IsAny()))
.ReturnsAsync(new InstallResult(true, "monitoring", "Reconfigured", new List { "retention=90d" }));
List installers = new() { monitoringInstaller.Object };
ConfigureComponentHandler handler = new(repository, installers);
ConfigureComponentRequest request = new("monitoring", Values: new Dictionary
{
["retention"] = "90d"
});
// Act — Apply the configuration change.
Result result = await handler.HandleAsync(cluster.Id, request);
// Assert — The persisted component configuration should reflect the new value.
result.IsSuccess.Should().BeTrue();
KubernetesCluster? persisted = await repository.GetByIdAsync(cluster.Id);
ClusterComponent monitoring = persisted!.Components.First(c => c.ComponentName == "monitoring");
monitoring.Configuration["retention"].Should().Be("90d");
}
[Fact]
public async Task DeployComponent_UpdatesPersistedComponentStatus()
{
// Arrange — A cluster with cert-manager detected as NotInstalled.
KubernetesCluster cluster = KubernetesCluster.Register(
"prod-cluster", "https://k8s.prod:6443", "kubeconfig-data", Guid.NewGuid(), "ctx", Guid.NewGuid());
cluster.MarkConnected();
cluster.UpdateComponents(new List
{
new("cert-manager", ComponentStatus.NotInstalled, new List(), new List { "Missing" })
});
await repository.AddAsync(cluster);
Mock certManagerInstaller = new();
certManagerInstaller.Setup(i => i.ComponentName).Returns("cert-manager");
certManagerInstaller
.Setup(i => i.InstallAsync(cluster, It.IsAny(), It.IsAny()))
.ReturnsAsync(new InstallResult(true, "cert-manager", "Installed v1.14.0",
new List { "Helm install cert-manager v1.14.0" }));
List installers = new() { certManagerInstaller.Object };
DeployComponentHandler handler = new(repository, installers, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance);
DeployComponentRequest request = new("cert-manager", Version: "1.14.0", Namespace: "cert-manager");
// Act — Deploy the component.
Result result = await handler.HandleAsync(cluster.Id, request);
// Assert — The persisted component should now show Installed.
result.IsSuccess.Should().BeTrue();
KubernetesCluster? persisted = await repository.GetByIdAsync(cluster.Id);
ClusterComponent certManager = persisted!.Components.First(c => c.ComponentName == "cert-manager");
certManager.Status.Should().Be(ComponentStatus.Installed);
}
[Fact]
public async Task ConfigureComponent_WhenInstallerFails_DoesNotUpdatePersisted()
{
// Arrange — Configuration that fails should NOT update the persisted state.
KubernetesCluster cluster = KubernetesCluster.Register(
"prod-cluster", "https://k8s.prod:6443", "kubeconfig-data", Guid.NewGuid(), "ctx", Guid.NewGuid());
cluster.MarkConnected();
cluster.UpdateComponents(new List
{
new("monitoring", ComponentStatus.Installed, new List(), new List(),
new DiscoveredConfiguration("45.0.0", "monitoring", "kube-prometheus-stack",
new Dictionary { ["retention"] = "30d" }))
});
await repository.AddAsync(cluster);
Mock monitoringInstaller = new();
monitoringInstaller.Setup(i => i.ComponentName).Returns("monitoring");
monitoringInstaller
.Setup(i => i.ConfigureAsync(cluster, It.IsAny(), It.IsAny()))
.ReturnsAsync(new InstallResult(false, "monitoring", "Helm failed", new List()));
List installers = new() { monitoringInstaller.Object };
ConfigureComponentHandler handler = new(repository, installers);
ConfigureComponentRequest request = new("monitoring", Values: new Dictionary
{
["retention"] = "90d"
});
// Act
Result result = await handler.HandleAsync(cluster.Id, request);
// Assert — Original value preserved.
result.IsFailure.Should().BeTrue();
KubernetesCluster? persisted = await repository.GetByIdAsync(cluster.Id);
ClusterComponent monitoring = persisted!.Components.First(c => c.ComponentName == "monitoring");
monitoring.Configuration["retention"].Should().Be("30d");
}
[Fact]
public async Task DeployComponent_WhenInstallerFails_DoesNotUpdatePersisted()
{
// Arrange — Failed deployment should not change persisted status.
KubernetesCluster cluster = KubernetesCluster.Register(
"prod-cluster", "https://k8s.prod:6443", "kubeconfig-data", Guid.NewGuid(), "ctx", Guid.NewGuid());
cluster.MarkConnected();
cluster.UpdateComponents(new List
{
new("cert-manager", ComponentStatus.NotInstalled, new List(), new List())
});
await repository.AddAsync(cluster);
Mock installer = new();
installer.Setup(i => i.ComponentName).Returns("cert-manager");
installer
.Setup(i => i.InstallAsync(cluster, It.IsAny(), It.IsAny()))
.ReturnsAsync(new InstallResult(false, "cert-manager", "Timeout", new List()));
List installers = new() { installer.Object };
DeployComponentHandler handler = new(repository, installers, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance);
DeployComponentRequest request = new("cert-manager");
// Act
Result result = await handler.HandleAsync(cluster.Id, request);
// Assert
result.IsFailure.Should().BeTrue();
KubernetesCluster? persisted = await repository.GetByIdAsync(cluster.Id);
ClusterComponent certManager = persisted!.Components.First(c => c.ComponentName == "cert-manager");
certManager.Status.Should().Be(ComponentStatus.NotInstalled);
}
}