| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319 |
- package admin
- import (
- "bytes"
- "encoding/json"
- "net/http"
- "net/http/httptest"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- )
- type dataResponse struct {
- Code int `json:"code"`
- Data dataPayload `json:"data"`
- }
- type dataPayload struct {
- Type string `json:"type"`
- Version int `json:"version"`
- Proxies []dataProxy `json:"proxies"`
- Accounts []dataAccount `json:"accounts"`
- SkippedShadows int `json:"skipped_shadows"`
- }
- type dataProxy struct {
- ProxyKey string `json:"proxy_key"`
- Name string `json:"name"`
- Protocol string `json:"protocol"`
- Host string `json:"host"`
- Port int `json:"port"`
- Username string `json:"username"`
- Password string `json:"password"`
- Status string `json:"status"`
- }
- type dataAccount struct {
- Name string `json:"name"`
- Platform string `json:"platform"`
- Type string `json:"type"`
- Credentials map[string]any `json:"credentials"`
- Extra map[string]any `json:"extra"`
- ProxyKey *string `json:"proxy_key"`
- Concurrency int `json:"concurrency"`
- Priority int `json:"priority"`
- }
- func setupAccountDataRouter() (*gin.Engine, *stubAdminService) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- adminSvc := newStubAdminService()
- h := NewAccountHandler(
- adminSvc,
- nil,
- nil,
- nil,
- nil,
- nil,
- nil,
- nil,
- nil,
- nil,
- nil,
- nil,
- nil,
- nil,
- )
- router.GET("/api/v1/admin/accounts/data", h.ExportData)
- router.POST("/api/v1/admin/accounts/data", h.ImportData)
- return router, adminSvc
- }
- func TestExportDataIncludesSecrets(t *testing.T) {
- router, adminSvc := setupAccountDataRouter()
- proxyID := int64(11)
- adminSvc.proxies = []service.Proxy{
- {
- ID: proxyID,
- Name: "proxy",
- Protocol: "http",
- Host: "127.0.0.1",
- Port: 8080,
- Username: "user",
- Password: "pass",
- Status: service.StatusActive,
- },
- {
- ID: 12,
- Name: "orphan",
- Protocol: "https",
- Host: "10.0.0.1",
- Port: 443,
- Username: "o",
- Password: "p",
- Status: service.StatusActive,
- },
- }
- adminSvc.accounts = []service.Account{
- {
- ID: 21,
- Name: "account",
- Platform: service.PlatformOpenAI,
- Type: service.AccountTypeOAuth,
- Credentials: map[string]any{"token": "secret"},
- Extra: map[string]any{"note": "x"},
- ProxyID: &proxyID,
- Concurrency: 3,
- Priority: 50,
- Status: service.StatusDisabled,
- },
- }
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts/data", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- var resp dataResponse
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
- require.Equal(t, 0, resp.Code)
- require.Empty(t, resp.Data.Type)
- require.Equal(t, 0, resp.Data.Version)
- require.Len(t, resp.Data.Proxies, 1)
- require.Equal(t, "pass", resp.Data.Proxies[0].Password)
- require.Len(t, resp.Data.Accounts, 1)
- require.Equal(t, "secret", resp.Data.Accounts[0].Credentials["token"])
- }
- func TestExportDataWithoutProxies(t *testing.T) {
- router, adminSvc := setupAccountDataRouter()
- proxyID := int64(11)
- adminSvc.proxies = []service.Proxy{
- {
- ID: proxyID,
- Name: "proxy",
- Protocol: "http",
- Host: "127.0.0.1",
- Port: 8080,
- Username: "user",
- Password: "pass",
- Status: service.StatusActive,
- },
- }
- adminSvc.accounts = []service.Account{
- {
- ID: 21,
- Name: "account",
- Platform: service.PlatformOpenAI,
- Type: service.AccountTypeOAuth,
- Credentials: map[string]any{"token": "secret"},
- ProxyID: &proxyID,
- Concurrency: 3,
- Priority: 50,
- Status: service.StatusDisabled,
- },
- }
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts/data?include_proxies=false", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- var resp dataResponse
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
- require.Equal(t, 0, resp.Code)
- require.Len(t, resp.Data.Proxies, 0)
- require.Len(t, resp.Data.Accounts, 1)
- require.Nil(t, resp.Data.Accounts[0].ProxyKey)
- }
- // TestExportDataExcludesSparkShadow 验证外审第5轮 P1/P2:导出时排除 spark 影子账号
- // (影子无凭据、导入侧强制 credentials 非空,混入会产出无法还原的坏备份),并透出跳过计数。
- func TestExportDataExcludesSparkShadow(t *testing.T) {
- router, adminSvc := setupAccountDataRouter()
- parentID := int64(21)
- adminSvc.accounts = []service.Account{
- {
- ID: parentID,
- Name: "mother",
- Platform: service.PlatformOpenAI,
- Type: service.AccountTypeOAuth,
- Credentials: map[string]any{"token": "secret"},
- Status: service.StatusActive,
- },
- {
- ID: 22,
- Name: "mother (Spark)",
- Platform: service.PlatformOpenAI,
- Type: service.AccountTypeOAuth,
- Credentials: map[string]any{}, // 影子恒空凭据
- ParentAccountID: &parentID, // 影子标记
- QuotaDimension: service.QuotaDimensionSpark,
- Status: service.StatusActive,
- },
- }
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts/data?include_proxies=false", nil)
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- var resp dataResponse
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
- require.Equal(t, 0, resp.Code)
- require.Len(t, resp.Data.Accounts, 1, "影子应被排除,仅导出母账号")
- require.Equal(t, "mother", resp.Data.Accounts[0].Name)
- require.Equal(t, 1, resp.Data.SkippedShadows, "跳过的影子数量应透出")
- }
- func TestExportDataPassesAccountFiltersAndSort(t *testing.T) {
- router, adminSvc := setupAccountDataRouter()
- adminSvc.accounts = []service.Account{
- {ID: 1, Name: "acc-1", Status: service.StatusActive},
- }
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(
- http.MethodGet,
- "/api/v1/admin/accounts/data?platform=openai&type=oauth&status=active&group=12&privacy_mode=blocked&search=keyword&sort_by=priority&sort_order=desc",
- nil,
- )
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- require.Equal(t, 1, adminSvc.lastListAccounts.calls)
- require.Equal(t, "openai", adminSvc.lastListAccounts.platform)
- require.Equal(t, "oauth", adminSvc.lastListAccounts.accountType)
- require.Equal(t, "active", adminSvc.lastListAccounts.status)
- require.Equal(t, int64(12), adminSvc.lastListAccounts.groupID)
- require.Equal(t, "blocked", adminSvc.lastListAccounts.privacyMode)
- require.Equal(t, "keyword", adminSvc.lastListAccounts.search)
- require.Equal(t, "priority", adminSvc.lastListAccounts.sortBy)
- require.Equal(t, "desc", adminSvc.lastListAccounts.sortOrder)
- }
- func TestExportDataSelectedIDsOverrideFilters(t *testing.T) {
- router, adminSvc := setupAccountDataRouter()
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(
- http.MethodGet,
- "/api/v1/admin/accounts/data?ids=1,2&platform=openai&search=keyword&sort_by=priority&sort_order=desc",
- nil,
- )
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- var resp dataResponse
- require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
- require.Equal(t, 0, resp.Code)
- require.Len(t, resp.Data.Accounts, 2)
- require.Equal(t, 0, adminSvc.lastListAccounts.calls)
- }
- func TestImportDataReusesProxyAndSkipsDefaultGroup(t *testing.T) {
- router, adminSvc := setupAccountDataRouter()
- adminSvc.proxies = []service.Proxy{
- {
- ID: 1,
- Name: "proxy",
- Protocol: "socks5",
- Host: "1.2.3.4",
- Port: 1080,
- Username: "u",
- Password: "p",
- Status: service.StatusActive,
- },
- }
- dataPayload := map[string]any{
- "data": map[string]any{
- "type": dataType,
- "version": dataVersion,
- "proxies": []map[string]any{
- {
- "proxy_key": "socks5|1.2.3.4|1080|u|p",
- "name": "proxy",
- "protocol": "socks5",
- "host": "1.2.3.4",
- "port": 1080,
- "username": "u",
- "password": "p",
- "status": "active",
- },
- },
- "accounts": []map[string]any{
- {
- "name": "acc",
- "platform": service.PlatformOpenAI,
- "type": service.AccountTypeOAuth,
- "credentials": map[string]any{"token": "x"},
- "proxy_key": "socks5|1.2.3.4|1080|u|p",
- "concurrency": 3,
- "priority": 50,
- },
- },
- },
- "skip_default_group_bind": true,
- }
- body, _ := json.Marshal(dataPayload)
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/data", bytes.NewReader(body))
- req.Header.Set("Content-Type", "application/json")
- router.ServeHTTP(rec, req)
- require.Equal(t, http.StatusOK, rec.Code)
- require.Len(t, adminSvc.createdProxies, 0)
- require.Len(t, adminSvc.createdAccounts, 1)
- require.True(t, adminSvc.createdAccounts[0].SkipDefaultGroupBind)
- }
|