- 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>
242 lines
6.7 KiB
Go
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")
|
|
}
|
|
}
|