using EntKube.Provisioning.Domain; using FluentAssertions; namespace EntKube.Provisioning.Tests.Domain; public class PrometheusStackTests { private readonly Guid clusterId = Guid.NewGuid(); private readonly Guid environmentId = Guid.NewGuid(); // ─── Creation ─────────────────────────────────────────────────────── [Fact] public void Create_WithValidInputs_CreatesStackInPendingState() { // Arrange & Act — An operator provisions a new Prometheus stack on a cluster. // Prometheus is per-cluster: each cluster gets its own metrics collector. PrometheusStack stack = PrometheusStack.Create(clusterId, environmentId, "prod-prometheus", "monitoring"); // Assert — The stack should be in a Pending state, with sensible defaults // for Prometheus retention, replicas, and Alertmanager. stack.Id.Should().NotBe(Guid.Empty); stack.ClusterId.Should().Be(clusterId); stack.EnvironmentId.Should().Be(environmentId); stack.Name.Should().Be("prod-prometheus"); stack.Namespace.Should().Be("monitoring"); stack.Status.Should().Be(PrometheusStackStatus.Pending); stack.CreatedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5)); // Prometheus defaults stack.Prometheus.Should().NotBeNull(); stack.Prometheus.Retention.Should().Be("30d"); stack.Prometheus.Replicas.Should().Be(2); stack.Prometheus.StorageSize.Should().Be("50Gi"); // Alertmanager defaults stack.Alertmanager.Should().NotBeNull(); stack.Alertmanager.Enabled.Should().BeTrue(); stack.Alertmanager.Replicas.Should().Be(2); } [Fact] public void Create_WithEmptyName_ThrowsArgumentException() { // Arrange & Act — A Prometheus stack must have a name. Action act = () => PrometheusStack.Create(clusterId, environmentId, "", "monitoring"); // Assert act.Should().Throw() .WithParameterName("name"); } [Fact] public void Create_WithEmptyNamespace_ThrowsArgumentException() { // Arrange & Act — A Prometheus stack must have a namespace. Action act = () => PrometheusStack.Create(clusterId, environmentId, "prod-prometheus", ""); // Assert act.Should().Throw() .WithParameterName("ns"); } [Fact] public void Create_WithEmptyClusterId_ThrowsArgumentException() { // Arrange & Act — A Prometheus stack must belong to a cluster. Action act = () => PrometheusStack.Create(Guid.Empty, environmentId, "prod-prometheus", "monitoring"); // Assert act.Should().Throw() .WithParameterName("clusterId"); } [Fact] public void Create_WithEmptyEnvironmentId_ThrowsArgumentException() { // Arrange & Act — A Prometheus stack must belong to an environment. Action act = () => PrometheusStack.Create(clusterId, Guid.Empty, "prod-prometheus", "monitoring"); // Assert act.Should().Throw() .WithParameterName("environmentId"); } // ─── Prometheus Configuration ───────────────────────────────────── [Fact] public void ConfigurePrometheus_UpdatesRetentionAndReplicas() { // Arrange — Start with a Prometheus stack using defaults. PrometheusStack stack = PrometheusStack.Create(clusterId, environmentId, "prod-prometheus", "monitoring"); // Act — The operator increases retention to 90 days and scales to 3 replicas. stack.ConfigurePrometheus(retention: "90d", replicas: 3, storageSize: "100Gi", retentionSize: "90GB"); // Assert — Prometheus settings should reflect the new configuration. stack.Prometheus.Retention.Should().Be("90d"); stack.Prometheus.Replicas.Should().Be(3); stack.Prometheus.StorageSize.Should().Be("100Gi"); stack.Prometheus.RetentionSize.Should().Be("90GB"); } [Fact] public void ConfigurePrometheus_WithInvalidReplicas_ThrowsArgumentException() { // Arrange PrometheusStack stack = PrometheusStack.Create(clusterId, environmentId, "prod-prometheus", "monitoring"); // Act & Assert — Zero replicas doesn't make sense for Prometheus. Action act = () => stack.ConfigurePrometheus(replicas: 0); act.Should().Throw() .WithParameterName("replicas"); } // ─── Alertmanager Configuration ───────────────────────────────── [Fact] public void ConfigureAlertmanager_UpdatesReplicasAndEnablement() { // Arrange PrometheusStack stack = PrometheusStack.Create(clusterId, environmentId, "prod-prometheus", "monitoring"); // Act — The operator disables Alertmanager for a dev cluster. stack.ConfigureAlertmanager(enabled: false, replicas: 1); // Assert stack.Alertmanager.Enabled.Should().BeFalse(); stack.Alertmanager.Replicas.Should().Be(1); } // ─── Ingress (Prometheus + Alertmanager) ──────────────────────── [Fact] public void ConfigureIngress_SetsPrometheusAndAlertmanagerRouting() { // Arrange — Publish Prometheus and Alertmanager via Gateway API. PrometheusStack stack = PrometheusStack.Create(clusterId, environmentId, "prod-prometheus", "monitoring"); // Act IngressConfiguration ingress = new( Enabled: true, Provider: IngressProvider.GatewayApi, PrometheusHostname: "prometheus.prod01.example.com", AlertmanagerHostname: "alertmanager.prod01.example.com", TlsEnabled: true, TlsCertificateMode: TlsCertificateMode.Manual, TlsSecretName: "monitoring-tls"); stack.ConfigureIngress(ingress); // Assert stack.Ingress.Should().NotBeNull(); stack.Ingress!.Enabled.Should().BeTrue(); stack.Ingress.Provider.Should().Be(IngressProvider.GatewayApi); stack.Ingress.PrometheusHostname.Should().Be("prometheus.prod01.example.com"); stack.Ingress.AlertmanagerHostname.Should().Be("alertmanager.prod01.example.com"); stack.Ingress.TlsEnabled.Should().BeTrue(); stack.Ingress.TlsCertificateMode.Should().Be(TlsCertificateMode.Manual); stack.Ingress.TlsSecretName.Should().Be("monitoring-tls"); } [Fact] public void ConfigureIngress_WithCertManager_DoesNotRequireSecretName() { // Arrange PrometheusStack stack = PrometheusStack.Create(clusterId, environmentId, "prod-prometheus", "monitoring"); // Act — cert-manager with Let's Encrypt handles certificates automatically. IngressConfiguration ingress = new( Enabled: true, Provider: IngressProvider.GatewayApi, PrometheusHostname: "prometheus.example.com", AlertmanagerHostname: "alertmanager.example.com", TlsEnabled: true, TlsCertificateMode: TlsCertificateMode.CertManager, TlsSecretName: null, ClusterIssuer: "letsencrypt-prod"); stack.ConfigureIngress(ingress); // Assert stack.Ingress!.TlsCertificateMode.Should().Be(TlsCertificateMode.CertManager); stack.Ingress.TlsSecretName.Should().BeNull(); stack.Ingress.ClusterIssuer.Should().Be("letsencrypt-prod"); } [Fact] public void DisableIngress_ClearsIngressConfig() { // Arrange PrometheusStack stack = PrometheusStack.Create(clusterId, environmentId, "prod-prometheus", "monitoring"); IngressConfiguration ingress = new( Enabled: true, Provider: IngressProvider.GatewayApi, PrometheusHostname: "prometheus.example.com", AlertmanagerHostname: null, TlsEnabled: true, TlsCertificateMode: TlsCertificateMode.Manual, TlsSecretName: "tls-secret"); stack.ConfigureIngress(ingress); // Act stack.DisableIngress(); // Assert stack.Ingress.Should().BeNull(); } // ─── Lifecycle ────────────────────────────────────────────────── [Fact] public void MarkRunning_SetsStatusToRunning() { // Arrange PrometheusStack stack = PrometheusStack.Create(clusterId, environmentId, "prod-prometheus", "monitoring"); // Act stack.MarkRunning(); // Assert stack.Status.Should().Be(PrometheusStackStatus.Running); } [Fact] public void MarkDegraded_SetsStatusToDegraded() { // Arrange PrometheusStack stack = PrometheusStack.Create(clusterId, environmentId, "prod-prometheus", "monitoring"); // Act stack.MarkDegraded(); // Assert stack.Status.Should().Be(PrometheusStackStatus.Degraded); } }