No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 

364 líneas
13 KiB

  1. package controller
  2. import (
  3. "io"
  4. "net/http"
  5. "net/http/httptest"
  6. "os"
  7. "strings"
  8. "testing"
  9. "github.com/QuantumNous/new-api/common"
  10. "github.com/QuantumNous/new-api/constant"
  11. "github.com/QuantumNous/new-api/dto"
  12. "github.com/QuantumNous/new-api/model"
  13. klingaiping "github.com/QuantumNous/new-api/relay/channel/task/kling/aiping"
  14. relaycommon "github.com/QuantumNous/new-api/relay/common"
  15. "github.com/QuantumNous/new-api/service"
  16. "github.com/gin-gonic/gin"
  17. "github.com/stretchr/testify/require"
  18. "gorm.io/gorm"
  19. "gorm.io/gorm/logger"
  20. )
  21. func TestKlingAipingSubmitPreparationUsesModelNameBeforeModel(t *testing.T) {
  22. c := newControllerJSONContext(t, "/v1/videos/text2video", `{
  23. "model":"Kling-V1.6",
  24. "model_name":"Kling-V2.6",
  25. "prompt":"prompt"
  26. }`)
  27. payload, err := readJSONPayload(c)
  28. require.NoError(t, err)
  29. route, ok := klingaiping.FindRoute(http.MethodPost, "/v1/videos/text2video", klingaiping.RouteKindSubmit)
  30. require.True(t, ok)
  31. info := &relaycommon.RelayInfo{TaskRelayInfo: &relaycommon.TaskRelayInfo{}}
  32. modelName := resolveKlingAipingModel(payload, route)
  33. relaycommon.StoreTaskRequest(c, info, route.Action, relaycommon.TaskSubmitReq{
  34. Model: modelName,
  35. Prompt: stringFromMap(payload, "prompt"),
  36. Metadata: payload,
  37. })
  38. stored, err := relaycommon.GetTaskRequest(c)
  39. require.NoError(t, err)
  40. require.Equal(t, "Kling-V2.6", stored.Model)
  41. require.Equal(t, "prompt", stored.Prompt)
  42. require.Equal(t, "Kling-V1.6", stored.Metadata["model"])
  43. require.Equal(t, "Kling-V2.6", stored.Metadata["model_name"])
  44. }
  45. func TestKlingAipingSubmitPreparationDoesNotLockChannel(t *testing.T) {
  46. c := newControllerJSONContext(t, "/v1/general/custom-voices", `{
  47. "voice_url":"https://example.com/voice.mp3",
  48. "voice_name":"voice"
  49. }`)
  50. payload, err := readJSONPayload(c)
  51. require.NoError(t, err)
  52. route, ok := klingaiping.FindRoute(http.MethodPost, "/v1/general/custom-voices", klingaiping.RouteKindSubmit)
  53. require.True(t, ok)
  54. info := &relaycommon.RelayInfo{TaskRelayInfo: &relaycommon.TaskRelayInfo{}}
  55. info.OriginModelName = resolveKlingAipingModel(payload, route)
  56. info.Action = route.Action
  57. relaycommon.StoreTaskRequest(c, info, route.Action, relaycommon.TaskSubmitReq{
  58. Model: info.OriginModelName,
  59. Metadata: payload,
  60. })
  61. require.Equal(t, klingaiping.ModelCustomVoices, info.OriginModelName)
  62. require.Equal(t, klingaiping.ActionVoicesCreate, info.Action)
  63. require.Nil(t, info.LockedChannel)
  64. }
  65. func TestConfigureKlingAipingTaskRelayInfoForcesChannelSelection(t *testing.T) {
  66. c := newControllerJSONContext(t, "/v1/videos/text2video", `{"model_name":"Kling-V2.6","prompt":"prompt"}`)
  67. payload, err := readJSONPayload(c)
  68. require.NoError(t, err)
  69. route, ok := klingaiping.FindRoute(http.MethodPost, "/v1/videos/text2video", klingaiping.RouteKindSubmit)
  70. require.True(t, ok)
  71. info := &relaycommon.RelayInfo{TaskRelayInfo: &relaycommon.TaskRelayInfo{}}
  72. configureKlingAipingTaskRelayInfo(c, info, route, payload)
  73. require.NotNil(t, info.ChannelMeta)
  74. require.Zero(t, info.ChannelMeta.ChannelType)
  75. require.Equal(t, "Kling-V2.6", info.OriginModelName)
  76. require.Equal(t, klingaiping.ActionText2Video, info.Action)
  77. require.Nil(t, info.LockedChannel)
  78. }
  79. func TestConfigureKlingAipingTaskRelayInfoStoresDuration(t *testing.T) {
  80. c := newControllerJSONContext(t, "/v1/videos/text2video", `{"model_name":"Kling-V2.6","prompt":"prompt","duration":5}`)
  81. payload, err := readJSONPayload(c)
  82. require.NoError(t, err)
  83. route, ok := klingaiping.FindRoute(http.MethodPost, "/v1/videos/text2video", klingaiping.RouteKindSubmit)
  84. require.True(t, ok)
  85. info := &relaycommon.RelayInfo{TaskRelayInfo: &relaycommon.TaskRelayInfo{}}
  86. configureKlingAipingTaskRelayInfo(c, info, route, payload)
  87. stored, err := relaycommon.GetTaskRequest(c)
  88. require.NoError(t, err)
  89. require.Equal(t, 5, stored.Duration)
  90. require.Empty(t, stored.Seconds)
  91. }
  92. func TestRequiredTaskChannelTypeOnlyMatchesKlingAipingNativePaths(t *testing.T) {
  93. c := newControllerJSONContext(t, "/v1/videos/text2video", `{}`)
  94. c.Request.URL.Path = "/v1/videos/text2video"
  95. require.Zero(t, requiredTaskChannelTypeForRequest(c))
  96. require.ElementsMatch(t,
  97. service.VideoAssetChannelTypesForFamily(service.VideoAssetFamilyKling),
  98. allowedTaskChannelTypesForRequest(c),
  99. )
  100. c = newControllerJSONContext(t, "/v1/videos/video-extend", `{}`)
  101. c.Request.URL.Path = "/v1/videos/video-extend"
  102. require.Zero(t, requiredTaskChannelTypeForRequest(c))
  103. require.ElementsMatch(t,
  104. service.VideoAssetChannelTypesForFamily(service.VideoAssetFamilyKling),
  105. allowedTaskChannelTypesForRequest(c),
  106. )
  107. c = newControllerJSONContext(t, "/v1/videos/video_123/remix", `{}`)
  108. c.Request.URL.Path = "/v1/videos/video_123/remix"
  109. require.Zero(t, requiredTaskChannelTypeForRequest(c))
  110. require.Nil(t, allowedTaskChannelTypesForRequest(c))
  111. }
  112. func TestKlingAipingTaskDataObjectUsesPublicTaskIDAndWatermarkURL(t *testing.T) {
  113. task := &model.Task{
  114. TaskID: "task_public",
  115. Status: model.TaskStatusSuccess,
  116. CreatedAt: 100,
  117. UpdatedAt: 200,
  118. Data: []byte(`{
  119. "code":0,
  120. "aiping_id":"internal",
  121. "data":{
  122. "task_id":"899333358055493641",
  123. "task_status":"succeed",
  124. "task_result":{"videos":[{"id":"v1","url":"https://example.com/video.mp4","duration":"5.041"}]}
  125. }
  126. }`),
  127. }
  128. data := taskDataObject(task)
  129. require.Equal(t, "task_public", data["task_id"])
  130. taskResult := data["task_result"].(map[string]any)
  131. videos := taskResult["videos"].([]any)
  132. require.Equal(t, "", videos[0].(map[string]any)["watermark_url"])
  133. }
  134. func TestDoKlingAipingProxyRequestUsesSelectedContextKey(t *testing.T) {
  135. service.InitHttpClient()
  136. var gotAuth string
  137. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  138. gotAuth = r.Header.Get("Authorization")
  139. w.Header().Set("Content-Type", "application/json")
  140. _, _ = w.Write([]byte(`{"code":0,"message":"success","data":[]}`))
  141. }))
  142. defer server.Close()
  143. c := newControllerJSONContext(t, "/v1/general/advanced-presets-elements", ``)
  144. common.SetContextKey(c, constant.ContextKeyChannelKey, "selected-key")
  145. route, ok := klingaiping.FindRoute(http.MethodGet, "/v1/general/advanced-presets-elements", klingaiping.RouteKindProxy)
  146. require.True(t, ok)
  147. channel := &model.Channel{
  148. Key: "raw-channel-key",
  149. BaseURL: common.GetPointer(server.URL),
  150. }
  151. resp, err := doKlingAipingProxyRequest(c, route, channel)
  152. require.NoError(t, err)
  153. defer resp.Body.Close()
  154. require.Equal(t, "Bearer selected-key", gotAuth)
  155. }
  156. func TestKlingAipingNativeProxyPersistsFallbackBinding(t *testing.T) {
  157. service.InitHttpClient()
  158. db := setupKlingAipingNativeProxyDB(t)
  159. upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  160. w.Header().Set("Content-Type", "application/json")
  161. _, _ = w.Write([]byte(`{"code":0,"message":"success","data":[]}`))
  162. }))
  163. defer upstream.Close()
  164. createKlingAipingProxyChannelForTest(t, db, 59, "default", "proxy-key", upstream.URL)
  165. createKlingAipingProxyAbilityForTest(t, db, "default", klingaiping.ModelKlingAdvancedElements, 59, true)
  166. w := httptest.NewRecorder()
  167. _, engine := gin.CreateTestContext(w)
  168. engine.GET("/v1/general/advanced-presets-elements", func(c *gin.Context) {
  169. c.Set("id", 10)
  170. common.SetContextKey(c, constant.ContextKeyUsingGroup, "default")
  171. KlingAipingNativeProxy(c)
  172. })
  173. engine.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/v1/general/advanced-presets-elements", nil))
  174. require.Equal(t, http.StatusOK, w.Code)
  175. binding, err := model.GetUserAssetChannel(10, constant.ChannelTypeKlingAiping, "default")
  176. require.NoError(t, err)
  177. require.NotNil(t, binding)
  178. require.Equal(t, 59, binding.ChannelId)
  179. }
  180. func TestCopyProxyResponseNormalizesMsgError(t *testing.T) {
  181. w := httptest.NewRecorder()
  182. c, _ := gin.CreateTestContext(w)
  183. c.Set(common.RequestIdKey, "req-test")
  184. resp := &http.Response{
  185. StatusCode: http.StatusUnauthorized,
  186. Header: http.Header{"Content-Type": []string{"application/json"}},
  187. Body: io.NopCloser(strings.NewReader(`{"code":401,"msg":"unauthorized","data":null}`)),
  188. }
  189. copyProxyResponse(c, resp)
  190. require.Equal(t, http.StatusUnauthorized, w.Code)
  191. require.Contains(t, w.Body.String(), `"message":"unauthorized"`)
  192. require.NotContains(t, w.Body.String(), `"msg"`)
  193. require.Contains(t, w.Body.String(), `"request_id":"req-test"`)
  194. }
  195. func TestCopyProxyResponseNormalizesPlainTextError(t *testing.T) {
  196. w := httptest.NewRecorder()
  197. c, _ := gin.CreateTestContext(w)
  198. c.Set(common.RequestIdKey, "req-test")
  199. resp := &http.Response{
  200. StatusCode: http.StatusMethodNotAllowed,
  201. Header: http.Header{"Content-Type": []string{"text/plain"}},
  202. Body: io.NopCloser(strings.NewReader("Method Not Allowed")),
  203. }
  204. copyProxyResponse(c, resp)
  205. require.Equal(t, http.StatusMethodNotAllowed, w.Code)
  206. require.Contains(t, w.Body.String(), `"message":"Method Not Allowed"`)
  207. require.Contains(t, w.Body.String(), `"request_id":"req-test"`)
  208. }
  209. func TestCopyProxyResponseNormalizesDetailMessageError(t *testing.T) {
  210. w := httptest.NewRecorder()
  211. c, _ := gin.CreateTestContext(w)
  212. c.Set(common.RequestIdKey, "req-test")
  213. resp := &http.Response{
  214. StatusCode: http.StatusServiceUnavailable,
  215. Header: http.Header{"Content-Type": []string{"application/json"}},
  216. Body: io.NopCloser(strings.NewReader(`{"detail":{"message":"not found","error_type":"not_found"},"aiping_id":"internal"}`)),
  217. }
  218. copyProxyResponse(c, resp)
  219. require.Equal(t, http.StatusServiceUnavailable, w.Code)
  220. require.Contains(t, w.Body.String(), `"message":"not found"`)
  221. require.NotContains(t, w.Body.String(), `map[`)
  222. require.Contains(t, w.Body.String(), `"request_id":"req-test"`)
  223. }
  224. func TestNormalizeKlingAipingTaskErrorMessageExtractsUpstreamJSONMessage(t *testing.T) {
  225. taskErr := &dto.TaskError{
  226. Code: "fail_to_fetch_task",
  227. Message: `{"code":400,"message":"ERROR: image download failed","request_id":"upstream"}`,
  228. StatusCode: http.StatusBadRequest,
  229. }
  230. normalizeKlingAipingTaskError(taskErr)
  231. require.Equal(t, "ERROR: image download failed", taskErr.Message)
  232. }
  233. func TestParseKlingAipingPageBoundaries(t *testing.T) {
  234. c := newControllerJSONContext(t, "/v1/videos/text2video?pageNum=1001&pageSize=30", `{}`)
  235. c.Request.URL.RawQuery = "pageNum=1001&pageSize=30"
  236. _, _, err := parseKlingAipingPage(c)
  237. require.ErrorContains(t, err, "pageNum")
  238. c = newControllerJSONContext(t, "/v1/videos/text2video?pageNum=1&pageSize=501", `{}`)
  239. c.Request.URL.RawQuery = "pageNum=1&pageSize=501"
  240. _, _, err = parseKlingAipingPage(c)
  241. require.ErrorContains(t, err, "pageSize")
  242. }
  243. func setupKlingAipingNativeProxyDB(t *testing.T) *gorm.DB {
  244. t.Helper()
  245. oldDB := model.DB
  246. oldSQLitePath := common.SQLitePath
  247. oldMemoryCacheEnabled := common.MemoryCacheEnabled
  248. oldIsMasterNode := common.IsMasterNode
  249. oldUsingSQLite := common.UsingSQLite
  250. oldUsingMySQL := common.UsingMySQL
  251. oldUsingPostgreSQL := common.UsingPostgreSQL
  252. oldSQLDSN, hadSQLDSN := os.LookupEnv("SQL_DSN")
  253. common.SQLitePath = "file:" + strings.NewReplacer("/", "_", " ", "_").Replace(t.Name()) + "?mode=memory&cache=shared"
  254. common.MemoryCacheEnabled = false
  255. common.IsMasterNode = false
  256. common.UsingSQLite = false
  257. common.UsingMySQL = false
  258. common.UsingPostgreSQL = false
  259. require.NoError(t, os.Setenv("SQL_DSN", "local"))
  260. require.NoError(t, model.InitDB())
  261. model.DB = model.DB.Session(&gorm.Session{Logger: logger.Default.LogMode(logger.Silent)})
  262. db := model.DB
  263. sqlDB, err := db.DB()
  264. require.NoError(t, err)
  265. sqlDB.SetMaxOpenConns(1)
  266. require.NoError(t, db.AutoMigrate(&model.Channel{}, &model.Ability{}, &model.UserAssetChannel{}))
  267. t.Cleanup(func() {
  268. _ = sqlDB.Close()
  269. model.DB = oldDB
  270. common.SQLitePath = oldSQLitePath
  271. common.MemoryCacheEnabled = oldMemoryCacheEnabled
  272. common.IsMasterNode = oldIsMasterNode
  273. common.UsingSQLite = oldUsingSQLite
  274. common.UsingMySQL = oldUsingMySQL
  275. common.UsingPostgreSQL = oldUsingPostgreSQL
  276. if hadSQLDSN {
  277. _ = os.Setenv("SQL_DSN", oldSQLDSN)
  278. } else {
  279. _ = os.Unsetenv("SQL_DSN")
  280. }
  281. })
  282. return db
  283. }
  284. func createKlingAipingProxyChannelForTest(t *testing.T, db *gorm.DB, id int, group string, key string, baseURL string) {
  285. t.Helper()
  286. priority := int64(id)
  287. weight := uint(10)
  288. autoBan := 1
  289. require.NoError(t, db.Create(&model.Channel{
  290. Id: id,
  291. Type: constant.ChannelTypeKlingAiping,
  292. Key: key,
  293. Status: common.ChannelStatusEnabled,
  294. Name: "kling-aiping-proxy",
  295. Group: group,
  296. Models: klingaiping.ModelKlingAdvancedElements,
  297. BaseURL: common.GetPointer(baseURL),
  298. Priority: &priority,
  299. Weight: &weight,
  300. AutoBan: &autoBan,
  301. }).Error)
  302. }
  303. func createKlingAipingProxyAbilityForTest(t *testing.T, db *gorm.DB, group string, modelName string, channelId int, enabled bool) {
  304. t.Helper()
  305. priority := int64(channelId)
  306. require.NoError(t, db.Create(&model.Ability{
  307. Group: group,
  308. Model: modelName,
  309. ChannelId: channelId,
  310. Enabled: enabled,
  311. Priority: &priority,
  312. Weight: 10,
  313. }).Error)
  314. }