using EntKube.Clusters.Domain; using EntKube.Clusters.Features.AdoptCluster; using EntKube.Clusters.Infrastructure; using EntKube.SharedKernel.Domain; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Moq; namespace EntKube.Clusters.Tests.Features; public class AdoptClusterHandlerTests { private readonly InMemoryClusterRepository repository; private readonly Mock kyvernoCheck; private readonly Mock securityPoliciesCheck; private readonly AdoptClusterHandler handler; public AdoptClusterHandlerTests() { repository = new InMemoryClusterRepository(); kyvernoCheck = new Mock(); securityPoliciesCheck = new Mock(); kyvernoCheck.Setup(c => c.ComponentName).Returns("kyverno"); securityPoliciesCheck.Setup(c => c.ComponentName).Returns("security-policies"); List checks = new() { kyvernoCheck.Object, securityPoliciesCheck.Object }; handler = new AdoptClusterHandler(repository, checks, NullLogger.Instance); } [Fact] public async Task HandleAsync_ClusterNotFound_ReturnsFailure() { // Arrange — Attempt to adopt a cluster that doesn't exist. Guid unknownId = Guid.NewGuid(); // Act Result result = await handler.HandleAsync(unknownId); // Assert result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("not found"); } [Fact] public async Task HandleAsync_ClusterNotConnected_ReturnsFailure() { // Arrange — The cluster exists but is still Pending (not yet reachable). KubernetesCluster cluster = KubernetesCluster.Register( "dev-cluster", "https://k8s.dev:6443", "kubeconfig-data", Guid.NewGuid(), "dev-ctx", Guid.NewGuid()); await repository.AddAsync(cluster); // Act Result result = await handler.HandleAsync(cluster.Id); // Assert result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("connected"); } [Fact] public async Task HandleAsync_AllComponentsInstalled_ReturnsReadyReport() { // Arrange — A connected cluster where all components report Installed. KubernetesCluster cluster = KubernetesCluster.Register( "prod-cluster", "https://k8s.prod:6443", "kubeconfig-data", Guid.NewGuid(), "prod-ctx", Guid.NewGuid()); cluster.MarkConnected(); await repository.AddAsync(cluster); kyvernoCheck.Setup(c => c.CheckAsync(cluster, It.IsAny())) .ReturnsAsync(new ComponentCheckResult("kyverno", ComponentStatus.Installed, new List { "Pod running: kyverno-admission-controller-0" }, new List())); securityPoliciesCheck.Setup(c => c.CheckAsync(cluster, It.IsAny())) .ReturnsAsync(new ComponentCheckResult("security-policies", ComponentStatus.Installed, new List { "Policy present: deny-loadbalancer-services" }, new List())); // Act Result result = await handler.HandleAsync(cluster.Id); // Assert — All components installed means Ready. result.IsSuccess.Should().BeTrue(); result.Value!.Status.Should().Be(AdoptionReadiness.Ready); result.Value.Components.Should().HaveCount(2); result.Value.Components.Should().AllSatisfy(c => c.Status.Should().Be(ComponentStatus.Installed)); } [Fact] public async Task HandleAsync_OneComponentNotInstalled_ReturnsNotReady() { // Arrange — Kyverno is missing entirely while policies exist (impossible // in practice, but tests the aggregation logic). KubernetesCluster cluster = KubernetesCluster.Register( "bare-cluster", "https://k8s.bare:6443", "kubeconfig-data", Guid.NewGuid(), "bare-ctx", Guid.NewGuid()); cluster.MarkConnected(); await repository.AddAsync(cluster); kyvernoCheck.Setup(c => c.CheckAsync(cluster, It.IsAny())) .ReturnsAsync(new ComponentCheckResult("kyverno", ComponentStatus.NotInstalled, new List(), new List { "No Kyverno pods found" })); securityPoliciesCheck.Setup(c => c.CheckAsync(cluster, It.IsAny())) .ReturnsAsync(new ComponentCheckResult("security-policies", ComponentStatus.NotInstalled, new List(), new List { "restrict-image-registries", "deny-loadbalancer-services" })); // Act Result result = await handler.HandleAsync(cluster.Id); // Assert — Any component NotInstalled means the cluster is NotReady. result.IsSuccess.Should().BeTrue(); result.Value!.Status.Should().Be(AdoptionReadiness.NotReady); } [Fact] public async Task HandleAsync_OneComponentDegraded_ReturnsPartiallyReady() { // Arrange — Kyverno is installed but security policies are only partially deployed. KubernetesCluster cluster = KubernetesCluster.Register( "partial-cluster", "https://k8s.partial:6443", "kubeconfig-data", Guid.NewGuid(), "partial-ctx", Guid.NewGuid()); cluster.MarkConnected(); await repository.AddAsync(cluster); kyvernoCheck.Setup(c => c.CheckAsync(cluster, It.IsAny())) .ReturnsAsync(new ComponentCheckResult("kyverno", ComponentStatus.Installed, new List { "Pod running: kyverno-admission-controller-0" }, new List())); securityPoliciesCheck.Setup(c => c.CheckAsync(cluster, It.IsAny())) .ReturnsAsync(new ComponentCheckResult("security-policies", ComponentStatus.Degraded, new List { "Policy present: deny-loadbalancer-services" }, new List { "restrict-image-registries", "require-seccomp-runtime-default" })); // Act Result result = await handler.HandleAsync(cluster.Id); // Assert — Degraded components mean PartiallyReady. result.IsSuccess.Should().BeTrue(); result.Value!.Status.Should().Be(AdoptionReadiness.PartiallyReady); } [Fact] public async Task HandleAsync_RunsAllChecksInParallel() { // Arrange — Verify that both checks are called for a connected cluster. KubernetesCluster cluster = KubernetesCluster.Register( "test-cluster", "https://k8s.test:6443", "kubeconfig-data", Guid.NewGuid(), "test-ctx", Guid.NewGuid()); cluster.MarkConnected(); await repository.AddAsync(cluster); kyvernoCheck.Setup(c => c.CheckAsync(cluster, It.IsAny())) .ReturnsAsync(new ComponentCheckResult("kyverno", ComponentStatus.Installed, new List(), new List())); securityPoliciesCheck.Setup(c => c.CheckAsync(cluster, It.IsAny())) .ReturnsAsync(new ComponentCheckResult("security-policies", ComponentStatus.Installed, new List(), new List())); // Act await handler.HandleAsync(cluster.Id); // Assert — Both checks were invoked. kyvernoCheck.Verify(c => c.CheckAsync(cluster, It.IsAny()), Times.Once); securityPoliciesCheck.Verify(c => c.CheckAsync(cluster, It.IsAny()), Times.Once); } }