Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,597 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type accountRepoStubForBulkUpdate struct {
|
||||
accountRepoStub
|
||||
bulkUpdateErr error
|
||||
bulkUpdateIDs []int64
|
||||
bulkUpdateCalls int
|
||||
lastBulkUpdate AccountBulkUpdate
|
||||
bindGroupErrByID map[int64]error
|
||||
bindGroupsCalls []int64
|
||||
bindGroupsByAccount map[int64][]int64
|
||||
createAccount *Account
|
||||
createID int64
|
||||
createErr error
|
||||
updatedAccounts []*Account
|
||||
updateErr error
|
||||
getByIDsAccounts []*Account
|
||||
getByIDsErr error
|
||||
getByIDsCalled bool
|
||||
getByIDsIDs []int64
|
||||
getByIDAccounts map[int64]*Account
|
||||
getByIDErrByID map[int64]error
|
||||
getByIDCalled []int64
|
||||
listByGroupData map[int64][]Account
|
||||
listByGroupErr map[int64]error
|
||||
listData []Account
|
||||
listResult *pagination.PaginationResult
|
||||
listErr error
|
||||
listCalled bool
|
||||
lastListParams pagination.PaginationParams
|
||||
lastListFilters struct {
|
||||
platform string
|
||||
accountType string
|
||||
status string
|
||||
search string
|
||||
groupID int64
|
||||
privacyMode string
|
||||
}
|
||||
}
|
||||
|
||||
func (s *accountRepoStubForBulkUpdate) BulkUpdate(_ context.Context, ids []int64, updates AccountBulkUpdate) (int64, error) {
|
||||
s.bulkUpdateCalls++
|
||||
s.bulkUpdateIDs = append([]int64{}, ids...)
|
||||
s.lastBulkUpdate = updates
|
||||
if s.bulkUpdateErr != nil {
|
||||
return 0, s.bulkUpdateErr
|
||||
}
|
||||
return int64(len(ids)), nil
|
||||
}
|
||||
|
||||
func requireApplicationErrorReason(t *testing.T, err error, reason string) {
|
||||
t.Helper()
|
||||
var appErr *infraerrors.ApplicationError
|
||||
require.ErrorAs(t, err, &appErr)
|
||||
require.Equal(t, reason, appErr.Reason)
|
||||
}
|
||||
|
||||
func (s *accountRepoStubForBulkUpdate) Create(_ context.Context, account *Account) error {
|
||||
s.createAccount = account
|
||||
if s.createID > 0 {
|
||||
account.ID = s.createID
|
||||
}
|
||||
return s.createErr
|
||||
}
|
||||
|
||||
func (s *accountRepoStubForBulkUpdate) Update(_ context.Context, account *Account) error {
|
||||
s.updatedAccounts = append(s.updatedAccounts, account)
|
||||
return s.updateErr
|
||||
}
|
||||
|
||||
func (s *accountRepoStubForBulkUpdate) BindGroups(_ context.Context, accountID int64, groupIDs []int64) error {
|
||||
s.bindGroupsCalls = append(s.bindGroupsCalls, accountID)
|
||||
if s.bindGroupsByAccount == nil {
|
||||
s.bindGroupsByAccount = make(map[int64][]int64)
|
||||
}
|
||||
s.bindGroupsByAccount[accountID] = append([]int64{}, groupIDs...)
|
||||
if err, ok := s.bindGroupErrByID[accountID]; ok {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *accountRepoStubForBulkUpdate) GetByIDs(_ context.Context, ids []int64) ([]*Account, error) {
|
||||
s.getByIDsCalled = true
|
||||
s.getByIDsIDs = append([]int64{}, ids...)
|
||||
if s.getByIDsErr != nil {
|
||||
return nil, s.getByIDsErr
|
||||
}
|
||||
return s.getByIDsAccounts, nil
|
||||
}
|
||||
|
||||
func (s *accountRepoStubForBulkUpdate) GetByID(_ context.Context, id int64) (*Account, error) {
|
||||
s.getByIDCalled = append(s.getByIDCalled, id)
|
||||
if err, ok := s.getByIDErrByID[id]; ok {
|
||||
return nil, err
|
||||
}
|
||||
if account, ok := s.getByIDAccounts[id]; ok {
|
||||
return account, nil
|
||||
}
|
||||
return nil, errors.New("account not found")
|
||||
}
|
||||
|
||||
func (s *accountRepoStubForBulkUpdate) ListByGroup(_ context.Context, groupID int64) ([]Account, error) {
|
||||
if err, ok := s.listByGroupErr[groupID]; ok {
|
||||
return nil, err
|
||||
}
|
||||
if rows, ok := s.listByGroupData[groupID]; ok {
|
||||
return rows, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *accountRepoStubForBulkUpdate) ListAllWithFilters(context.Context, string, string, string, string, int64, string) ([]Account, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *accountRepoStubForBulkUpdate) ListWithFilters(_ context.Context, params pagination.PaginationParams, platform, accountType, status, search string, groupID int64, privacyMode string) ([]Account, *pagination.PaginationResult, error) {
|
||||
s.listCalled = true
|
||||
s.lastListParams = params
|
||||
s.lastListFilters.platform = platform
|
||||
s.lastListFilters.accountType = accountType
|
||||
s.lastListFilters.status = status
|
||||
s.lastListFilters.search = search
|
||||
s.lastListFilters.groupID = groupID
|
||||
s.lastListFilters.privacyMode = privacyMode
|
||||
if s.listErr != nil {
|
||||
return nil, nil, s.listErr
|
||||
}
|
||||
if s.listResult != nil {
|
||||
return s.listData, s.listResult, nil
|
||||
}
|
||||
return s.listData, &pagination.PaginationResult{Total: int64(len(s.listData))}, nil
|
||||
}
|
||||
|
||||
// TestAdminService_BulkUpdateAccounts_AllSuccessIDs 验证批量更新成功时返回 success_ids/failed_ids。
|
||||
func TestAdminService_BulkUpdateAccounts_AllSuccessIDs(t *testing.T) {
|
||||
repo := &accountRepoStubForBulkUpdate{}
|
||||
svc := &adminServiceImpl{accountRepo: repo}
|
||||
|
||||
schedulable := true
|
||||
input := &BulkUpdateAccountsInput{
|
||||
AccountIDs: []int64{1, 2, 3},
|
||||
Schedulable: &schedulable,
|
||||
}
|
||||
|
||||
result, err := svc.BulkUpdateAccounts(context.Background(), input)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, result.Success)
|
||||
require.Equal(t, 0, result.Failed)
|
||||
require.ElementsMatch(t, []int64{1, 2, 3}, result.SuccessIDs)
|
||||
require.Empty(t, result.FailedIDs)
|
||||
require.Len(t, result.Results, 3)
|
||||
}
|
||||
|
||||
func TestAdminService_BulkUpdateAccounts_RejectsRateChangeForSyncedAccounts(t *testing.T) {
|
||||
repo := &accountRepoStubForBulkUpdate{
|
||||
getByIDsAccounts: []*Account{
|
||||
{
|
||||
ID: 1,
|
||||
Extra: map[string]any{
|
||||
UpstreamBillingProbeEnabledExtraKey: true,
|
||||
UpstreamBillingRateSyncEnabledExtraKey: true,
|
||||
},
|
||||
},
|
||||
{ID: 2, Extra: map[string]any{}},
|
||||
},
|
||||
}
|
||||
svc := &adminServiceImpl{accountRepo: repo}
|
||||
rateMultiplier := 0.5
|
||||
|
||||
result, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
|
||||
AccountIDs: []int64{1, 2},
|
||||
RateMultiplier: &rateMultiplier,
|
||||
})
|
||||
|
||||
require.Nil(t, result)
|
||||
require.Error(t, err)
|
||||
var appErr *infraerrors.ApplicationError
|
||||
require.ErrorAs(t, err, &appErr)
|
||||
require.Equal(t, int32(http.StatusConflict), appErr.Code)
|
||||
require.Equal(t, "UPSTREAM_BILLING_RATE_SYNC_BULK_CONFLICT", appErr.Reason)
|
||||
require.Equal(t, "1", appErr.Metadata["count"])
|
||||
require.True(t, repo.getByIDsCalled)
|
||||
require.Empty(t, repo.bulkUpdateIDs, "rate conflict must be rejected before any write")
|
||||
}
|
||||
|
||||
// TestAdminService_BulkUpdateAccounts_PartialFailureIDs 验证部分失败时 success_ids/failed_ids 正确。
|
||||
func TestAdminService_BulkUpdateAccounts_PartialFailureIDs(t *testing.T) {
|
||||
repo := &accountRepoStubForBulkUpdate{
|
||||
bindGroupErrByID: map[int64]error{
|
||||
2: errors.New("bind failed"),
|
||||
},
|
||||
}
|
||||
svc := &adminServiceImpl{
|
||||
accountRepo: repo,
|
||||
groupRepo: &groupRepoStubForAdmin{getByID: &Group{ID: 10, Name: "g10"}},
|
||||
}
|
||||
|
||||
groupIDs := []int64{10}
|
||||
schedulable := false
|
||||
input := &BulkUpdateAccountsInput{
|
||||
AccountIDs: []int64{1, 2, 3},
|
||||
GroupIDs: &groupIDs,
|
||||
Schedulable: &schedulable,
|
||||
SkipMixedChannelCheck: true,
|
||||
}
|
||||
|
||||
result, err := svc.BulkUpdateAccounts(context.Background(), input)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, result.Success)
|
||||
require.Equal(t, 1, result.Failed)
|
||||
require.ElementsMatch(t, []int64{1, 3}, result.SuccessIDs)
|
||||
require.ElementsMatch(t, []int64{2}, result.FailedIDs)
|
||||
require.Len(t, result.Results, 3)
|
||||
}
|
||||
|
||||
func TestAdminService_BulkUpdateAccounts_NilGroupRepoReturnsError(t *testing.T) {
|
||||
repo := &accountRepoStubForBulkUpdate{}
|
||||
svc := &adminServiceImpl{accountRepo: repo}
|
||||
|
||||
groupIDs := []int64{10}
|
||||
input := &BulkUpdateAccountsInput{
|
||||
AccountIDs: []int64{1},
|
||||
GroupIDs: &groupIDs,
|
||||
}
|
||||
|
||||
result, err := svc.BulkUpdateAccounts(context.Background(), input)
|
||||
require.Nil(t, result)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "group repository not configured")
|
||||
}
|
||||
|
||||
// TestAdminService_BulkUpdateAccounts_MixedChannelPreCheckBlocksOnExistingConflict verifies
|
||||
// that the global pre-check detects a conflict with existing group members and returns an
|
||||
// error before any DB write is performed.
|
||||
func TestAdminService_BulkUpdateAccounts_MixedChannelPreCheckBlocksOnExistingConflict(t *testing.T) {
|
||||
repo := &accountRepoStubForBulkUpdate{
|
||||
getByIDsAccounts: []*Account{
|
||||
{ID: 1, Platform: PlatformAntigravity},
|
||||
},
|
||||
// Group 10 already contains an Anthropic account.
|
||||
listByGroupData: map[int64][]Account{
|
||||
10: {{ID: 99, Platform: PlatformAnthropic}},
|
||||
},
|
||||
}
|
||||
svc := &adminServiceImpl{
|
||||
accountRepo: repo,
|
||||
groupRepo: &groupRepoStubForAdmin{getByID: &Group{ID: 10, Name: "target-group"}},
|
||||
}
|
||||
|
||||
groupIDs := []int64{10}
|
||||
input := &BulkUpdateAccountsInput{
|
||||
AccountIDs: []int64{1},
|
||||
GroupIDs: &groupIDs,
|
||||
}
|
||||
|
||||
result, err := svc.BulkUpdateAccounts(context.Background(), input)
|
||||
require.Nil(t, result)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "mixed channel")
|
||||
// No BindGroups should have been called since the check runs before any write.
|
||||
require.Empty(t, repo.bindGroupsCalls)
|
||||
}
|
||||
|
||||
func TestAdminServiceBulkUpdateAccounts_ResolvesIDsFromFilters(t *testing.T) {
|
||||
repo := &accountRepoStubForBulkUpdate{
|
||||
listData: []Account{
|
||||
{ID: 7},
|
||||
{ID: 11},
|
||||
},
|
||||
listResult: &pagination.PaginationResult{Total: 2},
|
||||
}
|
||||
svc := &adminServiceImpl{accountRepo: repo}
|
||||
|
||||
schedulable := true
|
||||
input := &BulkUpdateAccountsInput{
|
||||
Schedulable: &schedulable,
|
||||
}
|
||||
|
||||
filtersField := reflect.ValueOf(input).Elem().FieldByName("Filters")
|
||||
require.True(t, filtersField.IsValid(), "BulkUpdateAccountsInput should expose Filters for filter-target bulk update")
|
||||
require.Equal(t, reflect.Ptr, filtersField.Kind(), "BulkUpdateAccountsInput.Filters should be a pointer field")
|
||||
|
||||
filtersValue := reflect.New(filtersField.Type().Elem())
|
||||
filtersValue.Elem().FieldByName("Platform").SetString(PlatformOpenAI)
|
||||
filtersValue.Elem().FieldByName("Type").SetString(AccountTypeOAuth)
|
||||
filtersValue.Elem().FieldByName("Status").SetString(StatusActive)
|
||||
filtersValue.Elem().FieldByName("Group").SetString("12")
|
||||
filtersValue.Elem().FieldByName("PrivacyMode").SetString(PrivacyModeCFBlocked)
|
||||
filtersValue.Elem().FieldByName("Search").SetString("bulk-target")
|
||||
filtersField.Set(filtersValue)
|
||||
|
||||
result, err := svc.BulkUpdateAccounts(context.Background(), input)
|
||||
require.NoError(t, err)
|
||||
require.True(t, repo.listCalled, "expected filter-target bulk update to resolve matching IDs via account list filters")
|
||||
require.Equal(t, PlatformOpenAI, repo.lastListFilters.platform)
|
||||
require.Equal(t, AccountTypeOAuth, repo.lastListFilters.accountType)
|
||||
require.Equal(t, StatusActive, repo.lastListFilters.status)
|
||||
require.Equal(t, "bulk-target", repo.lastListFilters.search)
|
||||
require.Equal(t, int64(12), repo.lastListFilters.groupID)
|
||||
require.Equal(t, PrivacyModeCFBlocked, repo.lastListFilters.privacyMode)
|
||||
require.Equal(t, []int64{7, 11}, repo.bulkUpdateIDs)
|
||||
require.Equal(t, 2, result.Success)
|
||||
require.Equal(t, 0, result.Failed)
|
||||
require.Equal(t, []int64{7, 11}, result.SuccessIDs)
|
||||
}
|
||||
|
||||
func TestAdminServiceBulkUpdateAccounts_NormalizesOpenAISettings(t *testing.T) {
|
||||
repo := &accountRepoStubForBulkUpdate{getByIDsAccounts: []*Account{
|
||||
{ID: 1, Platform: PlatformOpenAI, Type: AccountTypeAPIKey},
|
||||
{ID: 2, Platform: PlatformOpenAI, Type: AccountTypeAPIKey},
|
||||
}}
|
||||
svc := &adminServiceImpl{accountRepo: repo}
|
||||
|
||||
result, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
|
||||
AccountIDs: []int64{1, 2},
|
||||
Credentials: map[string]any{
|
||||
openAIEndpointCapabilitiesCredentialKey: []any{"chat_completions", "embeddings"},
|
||||
},
|
||||
Extra: map[string]any{
|
||||
openAILongContextBillingEnabledKey: true,
|
||||
"openai_responses_mode": "auto",
|
||||
},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, result.Success)
|
||||
require.Zero(t, result.LongContextInheritedCount)
|
||||
require.Equal(t, 1, repo.bulkUpdateCalls)
|
||||
require.Contains(t, repo.lastBulkUpdate.Credentials, openAIEndpointCapabilitiesCredentialKey)
|
||||
require.Nil(t, repo.lastBulkUpdate.Credentials[openAIEndpointCapabilitiesCredentialKey])
|
||||
require.Equal(t, true, repo.lastBulkUpdate.Extra[openAILongContextBillingEnabledKey])
|
||||
require.Contains(t, repo.lastBulkUpdate.Extra, "openai_responses_mode")
|
||||
require.Nil(t, repo.lastBulkUpdate.Extra["openai_responses_mode"])
|
||||
}
|
||||
|
||||
func TestAdminServiceBulkUpdateAccounts_AcceptsLongContextAccountTypes(t *testing.T) {
|
||||
for _, accountType := range []string{AccountTypeOAuth, AccountTypeSetupToken, AccountTypeAPIKey} {
|
||||
t.Run(accountType, func(t *testing.T) {
|
||||
repo := &accountRepoStubForBulkUpdate{getByIDsAccounts: []*Account{{
|
||||
ID: 1, Platform: PlatformOpenAI, Type: accountType,
|
||||
}}}
|
||||
svc := &adminServiceImpl{accountRepo: repo}
|
||||
|
||||
result, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
|
||||
AccountIDs: []int64{1},
|
||||
Extra: map[string]any{openAILongContextBillingEnabledKey: false},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, result.Success)
|
||||
require.Equal(t, 1, repo.bulkUpdateCalls)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminServiceBulkUpdateAccounts_EmbeddingsOnlyResetsResponsesMode(t *testing.T) {
|
||||
repo := &accountRepoStubForBulkUpdate{getByIDsAccounts: []*Account{
|
||||
{ID: 1, Platform: PlatformOpenAI, Type: AccountTypeAPIKey},
|
||||
}}
|
||||
svc := &adminServiceImpl{accountRepo: repo}
|
||||
|
||||
_, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
|
||||
AccountIDs: []int64{1},
|
||||
Credentials: map[string]any{
|
||||
openAIEndpointCapabilitiesCredentialKey: []string{"embeddings"},
|
||||
},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"embeddings"}, repo.lastBulkUpdate.Credentials[openAIEndpointCapabilitiesCredentialKey])
|
||||
require.Contains(t, repo.lastBulkUpdate.Extra, "openai_responses_mode")
|
||||
require.Nil(t, repo.lastBulkUpdate.Extra["openai_responses_mode"])
|
||||
}
|
||||
|
||||
func TestAdminServiceBulkUpdateAccounts_RejectsInvalidOpenAISettingValuesBeforeWrite(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
credentials map[string]any
|
||||
extra map[string]any
|
||||
reason string
|
||||
}{
|
||||
{name: "long context type", extra: map[string]any{openAILongContextBillingEnabledKey: "true"}, reason: "OPENAI_LONG_CONTEXT_BILLING_INVALID"},
|
||||
{name: "empty capabilities", credentials: map[string]any{openAIEndpointCapabilitiesCredentialKey: []any{}}, reason: "OPENAI_ENDPOINT_CAPABILITIES_INVALID"},
|
||||
{name: "unknown capability", credentials: map[string]any{openAIEndpointCapabilitiesCredentialKey: []any{"responses"}}, reason: "OPENAI_ENDPOINT_CAPABILITIES_INVALID"},
|
||||
{name: "capabilities type", credentials: map[string]any{openAIEndpointCapabilitiesCredentialKey: "chat_completions"}, reason: "OPENAI_ENDPOINT_CAPABILITIES_INVALID"},
|
||||
{name: "responses mode", extra: map[string]any{"openai_responses_mode": "sometimes"}, reason: "OPENAI_RESPONSES_MODE_INVALID"},
|
||||
{name: "responses type", extra: map[string]any{"openai_responses_mode": true}, reason: "OPENAI_RESPONSES_MODE_INVALID"},
|
||||
{
|
||||
name: "embeddings conflict",
|
||||
credentials: map[string]any{openAIEndpointCapabilitiesCredentialKey: []any{"embeddings"}},
|
||||
extra: map[string]any{"openai_responses_mode": "force_responses"},
|
||||
reason: "OPENAI_RESPONSES_MODE_INVALID",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
repo := &accountRepoStubForBulkUpdate{}
|
||||
svc := &adminServiceImpl{accountRepo: repo}
|
||||
result, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
|
||||
AccountIDs: []int64{1},
|
||||
Credentials: tt.credentials,
|
||||
Extra: tt.extra,
|
||||
})
|
||||
require.Nil(t, result)
|
||||
requireApplicationErrorReason(t, err, tt.reason)
|
||||
require.Zero(t, repo.bulkUpdateCalls)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminServiceBulkUpdateAccounts_RejectsInvalidOpenAITargetsBeforeWrite(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
accounts []*Account
|
||||
input *BulkUpdateAccountsInput
|
||||
}{
|
||||
{
|
||||
name: "missing account",
|
||||
accounts: []*Account{{ID: 1, Platform: PlatformOpenAI, Type: AccountTypeOAuth}},
|
||||
input: &BulkUpdateAccountsInput{
|
||||
AccountIDs: []int64{1, 2},
|
||||
Extra: map[string]any{openAILongContextBillingEnabledKey: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed platform long context",
|
||||
accounts: []*Account{{ID: 1, Platform: PlatformAnthropic, Type: AccountTypeOAuth}},
|
||||
input: &BulkUpdateAccountsInput{
|
||||
AccountIDs: []int64{1},
|
||||
Extra: map[string]any{openAILongContextBillingEnabledKey: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "oauth endpoint capabilities",
|
||||
accounts: []*Account{{ID: 1, Platform: PlatformOpenAI, Type: AccountTypeOAuth}},
|
||||
input: &BulkUpdateAccountsInput{
|
||||
AccountIDs: []int64{1},
|
||||
Credentials: map[string]any{openAIEndpointCapabilitiesCredentialKey: nil},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unsupported OpenAI long context account type",
|
||||
accounts: []*Account{{ID: 1, Platform: PlatformOpenAI, Type: AccountTypeServiceAccount}},
|
||||
input: &BulkUpdateAccountsInput{
|
||||
AccountIDs: []int64{1},
|
||||
Extra: map[string]any{openAILongContextBillingEnabledKey: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
repo := &accountRepoStubForBulkUpdate{getByIDsAccounts: tt.accounts}
|
||||
svc := &adminServiceImpl{accountRepo: repo}
|
||||
result, err := svc.BulkUpdateAccounts(context.Background(), tt.input)
|
||||
require.Nil(t, result)
|
||||
requireApplicationErrorReason(t, err, "OPENAI_BULK_TARGET_INVALID")
|
||||
require.Zero(t, repo.bulkUpdateCalls)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminServiceBulkUpdateAccounts_ForcedResponsesRequiresChatCapability(t *testing.T) {
|
||||
repo := &accountRepoStubForBulkUpdate{getByIDsAccounts: []*Account{{
|
||||
ID: 1,
|
||||
Platform: PlatformOpenAI,
|
||||
Type: AccountTypeAPIKey,
|
||||
Credentials: map[string]any{
|
||||
openAIEndpointCapabilitiesCredentialKey: []any{"embeddings"},
|
||||
},
|
||||
}}}
|
||||
svc := &adminServiceImpl{accountRepo: repo}
|
||||
|
||||
result, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
|
||||
AccountIDs: []int64{1},
|
||||
Extra: map[string]any{"openai_responses_mode": "force_chat_completions"},
|
||||
})
|
||||
|
||||
require.Nil(t, result)
|
||||
requireApplicationErrorReason(t, err, "OPENAI_BULK_TARGET_INVALID")
|
||||
require.Zero(t, repo.bulkUpdateCalls)
|
||||
}
|
||||
|
||||
func TestAdminServiceBulkUpdateAccounts_ForcedResponsesAcceptsChatCapabilityUpdate(t *testing.T) {
|
||||
repo := &accountRepoStubForBulkUpdate{getByIDsAccounts: []*Account{{
|
||||
ID: 1,
|
||||
Platform: PlatformOpenAI,
|
||||
Type: AccountTypeAPIKey,
|
||||
Credentials: map[string]any{
|
||||
openAIEndpointCapabilitiesCredentialKey: []any{"embeddings"},
|
||||
},
|
||||
}}}
|
||||
svc := &adminServiceImpl{accountRepo: repo}
|
||||
|
||||
_, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
|
||||
AccountIDs: []int64{1},
|
||||
Credentials: map[string]any{
|
||||
openAIEndpointCapabilitiesCredentialKey: []any{"chat_completions"},
|
||||
},
|
||||
Extra: map[string]any{"openai_responses_mode": "force_responses"},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, repo.bulkUpdateCalls)
|
||||
}
|
||||
|
||||
func TestAdminServiceBulkUpdateAccounts_ReportsLongContextShadowInheritance(t *testing.T) {
|
||||
parentID := int64(1)
|
||||
repo := &accountRepoStubForBulkUpdate{getByIDsAccounts: []*Account{
|
||||
{ID: parentID, Platform: PlatformOpenAI, Type: AccountTypeOAuth},
|
||||
{ID: 2, Platform: PlatformOpenAI, Type: AccountTypeOAuth, ParentAccountID: &parentID},
|
||||
}}
|
||||
svc := &adminServiceImpl{accountRepo: repo}
|
||||
|
||||
result, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
|
||||
AccountIDs: []int64{parentID, 2},
|
||||
Extra: map[string]any{openAILongContextBillingEnabledKey: true},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, result.LongContextInheritedCount)
|
||||
require.Equal(t, 1, repo.bulkUpdateCalls)
|
||||
}
|
||||
|
||||
func TestAdminServiceBulkUpdateAccounts_RequiresParentForShadowOnlyLongContextUpdate(t *testing.T) {
|
||||
parentID := int64(10)
|
||||
repo := &accountRepoStubForBulkUpdate{getByIDsAccounts: []*Account{
|
||||
{ID: 1, Platform: PlatformOpenAI, Type: AccountTypeOAuth, ParentAccountID: &parentID},
|
||||
{ID: 2, Platform: PlatformOpenAI, Type: AccountTypeOAuth, ParentAccountID: &parentID},
|
||||
}}
|
||||
svc := &adminServiceImpl{accountRepo: repo}
|
||||
|
||||
result, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
|
||||
AccountIDs: []int64{1, 2},
|
||||
Extra: map[string]any{openAILongContextBillingEnabledKey: true},
|
||||
})
|
||||
|
||||
require.Nil(t, result)
|
||||
requireApplicationErrorReason(t, err, "OPENAI_LONG_CONTEXT_PARENT_REQUIRED")
|
||||
require.Zero(t, repo.bulkUpdateCalls)
|
||||
}
|
||||
|
||||
func TestAdminServiceBulkUpdateAccounts_ShadowLongContextAllowsOtherUpdates(t *testing.T) {
|
||||
parentID := int64(10)
|
||||
repo := &accountRepoStubForBulkUpdate{getByIDsAccounts: []*Account{{
|
||||
ID: 1, Platform: PlatformOpenAI, Type: AccountTypeOAuth, ParentAccountID: &parentID,
|
||||
}}}
|
||||
svc := &adminServiceImpl{accountRepo: repo}
|
||||
status := StatusDisabled
|
||||
|
||||
result, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
|
||||
AccountIDs: []int64{1},
|
||||
Status: status,
|
||||
Extra: map[string]any{openAILongContextBillingEnabledKey: false},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, result.LongContextInheritedCount)
|
||||
require.Equal(t, 1, repo.bulkUpdateCalls)
|
||||
require.NotNil(t, repo.lastBulkUpdate.Status)
|
||||
require.Equal(t, status, *repo.lastBulkUpdate.Status)
|
||||
}
|
||||
|
||||
func TestAdminServiceBulkUpdateAccounts_ValidatesFilterResolvedOpenAITargets(t *testing.T) {
|
||||
repo := &accountRepoStubForBulkUpdate{
|
||||
listData: []Account{{ID: 7}},
|
||||
listResult: &pagination.PaginationResult{Total: 1},
|
||||
getByIDsAccounts: []*Account{{ID: 7, Platform: PlatformAnthropic, Type: AccountTypeOAuth}},
|
||||
}
|
||||
svc := &adminServiceImpl{accountRepo: repo}
|
||||
|
||||
result, err := svc.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
|
||||
Filters: &BulkUpdateAccountFilters{Platform: PlatformOpenAI},
|
||||
Extra: map[string]any{openAILongContextBillingEnabledKey: true},
|
||||
})
|
||||
|
||||
require.Nil(t, result)
|
||||
requireApplicationErrorReason(t, err, "OPENAI_BULK_TARGET_INVALID")
|
||||
require.Equal(t, []int64{7}, repo.getByIDsIDs)
|
||||
require.Zero(t, repo.bulkUpdateCalls)
|
||||
}
|
||||
Reference in New Issue
Block a user