admin_service_apikey_test.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "errors"
  6. "testing"
  7. "time"
  8. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  9. "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
  10. "github.com/stretchr/testify/require"
  11. )
  12. // ---------------------------------------------------------------------------
  13. // Stubs
  14. // ---------------------------------------------------------------------------
  15. // userRepoStubForGroupUpdate implements UserRepository for AdminUpdateAPIKeyGroupID tests.
  16. type userRepoStubForGroupUpdate struct {
  17. addGroupErr error
  18. addGroupCalled bool
  19. addedUserID int64
  20. addedGroupID int64
  21. }
  22. func (s *userRepoStubForGroupUpdate) AddGroupToAllowedGroups(_ context.Context, userID int64, groupID int64) error {
  23. s.addGroupCalled = true
  24. s.addedUserID = userID
  25. s.addedGroupID = groupID
  26. return s.addGroupErr
  27. }
  28. func (s *userRepoStubForGroupUpdate) Create(context.Context, *User) error { panic("unexpected") }
  29. func (s *userRepoStubForGroupUpdate) CreateWithEmailAliasGuard(context.Context, *User) error {
  30. panic("unexpected")
  31. }
  32. func (s *userRepoStubForGroupUpdate) GetByID(context.Context, int64) (*User, error) {
  33. panic("unexpected")
  34. }
  35. func (s *userRepoStubForGroupUpdate) GetByEmail(context.Context, string) (*User, error) {
  36. panic("unexpected")
  37. }
  38. func (s *userRepoStubForGroupUpdate) GetFirstAdmin(context.Context) (*User, error) {
  39. panic("unexpected")
  40. }
  41. func (s *userRepoStubForGroupUpdate) Update(context.Context, *User, UserUpdateFields) error {
  42. panic("unexpected")
  43. }
  44. func (s *userRepoStubForGroupUpdate) Delete(context.Context, int64) error { panic("unexpected") }
  45. func (s *userRepoStubForGroupUpdate) GetUserAvatar(context.Context, int64) (*UserAvatar, error) {
  46. panic("unexpected")
  47. }
  48. func (s *userRepoStubForGroupUpdate) UpsertUserAvatar(context.Context, int64, UpsertUserAvatarInput) (*UserAvatar, error) {
  49. panic("unexpected")
  50. }
  51. func (s *userRepoStubForGroupUpdate) DeleteUserAvatar(context.Context, int64) error {
  52. panic("unexpected")
  53. }
  54. func (s *userRepoStubForGroupUpdate) List(context.Context, pagination.PaginationParams) ([]User, *pagination.PaginationResult, error) {
  55. panic("unexpected")
  56. }
  57. func (s *userRepoStubForGroupUpdate) ListWithFilters(context.Context, pagination.PaginationParams, UserListFilters) ([]User, *pagination.PaginationResult, error) {
  58. panic("unexpected")
  59. }
  60. func (s *userRepoStubForGroupUpdate) UpdateBalance(context.Context, int64, float64) error {
  61. panic("unexpected")
  62. }
  63. func (s *userRepoStubForGroupUpdate) DeductBalance(context.Context, int64, float64) error {
  64. panic("unexpected")
  65. }
  66. func (s *userRepoStubForGroupUpdate) AdjustBalance(ctx context.Context, id int64, delta float64) (BalanceChange, error) {
  67. panic("unexpected AdjustBalance call")
  68. }
  69. func (s *userRepoStubForGroupUpdate) SetBalance(ctx context.Context, id int64, value float64) (BalanceChange, error) {
  70. panic("unexpected SetBalance call")
  71. }
  72. func (s *userRepoStubForGroupUpdate) UpdateConcurrency(context.Context, int64, int) error {
  73. panic("unexpected")
  74. }
  75. func (s *userRepoStubForGroupUpdate) BatchSetConcurrency(context.Context, []int64, int) (int, error) {
  76. return 0, nil
  77. }
  78. func (s *userRepoStubForGroupUpdate) BatchAddConcurrency(context.Context, []int64, int) (int, error) {
  79. return 0, nil
  80. }
  81. func (s *userRepoStubForGroupUpdate) BatchUpdateLimits(context.Context, []int64, *int, *int) (int, error) {
  82. return 0, nil
  83. }
  84. func (s *userRepoStubForGroupUpdate) ExistsByEmail(context.Context, string) (bool, error) {
  85. panic("unexpected")
  86. }
  87. func (s *userRepoStubForGroupUpdate) ExistsByEmailAlias(context.Context, string) (bool, error) {
  88. panic("unexpected")
  89. }
  90. func (s *userRepoStubForGroupUpdate) RemoveGroupFromAllowedGroups(context.Context, int64) (int64, error) {
  91. panic("unexpected")
  92. }
  93. func (s *userRepoStubForGroupUpdate) UpdateTotpSecret(context.Context, int64, *string) error {
  94. panic("unexpected")
  95. }
  96. func (s *userRepoStubForGroupUpdate) EnableTotp(context.Context, int64) error { panic("unexpected") }
  97. func (s *userRepoStubForGroupUpdate) DisableTotp(context.Context, int64) error { panic("unexpected") }
  98. func (s *userRepoStubForGroupUpdate) GetByIDIncludeDeleted(ctx context.Context, id int64) (*User, error) {
  99. panic("unexpected GetByIDIncludeDeleted call")
  100. }
  101. func (s *userRepoStubForGroupUpdate) ListUserAuthIdentities(context.Context, int64) ([]UserAuthIdentityRecord, error) {
  102. panic("unexpected")
  103. }
  104. func (s *userRepoStubForGroupUpdate) UnbindUserAuthProvider(context.Context, int64, string) error {
  105. panic("unexpected")
  106. }
  107. func (s *userRepoStubForGroupUpdate) GetLatestUsedAtByUserIDs(context.Context, []int64) (map[int64]*time.Time, error) {
  108. panic("unexpected")
  109. }
  110. func (s *userRepoStubForGroupUpdate) GetLatestUsedAtByUserID(context.Context, int64) (*time.Time, error) {
  111. panic("unexpected")
  112. }
  113. func (s *userRepoStubForGroupUpdate) UpdateUserLastActiveAt(context.Context, int64, time.Time) error {
  114. panic("unexpected")
  115. }
  116. func (s *userRepoStubForGroupUpdate) RemoveGroupFromUserAllowedGroups(context.Context, int64, int64) error {
  117. panic("unexpected")
  118. }
  119. // apiKeyRepoStubForGroupUpdate implements APIKeyRepository for AdminUpdateAPIKeyGroupID tests.
  120. type apiKeyRepoStubForGroupUpdate struct {
  121. key *APIKey
  122. getErr error
  123. updateErr error
  124. updated *APIKey // captures what was passed to Update
  125. }
  126. func (s *apiKeyRepoStubForGroupUpdate) GetByID(_ context.Context, _ int64) (*APIKey, error) {
  127. if s.getErr != nil {
  128. return nil, s.getErr
  129. }
  130. clone := *s.key
  131. return &clone, nil
  132. }
  133. func (s *apiKeyRepoStubForGroupUpdate) Update(_ context.Context, key *APIKey, _ APIKeyUpdateFields) error {
  134. if s.updateErr != nil {
  135. return s.updateErr
  136. }
  137. clone := *key
  138. s.updated = &clone
  139. return nil
  140. }
  141. // Unused methods – panic on unexpected call.
  142. func (s *apiKeyRepoStubForGroupUpdate) Create(context.Context, *APIKey) error { panic("unexpected") }
  143. func (s *apiKeyRepoStubForGroupUpdate) GetKeyAndOwnerID(context.Context, int64) (string, int64, error) {
  144. panic("unexpected")
  145. }
  146. func (s *apiKeyRepoStubForGroupUpdate) GetByKey(context.Context, string) (*APIKey, error) {
  147. panic("unexpected")
  148. }
  149. func (s *apiKeyRepoStubForGroupUpdate) GetByKeyForAuth(context.Context, string) (*APIKey, error) {
  150. panic("unexpected")
  151. }
  152. func (s *apiKeyRepoStubForGroupUpdate) Delete(context.Context, int64) error { panic("unexpected") }
  153. func (s *apiKeyRepoStubForGroupUpdate) DeleteWithAudit(context.Context, int64) error {
  154. panic("unexpected")
  155. }
  156. func (s *apiKeyRepoStubForGroupUpdate) ListByUserID(context.Context, int64, pagination.PaginationParams, APIKeyListFilters) ([]APIKey, *pagination.PaginationResult, error) {
  157. panic("unexpected")
  158. }
  159. func (s *apiKeyRepoStubForGroupUpdate) VerifyOwnership(context.Context, int64, []int64) ([]int64, error) {
  160. panic("unexpected")
  161. }
  162. func (s *apiKeyRepoStubForGroupUpdate) CountByUserID(context.Context, int64) (int64, error) {
  163. panic("unexpected")
  164. }
  165. func (s *apiKeyRepoStubForGroupUpdate) ExistsByKey(context.Context, string) (bool, error) {
  166. panic("unexpected")
  167. }
  168. func (s *apiKeyRepoStubForGroupUpdate) ListByGroupID(context.Context, int64, pagination.PaginationParams) ([]APIKey, *pagination.PaginationResult, error) {
  169. panic("unexpected")
  170. }
  171. func (s *apiKeyRepoStubForGroupUpdate) SearchAPIKeys(context.Context, int64, string, int) ([]APIKey, error) {
  172. panic("unexpected")
  173. }
  174. func (s *apiKeyRepoStubForGroupUpdate) ClearGroupIDByGroupID(context.Context, int64) (int64, error) {
  175. panic("unexpected")
  176. }
  177. func (s *apiKeyRepoStubForGroupUpdate) CountByGroupID(context.Context, int64) (int64, error) {
  178. panic("unexpected")
  179. }
  180. func (s *apiKeyRepoStubForGroupUpdate) ListKeysByUserID(context.Context, int64) ([]string, error) {
  181. panic("unexpected")
  182. }
  183. func (s *apiKeyRepoStubForGroupUpdate) ListKeysByGroupID(context.Context, int64) ([]string, error) {
  184. panic("unexpected")
  185. }
  186. func (s *apiKeyRepoStubForGroupUpdate) IncrementQuotaUsed(context.Context, int64, float64) (float64, error) {
  187. panic("unexpected")
  188. }
  189. func (s *apiKeyRepoStubForGroupUpdate) UpdateLastUsed(context.Context, int64, time.Time) error {
  190. panic("unexpected")
  191. }
  192. func (s *apiKeyRepoStubForGroupUpdate) IncrementRateLimitUsage(context.Context, int64, float64) error {
  193. panic("unexpected")
  194. }
  195. func (s *apiKeyRepoStubForGroupUpdate) ResetRateLimitWindows(context.Context, int64) error {
  196. panic("unexpected")
  197. }
  198. func (s *apiKeyRepoStubForGroupUpdate) GetRateLimitData(context.Context, int64) (*APIKeyRateLimitData, error) {
  199. panic("unexpected")
  200. }
  201. func (s *apiKeyRepoStubForGroupUpdate) UpdateGroupIDByUserAndGroup(context.Context, int64, int64, int64) (int64, error) {
  202. panic("unexpected")
  203. }
  204. // groupRepoStubForGroupUpdate implements GroupRepository for AdminUpdateAPIKeyGroupID tests.
  205. type groupRepoStubForGroupUpdate struct {
  206. group *Group
  207. getErr error
  208. lastGetByIDArg int64
  209. }
  210. func (s *groupRepoStubForGroupUpdate) GetByID(_ context.Context, id int64) (*Group, error) {
  211. s.lastGetByIDArg = id
  212. if s.getErr != nil {
  213. return nil, s.getErr
  214. }
  215. clone := *s.group
  216. return &clone, nil
  217. }
  218. // Unused methods – panic on unexpected call.
  219. func (s *groupRepoStubForGroupUpdate) Create(context.Context, *Group) error { panic("unexpected") }
  220. func (s *groupRepoStubForGroupUpdate) GetByIDLite(context.Context, int64) (*Group, error) {
  221. panic("unexpected")
  222. }
  223. func (s *groupRepoStubForGroupUpdate) Update(context.Context, *Group) error { panic("unexpected") }
  224. func (s *groupRepoStubForGroupUpdate) Delete(context.Context, int64) error { panic("unexpected") }
  225. func (s *groupRepoStubForGroupUpdate) DeleteCascade(context.Context, int64) ([]int64, error) {
  226. panic("unexpected")
  227. }
  228. func (s *groupRepoStubForGroupUpdate) List(context.Context, pagination.PaginationParams) ([]Group, *pagination.PaginationResult, error) {
  229. panic("unexpected")
  230. }
  231. func (s *groupRepoStubForGroupUpdate) ListWithFilters(context.Context, pagination.PaginationParams, string, string, string, *bool) ([]Group, *pagination.PaginationResult, error) {
  232. panic("unexpected")
  233. }
  234. func (s *groupRepoStubForGroupUpdate) ListActive(context.Context) ([]Group, error) {
  235. panic("unexpected")
  236. }
  237. func (s *groupRepoStubForGroupUpdate) ListActiveByPlatform(context.Context, string) ([]Group, error) {
  238. panic("unexpected")
  239. }
  240. func (s *groupRepoStubForGroupUpdate) ExistsByName(context.Context, string) (bool, error) {
  241. panic("unexpected")
  242. }
  243. func (s *groupRepoStubForGroupUpdate) GetAccountCount(context.Context, int64) (int64, int64, error) {
  244. panic("unexpected")
  245. }
  246. func (s *groupRepoStubForGroupUpdate) DeleteAccountGroupsByGroupID(context.Context, int64) (int64, error) {
  247. panic("unexpected")
  248. }
  249. func (s *groupRepoStubForGroupUpdate) GetAccountIDsByGroupIDs(context.Context, []int64) ([]int64, error) {
  250. panic("unexpected")
  251. }
  252. func (s *groupRepoStubForGroupUpdate) BindAccountsToGroup(context.Context, int64, []int64) error {
  253. panic("unexpected")
  254. }
  255. func (s *groupRepoStubForGroupUpdate) UpdateSortOrders(context.Context, []GroupSortOrderUpdate) error {
  256. panic("unexpected")
  257. }
  258. type userSubRepoStubForGroupUpdate struct {
  259. userSubRepoNoop
  260. getActiveSub *UserSubscription
  261. getActiveErr error
  262. called bool
  263. calledUserID int64
  264. calledGroupID int64
  265. }
  266. func (s *userSubRepoStubForGroupUpdate) GetActiveByUserIDAndGroupID(_ context.Context, userID, groupID int64) (*UserSubscription, error) {
  267. s.called = true
  268. s.calledUserID = userID
  269. s.calledGroupID = groupID
  270. if s.getActiveErr != nil {
  271. return nil, s.getActiveErr
  272. }
  273. if s.getActiveSub == nil {
  274. return nil, ErrSubscriptionNotFound
  275. }
  276. clone := *s.getActiveSub
  277. return &clone, nil
  278. }
  279. // ---------------------------------------------------------------------------
  280. // Tests
  281. // ---------------------------------------------------------------------------
  282. func TestAdminService_AdminUpdateAPIKeyGroupID_KeyNotFound(t *testing.T) {
  283. repo := &apiKeyRepoStubForGroupUpdate{getErr: ErrAPIKeyNotFound}
  284. svc := &adminServiceImpl{apiKeyRepo: repo}
  285. _, err := svc.AdminUpdateAPIKeyGroupID(context.Background(), 999, int64Ptr(1))
  286. require.ErrorIs(t, err, ErrAPIKeyNotFound)
  287. }
  288. func TestAdminService_AdminUpdateAPIKeyGroupID_NilGroupID_NoOp(t *testing.T) {
  289. existing := &APIKey{ID: 1, Key: "sk-test", GroupID: int64Ptr(5)}
  290. repo := &apiKeyRepoStubForGroupUpdate{key: existing}
  291. svc := &adminServiceImpl{apiKeyRepo: repo}
  292. got, err := svc.AdminUpdateAPIKeyGroupID(context.Background(), 1, nil)
  293. require.NoError(t, err)
  294. require.Equal(t, int64(1), got.APIKey.ID)
  295. // Update should NOT have been called (updated stays nil)
  296. require.Nil(t, repo.updated)
  297. }
  298. func TestAdminService_AdminUpdateAPIKeyGroupID_Unbind(t *testing.T) {
  299. existing := &APIKey{ID: 1, Key: "sk-test", GroupID: int64Ptr(5), Group: &Group{ID: 5, Name: "Old"}}
  300. repo := &apiKeyRepoStubForGroupUpdate{key: existing}
  301. cache := &authCacheInvalidatorStub{}
  302. svc := &adminServiceImpl{apiKeyRepo: repo, authCacheInvalidator: cache}
  303. got, err := svc.AdminUpdateAPIKeyGroupID(context.Background(), 1, int64Ptr(0))
  304. require.NoError(t, err)
  305. require.Nil(t, got.APIKey.GroupID, "group_id should be nil after unbind")
  306. require.Nil(t, got.APIKey.Group, "group object should be nil after unbind")
  307. require.NotNil(t, repo.updated, "Update should have been called")
  308. require.Nil(t, repo.updated.GroupID)
  309. require.Equal(t, []string{"sk-test"}, cache.keys, "cache should be invalidated")
  310. }
  311. func TestAdminService_AdminUpdateAPIKeyGroupID_BindActiveGroup(t *testing.T) {
  312. existing := &APIKey{ID: 1, Key: "sk-test", GroupID: nil}
  313. apiKeyRepo := &apiKeyRepoStubForGroupUpdate{key: existing}
  314. groupRepo := &groupRepoStubForGroupUpdate{group: &Group{ID: 10, Name: "Pro", Status: StatusActive}}
  315. cache := &authCacheInvalidatorStub{}
  316. svc := &adminServiceImpl{apiKeyRepo: apiKeyRepo, groupRepo: groupRepo, authCacheInvalidator: cache}
  317. got, err := svc.AdminUpdateAPIKeyGroupID(context.Background(), 1, int64Ptr(10))
  318. require.NoError(t, err)
  319. require.NotNil(t, got.APIKey.GroupID)
  320. require.Equal(t, int64(10), *got.APIKey.GroupID)
  321. require.Equal(t, int64(10), *apiKeyRepo.updated.GroupID)
  322. require.Equal(t, []string{"sk-test"}, cache.keys)
  323. // M3: verify correct group ID was passed to repo
  324. require.Equal(t, int64(10), groupRepo.lastGetByIDArg)
  325. // C1 fix: verify Group object is populated
  326. require.NotNil(t, got.APIKey.Group)
  327. require.Equal(t, "Pro", got.APIKey.Group.Name)
  328. }
  329. func TestAdminService_AdminUpdateAPIKeyGroupID_SameGroup_Idempotent(t *testing.T) {
  330. existing := &APIKey{ID: 1, Key: "sk-test", GroupID: int64Ptr(10), Group: &Group{ID: 10, Name: "Pro"}}
  331. apiKeyRepo := &apiKeyRepoStubForGroupUpdate{key: existing}
  332. groupRepo := &groupRepoStubForGroupUpdate{group: &Group{ID: 10, Name: "Pro", Status: StatusActive}}
  333. cache := &authCacheInvalidatorStub{}
  334. svc := &adminServiceImpl{apiKeyRepo: apiKeyRepo, groupRepo: groupRepo, authCacheInvalidator: cache}
  335. got, err := svc.AdminUpdateAPIKeyGroupID(context.Background(), 1, int64Ptr(10))
  336. require.NoError(t, err)
  337. require.NotNil(t, got.APIKey.GroupID)
  338. require.Equal(t, int64(10), *got.APIKey.GroupID)
  339. // Update is still called (current impl doesn't short-circuit on same group)
  340. require.NotNil(t, apiKeyRepo.updated)
  341. require.Equal(t, []string{"sk-test"}, cache.keys)
  342. }
  343. func TestAdminService_AdminUpdateAPIKeyGroupID_GroupNotFound(t *testing.T) {
  344. existing := &APIKey{ID: 1, Key: "sk-test"}
  345. apiKeyRepo := &apiKeyRepoStubForGroupUpdate{key: existing}
  346. groupRepo := &groupRepoStubForGroupUpdate{getErr: ErrGroupNotFound}
  347. svc := &adminServiceImpl{apiKeyRepo: apiKeyRepo, groupRepo: groupRepo}
  348. _, err := svc.AdminUpdateAPIKeyGroupID(context.Background(), 1, int64Ptr(99))
  349. require.ErrorIs(t, err, ErrGroupNotFound)
  350. }
  351. func TestAdminService_AdminUpdateAPIKeyGroupID_GroupNotActive(t *testing.T) {
  352. existing := &APIKey{ID: 1, Key: "sk-test"}
  353. apiKeyRepo := &apiKeyRepoStubForGroupUpdate{key: existing}
  354. groupRepo := &groupRepoStubForGroupUpdate{group: &Group{ID: 5, Status: StatusDisabled}}
  355. svc := &adminServiceImpl{apiKeyRepo: apiKeyRepo, groupRepo: groupRepo}
  356. _, err := svc.AdminUpdateAPIKeyGroupID(context.Background(), 1, int64Ptr(5))
  357. require.Error(t, err)
  358. require.Equal(t, "GROUP_NOT_ACTIVE", infraerrors.Reason(err))
  359. }
  360. func TestAdminService_AdminUpdateAPIKeyGroupID_UpdateFails(t *testing.T) {
  361. existing := &APIKey{ID: 1, Key: "sk-test", GroupID: int64Ptr(3)}
  362. repo := &apiKeyRepoStubForGroupUpdate{key: existing, updateErr: errors.New("db write error")}
  363. svc := &adminServiceImpl{apiKeyRepo: repo}
  364. _, err := svc.AdminUpdateAPIKeyGroupID(context.Background(), 1, int64Ptr(0))
  365. require.Error(t, err)
  366. require.Contains(t, err.Error(), "update api key")
  367. }
  368. func TestAdminService_AdminUpdateAPIKeyGroupID_NegativeGroupID(t *testing.T) {
  369. existing := &APIKey{ID: 1, Key: "sk-test"}
  370. apiKeyRepo := &apiKeyRepoStubForGroupUpdate{key: existing}
  371. svc := &adminServiceImpl{apiKeyRepo: apiKeyRepo}
  372. _, err := svc.AdminUpdateAPIKeyGroupID(context.Background(), 1, int64Ptr(-5))
  373. require.Error(t, err)
  374. require.Equal(t, "INVALID_GROUP_ID", infraerrors.Reason(err))
  375. }
  376. func TestAdminService_AdminUpdateAPIKeyGroupID_PointerIsolation(t *testing.T) {
  377. existing := &APIKey{ID: 1, Key: "sk-test", GroupID: nil}
  378. apiKeyRepo := &apiKeyRepoStubForGroupUpdate{key: existing}
  379. groupRepo := &groupRepoStubForGroupUpdate{group: &Group{ID: 10, Name: "Pro", Status: StatusActive}}
  380. cache := &authCacheInvalidatorStub{}
  381. svc := &adminServiceImpl{apiKeyRepo: apiKeyRepo, groupRepo: groupRepo, authCacheInvalidator: cache}
  382. inputGID := int64(10)
  383. got, err := svc.AdminUpdateAPIKeyGroupID(context.Background(), 1, &inputGID)
  384. require.NoError(t, err)
  385. require.NotNil(t, got.APIKey.GroupID)
  386. // Mutating the input pointer must NOT affect the stored value
  387. inputGID = 999
  388. require.Equal(t, int64(10), *got.APIKey.GroupID)
  389. require.Equal(t, int64(10), *apiKeyRepo.updated.GroupID)
  390. }
  391. func TestAdminService_AdminUpdateAPIKeyGroupID_NilCacheInvalidator(t *testing.T) {
  392. existing := &APIKey{ID: 1, Key: "sk-test"}
  393. apiKeyRepo := &apiKeyRepoStubForGroupUpdate{key: existing}
  394. groupRepo := &groupRepoStubForGroupUpdate{group: &Group{ID: 7, Status: StatusActive}}
  395. // authCacheInvalidator is nil – should not panic
  396. svc := &adminServiceImpl{apiKeyRepo: apiKeyRepo, groupRepo: groupRepo}
  397. got, err := svc.AdminUpdateAPIKeyGroupID(context.Background(), 1, int64Ptr(7))
  398. require.NoError(t, err)
  399. require.NotNil(t, got.APIKey.GroupID)
  400. require.Equal(t, int64(7), *got.APIKey.GroupID)
  401. }
  402. // ---------------------------------------------------------------------------
  403. // Tests: AllowedGroup auto-sync
  404. // ---------------------------------------------------------------------------
  405. func TestAdminService_AdminUpdateAPIKeyGroupID_ExclusiveGroup_AddsAllowedGroup(t *testing.T) {
  406. existing := &APIKey{ID: 1, UserID: 42, Key: "sk-test", GroupID: nil}
  407. apiKeyRepo := &apiKeyRepoStubForGroupUpdate{key: existing}
  408. groupRepo := &groupRepoStubForGroupUpdate{group: &Group{ID: 10, Name: "Exclusive", Status: StatusActive, IsExclusive: true, SubscriptionType: SubscriptionTypeStandard}}
  409. userRepo := &userRepoStubForGroupUpdate{}
  410. cache := &authCacheInvalidatorStub{}
  411. svc := &adminServiceImpl{apiKeyRepo: apiKeyRepo, groupRepo: groupRepo, userRepo: userRepo, authCacheInvalidator: cache}
  412. got, err := svc.AdminUpdateAPIKeyGroupID(context.Background(), 1, int64Ptr(10))
  413. require.NoError(t, err)
  414. require.NotNil(t, got.APIKey.GroupID)
  415. require.Equal(t, int64(10), *got.APIKey.GroupID)
  416. // 验证 AddGroupToAllowedGroups 被调用,且参数正确
  417. require.True(t, userRepo.addGroupCalled)
  418. require.Equal(t, int64(42), userRepo.addedUserID)
  419. require.Equal(t, int64(10), userRepo.addedGroupID)
  420. // 验证 result 标记了自动授权
  421. require.True(t, got.AutoGrantedGroupAccess)
  422. require.NotNil(t, got.GrantedGroupID)
  423. require.Equal(t, int64(10), *got.GrantedGroupID)
  424. require.Equal(t, "Exclusive", got.GrantedGroupName)
  425. }
  426. func TestAdminService_AdminUpdateAPIKeyGroupID_NonExclusiveGroup_NoAllowedGroupUpdate(t *testing.T) {
  427. existing := &APIKey{ID: 1, UserID: 42, Key: "sk-test", GroupID: nil}
  428. apiKeyRepo := &apiKeyRepoStubForGroupUpdate{key: existing}
  429. groupRepo := &groupRepoStubForGroupUpdate{group: &Group{ID: 10, Name: "Public", Status: StatusActive, IsExclusive: false, SubscriptionType: SubscriptionTypeStandard}}
  430. userRepo := &userRepoStubForGroupUpdate{}
  431. cache := &authCacheInvalidatorStub{}
  432. svc := &adminServiceImpl{apiKeyRepo: apiKeyRepo, groupRepo: groupRepo, userRepo: userRepo, authCacheInvalidator: cache}
  433. got, err := svc.AdminUpdateAPIKeyGroupID(context.Background(), 1, int64Ptr(10))
  434. require.NoError(t, err)
  435. require.NotNil(t, got.APIKey.GroupID)
  436. // 非专属分组不触发 AddGroupToAllowedGroups
  437. require.False(t, userRepo.addGroupCalled)
  438. require.False(t, got.AutoGrantedGroupAccess)
  439. }
  440. func TestAdminService_AdminUpdateAPIKeyGroupID_SubscriptionGroup_Blocked(t *testing.T) {
  441. existing := &APIKey{ID: 1, UserID: 42, Key: "sk-test", GroupID: nil}
  442. apiKeyRepo := &apiKeyRepoStubForGroupUpdate{key: existing}
  443. groupRepo := &groupRepoStubForGroupUpdate{group: &Group{ID: 10, Name: "Sub", Status: StatusActive, IsExclusive: false, SubscriptionType: SubscriptionTypeSubscription}}
  444. userRepo := &userRepoStubForGroupUpdate{}
  445. userSubRepo := &userSubRepoStubForGroupUpdate{getActiveErr: ErrSubscriptionNotFound}
  446. svc := &adminServiceImpl{apiKeyRepo: apiKeyRepo, groupRepo: groupRepo, userRepo: userRepo, userSubRepo: userSubRepo}
  447. // 无有效订阅时应拒绝绑定
  448. _, err := svc.AdminUpdateAPIKeyGroupID(context.Background(), 1, int64Ptr(10))
  449. require.Error(t, err)
  450. require.Equal(t, "SUBSCRIPTION_REQUIRED", infraerrors.Reason(err))
  451. require.True(t, userSubRepo.called)
  452. require.Equal(t, int64(42), userSubRepo.calledUserID)
  453. require.Equal(t, int64(10), userSubRepo.calledGroupID)
  454. require.False(t, userRepo.addGroupCalled)
  455. }
  456. func TestAdminService_AdminUpdateAPIKeyGroupID_SubscriptionGroup_RequiresRepo(t *testing.T) {
  457. existing := &APIKey{ID: 1, UserID: 42, Key: "sk-test", GroupID: nil}
  458. apiKeyRepo := &apiKeyRepoStubForGroupUpdate{key: existing}
  459. groupRepo := &groupRepoStubForGroupUpdate{group: &Group{ID: 10, Name: "Sub", Status: StatusActive, IsExclusive: false, SubscriptionType: SubscriptionTypeSubscription}}
  460. userRepo := &userRepoStubForGroupUpdate{}
  461. svc := &adminServiceImpl{apiKeyRepo: apiKeyRepo, groupRepo: groupRepo, userRepo: userRepo}
  462. _, err := svc.AdminUpdateAPIKeyGroupID(context.Background(), 1, int64Ptr(10))
  463. require.Error(t, err)
  464. require.Equal(t, "SUBSCRIPTION_REPOSITORY_UNAVAILABLE", infraerrors.Reason(err))
  465. require.False(t, userRepo.addGroupCalled)
  466. }
  467. func TestAdminService_AdminUpdateAPIKeyGroupID_SubscriptionGroup_AllowsActiveSubscription(t *testing.T) {
  468. existing := &APIKey{ID: 1, UserID: 42, Key: "sk-test", GroupID: nil}
  469. apiKeyRepo := &apiKeyRepoStubForGroupUpdate{key: existing}
  470. groupRepo := &groupRepoStubForGroupUpdate{group: &Group{ID: 10, Name: "Sub", Status: StatusActive, IsExclusive: true, SubscriptionType: SubscriptionTypeSubscription}}
  471. userRepo := &userRepoStubForGroupUpdate{}
  472. userSubRepo := &userSubRepoStubForGroupUpdate{
  473. getActiveSub: &UserSubscription{ID: 99, UserID: 42, GroupID: 10},
  474. }
  475. svc := &adminServiceImpl{apiKeyRepo: apiKeyRepo, groupRepo: groupRepo, userRepo: userRepo, userSubRepo: userSubRepo}
  476. got, err := svc.AdminUpdateAPIKeyGroupID(context.Background(), 1, int64Ptr(10))
  477. require.NoError(t, err)
  478. require.True(t, userSubRepo.called)
  479. require.NotNil(t, got.APIKey.GroupID)
  480. require.Equal(t, int64(10), *got.APIKey.GroupID)
  481. require.False(t, userRepo.addGroupCalled)
  482. }
  483. func TestAdminService_AdminUpdateAPIKeyGroupID_ExclusiveGroup_AllowedGroupAddFails_ReturnsError(t *testing.T) {
  484. existing := &APIKey{ID: 1, UserID: 42, Key: "sk-test", GroupID: nil}
  485. apiKeyRepo := &apiKeyRepoStubForGroupUpdate{key: existing}
  486. groupRepo := &groupRepoStubForGroupUpdate{group: &Group{ID: 10, Name: "Exclusive", Status: StatusActive, IsExclusive: true, SubscriptionType: SubscriptionTypeStandard}}
  487. userRepo := &userRepoStubForGroupUpdate{addGroupErr: errors.New("db error")}
  488. svc := &adminServiceImpl{apiKeyRepo: apiKeyRepo, groupRepo: groupRepo, userRepo: userRepo}
  489. // 严格模式:AddGroupToAllowedGroups 失败时,整体操作报错
  490. _, err := svc.AdminUpdateAPIKeyGroupID(context.Background(), 1, int64Ptr(10))
  491. require.Error(t, err)
  492. require.Contains(t, err.Error(), "add group to user allowed groups")
  493. require.True(t, userRepo.addGroupCalled)
  494. // apiKey 不应被更新
  495. require.Nil(t, apiKeyRepo.updated)
  496. }
  497. func TestAdminService_AdminUpdateAPIKeyGroupID_Unbind_NoAllowedGroupUpdate(t *testing.T) {
  498. existing := &APIKey{ID: 1, UserID: 42, Key: "sk-test", GroupID: int64Ptr(10), Group: &Group{ID: 10, Name: "Exclusive"}}
  499. apiKeyRepo := &apiKeyRepoStubForGroupUpdate{key: existing}
  500. userRepo := &userRepoStubForGroupUpdate{}
  501. cache := &authCacheInvalidatorStub{}
  502. svc := &adminServiceImpl{apiKeyRepo: apiKeyRepo, userRepo: userRepo, authCacheInvalidator: cache}
  503. got, err := svc.AdminUpdateAPIKeyGroupID(context.Background(), 1, int64Ptr(0))
  504. require.NoError(t, err)
  505. require.Nil(t, got.APIKey.GroupID)
  506. // 解绑时不修改 allowed_groups
  507. require.False(t, userRepo.addGroupCalled)
  508. require.False(t, got.AutoGrantedGroupAccess)
  509. }