using EntKube.Clusters.Domain; using EntKube.Clusters.Features.AdoptCluster; using EntKube.Clusters.Features.AdoptCluster.ConfigureComponent; using EntKube.Clusters.Infrastructure; using EntKube.SharedKernel.Domain; using FluentAssertions; using Moq; namespace EntKube.Clusters.Tests.Features; /// /// Tests the configure handler with the new component installers. /// Verifies that reconfiguration requests route correctly and pass /// the right configuration values to each installer. /// public class ConfigureNewComponentsTests { 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 ConfigureComponentHandler handler; public ConfigureNewComponentsTests() { 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 ConfigureComponentHandler(repository, installers); } [Fact] public async Task HandleAsync_ConfigureCnpg_PassesMonitoringFlag() { // Arrange — Reconfigure CNPG to enable Prometheus monitoring. 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.ConfigureAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult(true, "cnpg", "CNPG reconfigured", new List { "monitoring=true" })); ConfigureComponentRequest request = new("cnpg", Values: new Dictionary { ["monitoringEnabled"] = "true", ["operatorReplicas"] = "2" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsSuccess.Should().BeTrue(); cnpgInstaller.Verify( i => i.ConfigureAsync(cluster, It.Is(c => c.Values["monitoringEnabled"] == "true" && c.Values["operatorReplicas"] == "2"), It.IsAny()), Times.Once); } [Fact] public async Task HandleAsync_ConfigureRedis_PassesSentinelAndPersistence() { // Arrange — Reconfigure Redis: scale replicas and change persistence size. 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.ConfigureAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult(true, "redis", "Redis reconfigured", new List { "replicas=5" })); ConfigureComponentRequest request = new("redis", Values: new Dictionary { ["replicaCount"] = "5", ["persistenceSize"] = "16Gi", ["maxMemory"] = "512mb" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsSuccess.Should().BeTrue(); redisInstaller.Verify( i => i.ConfigureAsync(cluster, It.Is(c => c.Values["replicaCount"] == "5" && c.Values["persistenceSize"] == "16Gi" && c.Values["maxMemory"] == "512mb"), It.IsAny()), Times.Once); } [Fact] public async Task HandleAsync_ConfigureCertManager_PassesDnsSolverConfig() { // Arrange — Switch cert-manager to use Route53 DNS01 solver. 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.ConfigureAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult(true, "cert-manager", "cert-manager reconfigured", new List { "Updated issuers with Route53 DNS01" })); ConfigureComponentRequest request = new("cert-manager", Values: new Dictionary { ["letsEncryptEmail"] = "ops@company.com", ["dnsSolverEnabled"] = "true", ["dnsSolverProvider"] = "route53", ["dnsSolverHostedZone"] = "Z1234567890" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsSuccess.Should().BeTrue(); certManagerInstaller.Verify( i => i.ConfigureAsync(cluster, It.Is(c => c.Values["dnsSolverProvider"] == "route53" && c.Values["dnsSolverHostedZone"] == "Z1234567890"), It.IsAny()), Times.Once); } [Fact] public async Task HandleAsync_ConfigureExternalSecrets_PassesStoreProvider() { // Arrange — Add an AWS Secrets Manager ClusterSecretStore. 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.ConfigureAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult(true, "external-secrets", "ESO reconfigured", new List { "ClusterSecretStore for AWS created" })); ConfigureComponentRequest request = new("external-secrets", Values: new Dictionary { ["storeProvider"] = "aws", ["storeRegion"] = "eu-north-1", ["storeSecretName"] = "aws-credentials" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsSuccess.Should().BeTrue(); externalSecretsInstaller.Verify( i => i.ConfigureAsync(cluster, It.Is(c => c.Values["storeProvider"] == "aws" && c.Values["storeRegion"] == "eu-north-1"), It.IsAny()), Times.Once); } [Fact] public async Task HandleAsync_ConfigureRabbitMQ_PassesTopologyConfig() { // Arrange — Enable topology operator on RabbitMQ. 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.ConfigureAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult(true, "rabbitmq", "RabbitMQ reconfigured", new List { "Topology operator enabled" })); ConfigureComponentRequest request = new("rabbitmq", Values: new Dictionary { ["topologyOperator"] = "true", ["operatorReplicas"] = "2" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsSuccess.Should().BeTrue(); rabbitmqInstaller.Verify( i => i.ConfigureAsync(cluster, It.Is(c => c.Values["topologyOperator"] == "true" && c.Values["operatorReplicas"] == "2"), It.IsAny()), Times.Once); } [Fact] public async Task HandleAsync_ConfigureCertManager_CaseInsensitiveName() { // Arrange — Component name matching should be case-insensitive. 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.ConfigureAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult(true, "cert-manager", "Reconfigured", new List())); ConfigureComponentRequest request = new("Cert-Manager", Values: new Dictionary { ["replicas"] = "2" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsSuccess.Should().BeTrue(); } [Fact] public async Task HandleAsync_ConfigureWithNamespace_PassesNamespaceThrough() { // Arrange — Specify a custom namespace for Redis reconfiguration. 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.ConfigureAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult(true, "redis", "Reconfigured", new List())); ConfigureComponentRequest request = new("redis", Namespace: "custom-redis-ns", Values: new Dictionary { ["replicaCount"] = "5" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsSuccess.Should().BeTrue(); redisInstaller.Verify( i => i.ConfigureAsync(cluster, It.Is(c => c.Namespace == "custom-redis-ns"), It.IsAny()), Times.Once); } [Fact] public async Task HandleAsync_ConfigureCustomDns_PassesStubZonesAndCorefile() { // Arrange — Reconfigure CoreDNS with new stub zones and custom Corefile entries. 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.ConfigureAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult(true, "custom-dns", "CoreDNS reconfigured", new List { "Stub zones updated", "Custom Corefile entries applied" })); ConfigureComponentRequest request = new("custom-dns", Values: new Dictionary { ["stubZones"] = "corp.internal=10.0.0.53;svc.internal=10.0.1.53", ["customCorefileEntries"] = "log\nrewrite name api.old.local api.new.local", ["customRecords"] = "myservice.local=10.10.10.10" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsSuccess.Should().BeTrue(); customDnsInstaller.Verify( i => i.ConfigureAsync(cluster, It.Is(c => c.Values["stubZones"] == "corp.internal=10.0.0.53;svc.internal=10.0.1.53" && c.Values["customCorefileEntries"] == "log\nrewrite name api.old.local api.new.local" && c.Values["customRecords"] == "myservice.local=10.10.10.10"), It.IsAny()), Times.Once); } [Fact] public async Task HandleAsync_WithSchema_RejectsInvalidIntegerValue() { // Arrange — CNPG has a schema requiring operatorReplicas as Integer with min/max. // Providing a non-numeric value should fail validation before reaching the installer. KubernetesCluster cluster = KubernetesCluster.Register( "prod-cluster", "https://k8s.example.com:6443", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); await repository.AddAsync(cluster); ConfigurationSchema cnpgSchema = ComponentSchemas.GetSchema("cnpg"); cnpgInstaller.Setup(i => i.GetSchema()).Returns(cnpgSchema); ConfigureComponentRequest request = new("cnpg", Values: new Dictionary { ["operatorReplicas"] = "not-a-number" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert — should fail validation without ever calling ConfigureAsync. result.IsSuccess.Should().BeFalse(); result.Error.Should().Contain("must be an integer"); cnpgInstaller.Verify( i => i.ConfigureAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task HandleAsync_WithSchema_RejectsValueOutOfRange() { // Arrange — Redis schema has maxReplicas=10 for replicas field. // Providing a value above max should fail. KubernetesCluster cluster = KubernetesCluster.Register( "prod-cluster", "https://k8s.example.com:6443", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); await repository.AddAsync(cluster); ConfigurationSchema redisSchema = ComponentSchemas.GetSchema("redis"); redisInstaller.Setup(i => i.GetSchema()).Returns(redisSchema); ConfigureComponentRequest request = new("redis", Values: new Dictionary { ["replicaCount"] = "99" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert result.IsSuccess.Should().BeFalse(); result.Error.Should().Contain("must be at most 10"); redisInstaller.Verify( i => i.ConfigureAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task HandleAsync_WithSchema_PassesValidConfiguration() { // Arrange — Provide valid values matching the CNPG schema. // Validation should pass and ConfigureAsync should be called. KubernetesCluster cluster = KubernetesCluster.Register( "prod-cluster", "https://k8s.example.com:6443", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); await repository.AddAsync(cluster); ConfigurationSchema cnpgSchema = ComponentSchemas.GetSchema("cnpg"); cnpgInstaller.Setup(i => i.GetSchema()).Returns(cnpgSchema); cnpgInstaller .Setup(i => i.ConfigureAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult(true, "cnpg", "Configured", new List())); ConfigureComponentRequest request = new("cnpg", Values: new Dictionary { ["operatorReplicas"] = "2", ["monitoringEnabled"] = "true" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert — validation passes, installer is called. result.IsSuccess.Should().BeTrue(); cnpgInstaller.Verify( i => i.ConfigureAsync(cluster, It.IsAny(), It.IsAny()), Times.Once); } }