package healthcheck import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "testing" ) // TestCheck_SimulatedDependencyFailureReflectedCorrectly ist die // geforderte Pflichtprüfung 1 (INT-07): simulierter Ausfall einer // Abhängigkeit wird korrekt im Health-Status abgebildet. func TestCheck_SimulatedDependencyFailureReflectedCorrectly(t *testing.T) { c := NewChecker() c.Register("database", func(context.Context) error { return nil }) c.Register("object_storage", func(context.Context) error { return errors.New("bucket nicht erreichbar") }) c.Register("search_index", func(context.Context) error { return nil }) c.Register("jobqueue", func(context.Context) error { return nil }) result := c.Check(context.Background()) if result.Status != StatusDegraded { t.Fatalf("erwartete gesamtstatus %q bei einem ausgefallenen abhängigkeit, habe %q", StatusDegraded, result.Status) } if len(result.Components) != 4 { t.Fatalf("erwartete 4 komponenten, habe %d", len(result.Components)) } for _, comp := range result.Components { want := StatusOK if comp.Name == "object_storage" { want = StatusDegraded } if comp.Status != want { t.Fatalf("komponente %q: erwartete status %q, habe %q", comp.Name, want, comp.Status) } } } // TestCheck_AllHealthyReportsOK stellt sicher, dass ein vollständig // gesunder Zustand nicht fälschlich als degraded gilt. func TestCheck_AllHealthyReportsOK(t *testing.T) { c := NewChecker() c.Register("database", func(context.Context) error { return nil }) c.Register("object_storage", func(context.Context) error { return nil }) result := c.Check(context.Background()) if result.Status != StatusOK { t.Fatalf("erwartete %q, habe %q", StatusOK, result.Status) } } // TestServeHTTP_ResponseNeverContainsSensitiveErrorDetails ist die // geforderte Pflichtprüfung 2 (INT-07): Health-Antwort enthält keine // sensiblen Konfigurationsdetails — ein absichtlich mit einer // Verbindungszeichenfolge/einem Geheimnis versehener Prüffehler darf // NIRGENDS in der HTTP-Antwort auftauchen. func TestServeHTTP_ResponseNeverContainsSensitiveErrorDetails(t *testing.T) { const secretDSN = "postgresql://nexarch:s3hr-geheimes-passwort@db.internal:5432/tenant_x" c := NewChecker() c.Register("database", func(context.Context) error { return errors.New("verbindung fehlgeschlagen: " + secretDSN) }) req := httptest.NewRequest(http.MethodGet, "/api/v1/mail/health", nil) rec := httptest.NewRecorder() c.ServeHTTP(rec, req) body := rec.Body.String() if strings.Contains(body, secretDSN) || strings.Contains(body, "geheimes-passwort") { t.Fatalf("health-antwort enthält sensible details: %s", body) } var parsed Result if err := json.Unmarshal(rec.Body.Bytes(), &parsed); err != nil { t.Fatalf("antwort ist kein gültiges JSON: %v", err) } if parsed.Status != StatusDegraded { t.Fatalf("erwartete degraded, habe %q", parsed.Status) } if rec.Code != http.StatusServiceUnavailable { t.Fatalf("erwartete HTTP 503 bei degraded, habe %d", rec.Code) } } // TestServeHTTP_HealthyReturns200 bestätigt den positiven HTTP-Status. func TestServeHTTP_HealthyReturns200(t *testing.T) { c := NewChecker() c.Register("database", func(context.Context) error { return nil }) req := httptest.NewRequest(http.MethodGet, "/api/v1/mail/health", nil) rec := httptest.NewRecorder() c.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("erwartete HTTP 200, habe %d", rec.Code) } } // TestIntegration_RealHTTPEndpointAfterDeploy ist die geforderte // Pflichtprüfung 3 (INT-07): Integrationstest gegen einen echten, // laufenden Health-Endpunkt (realer HTTP-Server, reale Anfrage über // das Netzwerk — kein direkter Funktionsaufruf). func TestIntegration_RealHTTPEndpointAfterDeploy(t *testing.T) { c := NewChecker() c.Register("database", func(context.Context) error { return nil }) c.Register("object_storage", func(context.Context) error { return nil }) c.Register("search_index", func(context.Context) error { return nil }) c.Register("jobqueue", func(context.Context) error { return nil }) mux := http.NewServeMux() c.RegisterRoutes(mux) srv := httptest.NewServer(mux) defer srv.Close() resp, err := http.Get(srv.URL + "/api/v1/mail/health") if err != nil { t.Fatalf("get: %v", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Fatalf("erwartete 200, habe %d", resp.StatusCode) } var result Result if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { t.Fatalf("antwort dekodieren: %v", err) } if result.Status != StatusOK || len(result.Components) != 4 { t.Fatalf("unerwartetes ergebnis: %+v", result) } }