package checker import ( "context" "net" "testing" "time" "git.ipng.ch/ipng/vpp-maglev/internal/config" "git.ipng.ch/ipng/vpp-maglev/internal/health" ) func makeTestConfig(interval time.Duration, fall, rise int) *config.Config { return &config.Config{ HealthChecker: config.HealthCheckerConfig{TransitionHistory: 5}, HealthChecks: map[string]config.HealthCheck{ "icmp": { Type: "icmp", Interval: interval, Timeout: time.Second, Fall: fall, Rise: rise, }, }, Backends: map[string]config.Backend{ "be0": { Address: net.ParseIP("10.0.0.2"), HealthCheck: "icmp", Enabled: true, Weight: 100, }, }, Frontends: map[string]config.Frontend{ "web": { Address: net.ParseIP("192.0.2.1"), Protocol: "tcp", Port: 80, Backends: []string{"be0"}, }, }, } } func TestHealthCheckEqual(t *testing.T) { a := config.HealthCheck{ Type: "http", Interval: time.Second, Timeout: 2 * time.Second, Fall: 3, Rise: 2, HTTP: &config.HTTPParams{Path: "/healthz", ResponseCodeMin: 200, ResponseCodeMax: 200}, } b := a if !healthCheckEqual(a, b) { t.Error("identical configs should be equal") } b.Fall = 5 if healthCheckEqual(a, b) { t.Error("different Fall should not be equal") } b = a b.FastInterval = 500 * time.Millisecond if healthCheckEqual(a, b) { t.Error("different FastInterval should not be equal") } b = a b.HTTP = &config.HTTPParams{Path: "/other", ResponseCodeMin: 200, ResponseCodeMax: 200} if healthCheckEqual(a, b) { t.Error("different HTTP.Path should not be equal") } } func TestStateMachineViaBackend(t *testing.T) { b := health.New("be0", net.ParseIP("10.0.0.2"), 2, 3) pass := health.ProbeResult{OK: true, Layer: health.LayerL7, Code: "L7OK"} fail := health.ProbeResult{OK: false, Layer: health.LayerL4, Code: "L4CON"} if !b.Record(fail, 5) { t.Error("first fail from Unknown should transition to Down") } if b.State != health.StateDown { t.Errorf("expected down, got %s", b.State) } if b.Record(pass, 5) { t.Error("should not transition after 1 pass (rise=2)") } if !b.Record(pass, 5) { t.Error("should transition to Up after 2 passes") } if b.State != health.StateUp { t.Errorf("expected up, got %s", b.State) } } func TestStaggerDelay(t *testing.T) { interval := 10 * time.Second if got := staggerDelay(interval, 0, 10); got != 0 { t.Errorf("pos=0: got %v, want 0", got) } if got := staggerDelay(interval, 5, 10); got != 5*time.Second { t.Errorf("pos=5/10: got %v, want 5s", got) } if got := staggerDelay(interval, 0, 1); got != 0 { t.Errorf("total=1: got %v, want 0", got) } } func TestReloadAddsBackend(t *testing.T) { cfg := makeTestConfig(10*time.Millisecond, 3, 2) c := New(cfg) newCfg := makeTestConfig(10*time.Millisecond, 3, 2) newCfg.Backends["be1"] = config.Backend{ Address: net.ParseIP("10.0.0.3"), HealthCheck: "icmp", Enabled: true, Weight: 100, } newCfg.Frontends["web2"] = config.Frontend{ Address: net.ParseIP("192.0.2.2"), Protocol: "tcp", Port: 443, Backends: []string{"be1"}, } ctx, cancel := context.WithCancel(context.Background()) cancel() if err := c.Reload(ctx, newCfg); err != nil { t.Fatalf("Reload: %v", err) } c.mu.RLock() _, ok := c.workers["be1"] c.mu.RUnlock() if !ok { t.Error("new backend not added after Reload") } } func TestReloadRemovesBackend(t *testing.T) { cfg := makeTestConfig(10*time.Millisecond, 3, 2) c := New(cfg) ctx, cancel := context.WithCancel(context.Background()) cancel() // Seed a worker manually. c.mu.Lock() wCtx, wCancel := context.WithCancel(context.Background()) c.workers["be0"] = &worker{ backend: health.New("be0", net.ParseIP("10.0.0.2"), 2, 3), hc: cfg.HealthChecks["icmp"], entry: cfg.Backends["be0"], cancel: wCancel, } c.mu.Unlock() _ = wCtx // Remove all frontends → be0 is no longer active. newCfg := &config.Config{ HealthChecker: cfg.HealthChecker, HealthChecks: cfg.HealthChecks, Backends: cfg.Backends, Frontends: map[string]config.Frontend{}, } if err := c.Reload(ctx, newCfg); err != nil { t.Fatalf("Reload: %v", err) } c.mu.RLock() _, ok := c.workers["be0"] c.mu.RUnlock() if ok { t.Error("removed backend still present after Reload") } } func TestSharedBackendProbedOnce(t *testing.T) { // be0 is referenced by two frontends — only one worker should exist. cfg := makeTestConfig(10*time.Millisecond, 3, 2) cfg.Frontends["web-tls"] = config.Frontend{ Address: net.ParseIP("192.0.2.3"), Protocol: "tcp", Port: 443, Backends: []string{"be0"}, } c := New(cfg) names := activeBackendNames(c.cfg) if len(names) != 1 || names[0] != "be0" { t.Errorf("expected exactly one active backend, got %v", names) } } func TestSubscribe(t *testing.T) { cfg := makeTestConfig(10*time.Millisecond, 1, 1) c := New(cfg) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go c.fanOut(ctx) ch, unsub := c.Subscribe() defer unsub() e := Event{ FrontendName: "web", BackendName: "be0", Backend: net.ParseIP("10.0.0.2"), Transition: health.Transition{ From: health.StateUnknown, To: health.StateUp, }, } c.mu.Lock() c.emit(e) c.mu.Unlock() select { case got := <-ch: if got.FrontendName != "web" { t.Errorf("event FrontendName: got %q, want web", got.FrontendName) } if got.BackendName != "be0" { t.Errorf("event BackendName: got %q, want be0", got.BackendName) } if got.Transition.To != health.StateUp { t.Errorf("event To state: got %s, want up", got.Transition.To) } case <-time.After(time.Second): t.Error("timed out waiting for event") } } func TestPauseResume(t *testing.T) { cfg := makeTestConfig(time.Hour, 3, 2) c := New(cfg) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go c.fanOut(ctx) c.mu.Lock() _, wCancel := context.WithCancel(ctx) c.workers["be0"] = &worker{ backend: health.New("be0", net.ParseIP("10.0.0.2"), 2, 3), hc: cfg.HealthChecks["icmp"], entry: cfg.Backends["be0"], cancel: wCancel, } c.mu.Unlock() b, ok := c.PauseBackend("be0") if !ok { t.Fatal("PauseBackend: not found") } if b.Health.State != health.StatePaused { t.Errorf("after pause: %s", b.Health.State) } b, ok = c.ResumeBackend("be0") if !ok { t.Fatal("ResumeBackend: not found") } if b.Health.State != health.StateUnknown { t.Errorf("after resume: %s", b.Health.State) } }