Files
sub2api/backend/internal/service/seedance_test.go
T

113 lines
5.9 KiB
Go

//go:build unit
package service
import (
"context"
"io"
"net/http"
"testing"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func seedanceTestAccount() *Account {
return &Account{ID: 9, Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Credentials: map[string]any{
"api_key": "ark-secret", "base_url": "https://ark.cn-beijing.volces.com/api/v3",
"openai_capabilities": []string{"seedance"},
"model_mapping": map[string]any{"video": "ep-seedance"},
}}
}
func TestSeedanceNativeForwarding(t *testing.T) {
body := []byte(`{"model":"video","content":[{"type":"text","text":"waves"},{"type":"image_url","image_url":{"url":"https://example.com/first.png"},"role":"first_frame"},{"type":"audio_url","audio_url":{"url":"https://example.com/audio.mp3"}}],"duration":-1,"generate_audio":true,"future_field":{"keep":true}}`)
upstream := &grokMediaContentUpstreamStub{response: grokMediaContentStatusResponse(`{"id":"task-1"}`)}
svc := &OpenAIGatewayService{httpUpstream: upstream}
c, w := grokMediaContentTestContext(http.MethodPost, "/api/v3/contents/generations/tasks", nil)
result, err := svc.ForwardSeedance(context.Background(), c, seedanceTestAccount(), SeedanceEndpointCreate, "", body)
require.NoError(t, err)
require.JSONEq(t, `{"id":"task-1"}`, w.Body.String())
require.Equal(t, "seedance:task-1", result.ResponseID)
require.Zero(t, result.Usage.OutputTokens)
require.Equal(t, "video", result.BillingModel)
require.Equal(t, "ep-seedance", result.UpstreamModel)
require.Equal(t, "https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks", upstream.request.URL.String())
require.Equal(t, "Bearer ark-secret", upstream.request.Header.Get("Authorization"))
forwarded, err := io.ReadAll(upstream.request.Body)
require.NoError(t, err)
require.Equal(t, "ep-seedance", gjson.GetBytes(forwarded, "model").String())
for _, field := range []string{"content", "duration", "generate_audio", "future_field"} {
require.Equal(t, gjson.GetBytes(body, field).Raw, gjson.GetBytes(forwarded, field).Raw)
}
}
func TestSeedanceStatusAndDelete(t *testing.T) {
for _, status := range []string{"queued", "running", "failed", "cancelled", "expired", "succeeded"} {
t.Run(status, func(t *testing.T) {
body := `{"id":"task-1","status":"` + status + `","model":"ep-seedance","content":{"video_url":"https://cdn.example/video.mp4"},"usage":{"completion_tokens":12345}}`
upstream := &grokMediaContentUpstreamStub{response: grokMediaContentStatusResponse(body)}
svc := &OpenAIGatewayService{httpUpstream: upstream}
c, w := grokMediaContentTestContext(http.MethodGet, "/api/v3/contents/generations/tasks/task-1", nil)
result, err := svc.ForwardSeedance(context.Background(), c, seedanceTestAccount(), SeedanceEndpointStatus, "seedance:task-1", nil)
require.NoError(t, err)
require.JSONEq(t, body, w.Body.String())
require.Equal(t, "/api/v3/contents/generations/tasks/task-1", upstream.request.URL.Path)
if status == "succeeded" {
require.Equal(t, 12345, result.Usage.OutputTokens)
} else {
require.Zero(t, result.Usage.OutputTokens)
}
require.Zero(t, result.VideoCount, "must use token billing, not Grok seconds")
})
}
upstream := &grokMediaContentUpstreamStub{response: grokMediaContentStatusResponse("")}
upstream.response.StatusCode = http.StatusNoContent
svc := &OpenAIGatewayService{httpUpstream: upstream}
c, w := grokMediaContentTestContext(http.MethodDelete, "/api/v3/contents/generations/tasks/task-1", nil)
_, err := svc.ForwardSeedance(context.Background(), c, seedanceTestAccount(), SeedanceEndpointDelete, "seedance:task-1", nil)
require.NoError(t, err)
require.Equal(t, http.MethodDelete, upstream.request.Method)
require.Equal(t, http.StatusNoContent, w.Code)
}
func TestSeedanceValidationAndCapability(t *testing.T) {
for _, body := range []string{`{`, `[]`, `{}`, `{"model":12,"content":[{}]}`, `{"model":"x","content":[]}`} {
_, err := ParseSeedanceRequest([]byte(body))
require.Error(t, err)
}
info, err := ParseSeedanceRequest([]byte(`{"model":"x","content":[{"type":"text","text":"first"},{"type":"text","text":"second"},{"type":"image_url","image_url":{"url":"https://example.com/a.png"}}]}`))
require.NoError(t, err)
require.Contains(t, string(info.ModerationBody()), "first")
require.Contains(t, string(info.ModerationBody()), "second")
require.Contains(t, string(info.ModerationBody()), "https://example.com/a.png")
for _, id := range []string{"", "..", "a/b", "a?b", "a#b", "%2e%2e"} {
_, err := buildSeedanceURL("https://example.com", SeedanceEndpointStatus, id)
require.Error(t, err, id)
}
for _, base := range []string{"https://example.com", "https://example.com/api/v3/", "https://example.com/v3"} {
url, err := buildSeedanceURL(base, SeedanceEndpointCreate, "")
require.NoError(t, err)
require.NotContains(t, url, "/v3/api/v3")
}
a := seedanceTestAccount()
require.True(t, a.SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilitySeedance))
a.Type = AccountTypeOAuth
require.False(t, a.SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilitySeedance))
a.Type = AccountTypeAPIKey
delete(a.Credentials, "openai_capabilities")
require.False(t, a.SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilitySeedance))
}
func TestSeedancePreservesUpstreamErrorsWithoutRetry(t *testing.T) {
upstream := &grokMediaContentUpstreamStub{response: grokMediaContentStatusResponse(`{"error":{"code":"QuotaExceeded","message":"quota exhausted"}}`)}
upstream.response.StatusCode = 429
svc := &OpenAIGatewayService{httpUpstream: upstream}
c, w := grokMediaContentTestContext(http.MethodPost, "/api/v3/contents/generations/tasks", nil)
_, err := svc.ForwardSeedance(context.Background(), c, seedanceTestAccount(), SeedanceEndpointCreate, "", []byte(`{"model":"video","content":[{"type":"text","text":"waves"}]}`))
require.Error(t, err)
require.Equal(t, 429, w.Code)
require.Contains(t, w.Body.String(), "QuotaExceeded")
require.Len(t, upstream.requests, 1)
}