Files
sub2api/backend/internal/handler/admin/account_opencode_go_usage_test.go
T

207 lines
7.8 KiB
Go
Raw Normal View History

package admin
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
type openCodeGoUsageHandlerTestRepo struct {
service.AccountRepository
account *service.Account
accounts []*service.Account
}
func (r *openCodeGoUsageHandlerTestRepo) GetByID(_ context.Context, id int64) (*service.Account, error) {
if r.account != nil && r.account.ID == id {
return r.account, nil
}
for _, account := range r.accounts {
if account.ID == id {
return account, nil
}
}
return nil, service.ErrAccountNotFound
}
func (r *openCodeGoUsageHandlerTestRepo) ListOpenCodeGoUsageGroupAccounts(_ context.Context, anchors []*service.Account) ([]service.Account, error) {
wanted := make(map[string]struct{}, len(anchors))
for _, anchor := range anchors {
if apiKey, ok := openCodeGoUsageHandlerTestAPIKey(anchor); ok {
wanted[apiKey] = struct{}{}
}
}
result := make([]service.Account, 0, len(r.accounts)+1)
if r.account != nil {
if apiKey, ok := openCodeGoUsageHandlerTestAPIKey(r.account); ok {
if _, match := wanted[apiKey]; match {
result = append(result, *r.account)
}
}
}
for _, account := range r.accounts {
if apiKey, ok := openCodeGoUsageHandlerTestAPIKey(account); ok {
if _, match := wanted[apiKey]; match {
result = append(result, *account)
}
}
}
return result, nil
}
func openCodeGoUsageHandlerTestAPIKey(account *service.Account) (string, bool) {
if account == nil || account.Credentials == nil {
return "", false
}
apiKey, ok := account.Credentials["api_key"].(string)
return apiKey, ok && apiKey != ""
}
func (r *openCodeGoUsageHandlerTestRepo) SetOpenCodeGoUsageAutoRefresh(context.Context, *service.Account, bool) error {
return nil
}
func (r *openCodeGoUsageHandlerTestRepo) UpdateOpenCodeGoUsageSnapshot(context.Context, *service.Account, *service.OpenCodeGoUsageSnapshot) error {
return nil
}
func (r *openCodeGoUsageHandlerTestRepo) DisableOpenCodeGoUsageAutoRefresh(context.Context, *service.Account) error {
return nil
}
func (r *openCodeGoUsageHandlerTestRepo) ListDueOpenCodeGoUsageAccounts(context.Context, time.Time, time.Duration, time.Duration, int) ([]service.Account, error) {
return nil, nil
}
func newOpenCodeGoUsageHandlerTestService(t *testing.T) *service.OpenCodeGoUsageService {
t.Helper()
svc := service.NewOpenCodeGoUsageService(nil, nil, nil)
t.Cleanup(svc.Stop)
return svc
}
func newOpenCodeGoUsageHandlerContext(method, target, body, id string) (*gin.Context, *httptest.ResponseRecorder) {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(method, target, bytes.NewBufferString(body))
request.Header.Set("Content-Type", "application/json")
ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = request
if id != "" {
ctx.Params = gin.Params{{Key: "id", Value: id}}
}
return ctx, recorder
}
func TestOpenCodeGoUsageHandlersValidateRequestsAndDependencies(t *testing.T) {
gin.SetMode(gin.TestMode)
svc := newOpenCodeGoUsageHandlerTestService(t)
t.Run("invalid account id", func(t *testing.T) {
ctx, recorder := newOpenCodeGoUsageHandlerContext(http.MethodGet, "/admin/accounts/not-an-id/opencode-go-usage", "", "not-an-id")
(&AccountHandler{opencodeGoUsage: svc}).GetOpenCodeGoUsage(ctx)
require.Equal(t, http.StatusBadRequest, recorder.Code)
})
t.Run("missing enabled", func(t *testing.T) {
ctx, recorder := newOpenCodeGoUsageHandlerContext(http.MethodPut, "/admin/accounts/7/opencode-go-usage/auto-refresh", `{}`, "7")
(&AccountHandler{opencodeGoUsage: svc}).SetOpenCodeGoUsageAutoRefresh(ctx)
require.Equal(t, http.StatusBadRequest, recorder.Code)
})
t.Run("service unavailable", func(t *testing.T) {
ctx, recorder := newOpenCodeGoUsageHandlerContext(http.MethodGet, "/admin/accounts/7/opencode-go-usage", "", "7")
(&AccountHandler{}).GetOpenCodeGoUsage(ctx)
require.Equal(t, http.StatusServiceUnavailable, recorder.Code)
require.Contains(t, recorder.Body.String(), "OPENCODE_GO_USAGE_UNAVAILABLE")
})
}
func TestGetOpenCodeGoUsageSettingsHandlerSuccess(t *testing.T) {
gin.SetMode(gin.TestMode)
ctx, recorder := newOpenCodeGoUsageHandlerContext(http.MethodGet, "/admin/accounts/opencode-go-usage/settings", "", "")
handler := &AccountHandler{opencodeGoUsage: newOpenCodeGoUsageHandlerTestService(t)}
handler.GetOpenCodeGoUsageSettings(ctx)
require.Equal(t, http.StatusOK, recorder.Code)
require.Contains(t, recorder.Body.String(), `"enabled":false`)
require.Contains(t, recorder.Body.String(), `"interval_minutes":15`)
}
func TestOpenCodeGoUsageStateEmbeddedInListAndDetail(t *testing.T) {
gin.SetMode(gin.TestMode)
now := time.Now().UTC()
account := &service.Account{
ID: 7, Name: "opencode", Platform: service.PlatformOpenAI, Type: service.AccountTypeAPIKey,
Credentials: map[string]any{"base_url": "https://opencode.ai/zen/go/v1", "api_key": "test-key"},
Extra: map[string]any{
service.OpenCodeGoUsageAutoRefreshExtraKey: true,
service.OpenCodeGoUsageSnapshotExtraKey: &service.OpenCodeGoUsageSnapshot{
Status: service.OpenCodeGoUsageStatusOK, Data: &service.OpenCodeGoUsageData{
Rolling: service.OpenCodeGoUsageWindow{Status: "ok", Percent: 6},
},
LastAttemptAt: now, NextRefreshAt: now.Add(time.Hour),
},
},
Status: service.StatusActive,
}
repo := &openCodeGoUsageHandlerTestRepo{account: account}
adminService := newStubAdminService()
adminService.accounts = []service.Account{*account}
adminService.getAccountResult = account
usageService := service.NewOpenCodeGoUsageService(repo, nil, nil)
t.Cleanup(usageService.Stop)
handler := NewAccountHandler(adminService, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
handler.SetOpenCodeGoUsageService(usageService)
router := gin.New()
router.GET("/accounts", handler.List)
router.GET("/accounts/:id", handler.GetByID)
router.GET("/accounts/:id/opencode-go-usage", handler.GetOpenCodeGoUsage)
listRecorder := httptest.NewRecorder()
router.ServeHTTP(listRecorder, httptest.NewRequest(http.MethodGet, "/accounts?page=1&page_size=20", nil))
require.Equal(t, http.StatusOK, listRecorder.Code)
var listPayload struct {
Data struct {
Items []struct {
OpenCodeGoUsage *service.OpenCodeGoUsageState `json:"opencode_go_usage"`
} `json:"items"`
} `json:"data"`
}
require.NoError(t, json.Unmarshal(listRecorder.Body.Bytes(), &listPayload))
require.Len(t, listPayload.Data.Items, 1)
require.NotNil(t, listPayload.Data.Items[0].OpenCodeGoUsage)
require.True(t, listPayload.Data.Items[0].OpenCodeGoUsage.AutoRefreshEnabled)
require.Equal(t, 6.0, listPayload.Data.Items[0].OpenCodeGoUsage.Snapshot.Data.Rolling.Percent)
detailRecorder := httptest.NewRecorder()
router.ServeHTTP(detailRecorder, httptest.NewRequest(http.MethodGet, "/accounts/7", nil))
require.Equal(t, http.StatusOK, detailRecorder.Code)
var detailPayload struct {
Data struct {
OpenCodeGoUsage *service.OpenCodeGoUsageState `json:"opencode_go_usage"`
} `json:"data"`
}
require.NoError(t, json.Unmarshal(detailRecorder.Body.Bytes(), &detailPayload))
require.NotNil(t, detailPayload.Data.OpenCodeGoUsage)
require.Equal(t, 6.0, detailPayload.Data.OpenCodeGoUsage.Snapshot.Data.Rolling.Percent)
stateRecorder := httptest.NewRecorder()
router.ServeHTTP(stateRecorder, httptest.NewRequest(http.MethodGet, "/accounts/7/opencode-go-usage", nil))
require.Equal(t, http.StatusOK, stateRecorder.Code)
var statePayload struct {
Data service.OpenCodeGoUsageState `json:"data"`
}
require.NoError(t, json.Unmarshal(stateRecorder.Body.Bytes(), &statePayload))
require.Equal(t, statePayload.Data.Snapshot, detailPayload.Data.OpenCodeGoUsage.Snapshot)
for _, body := range []string{listRecorder.Body.String(), detailRecorder.Body.String(), stateRecorder.Body.String()} {
require.NotContains(t, body, "test-key")
}
}