package httpapi_test import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "time" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/httpapi" ) func TestHealthReportsProcessAndDatabaseConfigurationWithoutReadinessProbe(t *testing.T) { readiness := &readinessStub{ status: httpapi.DatabaseStatus{Backend: "postgres", Configured: true}, } recorder := httptest.NewRecorder() httpapi.NewHandler(readiness).ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/api/health", nil)) if recorder.Code != http.StatusOK { t.Fatalf("status = %d, want %d", recorder.Code, http.StatusOK) } var response struct { OK bool `json:"ok"` AppID string `json:"appId"` WebOnly bool `json:"webOnly"` Database httpapi.DatabaseStatus `json:"database"` } if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil { t.Fatalf("decode response: %v", err) } if !response.OK || response.AppID != "zhinian-web-studio" || !response.WebOnly { t.Fatalf("process response = %+v", response) } if response.Database != readiness.status { t.Fatalf("database = %+v, want %+v", response.Database, readiness.status) } if readiness.readyCalls != 0 { t.Fatalf("health made %d readiness probes, want 0", readiness.readyCalls) } } func TestHealthIncludesInjectedRuntimeCompatibilityDetails(t *testing.T) { details := httpapi.HealthDetails{ VisualAPIMode: "volcengine", EvolinkMode: "missing", SeedanceMode: "seedance", BailianMode: "missing", AuthMode: "configured", Capabilities: []any{ map[string]any{"id": "image.generate", "engine": "jimeng"}, map[string]any{"id": "video.generate", "engine": "seedance"}, }, } recorder := httptest.NewRecorder() httpapi.NewHandler(&readinessStub{}, httpapi.WithHealthDetails(details)).ServeHTTP( recorder, httptest.NewRequest(http.MethodGet, "/api/health", nil), ) var response struct { VisualAPIMode string `json:"visualApiMode"` EvolinkMode string `json:"evolinkMode"` SeedanceMode string `json:"seedanceMode"` BailianMode string `json:"bailianMode"` AuthMode string `json:"authMode"` Capabilities []map[string]any `json:"capabilities"` } if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil { t.Fatalf("decode response: %v", err) } if response.VisualAPIMode != "volcengine" || response.EvolinkMode != "missing" || response.SeedanceMode != "seedance" || response.BailianMode != "missing" || response.AuthMode != "configured" { t.Fatalf("runtime response = %+v", response) } if len(response.Capabilities) != 2 || response.Capabilities[0]["id"] != "image.generate" || response.Capabilities[1]["id"] != "video.generate" { t.Fatalf("capabilities = %#v", response.Capabilities) } } func TestReadyReportsSuccessfulDatabaseProbe(t *testing.T) { readiness := &readinessStub{ status: httpapi.DatabaseStatus{Backend: "postgres", Configured: true}, readyStatus: httpapi.DatabaseStatus{Backend: "postgres", Configured: true}, } recorder := httptest.NewRecorder() httpapi.NewHandler(readiness).ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/api/ready", nil)) assertReadyResponse(t, recorder, http.StatusOK, true, readiness.readyStatus) } func TestReadyReportsUnavailableDatabase(t *testing.T) { readiness := &readinessStub{ status: httpapi.DatabaseStatus{Backend: "postgres", Configured: true}, readyStatus: httpapi.DatabaseStatus{Backend: "postgres", Configured: true}, readyErr: errors.New("database unavailable"), } recorder := httptest.NewRecorder() httpapi.NewHandler(readiness).ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/api/ready", nil)) assertReadyResponse(t, recorder, http.StatusServiceUnavailable, false, readiness.readyStatus) } func TestReadyUsesConfiguredProbeTimeout(t *testing.T) { readiness := &readinessStub{ status: httpapi.DatabaseStatus{Backend: "postgres", Configured: true}, readyStatus: httpapi.DatabaseStatus{Backend: "postgres", Configured: true}, } recorder := httptest.NewRecorder() timeout := 17 * time.Millisecond httpapi.NewHandler(readiness, httpapi.WithReadinessTimeout(timeout)).ServeHTTP( recorder, httptest.NewRequest(http.MethodGet, "/api/ready", nil), ) if readiness.deadlineRemaining <= 0 || readiness.deadlineRemaining > timeout { t.Fatalf("probe deadline remaining = %s, want within (0, %s]", readiness.deadlineRemaining, timeout) } } func TestReadyDefaultsToThreeSecondProbeTimeout(t *testing.T) { readiness := &readinessStub{ readyStatus: httpapi.DatabaseStatus{Backend: "postgres", Configured: true}, } recorder := httptest.NewRecorder() httpapi.NewHandler(readiness).ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/api/ready", nil)) if readiness.deadlineRemaining < 2900*time.Millisecond || readiness.deadlineRemaining > 3*time.Second { t.Fatalf("probe deadline remaining = %s, want approximately 3s", readiness.deadlineRemaining) } } func TestHealthReflectsInvalidDatabaseConfiguration(t *testing.T) { readiness := &readinessStub{ status: httpapi.DatabaseStatus{Backend: "invalid", Configured: false}, } recorder := httptest.NewRecorder() httpapi.NewHandler(readiness).ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/api/health", nil)) var response struct { Database httpapi.DatabaseStatus `json:"database"` } if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil { t.Fatalf("decode response: %v", err) } if response.Database != readiness.status { t.Fatalf("database = %+v, want %+v", response.Database, readiness.status) } } func TestHandlerRejectsUnsupportedMethodsAndUnknownPaths(t *testing.T) { tests := []struct { name string method string path string wantStatus int wantAllow string }{ {name: "health post", method: http.MethodPost, path: "/api/health", wantStatus: http.StatusMethodNotAllowed, wantAllow: http.MethodGet}, {name: "ready post", method: http.MethodPost, path: "/api/ready", wantStatus: http.StatusMethodNotAllowed, wantAllow: http.MethodGet}, {name: "unknown", method: http.MethodGet, path: "/api/missing", wantStatus: http.StatusNotFound}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { recorder := httptest.NewRecorder() httpapi.NewHandler(&readinessStub{}).ServeHTTP(recorder, httptest.NewRequest(test.method, test.path, nil)) if recorder.Code != test.wantStatus { t.Fatalf("status = %d, want %d", recorder.Code, test.wantStatus) } if allow := recorder.Header().Get("Allow"); allow != test.wantAllow { t.Fatalf("Allow = %q, want %q", allow, test.wantAllow) } }) } } func assertReadyResponse(t *testing.T, recorder *httptest.ResponseRecorder, wantStatus int, wantOK bool, wantDatabase httpapi.DatabaseStatus) { t.Helper() if recorder.Code != wantStatus { t.Fatalf("status = %d, want %d", recorder.Code, wantStatus) } var response struct { OK bool `json:"ok"` Database httpapi.DatabaseStatus `json:"database"` } if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil { t.Fatalf("decode response: %v", err) } if response.OK != wantOK || response.Database != wantDatabase { t.Fatalf("response = %+v, want ok=%t database=%+v", response, wantOK, wantDatabase) } } type readinessStub struct { status httpapi.DatabaseStatus readyStatus httpapi.DatabaseStatus readyErr error readyCalls int deadlineRemaining time.Duration } func (s *readinessStub) Status() httpapi.DatabaseStatus { return s.status } func (s *readinessStub) Ready(ctx context.Context) (httpapi.DatabaseStatus, error) { s.readyCalls++ if deadline, ok := ctx.Deadline(); ok { s.deadlineRemaining = time.Until(deadline) } return s.readyStatus, s.readyErr }