Files
Containr/app/backend/internal/ha/manager_test.go
T
2026-04-10 12:02:36 +02:00

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
}