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