package health import ( "context" "testing" "time" ) // healthyCheck liefert immer einen gesunden Zustand. func healthyCheck(context.Context) CheckResult { return CheckResult{Status: StatusHealthy} } func TestCheckReportsWorstComponentStatus(testInstance *testing.T) { testRegistry := NewRegistry(time.Second) testRegistry.Register("database", true, healthyCheck) testRegistry.Register("repositories", true, func(context.Context) CheckResult { return CheckResult{Status: StatusWarning, Message: "Repository-01 ist zu 82 % belegt."} }) testRegistry.Register("scheduler", false, func(context.Context) CheckResult { return CheckResult{Status: StatusCritical, Message: "Scheduler antwortet nicht."} }) systemReport := testRegistry.Check(context.Background()) // Der Gesamtzustand ist immer der schlechteste Einzelzustand. if systemReport.Status != StatusCritical { testInstance.Fatalf("Gesamtzustand soll critical sein, war %q", systemReport.Status) } if len(systemReport.Components) != 3 { testInstance.Fatalf("es sollen 3 Komponenten berichtet werden, waren %d", len(systemReport.Components)) } } func TestCheckMarksMissingStatusAsCritical(testInstance *testing.T) { testRegistry := NewRegistry(time.Second) // Eine Prüfung, die keinen Status meldet, ist ein Programmierfehler. // Sie darf keinesfalls als gesund durchgehen (Fail Secure). testRegistry.Register("repository", true, func(context.Context) CheckResult { return CheckResult{} }) systemReport := testRegistry.Check(context.Background()) if systemReport.Components["repository"].Status != StatusCritical { testInstance.Fatalf("ein fehlender Status muss als critical gelten, war %q", systemReport.Components["repository"].Status) } } func TestCheckTurnsTimeoutIntoCriticalStatus(testInstance *testing.T) { testRegistry := NewRegistry(50 * time.Millisecond) testRegistry.Register("slow-repository", true, func(checkContext context.Context) CheckResult { // Die Prüfung antwortet bewusst später als das Timeout erlaubt. select { case <-time.After(2 * time.Second): return CheckResult{Status: StatusHealthy} case <-checkContext.Done(): return CheckResult{Status: StatusHealthy} } }) systemReport := testRegistry.Check(context.Background()) if systemReport.Components["slow-repository"].Status != StatusCritical { testInstance.Fatalf("eine Zeitüberschreitung muss als critical gelten, war %q", systemReport.Components["slow-repository"].Status) } } func TestCheckSurvivesPanicInComponentCheck(testInstance *testing.T) { testRegistry := NewRegistry(time.Second) testRegistry.Register("faulty", true, func(context.Context) CheckResult { panic("fehler in der prüfroutine") }) // Ein Panic in einer Prüfung darf den Health-Endpunkt nicht mitreißen. systemReport := testRegistry.Check(context.Background()) if systemReport.Components["faulty"].Status != StatusCritical { testInstance.Fatalf("ein Panic muss als critical gemeldet werden, war %q", systemReport.Components["faulty"].Status) } } func TestIsReadyIgnoresNonCriticalComponents(testInstance *testing.T) { testRegistry := NewRegistry(time.Second) testRegistry.Register("database", true, healthyCheck) testRegistry.Register("optional-cache", false, func(context.Context) CheckResult { return CheckResult{Status: StatusCritical, Message: "Cache nicht erreichbar."} }) // Eine unkritische Komponente darf den Dienst nicht aus dem Verkehr nehmen. isReady, systemReport := testRegistry.IsReady(context.Background()) if !isReady { testInstance.Fatal("der Dienst soll trotz ausgefallener unkritischer Komponente bereit sein") } // Der Gesamtbericht bleibt dennoch ehrlich (PROMPT.md §140). if systemReport.Status != StatusCritical { testInstance.Fatalf("der Bericht soll das Problem weiterhin zeigen, war %q", systemReport.Status) } } func TestIsReadyFailsWhenCriticalComponentIsUnhealthy(testInstance *testing.T) { testRegistry := NewRegistry(time.Second) testRegistry.Register("database", true, func(context.Context) CheckResult { return CheckResult{Status: StatusOffline, Message: "Datenbank nicht erreichbar."} }) isReady, _ := testRegistry.IsReady(context.Background()) if isReady { testInstance.Fatal("bei ausgefallener Datenbank darf der Dienst nicht bereit sein") } } func TestStatusSeverityOrder(testInstance *testing.T) { // Die Reihenfolge bestimmt, welcher Zustand den Gesamtbericht dominiert. if !StatusCritical.IsWorseThan(StatusOffline) { testInstance.Error("critical soll schwerer wiegen als offline") } if !StatusWarning.IsWorseThan(StatusDegraded) { testInstance.Error("warning soll schwerer wiegen als degraded") } if StatusHealthy.IsWorseThan(StatusDegraded) { testInstance.Error("healthy darf nicht schwerer wiegen als degraded") } }