rdev/internal/worker/external_health_test.go
jordan 210064d490 feat: add diagnostics endpoint and external health monitoring
- Add /diagnostics endpoint for system health overview
- Add external health worker for monitoring Gitea, Woodpecker, Registry
- Add health check methods to Gitea and Woodpecker clients
- Remove hardcoded fallback projects (pantheon, aeries)
- Add diagnostics domain types and service layer
- Add comprehensive tests for diagnostics handler and service
- Fix tests to use registered test project instead of hardcoded one

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 19:10:56 -07:00

242 lines
6.7 KiB
Go

package worker
import (
"context"
"testing"
"time"
"github.com/orchard9/rdev/internal/domain"
)
// mockRegistryChecker is a mock implementation of port.RegistryChecker.
type mockRegistryChecker struct {
healthy bool
err string
latency time.Duration
}
func (m *mockRegistryChecker) Check(_ context.Context) domain.RegistryStatus {
status := domain.RegistryStatus{
Healthy: m.healthy,
URL: "https://registry.test",
Latency: m.latency.String(),
LastChecked: time.Now().UTC(),
}
if !m.healthy {
status.Error = m.err
}
return status
}
// mockExternalHealthChecker is a mock implementation of port.ExternalHealthChecker.
type mockExternalHealthChecker struct {
system domain.ExternalSystem
healthy bool
err string
latency time.Duration
}
func (m *mockExternalHealthChecker) Check(_ context.Context) domain.ExternalSystemStatus {
status := domain.ExternalSystemStatus{
System: m.system,
Healthy: m.healthy,
URL: "https://test.system",
Latency: m.latency,
LastChecked: time.Now().UTC(),
}
if m.healthy {
status.LastHealthy = status.LastChecked
} else {
status.Error = m.err
}
return status
}
func TestExternalHealthChecker_GetStatus(t *testing.T) {
registry := &mockRegistryChecker{healthy: true, latency: 50 * time.Millisecond}
ci := &mockExternalHealthChecker{system: domain.ExternalSystemCI, healthy: true, latency: 100 * time.Millisecond}
git := &mockExternalHealthChecker{system: domain.ExternalSystemGit, healthy: true, latency: 75 * time.Millisecond}
checker := NewExternalHealthChecker(registry, ci, git, ExternalHealthConfig{
CheckInterval: 100 * time.Millisecond,
})
// Run checks synchronously
checker.runChecks()
// Verify registry status
regStatus, ok := checker.GetStatus(domain.ExternalSystemRegistry)
if !ok {
t.Fatal("expected registry status to exist")
}
if !regStatus.Healthy {
t.Error("expected registry to be healthy")
}
// Verify CI status
ciStatus, ok := checker.GetStatus(domain.ExternalSystemCI)
if !ok {
t.Fatal("expected CI status to exist")
}
if !ciStatus.Healthy {
t.Error("expected CI to be healthy")
}
// Verify Git status
gitStatus, ok := checker.GetStatus(domain.ExternalSystemGit)
if !ok {
t.Fatal("expected Git status to exist")
}
if !gitStatus.Healthy {
t.Error("expected Git to be healthy")
}
}
func TestExternalHealthChecker_GetAllStatuses(t *testing.T) {
registry := &mockRegistryChecker{healthy: true}
ci := &mockExternalHealthChecker{system: domain.ExternalSystemCI, healthy: false, err: "connection refused"}
git := &mockExternalHealthChecker{system: domain.ExternalSystemGit, healthy: true}
checker := NewExternalHealthChecker(registry, ci, git, ExternalHealthConfig{
CheckInterval: 100 * time.Millisecond,
})
checker.runChecks()
statuses := checker.GetAllStatuses()
if len(statuses) != 3 {
t.Fatalf("expected 3 statuses, got %d", len(statuses))
}
if statuses[domain.ExternalSystemRegistry].Healthy != true {
t.Error("expected registry to be healthy")
}
if statuses[domain.ExternalSystemCI].Healthy != false {
t.Error("expected CI to be unhealthy")
}
if statuses[domain.ExternalSystemCI].Error != "connection refused" {
t.Errorf("expected CI error 'connection refused', got %q", statuses[domain.ExternalSystemCI].Error)
}
if statuses[domain.ExternalSystemGit].Healthy != true {
t.Error("expected Git to be healthy")
}
}
func TestExternalHealthChecker_NilCheckers(t *testing.T) {
// All nil checkers should result in empty statuses
checker := NewExternalHealthChecker(nil, nil, nil, ExternalHealthConfig{
CheckInterval: 100 * time.Millisecond,
})
checker.runChecks()
statuses := checker.GetAllStatuses()
if len(statuses) != 0 {
t.Fatalf("expected 0 statuses with nil checkers, got %d", len(statuses))
}
}
func TestExternalHealthChecker_StartStop(t *testing.T) {
registry := &mockRegistryChecker{healthy: true}
checker := NewExternalHealthChecker(registry, nil, nil, ExternalHealthConfig{
CheckInterval: 50 * time.Millisecond,
})
checker.Start()
// Wait for a couple of check cycles
time.Sleep(120 * time.Millisecond)
// Verify status was populated
status, ok := checker.GetStatus(domain.ExternalSystemRegistry)
if !ok {
t.Fatal("expected registry status after start")
}
if !status.Healthy {
t.Error("expected registry to be healthy")
}
checker.Stop()
// After stop, statuses should still be available (cached)
status, ok = checker.GetStatus(domain.ExternalSystemRegistry)
if !ok {
t.Fatal("expected registry status after stop")
}
}
func TestExternalHealthChecker_StateTransition(t *testing.T) {
registry := &mockRegistryChecker{healthy: true}
checker := NewExternalHealthChecker(registry, nil, nil, ExternalHealthConfig{
CheckInterval: 100 * time.Millisecond,
})
// Initial check - healthy
checker.runChecks()
status, _ := checker.GetStatus(domain.ExternalSystemRegistry)
if !status.Healthy {
t.Error("expected initial status to be healthy")
}
firstHealthy := status.LastHealthy
// Change to unhealthy
registry.healthy = false
registry.err = "connection refused"
checker.runChecks()
status, _ = checker.GetStatus(domain.ExternalSystemRegistry)
if status.Healthy {
t.Error("expected status to be unhealthy after state change")
}
// LastHealthy should be preserved from when it was healthy
if status.LastHealthy.IsZero() {
t.Error("expected LastHealthy to be preserved")
}
if !status.LastHealthy.Equal(firstHealthy) {
t.Error("expected LastHealthy to remain from healthy period")
}
// Recover to healthy
registry.healthy = true
registry.err = ""
checker.runChecks()
status, _ = checker.GetStatus(domain.ExternalSystemRegistry)
if !status.Healthy {
t.Error("expected status to be healthy after recovery")
}
// LastHealthy should be updated
if status.LastHealthy.Before(firstHealthy) {
t.Error("expected LastHealthy to be updated on recovery")
}
}
func TestExternalHealthChecker_PartialFailure(t *testing.T) {
// Registry healthy, CI unhealthy, Git healthy
registry := &mockRegistryChecker{healthy: true}
ci := &mockExternalHealthChecker{system: domain.ExternalSystemCI, healthy: false, err: "timeout"}
git := &mockExternalHealthChecker{system: domain.ExternalSystemGit, healthy: true}
checker := NewExternalHealthChecker(registry, ci, git, ExternalHealthConfig{
CheckInterval: 100 * time.Millisecond,
})
checker.runChecks()
statuses := checker.GetAllStatuses()
// Partial failure should not affect other systems
if !statuses[domain.ExternalSystemRegistry].Healthy {
t.Error("registry should be healthy despite CI failure")
}
if statuses[domain.ExternalSystemCI].Healthy {
t.Error("CI should be unhealthy")
}
if !statuses[domain.ExternalSystemGit].Healthy {
t.Error("git should be healthy despite CI failure")
}
}