260 lines
9.1 KiB
Go
260 lines
9.1 KiB
Go
package openaivideo
|
||
|
||
import (
|
||
"bytes"
|
||
"io"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"testing"
|
||
|
||
"github.com/QuantumNous/new-api/common"
|
||
"github.com/QuantumNous/new-api/model"
|
||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
func newSophnetAdaptor() *TaskAdaptor {
|
||
// 与真实 happyhorse 渠道一致:Sophnet 协议由 base URL 触发;测试中直接固定 protocol。
|
||
return &TaskAdaptor{
|
||
baseURL: "https://www.sophnet.com/api/open-apis/projects/easyllms",
|
||
protocol: ProtocolSophnet,
|
||
apiKey: "test-key",
|
||
}
|
||
}
|
||
|
||
func newTestGinAndInfo() (*gin.Context, *relaycommon.RelayInfo, *httptest.ResponseRecorder) {
|
||
gin.SetMode(gin.TestMode)
|
||
w := httptest.NewRecorder()
|
||
c, _ := gin.CreateTestContext(w)
|
||
info := &relaycommon.RelayInfo{
|
||
TaskRelayInfo: &relaycommon.TaskRelayInfo{
|
||
PublicTaskID: "task_upstream_shape_test",
|
||
},
|
||
OriginModelName: "happyhorse",
|
||
}
|
||
return c, info, w
|
||
}
|
||
|
||
// TestSophnetDoResponse_InformalUpstreamBodies 模拟 Sophnet/happyhorse 上游返回非规范或错误 JSON 时,
|
||
// 提交阶段 DoResponse 的错误码与分支(与预扣费之后的解析行为对齐,便于回归)。
|
||
func TestSophnetDoResponse_InformalUpstreamBodies(t *testing.T) {
|
||
a := newSophnetAdaptor()
|
||
|
||
t.Run("non_json", func(t *testing.T) {
|
||
c, info, w := newTestGinAndInfo()
|
||
resp := &http.Response{
|
||
StatusCode: http.StatusOK,
|
||
Body: io.NopCloser(bytes.NewBufferString(`not json at all`)),
|
||
}
|
||
_, _, taskErr := a.DoResponse(c, resp, info)
|
||
require.NotNil(t, taskErr)
|
||
assert.Equal(t, "unmarshal_response_body_failed", taskErr.Code)
|
||
assert.Equal(t, http.StatusInternalServerError, taskErr.StatusCode)
|
||
assert.Empty(t, w.Body.String())
|
||
})
|
||
|
||
t.Run("html_502", func(t *testing.T) {
|
||
c, info, w := newTestGinAndInfo()
|
||
resp := &http.Response{
|
||
StatusCode: http.StatusBadGateway,
|
||
Body: io.NopCloser(bytes.NewBufferString(`<html><body>502 Bad Gateway</body></html>`)),
|
||
}
|
||
_, _, taskErr := a.DoResponse(c, resp, info)
|
||
require.NotNil(t, taskErr)
|
||
assert.Equal(t, "unmarshal_response_body_failed", taskErr.Code)
|
||
assert.Empty(t, w.Body.String())
|
||
})
|
||
|
||
t.Run("empty_object_no_task_id", func(t *testing.T) {
|
||
c, info, w := newTestGinAndInfo()
|
||
resp := &http.Response{
|
||
StatusCode: http.StatusOK,
|
||
Body: io.NopCloser(bytes.NewBufferString(`{}`)),
|
||
}
|
||
_, _, taskErr := a.DoResponse(c, resp, info)
|
||
require.NotNil(t, taskErr)
|
||
assert.Equal(t, "invalid_response", taskErr.Code)
|
||
assert.Contains(t, taskErr.Message, "task id is empty")
|
||
assert.Empty(t, w.Body.String())
|
||
})
|
||
|
||
t.Run("status_20109_upstream_business_error", func(t *testing.T) {
|
||
c, info, w := newTestGinAndInfo()
|
||
body := `{"status":20109,"message":"余额不足","result":null}`
|
||
resp := &http.Response{
|
||
StatusCode: http.StatusOK,
|
||
Body: io.NopCloser(bytes.NewBufferString(body)),
|
||
}
|
||
_, _, taskErr := a.DoResponse(c, resp, info)
|
||
require.NotNil(t, taskErr)
|
||
assert.Equal(t, "video_submit_failed", taskErr.Code)
|
||
assert.Equal(t, http.StatusBadRequest, taskErr.StatusCode)
|
||
assert.Contains(t, taskErr.Message, "余额不足")
|
||
assert.Empty(t, w.Body.String())
|
||
})
|
||
|
||
t.Run("status_zero_but_task_id_missing", func(t *testing.T) {
|
||
c, info, w := newTestGinAndInfo()
|
||
body := `{"status":0,"message":"","result":{}}`
|
||
resp := &http.Response{
|
||
StatusCode: http.StatusOK,
|
||
Body: io.NopCloser(bytes.NewBufferString(body)),
|
||
}
|
||
_, _, taskErr := a.DoResponse(c, resp, info)
|
||
require.NotNil(t, taskErr)
|
||
assert.Equal(t, "invalid_response", taskErr.Code)
|
||
assert.Contains(t, taskErr.Message, "task id is empty")
|
||
assert.Empty(t, w.Body.String())
|
||
})
|
||
|
||
t.Run("success_returns_upstream_task_id", func(t *testing.T) {
|
||
c, info, w := newTestGinAndInfo()
|
||
body := `{"status":0,"message":"","result":{"task_id":"upstream-task-abc"}}`
|
||
resp := &http.Response{
|
||
StatusCode: http.StatusOK,
|
||
Body: io.NopCloser(bytes.NewBufferString(body)),
|
||
}
|
||
taskID, raw, taskErr := a.DoResponse(c, resp, info)
|
||
require.Nil(t, taskErr)
|
||
assert.Equal(t, "upstream-task-abc", taskID)
|
||
assert.Equal(t, body, string(raw))
|
||
assert.Equal(t, http.StatusOK, w.Code)
|
||
assert.Contains(t, w.Body.String(), "task_upstream_shape_test")
|
||
})
|
||
}
|
||
|
||
// TestSophnetParseTaskResult_InformalBodies 轮询阶段 ParseTaskResult:非 JSON / 非 Sophnet 形 / 业务失败。
|
||
func TestSophnetParseTaskResult_InformalBodies(t *testing.T) {
|
||
a := newSophnetAdaptor()
|
||
|
||
t.Run("garbage_falls_through_ark_and_errors", func(t *testing.T) {
|
||
_, err := a.ParseTaskResult([]byte(`{"id":123}`))
|
||
require.Error(t, err)
|
||
})
|
||
|
||
t.Run("sophnet_top_level_failure", func(t *testing.T) {
|
||
body := `{"status":500,"message":"内部错误","result":null}`
|
||
ti, err := a.ParseTaskResult([]byte(body))
|
||
require.NoError(t, err)
|
||
require.NotNil(t, ti)
|
||
assert.Equal(t, model.TaskStatusFailure, ti.Status)
|
||
assert.Equal(t, "100%", ti.Progress)
|
||
assert.Contains(t, ti.Reason, "内部错误")
|
||
})
|
||
|
||
t.Run("sophnet_wrapped_ark_success", func(t *testing.T) {
|
||
body := `{"status":0,"message":"","result":{"id":"vid-1","model":"happyhorse-1.0-t2v","status":"succeeded","content":{"video_url":"https://example.com/v.mp4"}}}`
|
||
ti, err := a.ParseTaskResult([]byte(body))
|
||
require.NoError(t, err)
|
||
require.NotNil(t, ti)
|
||
assert.Contains(t, ti.Url, "v.mp4")
|
||
})
|
||
}
|
||
|
||
// TestDetectProtocol_SophnetMarkers 与 happyhorse 渠道 base_url 判定一致。
|
||
func TestDetectProtocol_SophnetMarkers(t *testing.T) {
|
||
assert.Equal(t, ProtocolSophnet, DetectProtocol("https://www.sophnet.com/api/open-apis/projects/easyllms/foo"))
|
||
assert.Equal(t, ProtocolSophnet, DetectProtocol("http://127.0.0.1:9999/videogenerator/api"))
|
||
assert.Equal(t, ProtocolMaaS, DetectProtocol("https://maas.hidreamai.com/api/maas/gw"))
|
||
assert.Equal(t, ProtocolMaaS, DetectProtocol("https://hiharness.hidreamai.com"))
|
||
assert.Equal(t, ProtocolArk, DetectProtocol("https://reseller.example.com/v1"))
|
||
}
|
||
|
||
func TestNormalizeMaaSBaseURL_HiHarness(t *testing.T) {
|
||
assert.Equal(t, "https://hiharness.hidreamai.com/api/maas/gw", normalizeMaaSBaseURL("https://hiharness.hidreamai.com"))
|
||
assert.Equal(t, "https://hiharness.hidreamai.com/api/maas/gw", normalizeMaaSBaseURL("https://hiharness.hidreamai.com/api/maas/gw"))
|
||
assert.Equal(t, "https://maas.hidreamai.com", normalizeMaaSBaseURL("https://maas.hidreamai.com"))
|
||
}
|
||
|
||
func TestBuildMaasPayloadMap_AutoConvertsPlaygroundVideoFields(t *testing.T) {
|
||
a := &TaskAdaptor{}
|
||
req := &relaycommon.TaskSubmitReq{
|
||
Model: "Seedance2.0",
|
||
Prompt: "跳舞的小女孩",
|
||
Images: []string{"https://example.com/ref.png"},
|
||
Size: "1280x720",
|
||
Duration: 5,
|
||
Metadata: map[string]any{
|
||
"generate_audio": true,
|
||
"video_urls": []any{"https://example.com/ref.mp4?token=abc"},
|
||
"audio_urls": []string{"https://example.com/ref.mp3"},
|
||
},
|
||
}
|
||
|
||
body, err := a.buildMaasPayloadMap(req)
|
||
require.NoError(t, err)
|
||
|
||
assert.Equal(t, "Seedance2.0", body["model_id"])
|
||
assert.Equal(t, 5, body["duration"])
|
||
assert.Equal(t, "720p", body["resolution"])
|
||
assert.Equal(t, "16:9", body["ratio"])
|
||
assert.Equal(t, true, body["generate_audio"])
|
||
assert.NotContains(t, body, "video_urls")
|
||
assert.NotContains(t, body, "audio_urls")
|
||
|
||
content, ok := body["content"].([]map[string]any)
|
||
require.True(t, ok)
|
||
require.Len(t, content, 4)
|
||
assert.Equal(t, "text", content[0]["type"])
|
||
assert.Equal(t, "image_url", content[1]["type"])
|
||
assert.Equal(t, "video_url", content[2]["type"])
|
||
assert.Equal(t, "audio_url", content[3]["type"])
|
||
}
|
||
|
||
func TestConvertToOpenAIVideo_MaaSCompletedShape(t *testing.T) {
|
||
a := &TaskAdaptor{}
|
||
task := &model.Task{
|
||
TaskID: "task_wjlMGb4cfEgrqq7oubXjWPdXPmSjfRfe",
|
||
Status: model.TaskStatusSuccess,
|
||
Progress: "100%",
|
||
CreatedAt: 1778293580,
|
||
FinishTime: 1778293807,
|
||
Properties: model.Properties{OriginModelName: "Seedance2.0"},
|
||
Data: []byte(`{
|
||
"code": 0,
|
||
"message": "Success",
|
||
"result": {
|
||
"status": 1,
|
||
"sub_task_results": [
|
||
{
|
||
"url": "https://media.hidreamai.com/03fbb389-91ad-4f59-b5ce-c57c41770209.mp4",
|
||
"task_status": 1
|
||
}
|
||
]
|
||
}
|
||
}`),
|
||
}
|
||
|
||
body, err := a.ConvertToOpenAIVideo(task)
|
||
require.NoError(t, err)
|
||
|
||
var got map[string]any
|
||
require.NoError(t, common.Unmarshal(body, &got))
|
||
assert.Equal(t, "task_wjlMGb4cfEgrqq7oubXjWPdXPmSjfRfe", got["id"])
|
||
assert.Equal(t, "video.generation", got["object"])
|
||
assert.Equal(t, "Seedance2.0", got["model"])
|
||
assert.Equal(t, "completed", got["status"])
|
||
assert.Equal(t, float64(100), got["progress"])
|
||
assert.Nil(t, got["error"])
|
||
output, ok := got["output"].(map[string]any)
|
||
require.True(t, ok)
|
||
assert.Equal(t, "https://media.hidreamai.com/03fbb389-91ad-4f59-b5ce-c57c41770209.mp4", output["video_url"])
|
||
}
|
||
|
||
func TestDetectResponseProtocol_SophnetNumericStatusWithResult(t *testing.T) {
|
||
raw := []byte(`{"status":0,"result":{"task_id":"x"}}`)
|
||
assert.Equal(t, ProtocolSophnet, detectResponseProtocol(raw))
|
||
}
|
||
|
||
func TestDetectResponseProtocol_MalformedUsesArk(t *testing.T) {
|
||
raw := []byte(`not json`)
|
||
assert.Equal(t, ProtocolArk, detectResponseProtocol(raw))
|
||
}
|
||
|
||
func TestDetectResponseProtocol_ArrayBodyUsesArk(t *testing.T) {
|
||
raw := []byte(`[{"status":0}]`)
|
||
assert.Equal(t, ProtocolArk, detectResponseProtocol(raw))
|
||
}
|