batch_update_credentials_test.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. //go:build unit
  2. package admin
  3. import (
  4. "bytes"
  5. "context"
  6. "encoding/json"
  7. "errors"
  8. "net/http"
  9. "net/http/httptest"
  10. "sync/atomic"
  11. "testing"
  12. "github.com/gin-gonic/gin"
  13. "github.com/stretchr/testify/require"
  14. "github.com/Wei-Shaw/sub2api/internal/service"
  15. )
  16. // failingAdminService 嵌入 stubAdminService,可配置 UpdateAccount 在指定 ID 时失败。
  17. type failingAdminService struct {
  18. *stubAdminService
  19. failOnAccountID int64
  20. updateCallCount atomic.Int64
  21. }
  22. func (f *failingAdminService) UpdateAccount(ctx context.Context, id int64, input *service.UpdateAccountInput) (*service.Account, error) {
  23. f.updateCallCount.Add(1)
  24. if id == f.failOnAccountID {
  25. return nil, errors.New("database error")
  26. }
  27. return f.stubAdminService.UpdateAccount(ctx, id, input)
  28. }
  29. func setupAccountHandlerWithService(adminSvc service.AdminService) (*gin.Engine, *AccountHandler) {
  30. gin.SetMode(gin.TestMode)
  31. router := gin.New()
  32. handler := NewAccountHandler(adminSvc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
  33. router.POST("/api/v1/admin/accounts/batch-update-credentials", handler.BatchUpdateCredentials)
  34. return router, handler
  35. }
  36. func TestBatchUpdateCredentials_AllSuccess(t *testing.T) {
  37. svc := &failingAdminService{stubAdminService: newStubAdminService()}
  38. router, _ := setupAccountHandlerWithService(svc)
  39. body, _ := json.Marshal(BatchUpdateCredentialsRequest{
  40. AccountIDs: []int64{1, 2, 3},
  41. Field: "account_uuid",
  42. Value: "test-uuid",
  43. })
  44. w := httptest.NewRecorder()
  45. req, _ := http.NewRequest("POST", "/api/v1/admin/accounts/batch-update-credentials", bytes.NewReader(body))
  46. req.Header.Set("Content-Type", "application/json")
  47. router.ServeHTTP(w, req)
  48. require.Equal(t, http.StatusOK, w.Code, "全部成功时应返回 200")
  49. require.Equal(t, int64(3), svc.updateCallCount.Load(), "应调用 3 次 UpdateAccount")
  50. }
  51. func TestBatchUpdateCredentials_PartialFailure(t *testing.T) {
  52. // 让第 2 个账号(ID=2)更新时失败
  53. svc := &failingAdminService{
  54. stubAdminService: newStubAdminService(),
  55. failOnAccountID: 2,
  56. }
  57. router, _ := setupAccountHandlerWithService(svc)
  58. body, _ := json.Marshal(BatchUpdateCredentialsRequest{
  59. AccountIDs: []int64{1, 2, 3},
  60. Field: "org_uuid",
  61. Value: "test-org",
  62. })
  63. w := httptest.NewRecorder()
  64. req, _ := http.NewRequest("POST", "/api/v1/admin/accounts/batch-update-credentials", bytes.NewReader(body))
  65. req.Header.Set("Content-Type", "application/json")
  66. router.ServeHTTP(w, req)
  67. // 实现采用"部分成功"模式:总是返回 200 + 成功/失败明细
  68. require.Equal(t, http.StatusOK, w.Code, "批量更新返回 200 + 成功/失败明细")
  69. var resp map[string]any
  70. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
  71. data := resp["data"].(map[string]any)
  72. require.Equal(t, float64(2), data["success"], "应有 2 个成功")
  73. require.Equal(t, float64(1), data["failed"], "应有 1 个失败")
  74. // 所有 3 个账号都会被尝试更新(非 fail-fast)
  75. require.Equal(t, int64(3), svc.updateCallCount.Load(),
  76. "应调用 3 次 UpdateAccount(逐个尝试,失败后继续)")
  77. }
  78. func TestBatchUpdateCredentials_FirstAccountNotFound(t *testing.T) {
  79. // GetAccount 在 stubAdminService 中总是成功的,需要创建一个 GetAccount 会失败的 stub
  80. svc := &getAccountFailingService{
  81. stubAdminService: newStubAdminService(),
  82. failOnAccountID: 1,
  83. }
  84. router, _ := setupAccountHandlerWithService(svc)
  85. body, _ := json.Marshal(BatchUpdateCredentialsRequest{
  86. AccountIDs: []int64{1, 2, 3},
  87. Field: "account_uuid",
  88. Value: "test",
  89. })
  90. w := httptest.NewRecorder()
  91. req, _ := http.NewRequest("POST", "/api/v1/admin/accounts/batch-update-credentials", bytes.NewReader(body))
  92. req.Header.Set("Content-Type", "application/json")
  93. router.ServeHTTP(w, req)
  94. require.Equal(t, http.StatusNotFound, w.Code, "第一阶段验证失败应返回 404")
  95. }
  96. // getAccountFailingService 模拟 GetAccount 在特定 ID 时返回 not found。
  97. type getAccountFailingService struct {
  98. *stubAdminService
  99. failOnAccountID int64
  100. }
  101. func (f *getAccountFailingService) GetAccount(ctx context.Context, id int64) (*service.Account, error) {
  102. if id == f.failOnAccountID {
  103. return nil, errors.New("not found")
  104. }
  105. return f.stubAdminService.GetAccount(ctx, id)
  106. }
  107. func TestBatchUpdateCredentials_InterceptWarmupRequests_NonBool(t *testing.T) {
  108. svc := &failingAdminService{stubAdminService: newStubAdminService()}
  109. router, _ := setupAccountHandlerWithService(svc)
  110. // intercept_warmup_requests 传入非 bool 类型(string),应返回 400
  111. body, _ := json.Marshal(map[string]any{
  112. "account_ids": []int64{1},
  113. "field": "intercept_warmup_requests",
  114. "value": "not-a-bool",
  115. })
  116. w := httptest.NewRecorder()
  117. req, _ := http.NewRequest("POST", "/api/v1/admin/accounts/batch-update-credentials", bytes.NewReader(body))
  118. req.Header.Set("Content-Type", "application/json")
  119. router.ServeHTTP(w, req)
  120. require.Equal(t, http.StatusBadRequest, w.Code,
  121. "intercept_warmup_requests 传入非 bool 值应返回 400")
  122. }
  123. func TestBatchUpdateCredentials_InterceptWarmupRequests_ValidBool(t *testing.T) {
  124. svc := &failingAdminService{stubAdminService: newStubAdminService()}
  125. router, _ := setupAccountHandlerWithService(svc)
  126. body, _ := json.Marshal(map[string]any{
  127. "account_ids": []int64{1},
  128. "field": "intercept_warmup_requests",
  129. "value": true,
  130. })
  131. w := httptest.NewRecorder()
  132. req, _ := http.NewRequest("POST", "/api/v1/admin/accounts/batch-update-credentials", bytes.NewReader(body))
  133. req.Header.Set("Content-Type", "application/json")
  134. router.ServeHTTP(w, req)
  135. require.Equal(t, http.StatusOK, w.Code,
  136. "intercept_warmup_requests 传入合法 bool 值应返回 200")
  137. }
  138. func TestBatchUpdateCredentials_AccountUUID_NonString(t *testing.T) {
  139. svc := &failingAdminService{stubAdminService: newStubAdminService()}
  140. router, _ := setupAccountHandlerWithService(svc)
  141. // account_uuid 传入非 string 类型(number),应返回 400
  142. body, _ := json.Marshal(map[string]any{
  143. "account_ids": []int64{1},
  144. "field": "account_uuid",
  145. "value": 12345,
  146. })
  147. w := httptest.NewRecorder()
  148. req, _ := http.NewRequest("POST", "/api/v1/admin/accounts/batch-update-credentials", bytes.NewReader(body))
  149. req.Header.Set("Content-Type", "application/json")
  150. router.ServeHTTP(w, req)
  151. require.Equal(t, http.StatusBadRequest, w.Code,
  152. "account_uuid 传入非 string 值应返回 400")
  153. }
  154. func TestBatchUpdateCredentials_AccountUUID_NullValue(t *testing.T) {
  155. svc := &failingAdminService{stubAdminService: newStubAdminService()}
  156. router, _ := setupAccountHandlerWithService(svc)
  157. // account_uuid 传入 null(设置为空),应正常通过
  158. body, _ := json.Marshal(map[string]any{
  159. "account_ids": []int64{1},
  160. "field": "account_uuid",
  161. "value": nil,
  162. })
  163. w := httptest.NewRecorder()
  164. req, _ := http.NewRequest("POST", "/api/v1/admin/accounts/batch-update-credentials", bytes.NewReader(body))
  165. req.Header.Set("Content-Type", "application/json")
  166. router.ServeHTTP(w, req)
  167. require.Equal(t, http.StatusOK, w.Code,
  168. "account_uuid 传入 null 应返回 200")
  169. }