account_data_handler_test.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. package admin
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "net/http"
  6. "net/http/httptest"
  7. "testing"
  8. "github.com/Wei-Shaw/sub2api/internal/service"
  9. "github.com/gin-gonic/gin"
  10. "github.com/stretchr/testify/require"
  11. )
  12. type dataResponse struct {
  13. Code int `json:"code"`
  14. Data dataPayload `json:"data"`
  15. }
  16. type dataPayload struct {
  17. Type string `json:"type"`
  18. Version int `json:"version"`
  19. Proxies []dataProxy `json:"proxies"`
  20. Accounts []dataAccount `json:"accounts"`
  21. SkippedShadows int `json:"skipped_shadows"`
  22. }
  23. type dataProxy struct {
  24. ProxyKey string `json:"proxy_key"`
  25. Name string `json:"name"`
  26. Protocol string `json:"protocol"`
  27. Host string `json:"host"`
  28. Port int `json:"port"`
  29. Username string `json:"username"`
  30. Password string `json:"password"`
  31. Status string `json:"status"`
  32. }
  33. type dataAccount struct {
  34. Name string `json:"name"`
  35. Platform string `json:"platform"`
  36. Type string `json:"type"`
  37. Credentials map[string]any `json:"credentials"`
  38. Extra map[string]any `json:"extra"`
  39. ProxyKey *string `json:"proxy_key"`
  40. Concurrency int `json:"concurrency"`
  41. Priority int `json:"priority"`
  42. }
  43. func setupAccountDataRouter() (*gin.Engine, *stubAdminService) {
  44. gin.SetMode(gin.TestMode)
  45. router := gin.New()
  46. adminSvc := newStubAdminService()
  47. h := NewAccountHandler(
  48. adminSvc,
  49. nil,
  50. nil,
  51. nil,
  52. nil,
  53. nil,
  54. nil,
  55. nil,
  56. nil,
  57. nil,
  58. nil,
  59. nil,
  60. nil,
  61. nil,
  62. )
  63. router.GET("/api/v1/admin/accounts/data", h.ExportData)
  64. router.POST("/api/v1/admin/accounts/data", h.ImportData)
  65. return router, adminSvc
  66. }
  67. func TestExportDataIncludesSecrets(t *testing.T) {
  68. router, adminSvc := setupAccountDataRouter()
  69. proxyID := int64(11)
  70. adminSvc.proxies = []service.Proxy{
  71. {
  72. ID: proxyID,
  73. Name: "proxy",
  74. Protocol: "http",
  75. Host: "127.0.0.1",
  76. Port: 8080,
  77. Username: "user",
  78. Password: "pass",
  79. Status: service.StatusActive,
  80. },
  81. {
  82. ID: 12,
  83. Name: "orphan",
  84. Protocol: "https",
  85. Host: "10.0.0.1",
  86. Port: 443,
  87. Username: "o",
  88. Password: "p",
  89. Status: service.StatusActive,
  90. },
  91. }
  92. adminSvc.accounts = []service.Account{
  93. {
  94. ID: 21,
  95. Name: "account",
  96. Platform: service.PlatformOpenAI,
  97. Type: service.AccountTypeOAuth,
  98. Credentials: map[string]any{"token": "secret"},
  99. Extra: map[string]any{"note": "x"},
  100. ProxyID: &proxyID,
  101. Concurrency: 3,
  102. Priority: 50,
  103. Status: service.StatusDisabled,
  104. },
  105. }
  106. rec := httptest.NewRecorder()
  107. req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts/data", nil)
  108. router.ServeHTTP(rec, req)
  109. require.Equal(t, http.StatusOK, rec.Code)
  110. var resp dataResponse
  111. require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
  112. require.Equal(t, 0, resp.Code)
  113. require.Empty(t, resp.Data.Type)
  114. require.Equal(t, 0, resp.Data.Version)
  115. require.Len(t, resp.Data.Proxies, 1)
  116. require.Equal(t, "pass", resp.Data.Proxies[0].Password)
  117. require.Len(t, resp.Data.Accounts, 1)
  118. require.Equal(t, "secret", resp.Data.Accounts[0].Credentials["token"])
  119. }
  120. func TestExportDataWithoutProxies(t *testing.T) {
  121. router, adminSvc := setupAccountDataRouter()
  122. proxyID := int64(11)
  123. adminSvc.proxies = []service.Proxy{
  124. {
  125. ID: proxyID,
  126. Name: "proxy",
  127. Protocol: "http",
  128. Host: "127.0.0.1",
  129. Port: 8080,
  130. Username: "user",
  131. Password: "pass",
  132. Status: service.StatusActive,
  133. },
  134. }
  135. adminSvc.accounts = []service.Account{
  136. {
  137. ID: 21,
  138. Name: "account",
  139. Platform: service.PlatformOpenAI,
  140. Type: service.AccountTypeOAuth,
  141. Credentials: map[string]any{"token": "secret"},
  142. ProxyID: &proxyID,
  143. Concurrency: 3,
  144. Priority: 50,
  145. Status: service.StatusDisabled,
  146. },
  147. }
  148. rec := httptest.NewRecorder()
  149. req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts/data?include_proxies=false", nil)
  150. router.ServeHTTP(rec, req)
  151. require.Equal(t, http.StatusOK, rec.Code)
  152. var resp dataResponse
  153. require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
  154. require.Equal(t, 0, resp.Code)
  155. require.Len(t, resp.Data.Proxies, 0)
  156. require.Len(t, resp.Data.Accounts, 1)
  157. require.Nil(t, resp.Data.Accounts[0].ProxyKey)
  158. }
  159. // TestExportDataExcludesSparkShadow 验证外审第5轮 P1/P2:导出时排除 spark 影子账号
  160. // (影子无凭据、导入侧强制 credentials 非空,混入会产出无法还原的坏备份),并透出跳过计数。
  161. func TestExportDataExcludesSparkShadow(t *testing.T) {
  162. router, adminSvc := setupAccountDataRouter()
  163. parentID := int64(21)
  164. adminSvc.accounts = []service.Account{
  165. {
  166. ID: parentID,
  167. Name: "mother",
  168. Platform: service.PlatformOpenAI,
  169. Type: service.AccountTypeOAuth,
  170. Credentials: map[string]any{"token": "secret"},
  171. Status: service.StatusActive,
  172. },
  173. {
  174. ID: 22,
  175. Name: "mother (Spark)",
  176. Platform: service.PlatformOpenAI,
  177. Type: service.AccountTypeOAuth,
  178. Credentials: map[string]any{}, // 影子恒空凭据
  179. ParentAccountID: &parentID, // 影子标记
  180. QuotaDimension: service.QuotaDimensionSpark,
  181. Status: service.StatusActive,
  182. },
  183. }
  184. rec := httptest.NewRecorder()
  185. req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts/data?include_proxies=false", nil)
  186. router.ServeHTTP(rec, req)
  187. require.Equal(t, http.StatusOK, rec.Code)
  188. var resp dataResponse
  189. require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
  190. require.Equal(t, 0, resp.Code)
  191. require.Len(t, resp.Data.Accounts, 1, "影子应被排除,仅导出母账号")
  192. require.Equal(t, "mother", resp.Data.Accounts[0].Name)
  193. require.Equal(t, 1, resp.Data.SkippedShadows, "跳过的影子数量应透出")
  194. }
  195. func TestExportDataPassesAccountFiltersAndSort(t *testing.T) {
  196. router, adminSvc := setupAccountDataRouter()
  197. adminSvc.accounts = []service.Account{
  198. {ID: 1, Name: "acc-1", Status: service.StatusActive},
  199. }
  200. rec := httptest.NewRecorder()
  201. req := httptest.NewRequest(
  202. http.MethodGet,
  203. "/api/v1/admin/accounts/data?platform=openai&type=oauth&status=active&group=12&privacy_mode=blocked&search=keyword&sort_by=priority&sort_order=desc",
  204. nil,
  205. )
  206. router.ServeHTTP(rec, req)
  207. require.Equal(t, http.StatusOK, rec.Code)
  208. require.Equal(t, 1, adminSvc.lastListAccounts.calls)
  209. require.Equal(t, "openai", adminSvc.lastListAccounts.platform)
  210. require.Equal(t, "oauth", adminSvc.lastListAccounts.accountType)
  211. require.Equal(t, "active", adminSvc.lastListAccounts.status)
  212. require.Equal(t, int64(12), adminSvc.lastListAccounts.groupID)
  213. require.Equal(t, "blocked", adminSvc.lastListAccounts.privacyMode)
  214. require.Equal(t, "keyword", adminSvc.lastListAccounts.search)
  215. require.Equal(t, "priority", adminSvc.lastListAccounts.sortBy)
  216. require.Equal(t, "desc", adminSvc.lastListAccounts.sortOrder)
  217. }
  218. func TestExportDataSelectedIDsOverrideFilters(t *testing.T) {
  219. router, adminSvc := setupAccountDataRouter()
  220. rec := httptest.NewRecorder()
  221. req := httptest.NewRequest(
  222. http.MethodGet,
  223. "/api/v1/admin/accounts/data?ids=1,2&platform=openai&search=keyword&sort_by=priority&sort_order=desc",
  224. nil,
  225. )
  226. router.ServeHTTP(rec, req)
  227. require.Equal(t, http.StatusOK, rec.Code)
  228. var resp dataResponse
  229. require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
  230. require.Equal(t, 0, resp.Code)
  231. require.Len(t, resp.Data.Accounts, 2)
  232. require.Equal(t, 0, adminSvc.lastListAccounts.calls)
  233. }
  234. func TestImportDataReusesProxyAndSkipsDefaultGroup(t *testing.T) {
  235. router, adminSvc := setupAccountDataRouter()
  236. adminSvc.proxies = []service.Proxy{
  237. {
  238. ID: 1,
  239. Name: "proxy",
  240. Protocol: "socks5",
  241. Host: "1.2.3.4",
  242. Port: 1080,
  243. Username: "u",
  244. Password: "p",
  245. Status: service.StatusActive,
  246. },
  247. }
  248. dataPayload := map[string]any{
  249. "data": map[string]any{
  250. "type": dataType,
  251. "version": dataVersion,
  252. "proxies": []map[string]any{
  253. {
  254. "proxy_key": "socks5|1.2.3.4|1080|u|p",
  255. "name": "proxy",
  256. "protocol": "socks5",
  257. "host": "1.2.3.4",
  258. "port": 1080,
  259. "username": "u",
  260. "password": "p",
  261. "status": "active",
  262. },
  263. },
  264. "accounts": []map[string]any{
  265. {
  266. "name": "acc",
  267. "platform": service.PlatformOpenAI,
  268. "type": service.AccountTypeOAuth,
  269. "credentials": map[string]any{"token": "x"},
  270. "proxy_key": "socks5|1.2.3.4|1080|u|p",
  271. "concurrency": 3,
  272. "priority": 50,
  273. },
  274. },
  275. },
  276. "skip_default_group_bind": true,
  277. }
  278. body, _ := json.Marshal(dataPayload)
  279. rec := httptest.NewRecorder()
  280. req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/data", bytes.NewReader(body))
  281. req.Header.Set("Content-Type", "application/json")
  282. router.ServeHTTP(rec, req)
  283. require.Equal(t, http.StatusOK, rec.Code)
  284. require.Len(t, adminSvc.createdProxies, 0)
  285. require.Len(t, adminSvc.createdAccounts, 1)
  286. require.True(t, adminSvc.createdAccounts[0].SkipDefaultGroupBind)
  287. }