using EntKube.Clusters.Domain; using EntKube.Clusters.Features.ClusterHealth; using FluentAssertions; namespace EntKube.Clusters.Tests.Features; public class ClusterHealthHandlerTests { private readonly FakePrometheusQueryClient fakeQueryClient; private readonly FakeClusterRepository repository; private readonly ClusterHealthHandler sut; public ClusterHealthHandlerTests() { fakeQueryClient = new FakePrometheusQueryClient(); repository = new FakeClusterRepository(); sut = new ClusterHealthHandler(repository, fakeQueryClient); } [Fact] public async Task HandleAsync_WithHealthyCluster_ReturnsAllMetrics() { // Arrange — a cluster with a Prometheus endpoint, all metrics looking good. KubernetesCluster cluster = KubernetesCluster.Register( "prod-cluster", "https://k8s.example.com", "kubeconfig-data", Guid.NewGuid(), "prod", Guid.NewGuid()); cluster.SetPrometheusEndpoints(new List { new("http://prometheus.monitoring.svc.cluster.local:9090", "monitoring", "prometheus") }); await repository.AddAsync(cluster); fakeQueryClient.SetResults(new Dictionary { ["count(kube_node_info)"] = 3, ["sum(kube_node_status_condition{condition=\"Ready\",status=\"true\"})"] = 3, ["(1 - avg(rate(node_cpu_seconds_total{mode=\"idle\"}[5m]))) * 100"] = 42.5, ["(1 - sum(node_memory_MemAvailable_bytes) / sum(node_memory_MemTotal_bytes)) * 100"] = 68.2, ["sum(kube_pod_status_phase{phase=\"Running\"})"] = 45, ["sum(kube_pod_status_phase{phase=\"Pending\"})"] = 2, ["sum(kube_pod_status_phase{phase=\"Failed\"})"] = 0, ["sum(increase(kube_pod_container_status_restarts_total[1h]))"] = 3, ["sum(rate(apiserver_request_total{code=~\"5..\"}[5m])) / sum(rate(apiserver_request_total[5m])) * 100"] = 0.1, ["sum(kube_node_status_condition{condition=\"DiskPressure\",status=\"true\"})"] = 0 }); // Act SharedKernel.Domain.Result result = await sut.HandleAsync(cluster.Id); // Assert — we get a complete health report. result.IsSuccess.Should().BeTrue(); ClusterHealthReport report = result.Value!; report.TotalNodes.Should().Be(3); report.ReadyNodes.Should().Be(3); report.CpuUtilizationPercent.Should().BeApproximately(42.5, 0.1); report.MemoryUtilizationPercent.Should().BeApproximately(68.2, 0.1); report.RunningPods.Should().Be(45); report.PendingPods.Should().Be(2); report.FailedPods.Should().Be(0); report.ContainerRestartsLastHour.Should().Be(3); report.ApiServerErrorRatePercent.Should().BeApproximately(0.1, 0.01); report.NodesWithDiskPressure.Should().Be(0); report.OverallStatus.Should().Be(ClusterHealthStatus.Healthy); } [Fact] public async Task HandleAsync_WithNoPrometheusEndpoints_ReturnsFailure() { // Arrange — cluster without Prometheus discovered yet. KubernetesCluster cluster = KubernetesCluster.Register( "no-prom", "https://k8s.example.com", "kubeconfig-data", Guid.NewGuid(), "ctx", Guid.NewGuid()); await repository.AddAsync(cluster); // Act SharedKernel.Domain.Result result = await sut.HandleAsync(cluster.Id); // Assert result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("No Prometheus endpoints"); } [Fact] public async Task HandleAsync_WithNonExistentCluster_ReturnsFailure() { // Act SharedKernel.Domain.Result result = await sut.HandleAsync(Guid.NewGuid()); // Assert result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("not found"); } [Fact] public async Task HandleAsync_WithDegradedMetrics_ReturnsDegradedStatus() { // Arrange — high CPU, some pending pods, and container restarts indicate degradation. KubernetesCluster cluster = KubernetesCluster.Register( "degraded-cluster", "https://k8s.example.com", "kubeconfig-data", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.SetPrometheusEndpoints(new List { new("http://prometheus.monitoring.svc.cluster.local:9090", "monitoring", "prometheus") }); await repository.AddAsync(cluster); fakeQueryClient.SetResults(new Dictionary { ["count(kube_node_info)"] = 3, ["sum(kube_node_status_condition{condition=\"Ready\",status=\"true\"})"] = 2, ["(1 - avg(rate(node_cpu_seconds_total{mode=\"idle\"}[5m]))) * 100"] = 85.0, ["(1 - sum(node_memory_MemAvailable_bytes) / sum(node_memory_MemTotal_bytes)) * 100"] = 78.0, ["sum(kube_pod_status_phase{phase=\"Running\"})"] = 40, ["sum(kube_pod_status_phase{phase=\"Pending\"})"] = 8, ["sum(kube_pod_status_phase{phase=\"Failed\"})"] = 2, ["sum(increase(kube_pod_container_status_restarts_total[1h]))"] = 15, ["sum(rate(apiserver_request_total{code=~\"5..\"}[5m])) / sum(rate(apiserver_request_total[5m])) * 100"] = 3.0, ["sum(kube_node_status_condition{condition=\"DiskPressure\",status=\"true\"})"] = 0 }); // Act SharedKernel.Domain.Result result = await sut.HandleAsync(cluster.Id); // Assert result.IsSuccess.Should().BeTrue(); result.Value!.OverallStatus.Should().Be(ClusterHealthStatus.Degraded); } [Fact] public async Task HandleAsync_WithCriticalMetrics_ReturnsCriticalStatus() { // Arrange — nodes not ready and high API error rate = critical. KubernetesCluster cluster = KubernetesCluster.Register( "critical-cluster", "https://k8s.example.com", "kubeconfig-data", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.SetPrometheusEndpoints(new List { new("http://prometheus.monitoring.svc.cluster.local:9090", "monitoring", "prometheus") }); await repository.AddAsync(cluster); fakeQueryClient.SetResults(new Dictionary { ["count(kube_node_info)"] = 3, ["sum(kube_node_status_condition{condition=\"Ready\",status=\"true\"})"] = 1, ["(1 - avg(rate(node_cpu_seconds_total{mode=\"idle\"}[5m]))) * 100"] = 95.0, ["(1 - sum(node_memory_MemAvailable_bytes) / sum(node_memory_MemTotal_bytes)) * 100"] = 92.0, ["sum(kube_pod_status_phase{phase=\"Running\"})"] = 10, ["sum(kube_pod_status_phase{phase=\"Pending\"})"] = 20, ["sum(kube_pod_status_phase{phase=\"Failed\"})"] = 15, ["sum(increase(kube_pod_container_status_restarts_total[1h]))"] = 50, ["sum(rate(apiserver_request_total{code=~\"5..\"}[5m])) / sum(rate(apiserver_request_total[5m])) * 100"] = 12.0, ["sum(kube_node_status_condition{condition=\"DiskPressure\",status=\"true\"})"] = 2 }); // Act SharedKernel.Domain.Result result = await sut.HandleAsync(cluster.Id); // Assert result.IsSuccess.Should().BeTrue(); result.Value!.OverallStatus.Should().Be(ClusterHealthStatus.Critical); } } /// /// Fake Prometheus query client for testing — returns preconfigured values /// for specific PromQL queries without needing a real Prometheus instance. /// public class FakePrometheusQueryClient : IPrometheusQueryClient { private Dictionary results = new(); public void SetResults(Dictionary queryResults) { results = queryResults; } public Task QueryScalarAsync(KubernetesCluster cluster, string prometheusUrl, string promql, CancellationToken ct = default) { if (results.TryGetValue(promql, out double value)) { return Task.FromResult(value); } return Task.FromResult(null); } } /// /// Simple fake repository for test isolation. /// public class FakeClusterRepository : IClusterRepository { private readonly List clusters = new(); public Task GetByIdAsync(Guid id, CancellationToken ct = default) { return Task.FromResult(clusters.FirstOrDefault(c => c.Id == id)); } public Task> GetAllAsync(CancellationToken ct = default) { return Task.FromResult>(clusters.AsReadOnly()); } public Task AddAsync(KubernetesCluster cluster, CancellationToken ct = default) { clusters.Add(cluster); return Task.CompletedTask; } public Task UpdateAsync(KubernetesCluster cluster, CancellationToken ct = default) { return Task.CompletedTask; } public Task DeleteAsync(Guid id, CancellationToken ct = default) { clusters.RemoveAll(c => c.Id == id); return Task.CompletedTask; } }