Files
NianAIGC/backend/internal/httpapi/job_projection_test.go
T
2026-10-02 19:56:32 +08:00

152 lines
7.8 KiB
Go

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)
}
}