| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- package admin
- import (
- "bytes"
- "encoding/json"
- "net/http"
- "net/http/httptest"
- "testing"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- )
- func setupAdminRouter() (*gin.Engine, *stubAdminService) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- adminSvc := newStubAdminService()
- userHandler := NewUserHandler(adminSvc, nil, nil, nil, nil, nil, nil)
- groupHandler := NewGroupHandler(adminSvc, nil, nil)
- proxyHandler := NewProxyHandler(adminSvc)
- redeemHandler := NewRedeemHandler(adminSvc, nil)
- router.GET("/api/v1/admin/users", userHandler.List)
- router.GET("/api/v1/admin/users/:id", userHandler.GetByID)
- router.POST("/api/v1/admin/users/:id/auth-identities", userHandler.BindAuthIdentity)
- router.POST("/api/v1/admin/users", userHandler.Create)
- router.PUT("/api/v1/admin/users/:id", userHandler.Update)
- router.DELETE("/api/v1/admin/users/:id", userHandler.Delete)
- router.POST("/api/v1/admin/users/:id/balance", userHandler.UpdateBalance)
- router.GET("/api/v1/admin/users/:id/api-keys", userHandler.GetUserAPIKeys)
- router.GET("/api/v1/admin/users/:id/usage", userHandler.GetUserUsage)
- router.GET("/api/v1/admin/groups", groupHandler.List)
- router.GET("/api/v1/admin/groups/all", groupHandler.GetAll)
- router.GET("/api/v1/admin/groups/:id/model-allowlist-candidates", groupHandler.GetGroupModelAllowlistCandidates)
- router.GET("/api/v1/admin/groups/:id/composite-routes", groupHandler.ListCompositeRoutes)
- router.POST("/api/v1/admin/groups/:id/composite-routes", groupHandler.CreateCompositeRoute)
- router.POST("/api/v1/admin/groups/:id/composite-routes/preview", groupHandler.PreviewCompositeRoute)
- router.PUT("/api/v1/admin/groups/:id/composite-routes/:route_id", groupHandler.UpdateCompositeRoute)
- router.DELETE("/api/v1/admin/groups/:id/composite-routes/:route_id", groupHandler.DeleteCompositeRoute)
- router.GET("/api/v1/admin/groups/:id", groupHandler.GetByID)
- router.POST("/api/v1/admin/groups", groupHandler.Create)
- router.PUT("/api/v1/admin/groups/:id", groupHandler.Update)
- router.DELETE("/api/v1/admin/groups/:id", groupHandler.Delete)
- router.GET("/api/v1/admin/groups/:id/stats", groupHandler.GetStats)
- router.GET("/api/v1/admin/groups/:id/api-keys", groupHandler.GetGroupAPIKeys)
- router.GET("/api/v1/admin/proxies", proxyHandler.List)
- router.GET("/api/v1/admin/proxies/all", proxyHandler.GetAll)
- router.GET("/api/v1/admin/proxies/:id", proxyHandler.GetByID)
- router.POST("/api/v1/admin/proxies", proxyHandler.Create)
- router.PUT("/api/v1/admin/proxies/:id", proxyHandler.Update)
- router.DELETE("/api/v1/admin/proxies/:id", proxyHandler.Delete)
- router.POST("/api/v1/admin/proxies/batch-delete", proxyHandler.BatchDelete)
- router.POST("/api/v1/admin/proxies/:id/test", proxyHandler.Test)
- router.POST("/api/v1/admin/proxies/:id/quality-check", proxyHandler.CheckQuality)
- router.GET("/api/v1/admin/proxies/:id/stats", proxyHandler.GetStats)
- router.GET("/api/v1/admin/proxies/:id/accounts", proxyHandler.GetProxyAccounts)
- router.GET("/api/v1/admin/redeem-codes", redeemHandler.List)
- router.GET("/api/v1/admin/redeem-codes/:id", redeemHandler.GetByID)
- router.POST("/api/v1/admin/redeem-codes", redeemHandler.Generate)
- router.DELETE("/api/v1/admin/redeem-codes/:id", redeemHandler.Delete)
- router.POST("/api/v1/admin/redeem-codes/batch-delete", redeemHandler.BatchDelete)
- router.POST("/api/v1/admin/redeem-codes/:id/expire", redeemHandler.Expire)
- router.GET("/api/v1/admin/redeem-codes/:id/stats", redeemHandler.GetStats)
- return router, adminSvc
- }
- func TestUserHandlerEndpoints(t *testing.T) {
- router, _ := setupAdminRouter()
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/users?page=1&page_size=20", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/users/1", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- bindBody := map[string]any{
- "provider_type": "wechat",
- "provider_key": "wechat-main",
- "provider_subject": "union-123",
- "metadata": map[string]any{"source": "admin-repair"},
- "channel": map[string]any{
- "channel": "open",
- "channel_app_id": "wx-open",
- "channel_subject": "openid-123",
- },
- }
- body, _ := json.Marshal(bindBody)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/users/1/auth-identities", bytes.NewReader(body))
- req.Header.Set("Content-Type", "application/json")
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- createBody := map[string]any{"email": "new@example.com", "password": "pass123", "balance": 1, "concurrency": 2}
- body, _ = json.Marshal(createBody)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/users", bytes.NewReader(body))
- req.Header.Set("Content-Type", "application/json")
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- updateBody := map[string]any{"email": "updated@example.com"}
- body, _ = json.Marshal(updateBody)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodPut, "/api/v1/admin/users/1", bytes.NewReader(body))
- req.Header.Set("Content-Type", "application/json")
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodDelete, "/api/v1/admin/users/1", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/users/1/balance", bytes.NewBufferString(`{"balance":1,"operation":"add"}`))
- req.Header.Set("Content-Type", "application/json")
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/users/1/api-keys", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/users/1/usage?period=today", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- }
- func TestUserHandlerBindAuthIdentityMapsRequest(t *testing.T) {
- router, adminSvc := setupAdminRouter()
- body, err := json.Marshal(map[string]any{
- "provider_type": "oidc",
- "provider_key": "https://issuer.example",
- "provider_subject": "subject-123",
- "issuer": "https://issuer.example",
- "metadata": map[string]any{"report_id": 12},
- })
- require.NoError(t, err)
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/users/9/auth-identities", bytes.NewReader(body))
- req.Header.Set("Content-Type", "application/json")
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- require.Equal(t, int64(9), adminSvc.boundAuthIdentityFor)
- require.NotNil(t, adminSvc.boundAuthIdentity)
- require.Equal(t, "oidc", adminSvc.boundAuthIdentity.ProviderType)
- require.Equal(t, "https://issuer.example", adminSvc.boundAuthIdentity.ProviderKey)
- require.Equal(t, "subject-123", adminSvc.boundAuthIdentity.ProviderSubject)
- require.Nil(t, adminSvc.boundAuthIdentity.Channel)
- require.Equal(t, float64(12), adminSvc.boundAuthIdentity.Metadata["report_id"])
- }
- func TestGroupHandlerEndpoints(t *testing.T) {
- router, _ := setupAdminRouter()
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups/all", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups/2", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups/0/model-allowlist-candidates?platform=openai", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- require.Contains(t, rec.Body.String(), "gpt-5.5")
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups/2/composite-routes", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- require.Contains(t, rec.Body.String(), "openrouter/gpt-5")
- body, _ := json.Marshal(map[string]any{
- "public_model": "openrouter/gpt-5",
- "match_type": "exact",
- "target_platform": "openai",
- "upstream_model": "gpt-5",
- "endpoint": "chat_completions",
- "enabled": true,
- })
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/groups/2/composite-routes", bytes.NewReader(body))
- req.Header.Set("Content-Type", "application/json")
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusCreated, rec.Code)
- require.Contains(t, rec.Body.String(), "gpt-5")
- body, _ = json.Marshal(map[string]any{
- "public_model": "openrouter/gpt-5",
- "target_platform": "openai",
- "upstream_model": "gpt-5",
- "endpoint": "responses",
- "enabled": true,
- })
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodPut, "/api/v1/admin/groups/2/composite-routes/1", bytes.NewReader(body))
- req.Header.Set("Content-Type", "application/json")
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- body, _ = json.Marshal(map[string]any{"model": "gpt-5", "endpoint": "responses"})
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/groups/2/composite-routes/preview", bytes.NewReader(body))
- req.Header.Set("Content-Type", "application/json")
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- require.Contains(t, rec.Body.String(), `"source":"detector"`)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodDelete, "/api/v1/admin/groups/2/composite-routes/1", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- body, _ = json.Marshal(map[string]any{"name": "new", "platform": "anthropic", "subscription_type": "standard"})
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/groups", bytes.NewReader(body))
- req.Header.Set("Content-Type", "application/json")
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- body, _ = json.Marshal(map[string]any{"name": "update"})
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodPut, "/api/v1/admin/groups/2", bytes.NewReader(body))
- req.Header.Set("Content-Type", "application/json")
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodDelete, "/api/v1/admin/groups/2", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups/2/stats", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusServiceUnavailable, rec.Code)
- require.Contains(t, rec.Body.String(), "GROUP_STATS_UNAVAILABLE")
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups/2/api-keys", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- }
- func TestProxyHandlerEndpoints(t *testing.T) {
- router, _ := setupAdminRouter()
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/proxies", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/proxies/all", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/proxies/4", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- body, _ := json.Marshal(map[string]any{"name": "proxy", "protocol": "http", "host": "localhost", "port": 8080})
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/proxies", bytes.NewReader(body))
- req.Header.Set("Content-Type", "application/json")
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- body, _ = json.Marshal(map[string]any{"name": "proxy2"})
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodPut, "/api/v1/admin/proxies/4", bytes.NewReader(body))
- req.Header.Set("Content-Type", "application/json")
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodDelete, "/api/v1/admin/proxies/4", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/proxies/batch-delete", bytes.NewBufferString(`{"ids":[1,2]}`))
- req.Header.Set("Content-Type", "application/json")
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/proxies/4/test", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/proxies/4/quality-check", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/proxies/4/stats", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/proxies/4/accounts", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- }
- func TestRedeemHandlerEndpoints(t *testing.T) {
- router, _ := setupAdminRouter()
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/redeem-codes", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/redeem-codes/5", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- body, _ := json.Marshal(map[string]any{"count": 1, "type": "balance", "value": 10})
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/redeem-codes", bytes.NewReader(body))
- req.Header.Set("Content-Type", "application/json")
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodDelete, "/api/v1/admin/redeem-codes/5", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/redeem-codes/batch-delete", bytes.NewBufferString(`{"ids":[1,2]}`))
- req.Header.Set("Content-Type", "application/json")
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/redeem-codes/5/expire", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- rec = httptest.NewRecorder()
- req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/redeem-codes/5/stats", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- }
|