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