| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240 |
- package admin
- import (
- "bytes"
- "context"
- "encoding/json"
- "net/http"
- "net/http/httptest"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/config"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- )
- type simpleModeAccountService struct {
- *stubAdminService
- account service.Account
- createCalls int
- updateCalls int
- bulkCalls int
- }
- func (s *simpleModeAccountService) GetAccount(context.Context, int64) (*service.Account, error) {
- return &s.account, nil
- }
- func (s *simpleModeAccountService) CreateAccount(ctx context.Context, input *service.CreateAccountInput) (*service.Account, error) {
- if err := s.ValidateAccountGroupBindings(ctx, input.GroupIDs); err != nil {
- return nil, err
- }
- s.createCalls++
- return &s.account, nil
- }
- func (s *simpleModeAccountService) UpdateAccount(ctx context.Context, _ int64, input *service.UpdateAccountInput) (*service.Account, error) {
- if input.GroupIDs != nil {
- if err := s.ValidateAccountGroupBindings(ctx, *input.GroupIDs); err != nil {
- return nil, err
- }
- }
- s.updateCalls++
- return &s.account, nil
- }
- func (s *simpleModeAccountService) BulkUpdateAccounts(ctx context.Context, input *service.BulkUpdateAccountsInput) (*service.BulkUpdateAccountsResult, error) {
- if input.GroupIDs != nil {
- if err := s.ValidateAccountGroupBindings(ctx, *input.GroupIDs); err != nil {
- return nil, err
- }
- }
- s.bulkCalls++
- return &service.BulkUpdateAccountsResult{}, nil
- }
- func (s *simpleModeAccountService) ValidateAccountGroupBindings(_ context.Context, groupIDs []int64) error {
- for _, id := range groupIDs {
- for i := range s.groups {
- if s.groups[i].ID == id && s.groups[i].Platform == service.PlatformComposite {
- return infraerrors.BadRequest("SIMPLE_MODE_GROUP_NOT_BINDABLE", "composite groups cannot be bound in simple mode")
- }
- }
- }
- return nil
- }
- func TestAccountHandlerSimpleModeUsesMinimalGroupReferences(t *testing.T) {
- gin.SetMode(gin.TestMode)
- richGroup := &service.Group{ID: 7, Name: "basic", Platform: service.PlatformAnthropic, Status: service.StatusActive, RateMultiplier: 9, IsExclusive: true, SubscriptionType: service.SubscriptionTypeSubscription, RPMLimit: 99}
- historicalComposite := &service.Group{ID: 9, Name: "historical composite", Platform: service.PlatformComposite, Status: service.StatusActive}
- account := service.Account{
- ID: 3, Name: "account", Platform: service.PlatformAnthropic, Type: service.AccountTypeAPIKey, Status: service.StatusActive,
- GroupIDs: []int64{7, 9},
- Groups: []*service.Group{richGroup, historicalComposite},
- AccountGroups: []service.AccountGroup{
- {AccountID: 3, GroupID: 7, Priority: 2, Group: richGroup, Account: &service.Account{ID: 3, Extra: map[string]any{"secret": true}}},
- {AccountID: 3, GroupID: 9, Priority: 3, Group: historicalComposite},
- },
- }
- svc := &simpleModeAccountService{stubAdminService: newStubAdminService(), account: account}
- svc.accounts = []service.Account{account}
- h := NewAccountHandler(svc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
- h.cfg = &config.Config{RunMode: config.RunModeSimple}
- r := gin.New()
- r.GET("/accounts", h.List)
- r.GET("/accounts/:id", h.GetByID)
- r.POST("/accounts", h.Create)
- r.PUT("/accounts/:id", h.Update)
- tests := []struct {
- method string
- path string
- body string
- }{
- {http.MethodGet, "/accounts", ""},
- {http.MethodGet, "/accounts/3", ""},
- {http.MethodPost, "/accounts", `{"name":"account","platform":"anthropic","type":"apikey","credentials":{"key":"x"}}`},
- {http.MethodPut, "/accounts/3", `{"name":"account"}`},
- }
- for _, tt := range tests {
- t.Run(tt.method+tt.path, func(t *testing.T) {
- req := httptest.NewRequest(tt.method, tt.path, bytes.NewBufferString(tt.body))
- req.Header.Set("Content-Type", "application/json")
- res := httptest.NewRecorder()
- r.ServeHTTP(res, req)
- require.Equal(t, http.StatusOK, res.Code, res.Body.String())
- var payload map[string]any
- require.NoError(t, json.Unmarshal(res.Body.Bytes(), &payload))
- data, ok := payload["data"].(map[string]any)
- require.True(t, ok)
- if items, ok := data["items"].([]any); ok {
- require.Len(t, items, 1)
- data, ok = items[0].(map[string]any)
- require.True(t, ok)
- }
- groups, ok := data["groups"].([]any)
- require.True(t, ok)
- require.Len(t, groups, 1)
- require.Equal(t, []any{float64(7)}, data["group_ids"])
- group, ok := groups[0].(map[string]any)
- require.True(t, ok)
- require.ElementsMatch(t, []string{"id", "name", "platform", "status"}, mapKeys(group))
- accountGroups, ok := data["account_groups"].([]any)
- require.True(t, ok)
- require.Len(t, accountGroups, 1)
- accountGroup, ok := accountGroups[0].(map[string]any)
- require.True(t, ok)
- require.ElementsMatch(t, []string{"account_id", "group_id", "priority", "created_at", "group"}, mapKeys(accountGroup))
- nestedGroup, ok := accountGroup["group"].(map[string]any)
- require.True(t, ok)
- require.ElementsMatch(t, []string{"id", "name", "platform", "status"}, mapKeys(nestedGroup))
- })
- }
- }
- func TestAccountHandlerSimpleModeLitePreservesCompactShapeAndETag(t *testing.T) {
- richGroup := &service.Group{ID: 7, Name: "basic", Platform: service.PlatformAnthropic, Status: service.StatusActive, RateMultiplier: 9}
- historicalComposite := &service.Group{ID: 9, Name: "historical composite", Platform: service.PlatformComposite, Status: service.StatusActive}
- account := service.Account{
- ID: 3, Name: "account", Platform: service.PlatformAnthropic, Type: service.AccountTypeAPIKey, Status: service.StatusActive,
- GroupIDs: []int64{7, 9}, Groups: []*service.Group{richGroup, historicalComposite},
- AccountGroups: []service.AccountGroup{{AccountID: 3, GroupID: 7, Group: richGroup}, {AccountID: 3, GroupID: 9, Group: historicalComposite}},
- }
- svc := &simpleModeAccountService{stubAdminService: newStubAdminService(), account: account}
- svc.accounts = []service.Account{account}
- h := NewAccountHandler(svc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
- h.cfg = &config.Config{RunMode: config.RunModeSimple}
- r := gin.New()
- r.GET("/accounts", h.List)
- res := httptest.NewRecorder()
- r.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/accounts?lite=1", nil))
- require.Equal(t, http.StatusOK, res.Code, res.Body.String())
- require.NotEmpty(t, res.Header().Get("ETag"))
- var payload struct {
- Data struct {
- Items []map[string]any `json:"items"`
- } `json:"data"`
- }
- require.NoError(t, json.Unmarshal(res.Body.Bytes(), &payload))
- require.Len(t, payload.Data.Items, 1)
- require.Equal(t, []any{float64(7)}, payload.Data.Items[0]["group_ids"])
- require.NotContains(t, payload.Data.Items[0], "groups")
- require.NotContains(t, payload.Data.Items[0], "account_groups")
- notModified := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/accounts?lite=1", nil)
- req.Header.Set("If-None-Match", res.Header().Get("ETag"))
- r.ServeHTTP(notModified, req)
- require.Equal(t, http.StatusNotModified, notModified.Code)
- }
- func TestAccountHandlerSimpleModeRejectsCompositeGroupBindingsBeforeWrites(t *testing.T) {
- gin.SetMode(gin.TestMode)
- for _, tt := range []struct{ name, method, path, body string }{
- {"create", http.MethodPost, "/accounts", `{"name":"account","platform":"anthropic","type":"apikey","credentials":{"key":"x"},"group_ids":[9]}`},
- {"update", http.MethodPut, "/accounts/3", `{"group_ids":[9]}`},
- {"bulk", http.MethodPost, "/accounts/bulk-update", `{"account_ids":[3],"group_ids":[9]}`},
- } {
- t.Run(tt.name, func(t *testing.T) {
- svc := &simpleModeAccountService{stubAdminService: newStubAdminService()}
- svc.groups = []service.Group{{ID: 7, Platform: service.PlatformAnthropic}, {ID: 9, Platform: service.PlatformComposite}}
- h := NewAccountHandler(svc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
- h.cfg = &config.Config{RunMode: config.RunModeSimple}
- r := gin.New()
- r.POST("/accounts", h.Create)
- r.PUT("/accounts/:id", h.Update)
- r.POST("/accounts/bulk-update", h.BulkUpdate)
- res := httptest.NewRecorder()
- req := httptest.NewRequest(tt.method, tt.path, bytes.NewBufferString(tt.body))
- req.Header.Set("Content-Type", "application/json")
- r.ServeHTTP(res, req)
- require.Equal(t, http.StatusBadRequest, res.Code, res.Body.String())
- require.Zero(t, svc.createCalls)
- require.Zero(t, svc.updateCalls)
- require.Zero(t, svc.bulkCalls)
- })
- }
- }
- func TestAccountHandlerSimpleModePreservesBasicGroupBinding(t *testing.T) {
- svc := &simpleModeAccountService{stubAdminService: newStubAdminService(), account: service.Account{ID: 3}}
- svc.groups = []service.Group{{ID: 7, Platform: service.PlatformAnthropic}}
- h := NewAccountHandler(svc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
- h.cfg = &config.Config{RunMode: config.RunModeSimple}
- r := gin.New()
- r.POST("/accounts", h.Create)
- res := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodPost, "/accounts", bytes.NewBufferString(`{"name":"account","platform":"anthropic","type":"apikey","credentials":{"key":"x"},"group_ids":[7]}`))
- req.Header.Set("Content-Type", "application/json")
- r.ServeHTTP(res, req)
- require.Equal(t, http.StatusOK, res.Code, res.Body.String())
- require.Equal(t, 1, svc.createCalls)
- }
- func TestAccountHandlerSimpleModeBatchPrevalidatesAllGroupsAtomically(t *testing.T) {
- svc := &simpleModeAccountService{stubAdminService: newStubAdminService()}
- svc.groups = []service.Group{{ID: 7, Platform: service.PlatformAnthropic}, {ID: 9, Platform: service.PlatformComposite}}
- h := NewAccountHandler(svc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
- h.cfg = &config.Config{RunMode: config.RunModeSimple}
- r := gin.New()
- r.POST("/accounts/batch", h.BatchCreate)
- res := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodPost, "/accounts/batch", bytes.NewBufferString(`{"accounts":[{"name":"first","platform":"anthropic","type":"apikey","credentials":{"key":"x"},"group_ids":[7]},{"name":"second","platform":"anthropic","type":"apikey","credentials":{"key":"y"},"group_ids":[9]}]}`))
- req.Header.Set("Content-Type", "application/json")
- r.ServeHTTP(res, req)
- require.Equal(t, http.StatusBadRequest, res.Code, res.Body.String())
- require.Zero(t, svc.createCalls)
- }
- func TestAccountHandlerAdvancedModeKeepsFullGroupReferences(t *testing.T) {
- group := &service.Group{ID: 7, Name: "advanced", RateMultiplier: 9, SubscriptionType: service.SubscriptionTypeSubscription}
- h := NewAccountHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
- raw, err := json.Marshal(h.buildAccountResponseWithRuntime(context.Background(), &service.Account{Groups: []*service.Group{group}}))
- require.NoError(t, err)
- require.Contains(t, string(raw), `"rate_multiplier":9`)
- require.Contains(t, string(raw), `"subscription_type":"subscription"`)
- }
|