using EntKube.Clusters.Domain; using EntKube.Clusters.Features.AdoptCluster; using EntKube.Clusters.Features.AdoptCluster.UpgradeComponent; using EntKube.Clusters.Infrastructure; using EntKube.SharedKernel.Domain; using FluentAssertions; using Moq; namespace EntKube.Clusters.Tests.Features; /// /// Tests the upgrade handler — the flow for upgrading an already-installed /// component to a specific version. Unlike deploy, upgrade validates that /// the component is installed and requires an explicit target version. /// public class UpgradeComponentHandlerTests { private readonly InMemoryClusterRepository repository; private readonly Mock harborInstaller; private readonly UpgradeComponentHandler handler; public UpgradeComponentHandlerTests() { repository = new InMemoryClusterRepository(); harborInstaller = new Mock(); harborInstaller.Setup(i => i.ComponentName).Returns("harbor"); List installers = new() { harborInstaller.Object }; handler = new UpgradeComponentHandler(repository, installers, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); } [Fact] public async Task HandleAsync_ClusterNotFound_ReturnsFailure() { // Arrange — Upgrading a component on a cluster that doesn't exist. UpgradeComponentRequest request = new("harbor", "1.17.0"); // Act Result result = await handler.HandleAsync(Guid.NewGuid(), request); // Assert result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("not found"); } [Fact] public async Task HandleAsync_ClusterNotConnected_ReturnsFailure() { // Arrange — Can't upgrade components on a disconnected cluster. KubernetesCluster cluster = KubernetesCluster.Register( "offline-cluster", "https://k8s.dev:6443", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); await repository.AddAsync(cluster); UpgradeComponentRequest request = new("harbor", "1.17.0"); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("connected"); } [Fact] public async Task HandleAsync_UnknownComponent_ReturnsFailure() { // Arrange — Trying to upgrade a component that has no installer. KubernetesCluster cluster = KubernetesCluster.Register( "test-cluster", "https://k8s.test:6443", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); await repository.AddAsync(cluster); UpgradeComponentRequest request = new("nonexistent", "1.0.0"); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("No installer found"); } [Fact] public async Task HandleAsync_ComponentNotInstalled_ReturnsFailure() { // Arrange — Harbor exists as a component but is NotInstalled. You // can't upgrade something that isn't there — use Deploy first. KubernetesCluster cluster = KubernetesCluster.Register( "test-cluster", "https://k8s.test:6443", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); List components = new() { new ComponentCheckResult("harbor", ComponentStatus.NotInstalled, new List(), new List { "Not found" }) }; cluster.UpdateComponents(components); await repository.AddAsync(cluster); UpgradeComponentRequest request = new("harbor", "1.17.0"); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("not installed"); } [Fact] public async Task HandleAsync_ComponentNotTracked_ReturnsFailure() { // Arrange — The cluster has no components tracked at all. The // component hasn't been scanned, so we don't know its state. KubernetesCluster cluster = KubernetesCluster.Register( "empty-cluster", "https://k8s.test:6443", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); await repository.AddAsync(cluster); UpgradeComponentRequest request = new("harbor", "1.17.0"); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("not installed"); } [Fact] public async Task HandleAsync_InstalledComponent_CallsInstallerAndUpdatesVersion() { // Arrange — Harbor is installed at 1.16.0. The operator wants to // upgrade to 1.17.0. The handler should call the installer with // the new version and update the tracked version on success. KubernetesCluster cluster = KubernetesCluster.Register( "prod-cluster", "https://k8s.prod:6443", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); List components = new() { new ComponentCheckResult("harbor", ComponentStatus.Installed, new List { "Running" }, new List(), new DiscoveredConfiguration("1.16.0", "harbor", "harbor", new Dictionary())) }; cluster.UpdateComponents(components); await repository.AddAsync(cluster); harborInstaller .Setup(i => i.InstallAsync(cluster, It.Is(o => o.Version == "1.17.0"), It.IsAny())) .ReturnsAsync(new InstallResult(true, "harbor", "Harbor upgraded to 1.17.0", new List { "Helm upgrade harbor v1.17.0" })); UpgradeComponentRequest request = new("harbor", "1.17.0"); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert — The handler should succeed and the tracked version // should be updated to reflect the upgrade. result.IsSuccess.Should().BeTrue(); result.Value!.Success.Should().BeTrue(); result.Value.Message.Should().Contain("1.17.0"); KubernetesCluster? updated = await repository.GetByIdAsync(cluster.Id); ClusterComponent harbor = updated!.Components.First(c => c.ComponentName == "harbor"); harbor.Version.Should().Be("1.17.0"); } [Fact] public async Task HandleAsync_InstallerFails_ReturnsFailureWithoutUpdatingVersion() { // Arrange — The upgrade attempt fails (e.g., Helm reports an error). // The tracked version should NOT be updated. KubernetesCluster cluster = KubernetesCluster.Register( "prod-cluster", "https://k8s.prod:6443", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); List components = new() { new ComponentCheckResult("harbor", ComponentStatus.Installed, new List { "Running" }, new List(), new DiscoveredConfiguration("1.16.0", "harbor", "harbor", new Dictionary())) }; cluster.UpdateComponents(components); await repository.AddAsync(cluster); harborInstaller .Setup(i => i.InstallAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult(false, "harbor", "Helm upgrade failed: chart version not found", new List())); UpgradeComponentRequest request = new("harbor", "99.99.99"); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert — Failure reported, version unchanged. result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("failed"); KubernetesCluster? unchanged = await repository.GetByIdAsync(cluster.Id); ClusterComponent harbor = unchanged!.Components.First(c => c.ComponentName == "harbor"); harbor.Version.Should().Be("1.16.0"); } [Fact] public async Task HandleAsync_PassesExistingNamespaceToInstaller() { // Arrange — When upgrading, the handler should pass the component's // existing namespace so Helm upgrades in the right place. KubernetesCluster cluster = KubernetesCluster.Register( "test-cluster", "https://k8s.test:6443", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); List components = new() { new ComponentCheckResult("harbor", ComponentStatus.Installed, new List(), new List(), new DiscoveredConfiguration("1.16.0", "custom-harbor-ns", "harbor", new Dictionary())) }; cluster.UpdateComponents(components); await repository.AddAsync(cluster); harborInstaller .Setup(i => i.InstallAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult(true, "harbor", "Upgraded", new List())); UpgradeComponentRequest request = new("harbor", "1.17.0"); // Act await handler.HandleAsync(cluster.Id, request); // Assert — The installer should receive the existing namespace. harborInstaller.Verify(i => i.InstallAsync( cluster, It.Is(o => o.Namespace == "custom-harbor-ns" && o.Version == "1.17.0"), It.IsAny()), Times.Once); } }