Files
easyai-ai-gateway/apps/api/internal/httpapi/keling_compat_handlers_test.go

277 lines
9.7 KiB
Go

package httpapi
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
func TestNormalizeKelingOmniRequestMapsOfficialFields(t *testing.T) {
normalized, err := normalizeKelingOmniRequest(map[string]any{
"model_name": "kling-v3-omni",
"prompt": "A rainy street with natural ambience",
"mode": "pro",
"sound": "on",
"aspect_ratio": "9:16",
"duration": "5",
"external_task_id": "external-1",
"watermark_info": map[string]any{"enabled": true},
})
if err != nil {
t.Fatalf("normalize Kling request: %v", err)
}
if normalized["model"] != "kling-3.0-omni" ||
normalized["modelType"] != "omni_video" ||
normalized["resolution"] != "1080p" ||
normalized["aspect_ratio"] != "9:16" ||
normalized["duration"] != 5 ||
normalized["audio"] != true ||
normalized["sound"] != "on" ||
normalized["watermark"] != true ||
normalized["external_task_id"] != "external-1" ||
normalized["_gateway_compatibility"] != kelingOmniCompatibilityMarker {
t.Fatalf("unexpected normalized request: %+v", normalized)
}
content, _ := normalized["content"].([]any)
if len(content) != 1 {
t.Fatalf("unexpected content: %+v", normalized["content"])
}
text, _ := content[0].(map[string]any)
if text["type"] != "text" || text["text"] != "A rainy street with natural ambience" {
t.Fatalf("unexpected text content: %+v", text)
}
}
func TestNormalizeKelingOmniRequestBuildsMultiShotMedia(t *testing.T) {
normalized, err := normalizeKelingOmniRequest(map[string]any{
"model_name": "kling-3.0-omni",
"multi_shot": true,
"shot_type": "customize",
"aspect_ratio": "16:9",
"duration": 5,
"multi_prompt": []any{
map[string]any{"index": 1, "prompt": "First shot", "duration": "2"},
map[string]any{"index": 2, "prompt": "Second shot", "duration": 3},
},
"image_list": []any{
map[string]any{"image_url": "https://example.com/reference.png"},
},
"element_list": []any{
map[string]any{"element_id": float64(123)},
},
})
if err != nil {
t.Fatalf("normalize multi-shot request: %v", err)
}
if normalized["model"] != "kling-3.0-omni" || normalized["duration"] != 5 || normalized["multi_shot"] != true || normalized["shot_type"] != "customize" {
t.Fatalf("unexpected multi-shot fields: %+v", normalized)
}
content, _ := normalized["content"].([]any)
if len(content) != 4 {
t.Fatalf("expected image, element and two shot prompts, got %+v", content)
}
}
func TestNormalizeKelingOmniRequestRejectsUnsupportedCombinations(t *testing.T) {
tests := []struct {
name string
body map[string]any
}{
{
name: "callback",
body: map[string]any{"callback_url": "https://example.com/callback"},
},
{
name: "unknown model",
body: map[string]any{"model_name": "kling-unknown", "prompt": "x", "aspect_ratio": "16:9"},
},
{
name: "o1 duration",
body: map[string]any{"model_name": "kling-video-o1", "prompt": "x", "aspect_ratio": "16:9", "duration": 11},
},
{
name: "o1 text to video three seconds",
body: map[string]any{"model_name": "kling-video-o1", "prompt": "x", "aspect_ratio": "16:9", "duration": 3},
},
{
name: "o1 generated audio",
body: map[string]any{"model_name": "kling-video-o1", "prompt": "x", "sound": "on", "aspect_ratio": "16:9", "duration": 5},
},
{
name: "video sound",
body: map[string]any{
"model_name": "kling-v3-omni",
"prompt": "edit",
"sound": "on",
"video_list": []any{map[string]any{"video_url": "https://example.com/base.mp4", "refer_type": "base"}},
},
},
{
name: "first frame ratio",
body: map[string]any{
"prompt": "animate",
"aspect_ratio": "16:9",
"image_list": []any{map[string]any{"image_url": "https://example.com/first.png", "type": "first_frame"}},
},
},
}
for _, item := range tests {
t.Run(item.name, func(t *testing.T) {
_, err := normalizeKelingOmniRequest(item.body)
if err == nil || err.Code != 1201 && err.Code != 1203 {
t.Fatalf("expected official compatibility error, got %v", err)
}
})
}
}
func TestNormalizeKelingO1AllowsThreeSecondsWithReferenceImage(t *testing.T) {
normalized, err := normalizeKelingOmniRequest(map[string]any{
"model_name": "kling-video-o1",
"prompt": "Use the landscape as a visual reference",
"aspect_ratio": "16:9",
"duration": 3,
"image_list": []any{
map[string]any{"image_url": "https://placehold.co/1024x1024/png"},
},
})
if err != nil {
t.Fatalf("reference-image O1 request should allow three seconds: %v", err)
}
if normalized["duration"] != 3 {
t.Fatalf("unexpected duration: %+v", normalized)
}
}
func TestKelingCompatTaskDataAndOwnership(t *testing.T) {
createdAt := time.Unix(100, 0)
task := store.GatewayTask{
ID: "task-1",
Kind: "videos.generations",
GatewayUserID: "user-1",
Status: "succeeded",
FinalChargeAmount: 2.5,
Request: map[string]any{
"_gateway_compatibility": kelingOmniCompatibilityMarker,
"external_task_id": "external-1",
"watermark": true,
},
Result: map[string]any{"data": []any{map[string]any{
"id": "video-1",
"url": "https://example.com/video.mp4",
"watermark_url": "https://example.com/watermarked.mp4",
"duration": "5",
}}},
CreatedAt: createdAt,
UpdatedAt: createdAt.Add(time.Second),
}
if !isKelingCompatTask(task) || !kelingCompatTaskOwnedBy(task, &auth.User{GatewayUserID: "user-1"}) {
t.Fatalf("expected task ownership and compatibility marker")
}
if kelingCompatTaskOwnedBy(task, &auth.User{GatewayUserID: "user-2"}) {
t.Fatalf("cross-user task access must be rejected")
}
data := kelingCompatTaskData(task)
if data["task_status"] != "succeed" || data["final_unit_deduction"] != "2.5" {
t.Fatalf("unexpected task data: %+v", data)
}
result, _ := data["task_result"].(map[string]any)
videos, _ := result["videos"].([]any)
video, _ := videos[0].(map[string]any)
if video["id"] != "video-1" || video["watermark_url"] != "https://example.com/watermarked.mp4" || video["duration"] != "5" {
t.Fatalf("unexpected compatible video: %+v", video)
}
}
func TestRequireKelingAPIKeyWritesOfficialAuthEnvelope(t *testing.T) {
server := &Server{auth: auth.New("secret", "", "")}
server.auth.LocalAPIKeyVerifier = func(_ context.Context, key string) (*auth.User, error) {
if key != "sk-gw-valid" {
return nil, auth.ErrUnauthorized
}
return &auth.User{ID: "user-1", GatewayUserID: "user-1", APIKeyID: "key-1", APIKeyScopes: []string{"video"}}, nil
}
handler := server.requireKelingAPIKey(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, ok := auth.UserFromContext(r.Context()); !ok {
t.Fatal("authenticated user is missing")
}
w.WriteHeader(http.StatusNoContent)
}))
missing := httptest.NewRecorder()
handler.ServeHTTP(missing, httptest.NewRequest(http.MethodPost, "/v1/videos/omni-video", nil))
if missing.Code != http.StatusUnauthorized {
t.Fatalf("missing auth status=%d body=%s", missing.Code, missing.Body.String())
}
var missingBody KelingCompatibleEnvelope
if err := json.Unmarshal(missing.Body.Bytes(), &missingBody); err != nil || missingBody.Code != 1001 || missingBody.RequestID == "" {
t.Fatalf("unexpected missing auth envelope: %+v err=%v", missingBody, err)
}
invalid := httptest.NewRecorder()
invalidRequest := httptest.NewRequest(http.MethodPost, "/v1/videos/omni-video", nil)
invalidRequest.Header.Set("Authorization", "Bearer sk-gw-invalid")
handler.ServeHTTP(invalid, invalidRequest)
var invalidBody KelingCompatibleEnvelope
if err := json.Unmarshal(invalid.Body.Bytes(), &invalidBody); err != nil || invalid.Code != http.StatusUnauthorized || invalidBody.Code != 1002 {
t.Fatalf("unexpected invalid auth envelope: status=%d body=%+v err=%v", invalid.Code, invalidBody, err)
}
valid := httptest.NewRecorder()
validRequest := httptest.NewRequest(http.MethodPost, "/v1/videos/omni-video", nil)
validRequest.Header.Set("Authorization", "Bearer sk-gw-valid")
handler.ServeHTTP(valid, validRequest)
if valid.Code != http.StatusNoContent {
t.Fatalf("valid API Key status=%d body=%s", valid.Code, valid.Body.String())
}
}
func TestKelingCompatErrorImplementsError(t *testing.T) {
err := newKelingCompatError(http.StatusBadRequest, 1201, "invalid")
if !errors.Is(err, err) || err.Error() != "invalid" {
t.Fatalf("unexpected error behavior: %v", err)
}
}
func TestKelingCompatBusinessCodeMapping(t *testing.T) {
tests := []struct {
code string
message string
want int
}{
{code: "insufficient_balance", want: 1101},
{code: "permission_denied", want: 1103},
{code: "invalid_parameter", want: 1201},
{code: "no_model_candidate", want: 1203},
{code: "rate_limit_exceeded", want: 1302},
{code: "concurrency_limit", want: 1303},
{code: "network", want: 5001},
{code: "unknown", want: 5000},
}
for _, item := range tests {
if got := kelingCompatBusinessCode(item.code, item.message); got != item.want {
t.Fatalf("code=%q message=%q got=%d want=%d", item.code, item.message, got, item.want)
}
}
}
func TestKelingCompatCandidatesSupport4K(t *testing.T) {
candidates := []store.RuntimeModelCandidate{
{Provider: "keling", Capabilities: map[string]any{"omni_video": map[string]any{"output_resolutions": []any{"720p", "1080p"}}}},
{Provider: "keling", Capabilities: map[string]any{"omni_video": map[string]any{"output_resolutions": []any{"2160p"}}}},
}
if !kelingCompatCandidatesSupport4K(candidates) {
t.Fatal("expected 2160p Keling capability to enable mode=4k")
}
if kelingCompatCandidatesSupport4K(candidates[:1]) {
t.Fatal("mode=4k must stay disabled without an explicit capability")
}
}