package health import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "os" "testing" "time" "github.com/jackc/pgx/v5/pgxpool" ) // Akzeptanzkriterium 1 + Pruefung 1: simulierter Datenbankausfall fuehrt zu // "nicht bereit". func TestReadinessHandler_ReportsNotReadyOnDatabaseFailure(t *testing.T) { adminDSN := os.Getenv("TEST_ADMIN_DSN") if adminDSN == "" { t.Skip("TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen") } ctx := context.Background() pool, err := pgxpool.New(ctx, adminDSN) if err != nil { t.Fatalf("pool: %v", err) } // Datenbankausfall simulieren: Pool sofort schliessen, bevor der Check laeuft. pool.Close() reg := NewRegistry() reg.Register("database", DatabaseChecker(pool)) req := httptest.NewRequest(http.MethodGet, "/readyz", nil) rec := httptest.NewRecorder() reg.ReadinessHandler()(rec, req) if rec.Code != http.StatusServiceUnavailable { t.Fatalf("status = %d, want 503 bei db-ausfall", rec.Code) } var body struct { Status string `json:"status"` Checks map[string]interface{} `json:"checks"` } if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("body parsen: %v", err) } if body.Status != "not_ready" { t.Fatalf("status-feld = %q, want not_ready", body.Status) } if _, ok := body.Checks["database"]; !ok { t.Fatal("erwartet 'database' im checks-ergebnis") } } func TestReadinessHandler_ReportsReadyWhenAllChecksPass(t *testing.T) { adminDSN := os.Getenv("TEST_ADMIN_DSN") if adminDSN == "" { t.Skip("TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen") } ctx := context.Background() pool, err := pgxpool.New(ctx, adminDSN) if err != nil { t.Fatalf("pool: %v", err) } defer pool.Close() reg := NewRegistry() reg.Register("database", DatabaseChecker(pool)) reg.Register("queue", QueueChecker(pool)) req := httptest.NewRequest(http.MethodGet, "/readyz", nil) rec := httptest.NewRecorder() reg.ReadinessHandler()(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200 bei funktionierenden abhaengigkeiten", rec.Code) } } // Akzeptanzkriterium 2 + Pruefung 3: Liveness und Readiness unterscheiden // sich nachweislich im Fehlerfall. func TestLivenessAndReadiness_DifferOnDatabaseFailure(t *testing.T) { adminDSN := os.Getenv("TEST_ADMIN_DSN") if adminDSN == "" { t.Skip("TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen") } ctx := context.Background() pool, err := pgxpool.New(ctx, adminDSN) if err != nil { t.Fatalf("pool: %v", err) } pool.Close() // db-ausfall simulieren reg := NewRegistry() reg.Register("database", DatabaseChecker(pool)) livenessRec := httptest.NewRecorder() LivenessHandler(livenessRec, httptest.NewRequest(http.MethodGet, "/livez", nil)) if livenessRec.Code != http.StatusOK { t.Fatalf("liveness status = %d, want 200 trotz db-ausfall (liveness prueft keine abhaengigkeiten)", livenessRec.Code) } readinessRec := httptest.NewRecorder() reg.ReadinessHandler()(readinessRec, httptest.NewRequest(http.MethodGet, "/readyz", nil)) if readinessRec.Code != http.StatusServiceUnavailable { t.Fatalf("readiness status = %d, want 503 bei db-ausfall", readinessRec.Code) } if livenessRec.Code == readinessRec.Code { t.Fatal("liveness und readiness sollten sich im db-ausfall-fall unterscheiden") } } // Akzeptanzkriterium 2 + Pruefung 2: Health-Endpunkt antwortet auch bei // haengendem Check innerhalb definierter Zeit (Timeout begrenzt die Dauer). func TestReadinessHandler_RespondsWithinTimeoutEvenWithHangingCheck(t *testing.T) { reg := NewRegistry().WithTimeout(50 * time.Millisecond) reg.Register("haengender_dienst", CheckerFunc(func(ctx context.Context) error { select { case <-time.After(10 * time.Second): // wuerde ohne timeout ewig blockieren return nil case <-ctx.Done(): return ctx.Err() } })) start := time.Now() req := httptest.NewRequest(http.MethodGet, "/readyz", nil) rec := httptest.NewRecorder() reg.ReadinessHandler()(rec, req) elapsed := time.Since(start) if elapsed > time.Second { t.Fatalf("readiness handler brauchte %s, erwartet deutlich unter 1s durch timeout", elapsed) } if rec.Code != http.StatusServiceUnavailable { t.Fatalf("status = %d, want 503 fuer haengenden/timeout-check", rec.Code) } } func TestCheckAll_MultipleChecksRunConcurrently(t *testing.T) { reg := NewRegistry().WithTimeout(time.Second) reg.Register("a", CheckerFunc(func(ctx context.Context) error { return nil })) reg.Register("b", CheckerFunc(func(ctx context.Context) error { return errors.New("kaputt") })) ready, results := reg.CheckAll(context.Background()) if ready { t.Fatal("erwartet ready=false, da 'b' fehlschlaegt") } if !results["a"].OK { t.Fatalf("erwartet 'a' ok, habe %+v", results["a"]) } if results["b"].OK || results["b"].Error == "" { t.Fatalf("erwartet 'b' fehlgeschlagen mit fehlertext, habe %+v", results["b"]) } }