Files
Entkube/tests/EntKube.Clusters.Tests/Features/ConfigureNewComponentsTests.cs
2026-05-13 14:01:32 +02:00

449 lines
17 KiB
C#

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;
/// <summary>
/// Tests the configure handler with the new component installers.
/// Verifies that reconfiguration requests route correctly and pass
/// the right configuration values to each installer.
/// </summary>
public class ConfigureNewComponentsTests
{
private readonly InMemoryClusterRepository repository;
private readonly Mock<IComponentInstaller> cnpgInstaller;
private readonly Mock<IComponentInstaller> redisInstaller;
private readonly Mock<IComponentInstaller> certManagerInstaller;
private readonly Mock<IComponentInstaller> externalSecretsInstaller;
private readonly Mock<IComponentInstaller> rabbitmqInstaller;
private readonly Mock<IComponentInstaller> customDnsInstaller;
private readonly ConfigureComponentHandler handler;
public ConfigureNewComponentsTests()
{
repository = new InMemoryClusterRepository();
cnpgInstaller = new Mock<IComponentInstaller>();
cnpgInstaller.Setup(i => i.ComponentName).Returns("cnpg");
redisInstaller = new Mock<IComponentInstaller>();
redisInstaller.Setup(i => i.ComponentName).Returns("redis");
certManagerInstaller = new Mock<IComponentInstaller>();
certManagerInstaller.Setup(i => i.ComponentName).Returns("cert-manager");
externalSecretsInstaller = new Mock<IComponentInstaller>();
externalSecretsInstaller.Setup(i => i.ComponentName).Returns("external-secrets");
rabbitmqInstaller = new Mock<IComponentInstaller>();
rabbitmqInstaller.Setup(i => i.ComponentName).Returns("rabbitmq");
customDnsInstaller = new Mock<IComponentInstaller>();
customDnsInstaller.Setup(i => i.ComponentName).Returns("custom-dns");
List<IComponentInstaller> 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<ComponentConfiguration>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new InstallResult(true, "cnpg", "CNPG reconfigured", new List<string> { "monitoring=true" }));
ConfigureComponentRequest request = new("cnpg", Values: new Dictionary<string, string>
{
["monitoringEnabled"] = "true",
["operatorReplicas"] = "2"
});
// Act
Result<InstallResult> result = await handler.HandleAsync(cluster.Id, request);
// Assert
result.IsSuccess.Should().BeTrue();
cnpgInstaller.Verify(
i => i.ConfigureAsync(cluster,
It.Is<ComponentConfiguration>(c =>
c.Values["monitoringEnabled"] == "true" &&
c.Values["operatorReplicas"] == "2"),
It.IsAny<CancellationToken>()),
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<ComponentConfiguration>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new InstallResult(true, "redis", "Redis reconfigured", new List<string> { "replicas=5" }));
ConfigureComponentRequest request = new("redis", Values: new Dictionary<string, string>
{
["replicaCount"] = "5",
["persistenceSize"] = "16Gi",
["maxMemory"] = "512mb"
});
// Act
Result<InstallResult> result = await handler.HandleAsync(cluster.Id, request);
// Assert
result.IsSuccess.Should().BeTrue();
redisInstaller.Verify(
i => i.ConfigureAsync(cluster,
It.Is<ComponentConfiguration>(c =>
c.Values["replicaCount"] == "5" &&
c.Values["persistenceSize"] == "16Gi" &&
c.Values["maxMemory"] == "512mb"),
It.IsAny<CancellationToken>()),
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<ComponentConfiguration>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new InstallResult(true, "cert-manager", "cert-manager reconfigured",
new List<string> { "Updated issuers with Route53 DNS01" }));
ConfigureComponentRequest request = new("cert-manager", Values: new Dictionary<string, string>
{
["letsEncryptEmail"] = "ops@company.com",
["dnsSolverEnabled"] = "true",
["dnsSolverProvider"] = "route53",
["dnsSolverHostedZone"] = "Z1234567890"
});
// Act
Result<InstallResult> result = await handler.HandleAsync(cluster.Id, request);
// Assert
result.IsSuccess.Should().BeTrue();
certManagerInstaller.Verify(
i => i.ConfigureAsync(cluster,
It.Is<ComponentConfiguration>(c =>
c.Values["dnsSolverProvider"] == "route53" &&
c.Values["dnsSolverHostedZone"] == "Z1234567890"),
It.IsAny<CancellationToken>()),
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<ComponentConfiguration>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new InstallResult(true, "external-secrets", "ESO reconfigured",
new List<string> { "ClusterSecretStore for AWS created" }));
ConfigureComponentRequest request = new("external-secrets", Values: new Dictionary<string, string>
{
["storeProvider"] = "aws",
["storeRegion"] = "eu-north-1",
["storeSecretName"] = "aws-credentials"
});
// Act
Result<InstallResult> result = await handler.HandleAsync(cluster.Id, request);
// Assert
result.IsSuccess.Should().BeTrue();
externalSecretsInstaller.Verify(
i => i.ConfigureAsync(cluster,
It.Is<ComponentConfiguration>(c =>
c.Values["storeProvider"] == "aws" &&
c.Values["storeRegion"] == "eu-north-1"),
It.IsAny<CancellationToken>()),
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<ComponentConfiguration>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new InstallResult(true, "rabbitmq", "RabbitMQ reconfigured",
new List<string> { "Topology operator enabled" }));
ConfigureComponentRequest request = new("rabbitmq", Values: new Dictionary<string, string>
{
["topologyOperator"] = "true",
["operatorReplicas"] = "2"
});
// Act
Result<InstallResult> result = await handler.HandleAsync(cluster.Id, request);
// Assert
result.IsSuccess.Should().BeTrue();
rabbitmqInstaller.Verify(
i => i.ConfigureAsync(cluster,
It.Is<ComponentConfiguration>(c =>
c.Values["topologyOperator"] == "true" &&
c.Values["operatorReplicas"] == "2"),
It.IsAny<CancellationToken>()),
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<ComponentConfiguration>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new InstallResult(true, "cert-manager", "Reconfigured", new List<string>()));
ConfigureComponentRequest request = new("Cert-Manager", Values: new Dictionary<string, string>
{
["replicas"] = "2"
});
// Act
Result<InstallResult> 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<ComponentConfiguration>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new InstallResult(true, "redis", "Reconfigured", new List<string>()));
ConfigureComponentRequest request = new("redis", Namespace: "custom-redis-ns", Values: new Dictionary<string, string>
{
["replicaCount"] = "5"
});
// Act
Result<InstallResult> result = await handler.HandleAsync(cluster.Id, request);
// Assert
result.IsSuccess.Should().BeTrue();
redisInstaller.Verify(
i => i.ConfigureAsync(cluster,
It.Is<ComponentConfiguration>(c => c.Namespace == "custom-redis-ns"),
It.IsAny<CancellationToken>()),
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<ComponentConfiguration>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new InstallResult(true, "custom-dns", "CoreDNS reconfigured",
new List<string> { "Stub zones updated", "Custom Corefile entries applied" }));
ConfigureComponentRequest request = new("custom-dns", Values: new Dictionary<string, string>
{
["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<InstallResult> result = await handler.HandleAsync(cluster.Id, request);
// Assert
result.IsSuccess.Should().BeTrue();
customDnsInstaller.Verify(
i => i.ConfigureAsync(cluster,
It.Is<ComponentConfiguration>(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<CancellationToken>()),
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<string, string>
{
["operatorReplicas"] = "not-a-number"
});
// Act
Result<InstallResult> 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<KubernetesCluster>(), It.IsAny<ComponentConfiguration>(), It.IsAny<CancellationToken>()),
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<string, string>
{
["replicaCount"] = "99"
});
// Act
Result<InstallResult> 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<KubernetesCluster>(), It.IsAny<ComponentConfiguration>(), It.IsAny<CancellationToken>()),
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<ComponentConfiguration>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new InstallResult(true, "cnpg", "Configured", new List<string>()));
ConfigureComponentRequest request = new("cnpg", Values: new Dictionary<string, string>
{
["operatorReplicas"] = "2",
["monitoringEnabled"] = "true"
});
// Act
Result<InstallResult> 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<ComponentConfiguration>(), It.IsAny<CancellationToken>()),
Times.Once);
}
}