152 lines
7.8 KiB
Go
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)
|
|
}
|
|
}
|