| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407 |
- package admin
- import (
- "bytes"
- "compress/gzip"
- "encoding/json"
- "net/http"
- "net/http/httptest"
- "strconv"
- "strings"
- "testing"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- )
- func TestAccountHandlerListLiteUsesCompactDTOAndETag(t *testing.T) {
- router, adminSvc := setupAccountListRouter()
- now := time.Now().UTC()
- groupID := int64(77)
- adminSvc.accounts = []service.Account{{
- ID: 501, Name: "compact-account", Platform: service.PlatformOpenAI, Type: service.AccountTypeOAuth,
- Credentials: map[string]any{"email": "compact@example.com", "access_token": strings.Repeat("x", 4096)},
- Extra: map[string]any{"privacy_mode": "training_off"}, Status: service.StatusActive,
- Schedulable: true, Concurrency: 4, GroupIDs: []int64{groupID},
- Groups: []*service.Group{{ID: groupID, Name: "codex", Platform: service.PlatformOpenAI}},
- AccountGroups: []service.AccountGroup{{AccountID: 501, GroupID: groupID, Priority: 2, Group: &service.Group{ID: groupID, Name: "codex", Platform: service.PlatformOpenAI}}},
- CreatedAt: now, UpdatedAt: now,
- }}
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=20&lite=1", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- require.NotEmpty(t, rec.Header().Get("ETag"))
- var litePayload struct {
- Data struct {
- Items []map[string]any `json:"items"`
- } `json:"data"`
- }
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &litePayload))
- require.Len(t, litePayload.Data.Items, 1)
- liteItem := litePayload.Data.Items[0]
- require.Equal(t, float64(501), liteItem["id"])
- require.Equal(t, []any{float64(groupID)}, liteItem["group_ids"])
- require.Equal(t, true, liteItem["schedulable"])
- require.NotContains(t, liteItem, "groups")
- require.NotContains(t, liteItem, "account_groups")
- credentials, ok := liteItem["credentials"].(map[string]any)
- require.True(t, ok)
- require.Equal(t, "compact@example.com", credentials["email"])
- require.NotContains(t, credentials, "access_token")
- credentialsStatus, ok := liteItem["credentials_status"].(map[string]any)
- require.True(t, ok)
- require.Equal(t, true, credentialsStatus["has_access_token"])
- // The ETag must represent the same compact body and return 304 on refresh.
- rec304 := httptest.NewRecorder()
- req304 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=20&lite=1", nil)
- req304.Header.Set("If-None-Match", rec.Header().Get("ETag"))
- router.ServeHTTP(rec304, req304)
- require.Equal(t, http.StatusNotModified, rec304.Code)
- // Omitting lite preserves the legacy full response shape.
- recFull := httptest.NewRecorder()
- reqFull := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=20", nil)
- router.ServeHTTP(recFull, reqFull)
- require.Equal(t, http.StatusOK, recFull.Code)
- var fullPayload struct {
- Data struct {
- Items []map[string]any `json:"items"`
- } `json:"data"`
- }
- require.NoError(t, json.Unmarshal(recFull.Body.Bytes(), &fullPayload))
- require.Contains(t, fullPayload.Data.Items[0], "groups")
- require.Contains(t, fullPayload.Data.Items[0], "account_groups")
- }
- func TestAccountHandlerListLiteStaysBelowResponseBudget(t *testing.T) {
- router, adminSvc := setupAccountListRouter()
- now := time.Now().UTC()
- accounts := make([]service.Account, 20)
- for i := range accounts {
- id := int64(600 + i)
- groupID := int64(800 + i)
- accounts[i] = service.Account{
- ID: id, Name: "account-" + strconv.Itoa(i), Platform: service.PlatformOpenAI,
- Type: service.AccountTypeOAuth, Status: service.StatusActive, Schedulable: true,
- Concurrency: 4, GroupIDs: []int64{groupID},
- Groups: []*service.Group{{ID: groupID, Name: "group-" + strconv.Itoa(i), Description: strings.Repeat("description ", 20)}},
- AccountGroups: []service.AccountGroup{{AccountID: id, GroupID: groupID, Group: &service.Group{ID: groupID, Name: "group-" + strconv.Itoa(i), Description: strings.Repeat("description ", 20)}}},
- CreatedAt: now, UpdatedAt: now,
- }
- }
- adminSvc.accounts = accounts
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=20&lite=1", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- require.Less(t, rec.Body.Len(), 80*1024)
- var compressed bytes.Buffer
- zw := gzip.NewWriter(&compressed)
- _, err := zw.Write(rec.Body.Bytes())
- require.NoError(t, err)
- require.NoError(t, zw.Close())
- require.Less(t, compressed.Len(), 15*1024)
- }
- func setupAccountListRouter() (*gin.Engine, *stubAdminService) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- adminSvc := newStubAdminService()
- handler := NewAccountHandler(adminSvc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
- router.GET("/api/v1/admin/accounts", handler.List)
- return router, adminSvc
- }
- func TestAccountHandlerListIncludesCreatedAt(t *testing.T) {
- router, adminSvc := setupAccountListRouter()
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=20&sort_by=created_at&sort_order=desc", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- require.Equal(t, "created_at", adminSvc.lastListAccounts.sortBy)
- var payload struct {
- Data struct {
- Items []struct {
- ID int64 `json:"id"`
- CreatedAt string `json:"created_at"`
- } `json:"items"`
- } `json:"data"`
- }
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
- require.Len(t, payload.Data.Items, 1)
- createdAt := payload.Data.Items[0].CreatedAt
- require.NotEmpty(t, createdAt)
- require.True(t, strings.HasSuffix(createdAt, "Z"), "created_at should be serialized as UTC")
- parsed, err := time.Parse(time.RFC3339Nano, createdAt)
- require.NoError(t, err)
- _, offset := parsed.Zone()
- require.Equal(t, 0, offset)
- }
- func TestAccountHandlerListReturnsSchedulerScoresPerGroup(t *testing.T) {
- router, adminSvc := setupAccountListRouter()
- now := time.Now().UTC()
- groupID := int64(41)
- adminSvc.accounts = []service.Account{
- {
- ID: 101,
- Name: "account-high-priority",
- Platform: service.PlatformOpenAI,
- Type: service.AccountTypeAPIKey,
- Status: service.StatusActive,
- Schedulable: true,
- Concurrency: 10,
- Priority: 1,
- AccountGroups: []service.AccountGroup{
- {AccountID: 101, GroupID: groupID, Priority: 100, Group: &service.Group{ID: groupID, Name: "openai"}},
- },
- GroupIDs: []int64{groupID},
- CreatedAt: now,
- UpdatedAt: now,
- },
- {
- ID: 102,
- Name: "account-low-priority",
- Platform: service.PlatformOpenAI,
- Type: service.AccountTypeAPIKey,
- Status: service.StatusActive,
- Schedulable: true,
- Concurrency: 10,
- Priority: 100000,
- AccountGroups: []service.AccountGroup{
- {AccountID: 102, GroupID: groupID, Priority: 1, Group: &service.Group{ID: groupID, Name: "openai"}},
- },
- GroupIDs: []int64{groupID},
- CreatedAt: now,
- UpdatedAt: now,
- },
- }
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=20&platform=openai&include_scheduler_score=1", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- var payload struct {
- Data struct {
- Items []struct {
- ID int64 `json:"id"`
- SchedulerScore struct {
- BaseScore float64 `json:"base_score"`
- } `json:"scheduler_score"`
- SchedulerScores []struct {
- GroupID *int64 `json:"group_id"`
- GroupName string `json:"group_name"`
- GroupPriority *int `json:"group_priority"`
- BaseScore float64 `json:"base_score"`
- } `json:"scheduler_scores"`
- } `json:"items"`
- } `json:"data"`
- }
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
- require.Len(t, payload.Data.Items, 2)
- var high, low *struct {
- ID int64 `json:"id"`
- SchedulerScore struct {
- BaseScore float64 `json:"base_score"`
- } `json:"scheduler_score"`
- SchedulerScores []struct {
- GroupID *int64 `json:"group_id"`
- GroupName string `json:"group_name"`
- GroupPriority *int `json:"group_priority"`
- BaseScore float64 `json:"base_score"`
- } `json:"scheduler_scores"`
- }
- for i := range payload.Data.Items {
- item := &payload.Data.Items[i]
- switch item.ID {
- case 101:
- high = item
- case 102:
- low = item
- }
- }
- require.NotNil(t, high)
- require.NotNil(t, low)
- require.Len(t, high.SchedulerScores, 1)
- require.Len(t, low.SchedulerScores, 1)
- require.Equal(t, groupID, *high.SchedulerScores[0].GroupID)
- require.Equal(t, "openai", high.SchedulerScores[0].GroupName)
- require.Equal(t, 100, *high.SchedulerScores[0].GroupPriority)
- require.Equal(t, 1, *low.SchedulerScores[0].GroupPriority)
- require.Greater(t, high.SchedulerScores[0].BaseScore, low.SchedulerScores[0].BaseScore)
- }
- func TestAccountHandlerListSkipsSchedulerScoresByDefault(t *testing.T) {
- router, adminSvc := setupAccountListRouter()
- now := time.Now().UTC()
- adminSvc.accounts = []service.Account{
- {
- ID: 110,
- Name: "openai-account",
- Platform: service.PlatformOpenAI,
- Type: service.AccountTypeAPIKey,
- Status: service.StatusActive,
- Schedulable: true,
- Concurrency: 10,
- Priority: 1,
- CreatedAt: now,
- UpdatedAt: now,
- },
- }
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=20&platform=openai", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- require.Zero(t, adminSvc.schedulerScoreFilterCalls)
- require.Zero(t, adminSvc.openAISchedulerScorePoolCalls)
- var payload struct {
- Data struct {
- Items []map[string]any `json:"items"`
- } `json:"data"`
- }
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
- require.Len(t, payload.Data.Items, 1)
- require.NotContains(t, payload.Data.Items[0], "scheduler_score")
- require.NotContains(t, payload.Data.Items[0], "scheduler_scores")
- }
- func TestAccountHandlerListKeepsSchedulerScoreScopedToFilter(t *testing.T) {
- router, adminSvc := setupAccountListRouter()
- now := time.Now().UTC()
- groupID := int64(42)
- visibleAccount := service.Account{
- ID: 201,
- Name: "visible-low-priority",
- Platform: service.PlatformOpenAI,
- Type: service.AccountTypeAPIKey,
- Status: service.StatusActive,
- Schedulable: true,
- Concurrency: 10,
- Priority: 100000,
- AccountGroups: []service.AccountGroup{
- {AccountID: 201, GroupID: groupID, Priority: 1, Group: &service.Group{ID: groupID, Name: "openai"}},
- },
- GroupIDs: []int64{groupID},
- CreatedAt: now,
- UpdatedAt: now,
- }
- hiddenGroupPeer := service.Account{
- ID: 202,
- Name: "hidden-high-priority",
- Platform: service.PlatformOpenAI,
- Type: service.AccountTypeAPIKey,
- Status: service.StatusActive,
- Schedulable: true,
- Concurrency: 10,
- Priority: 1,
- AccountGroups: []service.AccountGroup{
- {AccountID: 202, GroupID: groupID, Priority: 2, Group: &service.Group{ID: groupID, Name: "openai"}},
- },
- GroupIDs: []int64{groupID},
- CreatedAt: now,
- UpdatedAt: now,
- }
- adminSvc.accounts = []service.Account{visibleAccount}
- adminSvc.accountSchedulerScoreFilterAccounts = []service.Account{visibleAccount, hiddenGroupPeer}
- adminSvc.openAISchedulerScorePoolAccounts = []service.Account{visibleAccount, hiddenGroupPeer}
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=1&platform=openai&include_scheduler_score=1", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- var payload struct {
- Data struct {
- Items []struct {
- ID int64 `json:"id"`
- SchedulerScore struct {
- BaseScore float64 `json:"base_score"`
- } `json:"scheduler_score"`
- SchedulerScores []struct {
- GroupID *int64 `json:"group_id"`
- BaseScore float64 `json:"base_score"`
- } `json:"scheduler_scores"`
- } `json:"items"`
- } `json:"data"`
- }
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
- require.Len(t, payload.Data.Items, 1)
- item := payload.Data.Items[0]
- require.Equal(t, int64(201), item.ID)
- require.Len(t, item.SchedulerScores, 1)
- require.Equal(t, groupID, *item.SchedulerScores[0].GroupID)
- require.Equal(t, item.SchedulerScores[0].BaseScore, item.SchedulerScore.BaseScore)
- }
- func TestAccountHandlerListSchedulerScoreIgnoresPagination(t *testing.T) {
- router, adminSvc := setupAccountListRouter()
- now := time.Now().UTC()
- visibleAccount := service.Account{
- ID: 301,
- Name: "visible-low-priority",
- Platform: service.PlatformOpenAI,
- Type: service.AccountTypeAPIKey,
- Status: service.StatusActive,
- Schedulable: true,
- Concurrency: 10,
- Priority: 100000,
- CreatedAt: now,
- UpdatedAt: now,
- }
- hiddenFilterPeer := service.Account{
- ID: 302,
- Name: "hidden-high-priority",
- Platform: service.PlatformOpenAI,
- Type: service.AccountTypeAPIKey,
- Status: service.StatusActive,
- Schedulable: true,
- Concurrency: 10,
- Priority: 1,
- CreatedAt: now,
- UpdatedAt: now,
- }
- adminSvc.accounts = []service.Account{visibleAccount}
- adminSvc.accountSchedulerScoreFilterAccounts = []service.Account{visibleAccount, hiddenFilterPeer}
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=1&platform=openai&include_scheduler_score=1", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- var payload struct {
- Data struct {
- Items []struct {
- ID int64 `json:"id"`
- SchedulerScore struct {
- BaseScore float64 `json:"base_score"`
- } `json:"scheduler_score"`
- SchedulerScores []struct {
- GroupID *int64 `json:"group_id"`
- BaseScore float64 `json:"base_score"`
- } `json:"scheduler_scores"`
- } `json:"items"`
- } `json:"data"`
- }
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
- require.Len(t, payload.Data.Items, 1)
- require.Equal(t, int64(301), payload.Data.Items[0].ID)
- require.Less(t, payload.Data.Items[0].SchedulerScore.BaseScore, 3.75)
- require.Empty(t, payload.Data.Items[0].SchedulerScores)
- }
|