mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-06-03 20:12:58 +00:00
219 lines
5.3 KiB
Go
219 lines
5.3 KiB
Go
package ha
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"containr/internal/deployment"
|
|
)
|
|
|
|
func TestHealthCheckerHTTPCheck(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/health" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer server.Close()
|
|
|
|
parsed, err := url.Parse(server.URL)
|
|
if err != nil {
|
|
t.Fatalf("failed to parse test server url: %v", err)
|
|
}
|
|
port := parsed.Port()
|
|
if port == "" {
|
|
t.Fatal("expected http test server to expose a port")
|
|
}
|
|
|
|
hc := &HealthChecker{}
|
|
check := &HealthCheck{
|
|
ID: "check-http",
|
|
Type: HealthCheckTypeHTTP,
|
|
Config: HealthCheckConfig{
|
|
Protocol: "http",
|
|
Port: mustAtoi(t, port),
|
|
Path: "/health",
|
|
Timeout: 2 * time.Second,
|
|
},
|
|
}
|
|
|
|
result := hc.performHealthCheck(context.Background(), check)
|
|
if result.Status != HealthStatusHealthy {
|
|
t.Fatalf("expected healthy status, got %s (%s)", result.Status, result.Message)
|
|
}
|
|
}
|
|
|
|
func TestHealthCheckerTCPCheck(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("failed to listen on tcp socket: %v", err)
|
|
}
|
|
defer ln.Close()
|
|
|
|
port := ln.Addr().(*net.TCPAddr).Port
|
|
|
|
hc := &HealthChecker{}
|
|
check := &HealthCheck{
|
|
ID: "check-tcp",
|
|
Type: HealthCheckTypeTCP,
|
|
Config: HealthCheckConfig{
|
|
Port: port,
|
|
Timeout: 2 * time.Second,
|
|
},
|
|
}
|
|
|
|
result := hc.performHealthCheck(context.Background(), check)
|
|
if result.Status != HealthStatusHealthy {
|
|
t.Fatalf("expected healthy status, got %s (%s)", result.Status, result.Message)
|
|
}
|
|
}
|
|
|
|
func TestHealthCheckerCommandCheck(t *testing.T) {
|
|
hc := &HealthChecker{}
|
|
|
|
okCheck := &HealthCheck{
|
|
ID: "check-cmd-ok",
|
|
Type: HealthCheckTypeCommand,
|
|
Config: HealthCheckConfig{
|
|
Command: "exit 0",
|
|
Timeout: 2 * time.Second,
|
|
},
|
|
}
|
|
okResult := hc.performHealthCheck(context.Background(), okCheck)
|
|
if okResult.Status != HealthStatusHealthy {
|
|
t.Fatalf("expected healthy status for command check, got %s (%s)", okResult.Status, okResult.Message)
|
|
}
|
|
|
|
failCheck := &HealthCheck{
|
|
ID: "check-cmd-fail",
|
|
Type: HealthCheckTypeCommand,
|
|
Config: HealthCheckConfig{
|
|
Command: "exit 1",
|
|
Timeout: 2 * time.Second,
|
|
},
|
|
}
|
|
failResult := hc.performHealthCheck(context.Background(), failCheck)
|
|
if failResult.Status != HealthStatusUnhealthy {
|
|
t.Fatalf("expected unhealthy status for failing command check, got %s", failResult.Status)
|
|
}
|
|
}
|
|
|
|
func TestWebhookAndSlackNotifierSend(t *testing.T) {
|
|
requests := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requests++
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer server.Close()
|
|
|
|
alert := &Alert{
|
|
ID: "alert-1",
|
|
RuleID: "rule-1",
|
|
Status: AlertStatusFiring,
|
|
Severity: AlertSeverityWarning,
|
|
Message: "cpu high",
|
|
StartsAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
webhook := &WebhookNotifier{URL: server.URL}
|
|
if err := webhook.Send(context.Background(), alert); err != nil {
|
|
t.Fatalf("webhook notifier send failed: %v", err)
|
|
}
|
|
|
|
slack := &SlackNotifier{WebhookURL: server.URL, Channel: "#ops"}
|
|
if err := slack.Send(context.Background(), alert); err != nil {
|
|
t.Fatalf("slack notifier send failed: %v", err)
|
|
}
|
|
|
|
if requests != 2 {
|
|
t.Fatalf("expected 2 webhook requests, got %d", requests)
|
|
}
|
|
}
|
|
|
|
func TestAlertManagerEvaluatesUnhealthyNodesMetric(t *testing.T) {
|
|
scheduler := deployment.NewScheduler()
|
|
if err := scheduler.RegisterNode(&deployment.Node{ID: "node-1", Name: "node-1", Status: "ready"}); err != nil {
|
|
t.Fatalf("failed to register node-1: %v", err)
|
|
}
|
|
if err := scheduler.RegisterNode(&deployment.Node{ID: "node-2", Name: "node-2", Status: "ready"}); err != nil {
|
|
t.Fatalf("failed to register node-2: %v", err)
|
|
}
|
|
|
|
nodes := scheduler.GetNodes()
|
|
if len(nodes) != 2 {
|
|
t.Fatalf("expected 2 nodes, got %d", len(nodes))
|
|
}
|
|
nodes[1].LastHeartbeat = time.Now().Add(-2 * time.Minute)
|
|
|
|
am := &AlertManager{
|
|
scheduler: scheduler,
|
|
rules: map[string]*AlertRule{},
|
|
activeAlerts: map[string]*Alert{},
|
|
notifiers: map[string]Notifier{},
|
|
metricsCollector: nil,
|
|
}
|
|
|
|
rule := &AlertRule{
|
|
ID: "rule-1",
|
|
Enabled: true,
|
|
Condition: AlertCondition{
|
|
Metric: "unhealthy_nodes",
|
|
Operator: ">",
|
|
Threshold: 0,
|
|
},
|
|
}
|
|
|
|
if !am.shouldTriggerAlert(rule) {
|
|
t.Fatalf("expected rule to trigger when unhealthy_nodes > 0")
|
|
}
|
|
}
|
|
|
|
func TestEmailNotifierValidatesRequiredFields(t *testing.T) {
|
|
notifier := &EmailNotifier{}
|
|
alert := &Alert{Message: "test"}
|
|
if err := notifier.Send(context.Background(), alert); err == nil {
|
|
t.Fatal("expected email notifier to reject missing smtp_host")
|
|
}
|
|
}
|
|
|
|
func TestCompareFloat(t *testing.T) {
|
|
if !compareFloat(5, ">", 1) {
|
|
t.Fatal("expected 5 > 1")
|
|
}
|
|
if !compareFloat(5, ">=", 5) {
|
|
t.Fatal("expected 5 >= 5")
|
|
}
|
|
if !compareFloat(1, "<", 2) {
|
|
t.Fatal("expected 1 < 2")
|
|
}
|
|
if !compareFloat(2, "<=", 2) {
|
|
t.Fatal("expected 2 <= 2")
|
|
}
|
|
if !compareFloat(3, "==", 3) {
|
|
t.Fatal("expected 3 == 3")
|
|
}
|
|
if !compareFloat(3, "!=", 4) {
|
|
t.Fatal("expected 3 != 4")
|
|
}
|
|
if compareFloat(3, "invalid", 4) {
|
|
t.Fatal("expected invalid operator to return false")
|
|
}
|
|
}
|
|
|
|
func mustAtoi(t *testing.T, raw string) int {
|
|
t.Helper()
|
|
v, err := strconv.Atoi(raw)
|
|
if err != nil {
|
|
t.Fatalf("failed to convert %q to int: %v", raw, err)
|
|
}
|
|
return v
|
|
}
|