package httpapi import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/identity" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/jobs" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/publicapi" ) const jobWithDiagnosticsPayload = `{"providerError":{"status":400,"code":"PrivateCode","requestId":"private-request","detail":"private diagnostic","phase":"submit"},"errorCode":"PrivateCode","errorDetail":"private diagnostic","requestId":"private-request","errorMessage":"private diagnostic","raw":{"providerMessage":"private diagnostic"},"outputUrls":["https://example.test/result.png"],"usage":{"images":2},"layers":[{"type":"image","url":"https://example.test/layer.png"}]}` func TestProjectJobForViewerKeepsSuccessMetadataWithoutMutatingStoredPayload(t *testing.T) { original := jobs.Job{Status: jobs.StatusFailed, ResponsePayload: json.RawMessage(jobWithDiagnosticsPayload)} viewer := projectJobForViewer(original, false) if got := string(original.ResponsePayload); got != jobWithDiagnosticsPayload { t.Fatalf("original responsePayload was mutated: %s", got) } if strings.Contains(string(viewer.ResponsePayload), "PrivateCode") || strings.Contains(string(viewer.ResponsePayload), "private diagnostic") || strings.Contains(string(viewer.ResponsePayload), "private-request") { t.Fatalf("viewer received diagnostic: %s", viewer.ResponsePayload) } for _, field := range []string{"outputUrls", "usage", "layers"} { var decoded map[string]json.RawMessage if err := json.Unmarshal(viewer.ResponsePayload, &decoded); err != nil || len(decoded[field]) == 0 { t.Fatalf("successful %s missing after projection: %s (%v)", field, viewer.ResponsePayload, err) } } for _, status := range []jobs.Status{jobs.StatusFailed, jobs.StatusCancelled, jobs.StatusExpired} { legacy := original legacy.Status = status got := projectJobForViewer(legacy, false) var decoded map[string]json.RawMessage if err := json.Unmarshal(got.ResponsePayload, &decoded); err != nil { t.Fatalf("status %s has invalid projected payload: %v", status, err) } for _, key := range []string{"raw", "errorMessage"} { if _, exists := decoded[key]; exists { t.Fatalf("status %s exposed historical %s: %s", status, key, got.ResponsePayload) } } } if got := projectJobForViewer(original, true); string(got.ResponsePayload) != jobWithDiagnosticsPayload { t.Fatalf("administrator response changed: %s", got.ResponsePayload) } succeeded := original succeeded.Status = jobs.StatusSucceeded if got := projectJobForViewer(succeeded, false); !bytes.Contains(got.ResponsePayload, []byte(`"raw"`)) { t.Fatalf("successful provider metadata was lost: %s", got.ResponsePayload) } if got := projectJobForViewer(jobs.Job{ResponsePayload: []byte(`{"providerError":{"detail":"private"}}`)}, false); got.ResponsePayload != nil { t.Fatalf("diagnostics-only payload should be omitted: %s", got.ResponsePayload) } } func TestJobsHTTPProjectsProviderDiagnosticsByRoleAndForPublicAPI(t *testing.T) { for _, testCase := range []struct { name string role string authMode identity.AuthMode wantDetail bool }{ {name: "ordinary user", role: "user", authMode: identity.AuthModeUser}, {name: "organization administrator", role: "organization_admin", authMode: identity.AuthModeAdmin, wantDetail: true}, {name: "platform administrator", role: "super_admin", authMode: identity.AuthModeAdmin, wantDetail: true}, } { t.Run(testCase.name, func(t *testing.T) { store := &httpJobStore{values: map[string]jobs.Job{ "failed": {ID: "failed", OwnerID: "owner", Capability: "image.generate", Status: jobs.StatusFailed, ResponsePayload: json.RawMessage(jobWithDiagnosticsPayload), Error: &jobs.JobError{Code: "PrivateCode", Message: "请求被服务商拒绝"}}, }} session := identity.Session{AuthMode: testCase.authMode, User: identity.User{ID: "owner", Role: testCase.role}} platform, err := NewPlatformAuthorizer(AuthState{Required: true, Configured: true}, &fixedSessionResolver{session: session}) if err != nil { t.Fatal(err) } h, err := NewJobsHandler(JobsDependencies{Service: jobs.NewService(store, time.Now), Platform: platform, Public: publicapi.NewAuthenticator(publicapi.Config{APIKeys: "agent-a:secret"}), Builder: &jobBuilderStub{}}, JobsConfig{}) if err != nil { t.Fatal(err) } for _, path := range []string{"/api/generations/image", "/api/generations/image/failed"} { request := httptest.NewRequest(http.MethodGet, path, nil) request.AddCookie(&http.Cookie{Name: identity.SessionCookieName, Value: "session-cookie"}) response := httptest.NewRecorder() h.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("%s status=%d body=%s", path, response.Code, response.Body.String()) } assertJobResponseProjection(t, response.Body.Bytes(), testCase.wantDetail) } if got := string(store.values["failed"].ResponsePayload); got != jobWithDiagnosticsPayload { t.Fatalf("store was mutated: %s", got) } }) } store := &httpJobStore{values: map[string]jobs.Job{ "external": {ID: "external", OwnerID: "api:agent-a", ExternalClientID: "agent-a", Capability: "image.generate", Status: jobs.StatusFailed, ResponsePayload: json.RawMessage(jobWithDiagnosticsPayload), Error: &jobs.JobError{Message: "请求被服务商拒绝"}}, }} platform, _ := NewPlatformAuthorizer(AuthState{}, nil) h, err := NewJobsHandler(JobsDependencies{Service: jobs.NewService(store, time.Now), Platform: platform, Public: publicapi.NewAuthenticator(publicapi.Config{APIKeys: "agent-a:secret"}), Builder: &jobBuilderStub{}}, JobsConfig{}) if err != nil { t.Fatal(err) } for _, path := range []string{"/api/v1/jobs", "/api/v1/jobs/external"} { request := httptest.NewRequest(http.MethodGet, path, nil) request.Header.Set("Authorization", "Bearer secret") response := httptest.NewRecorder() h.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("%s status=%d body=%s", path, response.Code, response.Body.String()) } assertJobResponseProjection(t, response.Body.Bytes(), false) } if got := string(store.values["external"].ResponsePayload); got != jobWithDiagnosticsPayload { t.Fatalf("public response mutated stored job: %s", got) } } func TestJobsHTTPProjectsDiagnosticsFromIdempotentCreate(t *testing.T) { h, store := newJobsHTTP(t) fingerprint, err := jobs.Fingerprint(map[string]any{"capability": "image.generate", "prompt": "hello"}) if err != nil { t.Fatal(err) } store.values["existing"] = jobs.Job{ID: "existing", OwnerID: "api:agent-a", ExternalClientID: "agent-a", Capability: "image.generate", Status: jobs.StatusFailed, IdempotencyKey: "same", IdempotencyFingerprint: fingerprint, ResponsePayload: json.RawMessage(jobWithDiagnosticsPayload)} request := httptest.NewRequest(http.MethodPost, "/api/v1/jobs", bytes.NewBufferString(`{"capability":"image.generate","prompt":"hello"}`)) request.Header.Set("Authorization", "Bearer secret") request.Header.Set("Idempotency-Key", "same") response := httptest.NewRecorder() h.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("status=%d body=%s", response.Code, response.Body.String()) } assertJobResponseProjection(t, response.Body.Bytes(), false) } func assertJobResponseProjection(t *testing.T, body []byte, wantDetail bool) { t.Helper() hasDetail := bytes.Contains(body, []byte("private diagnostic")) || bytes.Contains(body, []byte("private-request")) if hasDetail != wantDetail { t.Fatalf("diagnostic present=%t want=%t body=%s", hasDetail, wantDetail, body) } if !bytes.Contains(body, []byte("outputUrls")) || !bytes.Contains(body, []byte("请求被服务商拒绝")) && !bytes.Contains(body, []byte("existing")) { t.Fatalf("job payload missing after projection: %s", body) } }