admin_service_bulk_update_test.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "errors"
  6. "net/http"
  7. "reflect"
  8. "testing"
  9. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  10. "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
  11. "github.com/stretchr/testify/require"
  12. )
  13. type accountRepoStubForBulkUpdate struct {
  14. accountRepoStub
  15. bulkUpdateErr error
  16. bulkUpdateIDs []int64
  17. bulkUpdateCalls int
  18. lastBulkUpdate AccountBulkUpdate
  19. bindGroupErrByID map[int64]error
  20. bindGroupsCalls []int64
  21. bindGroupsByAccount map[int64][]int64
  22. createAccount *Account
  23. createID int64
  24. createErr error
  25. updatedAccounts []*Account
  26. updateErr error
  27. getByIDsAccounts []*Account
  28. getByIDsErr error
  29. getByIDsCalled bool
  30. getByIDsIDs []int64
  31. getByIDAccounts map[int64]*Account
  32. getByIDErrByID map[int64]error
  33. getByIDCalled []int64
  34. listByGroupData map[int64][]Account
  35. listByGroupErr map[int64]error
  36. listData []Account
  37. listResult *pagination.PaginationResult
  38. listErr error
  39. listCalled bool
  40. lastListParams pagination.PaginationParams
  41. lastListFilters struct {
  42. platform string
  43. accountType string
  44. status string
  45. search string
  46. groupID int64
  47. privacyMode string
  48. }
  49. }
  50. func (s *accountRepoStubForBulkUpdate) BulkUpdate(_ context.Context, ids []int64, updates AccountBulkUpdate) (int64, error) {
  51. s.bulkUpdateCalls++
  52. s.bulkUpdateIDs = append([]int64{}, ids...)
  53. s.lastBulkUpdate = updates
  54. if s.bulkUpdateErr != nil {
  55. return 0, s.bulkUpdateErr
  56. }
  57. return int64(len(ids)), nil
  58. }
  59. func requireApplicationErrorReason(t *testing.T, err error, reason string) {
  60. t.Helper()
  61. var appErr *infraerrors.ApplicationError
  62. require.ErrorAs(t, err, &appErr)
  63. require.Equal(t, reason, appErr.Reason)
  64. }
  65. func (s *accountRepoStubForBulkUpdate) Create(_ context.Context, account *Account) error {
  66. s.createAccount = account
  67. if s.createID > 0 {
  68. account.ID = s.createID
  69. }
  70. return s.createErr
  71. }
  72. func (s *accountRepoStubForBulkUpdate) Update(_ context.Context, account *Account) error {
  73. s.updatedAccounts = append(s.updatedAccounts, account)
  74. return s.updateErr
  75. }
  76. func (s *accountRepoStubForBulkUpdate) BindGroups(_ context.Context, accountID int64, groupIDs []int64) error {
  77. s.bindGroupsCalls = append(s.bindGroupsCalls, accountID)
  78. if s.bindGroupsByAccount == nil {
  79. s.bindGroupsByAccount = make(map[int64][]int64)
  80. }
  81. s.bindGroupsByAccount[accountID] = append([]int64{}, groupIDs...)
  82. if err, ok := s.bindGroupErrByID[accountID]; ok {
  83. return err
  84. }
  85. return nil
  86. }
  87. func (s *accountRepoStubForBulkUpdate) GetByIDs(_ context.Context, ids []int64) ([]*Account, error) {
  88. s.getByIDsCalled = true
  89. s.getByIDsIDs = append([]int64{}, ids...)
  90. if s.getByIDsErr != nil {
  91. return nil, s.getByIDsErr
  92. }
  93. return s.getByIDsAccounts, nil
  94. }
  95. func (s *accountRepoStubForBulkUpdate) GetByID(_ context.Context, id int64) (*Account, error) {
  96. s.getByIDCalled = append(s.getByIDCalled, id)
  97. if err, ok := s.getByIDErrByID[id]; ok {
  98. return nil, err
  99. }
  100. if account, ok := s.getByIDAccounts[id]; ok {
  101. return account, nil
  102. }
  103. return nil, errors.New("account not found")
  104. }
  105. func (s *accountRepoStubForBulkUpdate) ListByGroup(_ context.Context, groupID int64) ([]Account, error) {
  106. if err, ok := s.listByGroupErr[groupID]; ok {
  107. return nil, err
  108. }
  109. if rows, ok := s.listByGroupData[groupID]; ok {
  110. return rows, nil
  111. }
  112. return nil, nil
  113. }
  114. func (s *accountRepoStubForBulkUpdate) ListAllWithFilters(context.Context, string, string, string, string, int64, string) ([]Account, error) {
  115. return nil, nil
  116. }
  117. func (s *accountRepoStubForBulkUpdate) ListWithFilters(_ context.Context, params pagination.PaginationParams, platform, accountType, status, search string, groupID int64, privacyMode string) ([]Account, *pagination.PaginationResult, error) {
  118. s.listCalled = true
  119. s.lastListParams = params
  120. s.lastListFilters.platform = platform
  121. s.lastListFilters.accountType = accountType
  122. s.lastListFilters.status = status
  123. s.lastListFilters.search = search
  124. s.lastListFilters.groupID = groupID
  125. s.lastListFilters.privacyMode = privacyMode
  126. if s.listErr != nil {
  127. return nil, nil, s.listErr
  128. }
  129. if s.listResult != nil {
  130. return s.listData, s.listResult, nil
  131. }
  132. return s.listData, &pagination.PaginationResult{Total: int64(len(s.listData))}, nil
  133. }
  134. // TestAdminService_BulkUpdateAccounts_AllSuccessIDs 验证批量更新成功时返回 success_ids/failed_ids。
  135. func TestAdminService_BulkUpdateAccounts_AllSuccessIDs(t *testing.T) {
  136. repo := &accountRepoStubForBulkUpdate{}
  137. svc := &adminServiceImpl{accountRepo: repo}
  138. schedulable := true
  139. input := &BulkUpdateAccountsInput{
  140. AccountIDs: []int64{1, 2, 3},
  141. Schedulable: &schedulable,
  142. }
  143. result, err := svc.BulkUpdateAccounts(context.Background(), input)
  144. require.NoError(t, err)
  145. require.Equal(t, 3, result.Success)
  146. require.Equal(t, 0, result.Failed)
  147. require.ElementsMatch(t, []int64{1, 2, 3}, result.SuccessIDs)
  148. require.Empty(t, result.FailedIDs)
  149. require.Len(t, result.Results, 3)
  150. }
  151. func TestAdminService_BulkUpdateAccounts_RejectsRateChangeForSyncedAccounts(t *testing.T) {
  152. repo := &accountRepoStubForBulkUpdate{
  153. getByIDsAccounts: []*Account{
  154. {
  155. ID: 1,
  156. Extra: map[string]any{
  157. UpstreamBillingProbeEnabledExtraKey: true,
  158. UpstreamBillingRateSyncEnabledExtraKey: true,
  159. },
  160. },
  161. {ID: 2, Extra: map[string]any{}},
  162. },
  163. }
  164. svc := &adminServiceImpl{accountRepo: repo}
  165. rateMultiplier := 0.5
  166. result, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
  167. AccountIDs: []int64{1, 2},
  168. RateMultiplier: &rateMultiplier,
  169. })
  170. require.Nil(t, result)
  171. require.Error(t, err)
  172. var appErr *infraerrors.ApplicationError
  173. require.ErrorAs(t, err, &appErr)
  174. require.Equal(t, int32(http.StatusConflict), appErr.Code)
  175. require.Equal(t, "UPSTREAM_BILLING_RATE_SYNC_BULK_CONFLICT", appErr.Reason)
  176. require.Equal(t, "1", appErr.Metadata["count"])
  177. require.True(t, repo.getByIDsCalled)
  178. require.Empty(t, repo.bulkUpdateIDs, "rate conflict must be rejected before any write")
  179. }
  180. // TestAdminService_BulkUpdateAccounts_PartialFailureIDs 验证部分失败时 success_ids/failed_ids 正确。
  181. func TestAdminService_BulkUpdateAccounts_PartialFailureIDs(t *testing.T) {
  182. repo := &accountRepoStubForBulkUpdate{
  183. bindGroupErrByID: map[int64]error{
  184. 2: errors.New("bind failed"),
  185. },
  186. }
  187. svc := &adminServiceImpl{
  188. accountRepo: repo,
  189. groupRepo: &groupRepoStubForAdmin{getByID: &Group{ID: 10, Name: "g10"}},
  190. }
  191. groupIDs := []int64{10}
  192. schedulable := false
  193. input := &BulkUpdateAccountsInput{
  194. AccountIDs: []int64{1, 2, 3},
  195. GroupIDs: &groupIDs,
  196. Schedulable: &schedulable,
  197. SkipMixedChannelCheck: true,
  198. }
  199. result, err := svc.BulkUpdateAccounts(context.Background(), input)
  200. require.NoError(t, err)
  201. require.Equal(t, 2, result.Success)
  202. require.Equal(t, 1, result.Failed)
  203. require.ElementsMatch(t, []int64{1, 3}, result.SuccessIDs)
  204. require.ElementsMatch(t, []int64{2}, result.FailedIDs)
  205. require.Len(t, result.Results, 3)
  206. }
  207. func TestAdminService_BulkUpdateAccounts_NilGroupRepoReturnsError(t *testing.T) {
  208. repo := &accountRepoStubForBulkUpdate{}
  209. svc := &adminServiceImpl{accountRepo: repo}
  210. groupIDs := []int64{10}
  211. input := &BulkUpdateAccountsInput{
  212. AccountIDs: []int64{1},
  213. GroupIDs: &groupIDs,
  214. }
  215. result, err := svc.BulkUpdateAccounts(context.Background(), input)
  216. require.Nil(t, result)
  217. require.Error(t, err)
  218. require.Contains(t, err.Error(), "group repository not configured")
  219. }
  220. // TestAdminService_BulkUpdateAccounts_MixedChannelPreCheckBlocksOnExistingConflict verifies
  221. // that the global pre-check detects a conflict with existing group members and returns an
  222. // error before any DB write is performed.
  223. func TestAdminService_BulkUpdateAccounts_MixedChannelPreCheckBlocksOnExistingConflict(t *testing.T) {
  224. repo := &accountRepoStubForBulkUpdate{
  225. getByIDsAccounts: []*Account{
  226. {ID: 1, Platform: PlatformAntigravity},
  227. },
  228. // Group 10 already contains an Anthropic account.
  229. listByGroupData: map[int64][]Account{
  230. 10: {{ID: 99, Platform: PlatformAnthropic}},
  231. },
  232. }
  233. svc := &adminServiceImpl{
  234. accountRepo: repo,
  235. groupRepo: &groupRepoStubForAdmin{getByID: &Group{ID: 10, Name: "target-group"}},
  236. }
  237. groupIDs := []int64{10}
  238. input := &BulkUpdateAccountsInput{
  239. AccountIDs: []int64{1},
  240. GroupIDs: &groupIDs,
  241. }
  242. result, err := svc.BulkUpdateAccounts(context.Background(), input)
  243. require.Nil(t, result)
  244. require.Error(t, err)
  245. require.Contains(t, err.Error(), "mixed channel")
  246. // No BindGroups should have been called since the check runs before any write.
  247. require.Empty(t, repo.bindGroupsCalls)
  248. }
  249. func TestAdminServiceBulkUpdateAccounts_ResolvesIDsFromFilters(t *testing.T) {
  250. repo := &accountRepoStubForBulkUpdate{
  251. listData: []Account{
  252. {ID: 7},
  253. {ID: 11},
  254. },
  255. listResult: &pagination.PaginationResult{Total: 2},
  256. }
  257. svc := &adminServiceImpl{accountRepo: repo}
  258. schedulable := true
  259. input := &BulkUpdateAccountsInput{
  260. Schedulable: &schedulable,
  261. }
  262. filtersField := reflect.ValueOf(input).Elem().FieldByName("Filters")
  263. require.True(t, filtersField.IsValid(), "BulkUpdateAccountsInput should expose Filters for filter-target bulk update")
  264. require.Equal(t, reflect.Ptr, filtersField.Kind(), "BulkUpdateAccountsInput.Filters should be a pointer field")
  265. filtersValue := reflect.New(filtersField.Type().Elem())
  266. filtersValue.Elem().FieldByName("Platform").SetString(PlatformOpenAI)
  267. filtersValue.Elem().FieldByName("Type").SetString(AccountTypeOAuth)
  268. filtersValue.Elem().FieldByName("Status").SetString(StatusActive)
  269. filtersValue.Elem().FieldByName("Group").SetString("12")
  270. filtersValue.Elem().FieldByName("PrivacyMode").SetString(PrivacyModeCFBlocked)
  271. filtersValue.Elem().FieldByName("Search").SetString("bulk-target")
  272. filtersField.Set(filtersValue)
  273. result, err := svc.BulkUpdateAccounts(context.Background(), input)
  274. require.NoError(t, err)
  275. require.True(t, repo.listCalled, "expected filter-target bulk update to resolve matching IDs via account list filters")
  276. require.Equal(t, PlatformOpenAI, repo.lastListFilters.platform)
  277. require.Equal(t, AccountTypeOAuth, repo.lastListFilters.accountType)
  278. require.Equal(t, StatusActive, repo.lastListFilters.status)
  279. require.Equal(t, "bulk-target", repo.lastListFilters.search)
  280. require.Equal(t, int64(12), repo.lastListFilters.groupID)
  281. require.Equal(t, PrivacyModeCFBlocked, repo.lastListFilters.privacyMode)
  282. require.Equal(t, []int64{7, 11}, repo.bulkUpdateIDs)
  283. require.Equal(t, 2, result.Success)
  284. require.Equal(t, 0, result.Failed)
  285. require.Equal(t, []int64{7, 11}, result.SuccessIDs)
  286. }
  287. func TestAdminServiceBulkUpdateAccounts_NormalizesOpenAISettings(t *testing.T) {
  288. repo := &accountRepoStubForBulkUpdate{getByIDsAccounts: []*Account{
  289. {ID: 1, Platform: PlatformOpenAI, Type: AccountTypeAPIKey},
  290. {ID: 2, Platform: PlatformOpenAI, Type: AccountTypeAPIKey},
  291. }}
  292. svc := &adminServiceImpl{accountRepo: repo}
  293. result, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
  294. AccountIDs: []int64{1, 2},
  295. Credentials: map[string]any{
  296. openAIEndpointCapabilitiesCredentialKey: []any{"chat_completions", "embeddings"},
  297. },
  298. Extra: map[string]any{
  299. openAILongContextBillingEnabledKey: true,
  300. "openai_responses_mode": "auto",
  301. },
  302. })
  303. require.NoError(t, err)
  304. require.Equal(t, 2, result.Success)
  305. require.Zero(t, result.LongContextInheritedCount)
  306. require.Equal(t, 1, repo.bulkUpdateCalls)
  307. require.Contains(t, repo.lastBulkUpdate.Credentials, openAIEndpointCapabilitiesCredentialKey)
  308. require.Nil(t, repo.lastBulkUpdate.Credentials[openAIEndpointCapabilitiesCredentialKey])
  309. require.Equal(t, true, repo.lastBulkUpdate.Extra[openAILongContextBillingEnabledKey])
  310. require.Contains(t, repo.lastBulkUpdate.Extra, "openai_responses_mode")
  311. require.Nil(t, repo.lastBulkUpdate.Extra["openai_responses_mode"])
  312. }
  313. func TestAdminServiceBulkUpdateAccounts_AcceptsLongContextAccountTypes(t *testing.T) {
  314. for _, accountType := range []string{AccountTypeOAuth, AccountTypeSetupToken, AccountTypeAPIKey} {
  315. t.Run(accountType, func(t *testing.T) {
  316. repo := &accountRepoStubForBulkUpdate{getByIDsAccounts: []*Account{{
  317. ID: 1, Platform: PlatformOpenAI, Type: accountType,
  318. }}}
  319. svc := &adminServiceImpl{accountRepo: repo}
  320. result, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
  321. AccountIDs: []int64{1},
  322. Extra: map[string]any{openAILongContextBillingEnabledKey: false},
  323. })
  324. require.NoError(t, err)
  325. require.Equal(t, 1, result.Success)
  326. require.Equal(t, 1, repo.bulkUpdateCalls)
  327. })
  328. }
  329. }
  330. func TestAdminServiceBulkUpdateAccounts_EmbeddingsOnlyResetsResponsesMode(t *testing.T) {
  331. repo := &accountRepoStubForBulkUpdate{getByIDsAccounts: []*Account{
  332. {ID: 1, Platform: PlatformOpenAI, Type: AccountTypeAPIKey},
  333. }}
  334. svc := &adminServiceImpl{accountRepo: repo}
  335. _, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
  336. AccountIDs: []int64{1},
  337. Credentials: map[string]any{
  338. openAIEndpointCapabilitiesCredentialKey: []string{"embeddings"},
  339. },
  340. })
  341. require.NoError(t, err)
  342. require.Equal(t, []string{"embeddings"}, repo.lastBulkUpdate.Credentials[openAIEndpointCapabilitiesCredentialKey])
  343. require.Contains(t, repo.lastBulkUpdate.Extra, "openai_responses_mode")
  344. require.Nil(t, repo.lastBulkUpdate.Extra["openai_responses_mode"])
  345. }
  346. func TestAdminServiceBulkUpdateAccounts_RejectsInvalidOpenAISettingValuesBeforeWrite(t *testing.T) {
  347. tests := []struct {
  348. name string
  349. credentials map[string]any
  350. extra map[string]any
  351. reason string
  352. }{
  353. {name: "long context type", extra: map[string]any{openAILongContextBillingEnabledKey: "true"}, reason: "OPENAI_LONG_CONTEXT_BILLING_INVALID"},
  354. {name: "empty capabilities", credentials: map[string]any{openAIEndpointCapabilitiesCredentialKey: []any{}}, reason: "OPENAI_ENDPOINT_CAPABILITIES_INVALID"},
  355. {name: "unknown capability", credentials: map[string]any{openAIEndpointCapabilitiesCredentialKey: []any{"responses"}}, reason: "OPENAI_ENDPOINT_CAPABILITIES_INVALID"},
  356. {name: "capabilities type", credentials: map[string]any{openAIEndpointCapabilitiesCredentialKey: "chat_completions"}, reason: "OPENAI_ENDPOINT_CAPABILITIES_INVALID"},
  357. {name: "responses mode", extra: map[string]any{"openai_responses_mode": "sometimes"}, reason: "OPENAI_RESPONSES_MODE_INVALID"},
  358. {name: "responses type", extra: map[string]any{"openai_responses_mode": true}, reason: "OPENAI_RESPONSES_MODE_INVALID"},
  359. {
  360. name: "embeddings conflict",
  361. credentials: map[string]any{openAIEndpointCapabilitiesCredentialKey: []any{"embeddings"}},
  362. extra: map[string]any{"openai_responses_mode": "force_responses"},
  363. reason: "OPENAI_RESPONSES_MODE_INVALID",
  364. },
  365. }
  366. for _, tt := range tests {
  367. t.Run(tt.name, func(t *testing.T) {
  368. repo := &accountRepoStubForBulkUpdate{}
  369. svc := &adminServiceImpl{accountRepo: repo}
  370. result, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
  371. AccountIDs: []int64{1},
  372. Credentials: tt.credentials,
  373. Extra: tt.extra,
  374. })
  375. require.Nil(t, result)
  376. requireApplicationErrorReason(t, err, tt.reason)
  377. require.Zero(t, repo.bulkUpdateCalls)
  378. })
  379. }
  380. }
  381. func TestAdminServiceBulkUpdateAccounts_RejectsInvalidOpenAITargetsBeforeWrite(t *testing.T) {
  382. tests := []struct {
  383. name string
  384. accounts []*Account
  385. input *BulkUpdateAccountsInput
  386. }{
  387. {
  388. name: "missing account",
  389. accounts: []*Account{{ID: 1, Platform: PlatformOpenAI, Type: AccountTypeOAuth}},
  390. input: &BulkUpdateAccountsInput{
  391. AccountIDs: []int64{1, 2},
  392. Extra: map[string]any{openAILongContextBillingEnabledKey: true},
  393. },
  394. },
  395. {
  396. name: "mixed platform long context",
  397. accounts: []*Account{{ID: 1, Platform: PlatformAnthropic, Type: AccountTypeOAuth}},
  398. input: &BulkUpdateAccountsInput{
  399. AccountIDs: []int64{1},
  400. Extra: map[string]any{openAILongContextBillingEnabledKey: true},
  401. },
  402. },
  403. {
  404. name: "oauth endpoint capabilities",
  405. accounts: []*Account{{ID: 1, Platform: PlatformOpenAI, Type: AccountTypeOAuth}},
  406. input: &BulkUpdateAccountsInput{
  407. AccountIDs: []int64{1},
  408. Credentials: map[string]any{openAIEndpointCapabilitiesCredentialKey: nil},
  409. },
  410. },
  411. {
  412. name: "unsupported OpenAI long context account type",
  413. accounts: []*Account{{ID: 1, Platform: PlatformOpenAI, Type: AccountTypeServiceAccount}},
  414. input: &BulkUpdateAccountsInput{
  415. AccountIDs: []int64{1},
  416. Extra: map[string]any{openAILongContextBillingEnabledKey: true},
  417. },
  418. },
  419. }
  420. for _, tt := range tests {
  421. t.Run(tt.name, func(t *testing.T) {
  422. repo := &accountRepoStubForBulkUpdate{getByIDsAccounts: tt.accounts}
  423. svc := &adminServiceImpl{accountRepo: repo}
  424. result, err := svc.BulkUpdateAccounts(context.Background(), tt.input)
  425. require.Nil(t, result)
  426. requireApplicationErrorReason(t, err, "OPENAI_BULK_TARGET_INVALID")
  427. require.Zero(t, repo.bulkUpdateCalls)
  428. })
  429. }
  430. }
  431. func TestAdminServiceBulkUpdateAccounts_ForcedResponsesRequiresChatCapability(t *testing.T) {
  432. repo := &accountRepoStubForBulkUpdate{getByIDsAccounts: []*Account{{
  433. ID: 1,
  434. Platform: PlatformOpenAI,
  435. Type: AccountTypeAPIKey,
  436. Credentials: map[string]any{
  437. openAIEndpointCapabilitiesCredentialKey: []any{"embeddings"},
  438. },
  439. }}}
  440. svc := &adminServiceImpl{accountRepo: repo}
  441. result, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
  442. AccountIDs: []int64{1},
  443. Extra: map[string]any{"openai_responses_mode": "force_chat_completions"},
  444. })
  445. require.Nil(t, result)
  446. requireApplicationErrorReason(t, err, "OPENAI_BULK_TARGET_INVALID")
  447. require.Zero(t, repo.bulkUpdateCalls)
  448. }
  449. func TestAdminServiceBulkUpdateAccounts_ForcedResponsesAcceptsChatCapabilityUpdate(t *testing.T) {
  450. repo := &accountRepoStubForBulkUpdate{getByIDsAccounts: []*Account{{
  451. ID: 1,
  452. Platform: PlatformOpenAI,
  453. Type: AccountTypeAPIKey,
  454. Credentials: map[string]any{
  455. openAIEndpointCapabilitiesCredentialKey: []any{"embeddings"},
  456. },
  457. }}}
  458. svc := &adminServiceImpl{accountRepo: repo}
  459. _, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
  460. AccountIDs: []int64{1},
  461. Credentials: map[string]any{
  462. openAIEndpointCapabilitiesCredentialKey: []any{"chat_completions"},
  463. },
  464. Extra: map[string]any{"openai_responses_mode": "force_responses"},
  465. })
  466. require.NoError(t, err)
  467. require.Equal(t, 1, repo.bulkUpdateCalls)
  468. }
  469. func TestAdminServiceBulkUpdateAccounts_ReportsLongContextShadowInheritance(t *testing.T) {
  470. parentID := int64(1)
  471. repo := &accountRepoStubForBulkUpdate{getByIDsAccounts: []*Account{
  472. {ID: parentID, Platform: PlatformOpenAI, Type: AccountTypeOAuth},
  473. {ID: 2, Platform: PlatformOpenAI, Type: AccountTypeOAuth, ParentAccountID: &parentID},
  474. }}
  475. svc := &adminServiceImpl{accountRepo: repo}
  476. result, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
  477. AccountIDs: []int64{parentID, 2},
  478. Extra: map[string]any{openAILongContextBillingEnabledKey: true},
  479. })
  480. require.NoError(t, err)
  481. require.Equal(t, 1, result.LongContextInheritedCount)
  482. require.Equal(t, 1, repo.bulkUpdateCalls)
  483. }
  484. func TestAdminServiceBulkUpdateAccounts_RequiresParentForShadowOnlyLongContextUpdate(t *testing.T) {
  485. parentID := int64(10)
  486. repo := &accountRepoStubForBulkUpdate{getByIDsAccounts: []*Account{
  487. {ID: 1, Platform: PlatformOpenAI, Type: AccountTypeOAuth, ParentAccountID: &parentID},
  488. {ID: 2, Platform: PlatformOpenAI, Type: AccountTypeOAuth, ParentAccountID: &parentID},
  489. }}
  490. svc := &adminServiceImpl{accountRepo: repo}
  491. result, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
  492. AccountIDs: []int64{1, 2},
  493. Extra: map[string]any{openAILongContextBillingEnabledKey: true},
  494. })
  495. require.Nil(t, result)
  496. requireApplicationErrorReason(t, err, "OPENAI_LONG_CONTEXT_PARENT_REQUIRED")
  497. require.Zero(t, repo.bulkUpdateCalls)
  498. }
  499. func TestAdminServiceBulkUpdateAccounts_ShadowLongContextAllowsOtherUpdates(t *testing.T) {
  500. parentID := int64(10)
  501. repo := &accountRepoStubForBulkUpdate{getByIDsAccounts: []*Account{{
  502. ID: 1, Platform: PlatformOpenAI, Type: AccountTypeOAuth, ParentAccountID: &parentID,
  503. }}}
  504. svc := &adminServiceImpl{accountRepo: repo}
  505. status := StatusDisabled
  506. result, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
  507. AccountIDs: []int64{1},
  508. Status: status,
  509. Extra: map[string]any{openAILongContextBillingEnabledKey: false},
  510. })
  511. require.NoError(t, err)
  512. require.Equal(t, 1, result.LongContextInheritedCount)
  513. require.Equal(t, 1, repo.bulkUpdateCalls)
  514. require.NotNil(t, repo.lastBulkUpdate.Status)
  515. require.Equal(t, status, *repo.lastBulkUpdate.Status)
  516. }
  517. func TestAdminServiceBulkUpdateAccounts_ValidatesFilterResolvedOpenAITargets(t *testing.T) {
  518. repo := &accountRepoStubForBulkUpdate{
  519. listData: []Account{{ID: 7}},
  520. listResult: &pagination.PaginationResult{Total: 1},
  521. getByIDsAccounts: []*Account{{ID: 7, Platform: PlatformAnthropic, Type: AccountTypeOAuth}},
  522. }
  523. svc := &adminServiceImpl{accountRepo: repo}
  524. result, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
  525. Filters: &BulkUpdateAccountFilters{Platform: PlatformOpenAI},
  526. Extra: map[string]any{openAILongContextBillingEnabledKey: true},
  527. })
  528. require.Nil(t, result)
  529. requireApplicationErrorReason(t, err, "OPENAI_BULK_TARGET_INVALID")
  530. require.Equal(t, []int64{7}, repo.getByIDsIDs)
  531. require.Zero(t, repo.bulkUpdateCalls)
  532. }