package clients import ( "context" "encoding/json" "io" "net" "net/http" "net/http/httptest" "strings" "sync" "testing" "github.com/easyai/easyai-ai-gateway/apps/api/internal/store" ) func TestVectorizerClientMultipartAndPrivateResumeState(t *testing.T) { var receivedPath string var receivedFields map[string]string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { receivedPath = r.URL.Path if username, password, ok := r.BasicAuth(); !ok || username != "id" || password != "secret" { t.Fatalf("unexpected vectorizer auth") } if err := r.ParseMultipartForm(1 << 20); err != nil { t.Fatalf("parse multipart: %v", err) } receivedFields = map[string]string{} for key, values := range r.MultipartForm.Value { receivedFields[key] = values[0] } w.Header().Set("Content-Type", "image/svg+xml") w.Header().Set("X-Image-Token", "private-image-token") w.Header().Set("X-Receipt", "private-receipt") _, _ = io.WriteString(w, ``) })) defer server.Close() privateState := map[string]any{} response, err := (VectorizerClient{LookupIP: func(context.Context, string) ([]net.IPAddr, error) { return []net.IPAddr{{IP: net.ParseIP("93.184.216.34")}}, nil }}).Run(context.Background(), Request{ Kind: "images.vectorize", Model: "easy-image-vectorizer-1", Body: map[string]any{"source": map[string]any{"url": "https://example.com/input.png"}, "format": "svg", "maxColors": 16, "cleanupLevel": "strong"}, Candidate: storeCandidate(server.URL, "vectorizer", map[string]any{"accessKey": "id", "secretKey": "secret"}), OnRemoteTaskSubmitted: func(_ string, payload map[string]any) error { privateState = payload; return nil }, }) if err != nil { t.Fatalf("run vectorizer: %v", err) } if receivedPath != "/vectorize" || receivedFields["image.url"] == "" || receivedFields["output.file_format"] != "svg" || receivedFields["processing.max_colors"] != "16" { t.Fatalf("unexpected multipart request: path=%s fields=%+v", receivedPath, receivedFields) } if privateState["imageToken"] != "private-image-token" || privateState["receipt"] != "private-receipt" { t.Fatalf("private resume state missing: %+v", privateState) } serialized := strings.ToLower(toJSONForTest(response.Result)) if strings.Contains(serialized, "private-image-token") || strings.Contains(serialized, "private-receipt") { t.Fatalf("private vectorizer state leaked into response: %s", serialized) } } func TestVectorizerClientRejectsPrivateSourceBeforeProviderCall(t *testing.T) { providerCalls := 0 server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { providerCalls++ })) defer server.Close() _, err := (VectorizerClient{LookupIP: func(context.Context, string) ([]net.IPAddr, error) { return []net.IPAddr{{IP: net.ParseIP("127.0.0.1")}}, nil }}).Run(context.Background(), Request{ Body: map[string]any{"source": map[string]any{"url": "https://assets.example.test/input.png"}}, Candidate: storeCandidate(server.URL, "vectorizer", map[string]any{"apiKey": "key"}), }) if err == nil || !strings.Contains(err.Error(), "blocked network") { t.Fatalf("expected blocked source error, got %v", err) } if providerCalls != 0 { t.Fatalf("provider received %d calls for blocked source", providerCalls) } } func TestVectorizerClientReusesImageTokenForExtraFormat(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/download" { t.Fatalf("unexpected path %s", r.URL.Path) } if err := r.ParseMultipartForm(1 << 20); err != nil { t.Fatal(err) } if r.FormValue("image.token") != "token" || r.FormValue("receipt") != "receipt" || r.FormValue("output.file_format") != "pdf" { t.Fatalf("unexpected reuse form: %+v", r.MultipartForm.Value) } w.Header().Set("Content-Type", "application/pdf") _, _ = w.Write([]byte("%PDF-1.7\n")) })) defer server.Close() response, err := (VectorizerClient{}).Run(context.Background(), Request{ Body: map[string]any{"_vectorizer_image_token": "token", "_vectorizer_receipt": "receipt", "format": "pdf"}, Candidate: storeCandidate(server.URL, "vectorizer", map[string]any{"apiKey": "key"}), }) if err != nil { t.Fatal(err) } item := response.Result["data"].([]any)[0].(map[string]any) if item["type"] != "file" || item["mime_type"] != "application/pdf" { t.Fatalf("unexpected pdf result: %+v", item) } } func TestTopazClientUploadsPartsPollsAndValidatesOutput(t *testing.T) { var mu sync.Mutex called := map[string]int{} var server *httptest.Server server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mu.Lock() called[r.Method+" "+r.URL.Path]++ mu.Unlock() switch { case r.Method == http.MethodGet && r.URL.Path == "/source.mp4": _, _ = w.Write([]byte("small-video-source")) case r.Method == http.MethodPost && r.URL.Path == "/video/": if r.Header.Get("X-API-Key") != "topaz-key" { t.Fatalf("missing Topaz API key") } _, _ = io.WriteString(w, `{"requestId":"job-1"}`) case r.Method == http.MethodPatch && r.URL.Path == "/video/job-1/accept": _, _ = io.WriteString(w, `{"urls":["`+server.URL+`/upload/1","`+server.URL+`/upload/2"]}`) case r.Method == http.MethodPut && strings.HasPrefix(r.URL.Path, "/upload/"): w.Header().Set("ETag", `"etag-`+strings.TrimPrefix(r.URL.Path, "/upload/")+`"`) w.WriteHeader(http.StatusOK) case r.Method == http.MethodPatch && r.URL.Path == "/video/job-1/complete-upload/": w.WriteHeader(http.StatusOK) _, _ = io.WriteString(w, `{}`) case r.Method == http.MethodGet && r.URL.Path == "/video/job-1/status": _, _ = io.WriteString(w, `{"status":"completed","download":{"url":"`+server.URL+`/output.mp4"}}`) default: http.NotFound(w, r) } })) defer server.Close() phases := []string{} client := TopazClient{Probe: func(_ context.Context, target string) (TopazVideoMetadata, error) { if strings.HasPrefix(target, "http") { return TopazVideoMetadata{Width: 640, Height: 360, Duration: 3, FrameRate: 24, HasAudio: true}, nil } return TopazVideoMetadata{Width: 320, Height: 180, Duration: 3, FrameRate: 24, FrameCount: 72, HasAudio: true}, nil }} response, err := client.Run(context.Background(), Request{ Model: "easy-proteus-standard-4", Body: map[string]any{"video_url": server.URL + "/source.mp4", "output_width": 640, "output_height": 360, "preserve_audio": true}, Candidate: storeCandidateWithConfig(server.URL, "topaz", map[string]any{"apiKey": "topaz-key"}, map[string]any{"allowPrivateSourceDownloads": true, "pollIntervalMs": 1, "pollTimeoutMs": 100}), OnRemoteTaskSubmitted: func(_ string, payload map[string]any) error { phases = append(phases, payload["phase"].(string)) return nil }, OnRemoteTaskPolled: func(_ string, payload map[string]any) error { phases = append(phases, payload["phase"].(string)) return nil }, }) if err != nil { t.Fatalf("run Topaz: %v", err) } if response.RequestID != "job-1" || !containsStringForTest(phases, "created") || !containsStringForTest(phases, "uploaded") { t.Fatalf("unexpected Topaz response/phases: response=%+v phases=%+v", response, phases) } if called["PUT /upload/1"] != 1 || called["PUT /upload/2"] != 1 || called["PATCH /video/job-1/complete-upload/"] != 1 { t.Fatalf("upload lifecycle incomplete: %+v", called) } } func TestTopazClientResumesUploadedTaskWithoutDownloadingSource(t *testing.T) { var sourceCalls int var server *httptest.Server server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/source.mp4" { sourceCalls++ } if r.URL.Path == "/video/job-resume/status" { _, _ = io.WriteString(w, `{"status":"completed","download":{"url":"`+server.URL+`/result.mp4"}}`) return } http.NotFound(w, r) })) defer server.Close() _, err := (TopazClient{Probe: func(context.Context, string) (TopazVideoMetadata, error) { return TopazVideoMetadata{Width: 640, Height: 360, Duration: 3, FrameRate: 24}, nil }}).Run(context.Background(), Request{ RemoteTaskID: "job-resume", RemoteTaskPayload: map[string]any{"phase": "uploaded", "targetResolution": map[string]any{"width": 640, "height": 360}}, Body: map[string]any{"video_url": server.URL + "/source.mp4"}, Candidate: storeCandidateWithConfig(server.URL, "topaz", map[string]any{"apiKey": "topaz-key"}, map[string]any{"pollIntervalMs": 1, "pollTimeoutMs": 100}), }) if err != nil { t.Fatal(err) } if sourceCalls != 0 { t.Fatalf("resume downloaded source %d times", sourceCalls) } } func TestTopazClientReportsProviderFailureAndPollingTimeout(t *testing.T) { tests := []struct { name string statusBody string timeoutMS int want string }{ {name: "provider failure", statusBody: `{"status":"failed","message":"upstream rejected"}`, timeoutMS: 100, want: "upstream rejected"}, {name: "poll timeout", statusBody: `{"status":"processing"}`, timeoutMS: 3, want: "polling timed out"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/video/job/status" { http.NotFound(w, r) return } _, _ = io.WriteString(w, test.statusBody) })) defer server.Close() _, err := (TopazClient{}).Run(context.Background(), Request{ RemoteTaskID: "job", RemoteTaskPayload: map[string]any{"phase": "uploaded", "targetResolution": map[string]any{"width": 640, "height": 360}}, Body: map[string]any{"video_url": "https://assets.example.test/source.mp4"}, Candidate: storeCandidateWithConfig(server.URL, "topaz", map[string]any{"apiKey": "topaz-key"}, map[string]any{ "pollIntervalMs": 1, "pollTimeoutMs": test.timeoutMS, }), }) if err == nil || !strings.Contains(err.Error(), test.want) { t.Fatalf("expected %q error, got %v", test.want, err) } }) } } func TestTopazClientRejectsOversizedInputBeforeProbe(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/source.mp4" { _, _ = io.WriteString(w, "too-large") return } http.NotFound(w, r) })) defer server.Close() probeCalls := 0 _, err := (TopazClient{Probe: func(context.Context, string) (TopazVideoMetadata, error) { probeCalls++ return TopazVideoMetadata{}, nil }}).Run(context.Background(), Request{ Body: map[string]any{"video_url": server.URL + "/source.mp4"}, Candidate: storeCandidateWithConfig(server.URL, "topaz", map[string]any{"apiKey": "topaz-key"}, map[string]any{ "allowPrivateSourceDownloads": true, "maxInputBytes": 3, }), }) if err == nil || !strings.Contains(err.Error(), "size limit") { t.Fatalf("expected input size limit error, got %v", err) } if probeCalls != 0 { t.Fatalf("oversized input was probed %d times", probeCalls) } } func storeCandidate(baseURL, specType string, credentials map[string]any) store.RuntimeModelCandidate { return storeCandidateWithConfig(baseURL, specType, credentials, nil) } func storeCandidateWithConfig(baseURL, specType string, credentials, config map[string]any) store.RuntimeModelCandidate { return store.RuntimeModelCandidate{BaseURL: baseURL, SpecType: specType, Provider: specType, Credentials: credentials, PlatformConfig: config} } func toJSONForTest(value any) string { raw, _ := json.Marshal(value) return string(raw) } func containsStringForTest(values []string, expected string) bool { for _, value := range values { if value == expected { return true } } return false }