package api import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "time" ) func TestNewHealthHandler(t *testing.T) { tests := []struct { name string config HealthConfig wantStatus int wantHealth string }{ { name: "all checks healthy", config: HealthConfig{ Service: "test-service", Timeout: 5 * time.Second, Checks: map[string]HealthChecker{ "db": func(ctx context.Context) error { return nil }, "cache": func(ctx context.Context) error { return nil }, }, }, wantStatus: http.StatusOK, wantHealth: "healthy", }, { name: "one check unhealthy", config: HealthConfig{ Service: "test-service", Timeout: 5 * time.Second, Checks: map[string]HealthChecker{ "db": func(ctx context.Context) error { return nil }, "cache": func(ctx context.Context) error { return errors.New("connection refused") }, }, }, wantStatus: http.StatusServiceUnavailable, wantHealth: "unhealthy", }, { name: "no checks configured", config: HealthConfig{ Service: "test-service", }, wantStatus: http.StatusOK, wantHealth: "healthy", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { handler := NewHealthHandler(tt.config) req := httptest.NewRequest(http.MethodGet, "/health", nil) rec := httptest.NewRecorder() handler(rec, req) if rec.Code != tt.wantStatus { t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus) } var resp Response if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("failed to decode response: %v", err) } data, ok := resp.Data.(map[string]any) if !ok { t.Fatalf("expected map in data, got %T", resp.Data) } if status := data["status"]; status != tt.wantHealth { t.Errorf("health status = %q, want %q", status, tt.wantHealth) } if service := data["service"]; service != tt.config.Service { t.Errorf("service = %q, want %q", service, tt.config.Service) } }) } } func TestNewHealthHandler_Timeout(t *testing.T) { config := HealthConfig{ Service: "test-service", Timeout: 100 * time.Millisecond, Checks: map[string]HealthChecker{ "slow": func(ctx context.Context) error { select { case <-time.After(1 * time.Second): return nil case <-ctx.Done(): return ctx.Err() } }, }, } handler := NewHealthHandler(config) req := httptest.NewRequest(http.MethodGet, "/health", nil) rec := httptest.NewRecorder() start := time.Now() handler(rec, req) duration := time.Since(start) // Should return before the full second if duration > 500*time.Millisecond { t.Errorf("took %v, expected less than 500ms", duration) } // Should be unhealthy due to timeout if rec.Code != http.StatusServiceUnavailable { t.Errorf("status = %d, want %d", rec.Code, http.StatusServiceUnavailable) } } func TestNewLivenessHandler(t *testing.T) { handler := NewLivenessHandler("test-service") req := httptest.NewRequest(http.MethodGet, "/health/live", nil) rec := httptest.NewRecorder() handler(rec, req) if rec.Code != http.StatusOK { t.Errorf("status = %d, want %d", rec.Code, http.StatusOK) } var resp Response if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("failed to decode response: %v", err) } data, ok := resp.Data.(map[string]any) if !ok { t.Fatalf("expected map in data, got %T", resp.Data) } if status := data["status"]; status != "ok" { t.Errorf("status = %q, want %q", status, "ok") } if service := data["service"]; service != "test-service" { t.Errorf("service = %q, want %q", service, "test-service") } } func TestPingChecker(t *testing.T) { t.Run("healthy ping", func(t *testing.T) { pingFn := func(ctx context.Context) error { return nil } checker := PingChecker(pingFn) err := checker(context.Background()) if err != nil { t.Errorf("expected nil, got %v", err) } }) t.Run("unhealthy ping", func(t *testing.T) { pingErr := errors.New("connection refused") pingFn := func(ctx context.Context) error { return pingErr } checker := PingChecker(pingFn) err := checker(context.Background()) if err != pingErr { t.Errorf("expected %v, got %v", pingErr, err) } }) } func TestHTTPChecker(t *testing.T) { t.Run("healthy endpoint", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) defer server.Close() checker := HTTPChecker(server.URL) err := checker(context.Background()) if err != nil { t.Errorf("expected nil, got %v", err) } }) t.Run("unhealthy endpoint", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusServiceUnavailable) })) defer server.Close() checker := HTTPChecker(server.URL) err := checker(context.Background()) if err == nil { t.Error("expected error, got nil") } }) t.Run("connection refused", func(t *testing.T) { checker := HTTPChecker("http://localhost:99999") err := checker(context.Background()) if err == nil { t.Error("expected error, got nil") } }) }