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