mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
241 lines
11 KiB
Go
241 lines
11 KiB
Go
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"`)
|
|
}
|