too much for one commit
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
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<PrometheusEndpoint>
|
||||
{
|
||||
new("http://prometheus.monitoring.svc.cluster.local:9090", "monitoring", "prometheus")
|
||||
});
|
||||
|
||||
await repository.AddAsync(cluster);
|
||||
|
||||
fakeQueryClient.SetResults(new Dictionary<string, double>
|
||||
{
|
||||
["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<ClusterHealthReport> 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<ClusterHealthReport> 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<ClusterHealthReport> 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<PrometheusEndpoint>
|
||||
{
|
||||
new("http://prometheus.monitoring.svc.cluster.local:9090", "monitoring", "prometheus")
|
||||
});
|
||||
|
||||
await repository.AddAsync(cluster);
|
||||
|
||||
fakeQueryClient.SetResults(new Dictionary<string, double>
|
||||
{
|
||||
["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<ClusterHealthReport> 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<PrometheusEndpoint>
|
||||
{
|
||||
new("http://prometheus.monitoring.svc.cluster.local:9090", "monitoring", "prometheus")
|
||||
});
|
||||
|
||||
await repository.AddAsync(cluster);
|
||||
|
||||
fakeQueryClient.SetResults(new Dictionary<string, double>
|
||||
{
|
||||
["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<ClusterHealthReport> result = await sut.HandleAsync(cluster.Id);
|
||||
|
||||
// Assert
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value!.OverallStatus.Should().Be(ClusterHealthStatus.Critical);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fake Prometheus query client for testing — returns preconfigured values
|
||||
/// for specific PromQL queries without needing a real Prometheus instance.
|
||||
/// </summary>
|
||||
public class FakePrometheusQueryClient : IPrometheusQueryClient
|
||||
{
|
||||
private Dictionary<string, double> results = new();
|
||||
|
||||
public void SetResults(Dictionary<string, double> queryResults)
|
||||
{
|
||||
results = queryResults;
|
||||
}
|
||||
|
||||
public Task<double?> QueryScalarAsync(KubernetesCluster cluster, string prometheusUrl, string promql, CancellationToken ct = default)
|
||||
{
|
||||
if (results.TryGetValue(promql, out double value))
|
||||
{
|
||||
return Task.FromResult<double?>(value);
|
||||
}
|
||||
|
||||
return Task.FromResult<double?>(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple fake repository for test isolation.
|
||||
/// </summary>
|
||||
public class FakeClusterRepository : IClusterRepository
|
||||
{
|
||||
private readonly List<KubernetesCluster> clusters = new();
|
||||
|
||||
public Task<KubernetesCluster?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult(clusters.FirstOrDefault(c => c.Id == id));
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<KubernetesCluster>> GetAllAsync(CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<KubernetesCluster>>(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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user