admin_basic_handlers_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. package admin
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "net/http"
  6. "net/http/httptest"
  7. "testing"
  8. "github.com/gin-gonic/gin"
  9. "github.com/stretchr/testify/require"
  10. )
  11. func setupAdminRouter() (*gin.Engine, *stubAdminService) {
  12. gin.SetMode(gin.TestMode)
  13. router := gin.New()
  14. adminSvc := newStubAdminService()
  15. userHandler := NewUserHandler(adminSvc, nil, nil, nil, nil, nil, nil)
  16. groupHandler := NewGroupHandler(adminSvc, nil, nil)
  17. proxyHandler := NewProxyHandler(adminSvc)
  18. redeemHandler := NewRedeemHandler(adminSvc, nil)
  19. router.GET("/api/v1/admin/users", userHandler.List)
  20. router.GET("/api/v1/admin/users/:id", userHandler.GetByID)
  21. router.POST("/api/v1/admin/users/:id/auth-identities", userHandler.BindAuthIdentity)
  22. router.POST("/api/v1/admin/users", userHandler.Create)
  23. router.PUT("/api/v1/admin/users/:id", userHandler.Update)
  24. router.DELETE("/api/v1/admin/users/:id", userHandler.Delete)
  25. router.POST("/api/v1/admin/users/:id/balance", userHandler.UpdateBalance)
  26. router.GET("/api/v1/admin/users/:id/api-keys", userHandler.GetUserAPIKeys)
  27. router.GET("/api/v1/admin/users/:id/usage", userHandler.GetUserUsage)
  28. router.GET("/api/v1/admin/groups", groupHandler.List)
  29. router.GET("/api/v1/admin/groups/all", groupHandler.GetAll)
  30. router.GET("/api/v1/admin/groups/:id/model-allowlist-candidates", groupHandler.GetGroupModelAllowlistCandidates)
  31. router.GET("/api/v1/admin/groups/:id/composite-routes", groupHandler.ListCompositeRoutes)
  32. router.POST("/api/v1/admin/groups/:id/composite-routes", groupHandler.CreateCompositeRoute)
  33. router.POST("/api/v1/admin/groups/:id/composite-routes/preview", groupHandler.PreviewCompositeRoute)
  34. router.PUT("/api/v1/admin/groups/:id/composite-routes/:route_id", groupHandler.UpdateCompositeRoute)
  35. router.DELETE("/api/v1/admin/groups/:id/composite-routes/:route_id", groupHandler.DeleteCompositeRoute)
  36. router.GET("/api/v1/admin/groups/:id", groupHandler.GetByID)
  37. router.POST("/api/v1/admin/groups", groupHandler.Create)
  38. router.PUT("/api/v1/admin/groups/:id", groupHandler.Update)
  39. router.DELETE("/api/v1/admin/groups/:id", groupHandler.Delete)
  40. router.GET("/api/v1/admin/groups/:id/stats", groupHandler.GetStats)
  41. router.GET("/api/v1/admin/groups/:id/api-keys", groupHandler.GetGroupAPIKeys)
  42. router.GET("/api/v1/admin/proxies", proxyHandler.List)
  43. router.GET("/api/v1/admin/proxies/all", proxyHandler.GetAll)
  44. router.GET("/api/v1/admin/proxies/:id", proxyHandler.GetByID)
  45. router.POST("/api/v1/admin/proxies", proxyHandler.Create)
  46. router.PUT("/api/v1/admin/proxies/:id", proxyHandler.Update)
  47. router.DELETE("/api/v1/admin/proxies/:id", proxyHandler.Delete)
  48. router.POST("/api/v1/admin/proxies/batch-delete", proxyHandler.BatchDelete)
  49. router.POST("/api/v1/admin/proxies/:id/test", proxyHandler.Test)
  50. router.POST("/api/v1/admin/proxies/:id/quality-check", proxyHandler.CheckQuality)
  51. router.GET("/api/v1/admin/proxies/:id/stats", proxyHandler.GetStats)
  52. router.GET("/api/v1/admin/proxies/:id/accounts", proxyHandler.GetProxyAccounts)
  53. router.GET("/api/v1/admin/redeem-codes", redeemHandler.List)
  54. router.GET("/api/v1/admin/redeem-codes/:id", redeemHandler.GetByID)
  55. router.POST("/api/v1/admin/redeem-codes", redeemHandler.Generate)
  56. router.DELETE("/api/v1/admin/redeem-codes/:id", redeemHandler.Delete)
  57. router.POST("/api/v1/admin/redeem-codes/batch-delete", redeemHandler.BatchDelete)
  58. router.POST("/api/v1/admin/redeem-codes/:id/expire", redeemHandler.Expire)
  59. router.GET("/api/v1/admin/redeem-codes/:id/stats", redeemHandler.GetStats)
  60. return router, adminSvc
  61. }
  62. func TestUserHandlerEndpoints(t *testing.T) {
  63. router, _ := setupAdminRouter()
  64. rec := httptest.NewRecorder()
  65. req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/users?page=1&page_size=20", nil)
  66. router.ServeHTTP(rec, req)
  67. require.Equal(t, http.StatusOK, rec.Code)
  68. rec = httptest.NewRecorder()
  69. req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/users/1", nil)
  70. router.ServeHTTP(rec, req)
  71. require.Equal(t, http.StatusOK, rec.Code)
  72. bindBody := map[string]any{
  73. "provider_type": "wechat",
  74. "provider_key": "wechat-main",
  75. "provider_subject": "union-123",
  76. "metadata": map[string]any{"source": "admin-repair"},
  77. "channel": map[string]any{
  78. "channel": "open",
  79. "channel_app_id": "wx-open",
  80. "channel_subject": "openid-123",
  81. },
  82. }
  83. body, _ := json.Marshal(bindBody)
  84. rec = httptest.NewRecorder()
  85. req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/users/1/auth-identities", bytes.NewReader(body))
  86. req.Header.Set("Content-Type", "application/json")
  87. router.ServeHTTP(rec, req)
  88. require.Equal(t, http.StatusOK, rec.Code)
  89. createBody := map[string]any{"email": "new@example.com", "password": "pass123", "balance": 1, "concurrency": 2}
  90. body, _ = json.Marshal(createBody)
  91. rec = httptest.NewRecorder()
  92. req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/users", bytes.NewReader(body))
  93. req.Header.Set("Content-Type", "application/json")
  94. router.ServeHTTP(rec, req)
  95. require.Equal(t, http.StatusOK, rec.Code)
  96. updateBody := map[string]any{"email": "updated@example.com"}
  97. body, _ = json.Marshal(updateBody)
  98. rec = httptest.NewRecorder()
  99. req = httptest.NewRequest(http.MethodPut, "/api/v1/admin/users/1", bytes.NewReader(body))
  100. req.Header.Set("Content-Type", "application/json")
  101. router.ServeHTTP(rec, req)
  102. require.Equal(t, http.StatusOK, rec.Code)
  103. rec = httptest.NewRecorder()
  104. req = httptest.NewRequest(http.MethodDelete, "/api/v1/admin/users/1", nil)
  105. router.ServeHTTP(rec, req)
  106. require.Equal(t, http.StatusOK, rec.Code)
  107. rec = httptest.NewRecorder()
  108. req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/users/1/balance", bytes.NewBufferString(`{"balance":1,"operation":"add"}`))
  109. req.Header.Set("Content-Type", "application/json")
  110. router.ServeHTTP(rec, req)
  111. require.Equal(t, http.StatusOK, rec.Code)
  112. rec = httptest.NewRecorder()
  113. req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/users/1/api-keys", nil)
  114. router.ServeHTTP(rec, req)
  115. require.Equal(t, http.StatusOK, rec.Code)
  116. rec = httptest.NewRecorder()
  117. req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/users/1/usage?period=today", nil)
  118. router.ServeHTTP(rec, req)
  119. require.Equal(t, http.StatusOK, rec.Code)
  120. }
  121. func TestUserHandlerBindAuthIdentityMapsRequest(t *testing.T) {
  122. router, adminSvc := setupAdminRouter()
  123. body, err := json.Marshal(map[string]any{
  124. "provider_type": "oidc",
  125. "provider_key": "https://issuer.example",
  126. "provider_subject": "subject-123",
  127. "issuer": "https://issuer.example",
  128. "metadata": map[string]any{"report_id": 12},
  129. })
  130. require.NoError(t, err)
  131. rec := httptest.NewRecorder()
  132. req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/users/9/auth-identities", bytes.NewReader(body))
  133. req.Header.Set("Content-Type", "application/json")
  134. router.ServeHTTP(rec, req)
  135. require.Equal(t, http.StatusOK, rec.Code)
  136. require.Equal(t, int64(9), adminSvc.boundAuthIdentityFor)
  137. require.NotNil(t, adminSvc.boundAuthIdentity)
  138. require.Equal(t, "oidc", adminSvc.boundAuthIdentity.ProviderType)
  139. require.Equal(t, "https://issuer.example", adminSvc.boundAuthIdentity.ProviderKey)
  140. require.Equal(t, "subject-123", adminSvc.boundAuthIdentity.ProviderSubject)
  141. require.Nil(t, adminSvc.boundAuthIdentity.Channel)
  142. require.Equal(t, float64(12), adminSvc.boundAuthIdentity.Metadata["report_id"])
  143. }
  144. func TestGroupHandlerEndpoints(t *testing.T) {
  145. router, _ := setupAdminRouter()
  146. rec := httptest.NewRecorder()
  147. req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups", nil)
  148. router.ServeHTTP(rec, req)
  149. require.Equal(t, http.StatusOK, rec.Code)
  150. rec = httptest.NewRecorder()
  151. req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups/all", nil)
  152. router.ServeHTTP(rec, req)
  153. require.Equal(t, http.StatusOK, rec.Code)
  154. rec = httptest.NewRecorder()
  155. req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups/2", nil)
  156. router.ServeHTTP(rec, req)
  157. require.Equal(t, http.StatusOK, rec.Code)
  158. rec = httptest.NewRecorder()
  159. req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups/0/model-allowlist-candidates?platform=openai", nil)
  160. router.ServeHTTP(rec, req)
  161. require.Equal(t, http.StatusOK, rec.Code)
  162. require.Contains(t, rec.Body.String(), "gpt-5.5")
  163. rec = httptest.NewRecorder()
  164. req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups/2/composite-routes", nil)
  165. router.ServeHTTP(rec, req)
  166. require.Equal(t, http.StatusOK, rec.Code)
  167. require.Contains(t, rec.Body.String(), "openrouter/gpt-5")
  168. body, _ := json.Marshal(map[string]any{
  169. "public_model": "openrouter/gpt-5",
  170. "match_type": "exact",
  171. "target_platform": "openai",
  172. "upstream_model": "gpt-5",
  173. "endpoint": "chat_completions",
  174. "enabled": true,
  175. })
  176. rec = httptest.NewRecorder()
  177. req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/groups/2/composite-routes", bytes.NewReader(body))
  178. req.Header.Set("Content-Type", "application/json")
  179. router.ServeHTTP(rec, req)
  180. require.Equal(t, http.StatusCreated, rec.Code)
  181. require.Contains(t, rec.Body.String(), "gpt-5")
  182. body, _ = json.Marshal(map[string]any{
  183. "public_model": "openrouter/gpt-5",
  184. "target_platform": "openai",
  185. "upstream_model": "gpt-5",
  186. "endpoint": "responses",
  187. "enabled": true,
  188. })
  189. rec = httptest.NewRecorder()
  190. req = httptest.NewRequest(http.MethodPut, "/api/v1/admin/groups/2/composite-routes/1", bytes.NewReader(body))
  191. req.Header.Set("Content-Type", "application/json")
  192. router.ServeHTTP(rec, req)
  193. require.Equal(t, http.StatusOK, rec.Code)
  194. body, _ = json.Marshal(map[string]any{"model": "gpt-5", "endpoint": "responses"})
  195. rec = httptest.NewRecorder()
  196. req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/groups/2/composite-routes/preview", bytes.NewReader(body))
  197. req.Header.Set("Content-Type", "application/json")
  198. router.ServeHTTP(rec, req)
  199. require.Equal(t, http.StatusOK, rec.Code)
  200. require.Contains(t, rec.Body.String(), `"source":"detector"`)
  201. rec = httptest.NewRecorder()
  202. req = httptest.NewRequest(http.MethodDelete, "/api/v1/admin/groups/2/composite-routes/1", nil)
  203. router.ServeHTTP(rec, req)
  204. require.Equal(t, http.StatusOK, rec.Code)
  205. body, _ = json.Marshal(map[string]any{"name": "new", "platform": "anthropic", "subscription_type": "standard"})
  206. rec = httptest.NewRecorder()
  207. req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/groups", bytes.NewReader(body))
  208. req.Header.Set("Content-Type", "application/json")
  209. router.ServeHTTP(rec, req)
  210. require.Equal(t, http.StatusOK, rec.Code)
  211. body, _ = json.Marshal(map[string]any{"name": "update"})
  212. rec = httptest.NewRecorder()
  213. req = httptest.NewRequest(http.MethodPut, "/api/v1/admin/groups/2", bytes.NewReader(body))
  214. req.Header.Set("Content-Type", "application/json")
  215. router.ServeHTTP(rec, req)
  216. require.Equal(t, http.StatusOK, rec.Code)
  217. rec = httptest.NewRecorder()
  218. req = httptest.NewRequest(http.MethodDelete, "/api/v1/admin/groups/2", nil)
  219. router.ServeHTTP(rec, req)
  220. require.Equal(t, http.StatusOK, rec.Code)
  221. rec = httptest.NewRecorder()
  222. req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups/2/stats", nil)
  223. router.ServeHTTP(rec, req)
  224. require.Equal(t, http.StatusServiceUnavailable, rec.Code)
  225. require.Contains(t, rec.Body.String(), "GROUP_STATS_UNAVAILABLE")
  226. rec = httptest.NewRecorder()
  227. req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups/2/api-keys", nil)
  228. router.ServeHTTP(rec, req)
  229. require.Equal(t, http.StatusOK, rec.Code)
  230. }
  231. func TestProxyHandlerEndpoints(t *testing.T) {
  232. router, _ := setupAdminRouter()
  233. rec := httptest.NewRecorder()
  234. req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/proxies", nil)
  235. router.ServeHTTP(rec, req)
  236. require.Equal(t, http.StatusOK, rec.Code)
  237. rec = httptest.NewRecorder()
  238. req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/proxies/all", nil)
  239. router.ServeHTTP(rec, req)
  240. require.Equal(t, http.StatusOK, rec.Code)
  241. rec = httptest.NewRecorder()
  242. req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/proxies/4", nil)
  243. router.ServeHTTP(rec, req)
  244. require.Equal(t, http.StatusOK, rec.Code)
  245. body, _ := json.Marshal(map[string]any{"name": "proxy", "protocol": "http", "host": "localhost", "port": 8080})
  246. rec = httptest.NewRecorder()
  247. req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/proxies", bytes.NewReader(body))
  248. req.Header.Set("Content-Type", "application/json")
  249. router.ServeHTTP(rec, req)
  250. require.Equal(t, http.StatusOK, rec.Code)
  251. body, _ = json.Marshal(map[string]any{"name": "proxy2"})
  252. rec = httptest.NewRecorder()
  253. req = httptest.NewRequest(http.MethodPut, "/api/v1/admin/proxies/4", bytes.NewReader(body))
  254. req.Header.Set("Content-Type", "application/json")
  255. router.ServeHTTP(rec, req)
  256. require.Equal(t, http.StatusOK, rec.Code)
  257. rec = httptest.NewRecorder()
  258. req = httptest.NewRequest(http.MethodDelete, "/api/v1/admin/proxies/4", nil)
  259. router.ServeHTTP(rec, req)
  260. require.Equal(t, http.StatusOK, rec.Code)
  261. rec = httptest.NewRecorder()
  262. req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/proxies/batch-delete", bytes.NewBufferString(`{"ids":[1,2]}`))
  263. req.Header.Set("Content-Type", "application/json")
  264. router.ServeHTTP(rec, req)
  265. require.Equal(t, http.StatusOK, rec.Code)
  266. rec = httptest.NewRecorder()
  267. req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/proxies/4/test", nil)
  268. router.ServeHTTP(rec, req)
  269. require.Equal(t, http.StatusOK, rec.Code)
  270. rec = httptest.NewRecorder()
  271. req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/proxies/4/quality-check", nil)
  272. router.ServeHTTP(rec, req)
  273. require.Equal(t, http.StatusOK, rec.Code)
  274. rec = httptest.NewRecorder()
  275. req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/proxies/4/stats", nil)
  276. router.ServeHTTP(rec, req)
  277. require.Equal(t, http.StatusOK, rec.Code)
  278. rec = httptest.NewRecorder()
  279. req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/proxies/4/accounts", nil)
  280. router.ServeHTTP(rec, req)
  281. require.Equal(t, http.StatusOK, rec.Code)
  282. }
  283. func TestRedeemHandlerEndpoints(t *testing.T) {
  284. router, _ := setupAdminRouter()
  285. rec := httptest.NewRecorder()
  286. req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/redeem-codes", nil)
  287. router.ServeHTTP(rec, req)
  288. require.Equal(t, http.StatusOK, rec.Code)
  289. rec = httptest.NewRecorder()
  290. req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/redeem-codes/5", nil)
  291. router.ServeHTTP(rec, req)
  292. require.Equal(t, http.StatusOK, rec.Code)
  293. body, _ := json.Marshal(map[string]any{"count": 1, "type": "balance", "value": 10})
  294. rec = httptest.NewRecorder()
  295. req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/redeem-codes", bytes.NewReader(body))
  296. req.Header.Set("Content-Type", "application/json")
  297. router.ServeHTTP(rec, req)
  298. require.Equal(t, http.StatusOK, rec.Code)
  299. rec = httptest.NewRecorder()
  300. req = httptest.NewRequest(http.MethodDelete, "/api/v1/admin/redeem-codes/5", nil)
  301. router.ServeHTTP(rec, req)
  302. require.Equal(t, http.StatusOK, rec.Code)
  303. rec = httptest.NewRecorder()
  304. req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/redeem-codes/batch-delete", bytes.NewBufferString(`{"ids":[1,2]}`))
  305. req.Header.Set("Content-Type", "application/json")
  306. router.ServeHTTP(rec, req)
  307. require.Equal(t, http.StatusOK, rec.Code)
  308. rec = httptest.NewRecorder()
  309. req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/redeem-codes/5/expire", nil)
  310. router.ServeHTTP(rec, req)
  311. require.Equal(t, http.StatusOK, rec.Code)
  312. rec = httptest.NewRecorder()
  313. req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/redeem-codes/5/stats", nil)
  314. router.ServeHTTP(rec, req)
  315. require.Equal(t, http.StatusOK, rec.Code)
  316. }