using EntKube.Clusters.Domain; using EntKube.Clusters.Features.AdoptCluster; using EntKube.Clusters.Features.AdoptCluster.DeployComponent; using EntKube.Clusters.Infrastructure; using EntKube.SharedKernel.Domain; using FluentAssertions; using Moq; namespace EntKube.Clusters.Tests.Features; /// /// Tests the deploy handler with all new component installers registered. /// Verifies that the handler correctly routes to the right installer based on /// component name and passes parameters through. /// public class DeployNewComponentsTests { private readonly InMemoryClusterRepository repository; private readonly Mock cnpgInstaller; private readonly Mock redisInstaller; private readonly Mock certManagerInstaller; private readonly Mock externalSecretsInstaller; private readonly Mock rabbitmqInstaller; private readonly Mock customDnsInstaller; private readonly DeployComponentHandler handler; public DeployNewComponentsTests() { repository = new InMemoryClusterRepository(); cnpgInstaller = new Mock(); cnpgInstaller.Setup(i => i.ComponentName).Returns("cnpg"); redisInstaller = new Mock(); redisInstaller.Setup(i => i.ComponentName).Returns("redis"); certManagerInstaller = new Mock(); certManagerInstaller.Setup(i => i.ComponentName).Returns("cert-manager"); externalSecretsInstaller = new Mock(); externalSecretsInstaller.Setup(i => i.ComponentName).Returns("external-secrets"); rabbitmqInstaller = new Mock(); rabbitmqInstaller.Setup(i => i.ComponentName).Returns("rabbitmq"); customDnsInstaller = new Mock(); customDnsInstaller.Setup(i => i.ComponentName).Returns("custom-dns"); List installers = new() { cnpgInstaller.Object, redisInstaller.Object, certManagerInstaller.Object, externalSecretsInstaller.Object, rabbitmqInstaller.Object, customDnsInstaller.Object }; handler = new DeployComponentHandler(repository, installers, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); } [Fact] public async Task HandleAsync_DeployCnpg_CallsInstaller() { // Arrange — A connected cluster wants CloudNativePG installed. KubernetesCluster cluster = KubernetesCluster.Register( "prod-cluster", "https://k8s.example.com:6443", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); await repository.AddAsync(cluster); cnpgInstaller .Setup(i => i.InstallAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult(true, "cnpg", "CloudNativePG 0.22.0 installed", new List { "Helm install" })); DeployComponentRequest request = new("cnpg", Version: "0.22.0"); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsSuccess.Should().BeTrue(); result.Value!.ComponentName.Should().Be("cnpg"); cnpgInstaller.Verify( i => i.InstallAsync(cluster, It.Is(o => o.Version == "0.22.0"), It.IsAny()), Times.Once); } [Fact] public async Task HandleAsync_DeployRedis_PassesSentinelParameter() { // Arrange — Deploy Redis with Sentinel HA enabled. KubernetesCluster cluster = KubernetesCluster.Register( "prod-cluster", "https://k8s.example.com:6443", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); await repository.AddAsync(cluster); redisInstaller .Setup(i => i.InstallAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult(true, "redis", "Redis installed", new List { "Helm install" })); DeployComponentRequest request = new("redis", Parameters: new Dictionary { ["sentinelEnabled"] = "true", ["replicaCount"] = "3" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsSuccess.Should().BeTrue(); redisInstaller.Verify( i => i.InstallAsync(cluster, It.Is(o => o.Parameters!["sentinelEnabled"] == "true" && o.Parameters["replicaCount"] == "3"), It.IsAny()), Times.Once); } [Fact] public async Task HandleAsync_DeployCertManager_PassesLetsEncryptConfig() { // Arrange — Deploy cert-manager with Let's Encrypt + Cloudflare DNS01. KubernetesCluster cluster = KubernetesCluster.Register( "prod-cluster", "https://k8s.example.com:6443", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); await repository.AddAsync(cluster); certManagerInstaller .Setup(i => i.InstallAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult(true, "cert-manager", "cert-manager installed with LE issuers", new List { "Helm install", "ClusterIssuer created" })); DeployComponentRequest request = new("cert-manager", Parameters: new Dictionary { ["letsEncryptEmail"] = "admin@example.com", ["httpSolverEnabled"] = "true", ["dnsSolverEnabled"] = "true", ["dnsSolverProvider"] = "cloudflare", ["dnsSolverSecretName"] = "cloudflare-api-token" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsSuccess.Should().BeTrue(); certManagerInstaller.Verify( i => i.InstallAsync(cluster, It.Is(o => o.Parameters!["letsEncryptEmail"] == "admin@example.com" && o.Parameters["dnsSolverProvider"] == "cloudflare"), It.IsAny()), Times.Once); } [Fact] public async Task HandleAsync_DeployExternalSecrets_PassesVaultConfig() { // Arrange — Deploy ESO with Vault as the secret store provider. KubernetesCluster cluster = KubernetesCluster.Register( "prod-cluster", "https://k8s.example.com:6443", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); await repository.AddAsync(cluster); externalSecretsInstaller .Setup(i => i.InstallAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult(true, "external-secrets", "ESO installed", new List { "Helm install", "ClusterSecretStore created" })); DeployComponentRequest request = new("external-secrets", Parameters: new Dictionary { ["storeProvider"] = "vault", ["storeEndpoint"] = "http://vault.vault:8200" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsSuccess.Should().BeTrue(); externalSecretsInstaller.Verify( i => i.InstallAsync(cluster, It.Is(o => o.Parameters!["storeProvider"] == "vault" && o.Parameters["storeEndpoint"] == "http://vault.vault:8200"), It.IsAny()), Times.Once); } [Fact] public async Task HandleAsync_DeployRabbitMQ_PassesTopologyOperatorParam() { // Arrange — Deploy RabbitMQ with messaging topology operator. KubernetesCluster cluster = KubernetesCluster.Register( "prod-cluster", "https://k8s.example.com:6443", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); await repository.AddAsync(cluster); rabbitmqInstaller .Setup(i => i.InstallAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult(true, "rabbitmq", "RabbitMQ operator installed", new List { "Helm install", "Topology operator enabled" })); DeployComponentRequest request = new("rabbitmq", Parameters: new Dictionary { ["topologyOperator"] = "true", ["monitoringEnabled"] = "true" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsSuccess.Should().BeTrue(); rabbitmqInstaller.Verify( i => i.InstallAsync(cluster, It.Is(o => o.Parameters!["topologyOperator"] == "true"), It.IsAny()), Times.Once); } [Fact] public async Task HandleAsync_DisconnectedCluster_ReturnsFailure() { // Arrange — A cluster that isn't connected can't have components deployed. KubernetesCluster cluster = KubernetesCluster.Register( "offline-cluster", "https://k8s.example.com:6443", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); await repository.AddAsync(cluster); DeployComponentRequest request = new("cnpg"); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("connected"); } [Fact] public async Task HandleAsync_InstallerFails_ReturnsFailure() { // Arrange — The installer reports a failure. KubernetesCluster cluster = KubernetesCluster.Register( "prod-cluster", "https://k8s.example.com:6443", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); await repository.AddAsync(cluster); redisInstaller .Setup(i => i.InstallAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult(false, "redis", "Helm timeout", new List { "Error: timeout" })); DeployComponentRequest request = new("redis"); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("Helm timeout"); } [Fact] public async Task HandleAsync_DeployCustomDns_PassesStubZonesAndRecords() { // Arrange — Deploy custom DNS with stub zones and static records. KubernetesCluster cluster = KubernetesCluster.Register( "prod-cluster", "https://k8s.example.com:6443", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); await repository.AddAsync(cluster); customDnsInstaller .Setup(i => i.InstallAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult(true, "custom-dns", "CoreDNS customized", new List { "Stub zones configured", "Custom records applied" })); DeployComponentRequest request = new("custom-dns", Parameters: new Dictionary { ["stubZones"] = "corp.internal=10.0.0.53,10.0.0.54;db.internal=10.1.0.53", ["customRecords"] = "api.local=192.168.1.100;cache.local=192.168.1.101" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsSuccess.Should().BeTrue(); result.Value!.ComponentName.Should().Be("custom-dns"); customDnsInstaller.Verify( i => i.InstallAsync(cluster, It.Is(o => o.Parameters!["stubZones"] == "corp.internal=10.0.0.53,10.0.0.54;db.internal=10.1.0.53" && o.Parameters["customRecords"] == "api.local=192.168.1.100;cache.local=192.168.1.101"), It.IsAny()), Times.Once); } }