mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-04 08:25:26 +08:00
Add HTTP token-runtime coverage and fix gate tests for tokenless config
This commit is contained in:
@@ -194,7 +194,7 @@ func TestPoolAccountConcurrencyAliasEnv(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolSupportsTokenOnlyAccount(t *testing.T) {
|
||||
func TestPoolSkipsTokenOnlyAccount(t *testing.T) {
|
||||
t.Setenv("DS2API_ACCOUNT_MAX_INFLIGHT", "1")
|
||||
t.Setenv("DS2API_CONFIG_JSON", `{
|
||||
"keys":["k1"],
|
||||
@@ -206,16 +206,12 @@ func TestPoolSupportsTokenOnlyAccount(t *testing.T) {
|
||||
if got, ok := status["total"].(int); !ok || got != 1 {
|
||||
t.Fatalf("unexpected total in pool status: %#v", status["total"])
|
||||
}
|
||||
if got, ok := status["available"].(int); !ok || got != 1 {
|
||||
if got, ok := status["available"].(int); !ok || got != 0 {
|
||||
t.Fatalf("unexpected available in pool status: %#v", status["available"])
|
||||
}
|
||||
|
||||
acc, ok := pool.Acquire("", nil)
|
||||
if !ok {
|
||||
t.Fatalf("expected acquire success for token-only account")
|
||||
}
|
||||
if acc.Token != "token-only-account" {
|
||||
t.Fatalf("unexpected token on acquired account: %q", acc.Token)
|
||||
if _, ok := pool.Acquire("", nil); ok {
|
||||
t.Fatalf("expected acquire to fail for token-only account")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -26,9 +25,9 @@ func newAdminTestHandler(t *testing.T, raw string) *Handler {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAccountsIncludesTokenOnlyIdentifier(t *testing.T) {
|
||||
func TestListAccountsUsesEmailIdentifier(t *testing.T) {
|
||||
h := newAdminTestHandler(t, `{
|
||||
"accounts":[{"token":"token-only-account"}]
|
||||
"accounts":[{"email":"u@example.com","password":"pwd"}]
|
||||
}`)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/admin/accounts?page=1&page_size=10", nil)
|
||||
@@ -49,38 +48,8 @@ func TestListAccountsIncludesTokenOnlyIdentifier(t *testing.T) {
|
||||
}
|
||||
first, _ := items[0].(map[string]any)
|
||||
identifier, _ := first["identifier"].(string)
|
||||
if identifier == "" {
|
||||
t.Fatalf("expected non-empty identifier: %#v", first)
|
||||
}
|
||||
if !strings.HasPrefix(identifier, "token:") {
|
||||
t.Fatalf("expected token synthetic identifier, got %q", identifier)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteAccountSupportsTokenOnlyIdentifier(t *testing.T) {
|
||||
h := newAdminTestHandler(t, `{
|
||||
"accounts":[{"token":"token-only-account"}]
|
||||
}`)
|
||||
accounts := h.Store.Accounts()
|
||||
if len(accounts) != 1 {
|
||||
t.Fatalf("expected 1 account, got %d", len(accounts))
|
||||
}
|
||||
id := accounts[0].Identifier()
|
||||
if id == "" {
|
||||
t.Fatal("expected token-only synthetic identifier")
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Delete("/admin/accounts/{identifier}", h.deleteAccount)
|
||||
req := httptest.NewRequest(http.MethodDelete, "/admin/accounts/"+url.PathEscape(id), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if got := len(h.Store.Accounts()); got != 0 {
|
||||
t.Fatalf("expected account removed, remaining=%d", got)
|
||||
if identifier != "u@example.com" {
|
||||
t.Fatalf("expected email identifier, got %q", identifier)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,11 +111,10 @@ func TestAddAccountRejectsCanonicalMobileDuplicate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindAccountByIdentifierSupportsMobileAndTokenOnly(t *testing.T) {
|
||||
func TestFindAccountByIdentifierSupportsMobile(t *testing.T) {
|
||||
h := newAdminTestHandler(t, `{
|
||||
"accounts":[
|
||||
{"email":"u@example.com","mobile":"13800138000","password":"pwd"},
|
||||
{"token":"token-only-account"}
|
||||
{"email":"u@example.com","mobile":"13800138000","password":"pwd"}
|
||||
]
|
||||
}`)
|
||||
|
||||
@@ -165,21 +133,4 @@ func TestFindAccountByIdentifierSupportsMobileAndTokenOnly(t *testing.T) {
|
||||
t.Fatalf("unexpected account by +86 mobile: %#v", accByMobileWithCountryCode)
|
||||
}
|
||||
|
||||
tokenOnlyID := ""
|
||||
for _, acc := range h.Store.Accounts() {
|
||||
if strings.TrimSpace(acc.Email) == "" && strings.TrimSpace(acc.Mobile) == "" {
|
||||
tokenOnlyID = acc.Identifier()
|
||||
break
|
||||
}
|
||||
}
|
||||
if tokenOnlyID == "" {
|
||||
t.Fatal("expected token-only account identifier")
|
||||
}
|
||||
accByTokenOnly, ok := findAccountByIdentifier(h.Store, tokenOnlyID)
|
||||
if !ok {
|
||||
t.Fatalf("expected find by token-only id=%q", tokenOnlyID)
|
||||
}
|
||||
if accByTokenOnly.Token != "token-only-account" {
|
||||
t.Fatalf("unexpected token-only account: %#v", accByTokenOnly)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,18 +105,14 @@ func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, me
|
||||
}
|
||||
_ = h.Store.UpdateAccountTestStatus(identifier, status)
|
||||
}()
|
||||
token := strings.TrimSpace(acc.Token)
|
||||
if token == "" {
|
||||
newToken, err := h.DS.Login(ctx, acc)
|
||||
if err != nil {
|
||||
result["message"] = "登录失败: " + err.Error()
|
||||
return result
|
||||
}
|
||||
token = newToken
|
||||
if err := h.Store.UpdateAccountToken(acc.Identifier(), token); err != nil {
|
||||
result["message"] = "登录成功但写入配置失败: " + err.Error()
|
||||
return result
|
||||
}
|
||||
token, err := h.DS.Login(ctx, acc)
|
||||
if err != nil {
|
||||
result["message"] = "登录失败: " + err.Error()
|
||||
return result
|
||||
}
|
||||
if err := h.Store.UpdateAccountToken(acc.Identifier(), token); err != nil {
|
||||
result["message"] = "登录成功但写入运行时 token 失败: " + err.Error()
|
||||
return result
|
||||
}
|
||||
authCtx := &authn.RequestAuth{UseConfigToken: false, DeepSeekToken: token}
|
||||
sessionID, err := h.DS.CreateSession(ctx, authCtx, 1)
|
||||
@@ -129,7 +125,7 @@ func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, me
|
||||
token = newToken
|
||||
authCtx.DeepSeekToken = token
|
||||
if err := h.Store.UpdateAccountToken(acc.Identifier(), token); err != nil {
|
||||
result["message"] = "刷新 token 成功但写入配置失败: " + err.Error()
|
||||
result["message"] = "刷新 token 成功但写入运行时 token 失败: " + err.Error()
|
||||
return result
|
||||
}
|
||||
sessionID, err = h.DS.CreateSession(ctx, authCtx, 1)
|
||||
@@ -147,7 +143,7 @@ func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, me
|
||||
|
||||
if strings.TrimSpace(message) == "" {
|
||||
result["success"] = true
|
||||
result["message"] = "API 测试成功(仅会话创建)"
|
||||
result["message"] = "Token 刷新成功(登录与会话创建成功)"
|
||||
result["response_time"] = int(time.Since(start).Milliseconds())
|
||||
return result
|
||||
}
|
||||
@@ -246,20 +242,16 @@ func (h *Handler) deleteAllSessions(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取 token
|
||||
token := strings.TrimSpace(acc.Token)
|
||||
if token == "" {
|
||||
newToken, err := h.DS.Login(r.Context(), acc)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"success": false, "message": "登录失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
token = newToken
|
||||
_ = h.Store.UpdateAccountToken(acc.Identifier(), token)
|
||||
// 每次先登录刷新一次 token,避免使用过期 token。
|
||||
token, err := h.DS.Login(r.Context(), acc)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"success": false, "message": "登录失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
_ = h.Store.UpdateAccountToken(acc.Identifier(), token)
|
||||
|
||||
// 删除所有会话
|
||||
err := h.DS.DeleteAllSessionsForToken(r.Context(), token)
|
||||
err = h.DS.DeleteAllSessionsForToken(r.Context(), token)
|
||||
if err != nil {
|
||||
// token 可能过期,尝试重新登录并重试一次
|
||||
newToken, loginErr := h.DS.Login(r.Context(), acc)
|
||||
|
||||
@@ -77,7 +77,7 @@ func TestTestAccount_BatchModeOnlyCreatesSession(t *testing.T) {
|
||||
t.Fatalf("expected success=true, got %#v", result)
|
||||
}
|
||||
msg, _ := result["message"].(string)
|
||||
if !strings.Contains(msg, "仅会话创建") {
|
||||
if !strings.Contains(msg, "Token 刷新成功") {
|
||||
t.Fatalf("expected session-only success message, got %q", msg)
|
||||
}
|
||||
if ds.loginCalls != 1 || ds.createSessionCalls != 1 {
|
||||
@@ -118,8 +118,8 @@ func TestDeleteAllSessions_RetryWithReloginOnDeleteFailure(t *testing.T) {
|
||||
if ok, _ := resp["success"].(bool); !ok {
|
||||
t.Fatalf("expected success response, got %#v", resp)
|
||||
}
|
||||
if ds.loginCalls != 1 {
|
||||
t.Fatalf("expected relogin once, got %d", ds.loginCalls)
|
||||
if ds.loginCalls != 2 {
|
||||
t.Fatalf("expected initial login plus relogin, got %d", ds.loginCalls)
|
||||
}
|
||||
if ds.deleteAllSessionsCalls != 2 {
|
||||
t.Fatalf("expected delete called twice, got %d", ds.deleteAllSessionsCalls)
|
||||
|
||||
@@ -43,6 +43,7 @@ func (h *Handler) configImport(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()})
|
||||
return
|
||||
}
|
||||
incoming.ClearAccountTokens()
|
||||
|
||||
importedKeys, importedAccounts := 0, 0
|
||||
err = h.Store.Update(func(c *config.Config) error {
|
||||
@@ -180,6 +181,7 @@ func (h *Handler) configImport(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (h *Handler) computeSyncHash() string {
|
||||
snap := h.Store.Snapshot().Clone()
|
||||
snap.ClearAccountTokens()
|
||||
snap.VercelSyncHash = ""
|
||||
snap.VercelSyncTime = 0
|
||||
b, _ := json.Marshal(snap)
|
||||
|
||||
@@ -50,9 +50,6 @@ func (h *Handler) updateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.TrimSpace(acc.Password) == "" {
|
||||
acc.Password = prev.Password
|
||||
}
|
||||
if strings.TrimSpace(acc.Token) == "" {
|
||||
acc.Token = prev.Token
|
||||
}
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
accounts = append(accounts, acc)
|
||||
|
||||
@@ -65,7 +65,6 @@ func toAccount(m map[string]any) config.Account {
|
||||
Email: email,
|
||||
Mobile: mobile,
|
||||
Password: fieldString(m, "password"),
|
||||
Token: fieldString(m, "token"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -188,8 +188,8 @@ func TestToAccountAllFields(t *testing.T) {
|
||||
if acc.Password != "secret" {
|
||||
t.Fatalf("unexpected password: %q", acc.Password)
|
||||
}
|
||||
if acc.Token != "tok123" {
|
||||
t.Fatalf("unexpected token: %q", acc.Token)
|
||||
if acc.Token != "" {
|
||||
t.Fatalf("expected token to be ignored, got %q", acc.Token)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
109
internal/admin/token_runtime_http_test.go
Normal file
109
internal/admin/token_runtime_http_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"ds2api/internal/account"
|
||||
"ds2api/internal/config"
|
||||
)
|
||||
|
||||
func newHTTPAdminHarness(t *testing.T, rawConfig string, ds DeepSeekCaller) http.Handler {
|
||||
t.Helper()
|
||||
t.Setenv("DS2API_CONFIG_JSON", rawConfig)
|
||||
t.Setenv("CONFIG_JSON", "")
|
||||
store := config.LoadStore()
|
||||
h := &Handler{
|
||||
Store: store,
|
||||
Pool: account.NewPool(store),
|
||||
DS: ds,
|
||||
}
|
||||
r := chi.NewRouter()
|
||||
RegisterRoutes(r, h)
|
||||
return r
|
||||
}
|
||||
|
||||
func adminReq(method, path string, body []byte) *http.Request {
|
||||
req := httptest.NewRequest(method, path, bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", "Bearer admin")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return req
|
||||
}
|
||||
|
||||
func TestConfigImportIgnoresTokenFieldInPayload(t *testing.T) {
|
||||
ds := &testingDSMock{}
|
||||
router := newHTTPAdminHarness(t, `{"accounts":[]}`, ds)
|
||||
|
||||
payload := []byte(`{
|
||||
"mode":"replace",
|
||||
"config":{
|
||||
"accounts":[{"email":"u@example.com","password":"pwd","token":"expired-token"}]
|
||||
}
|
||||
}`)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, adminReq(http.MethodPost, "/config/import", payload))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("import status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
readRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(readRec, adminReq(http.MethodGet, "/config", nil))
|
||||
if readRec.Code != http.StatusOK {
|
||||
t.Fatalf("get config status=%d body=%s", readRec.Code, readRec.Body.String())
|
||||
}
|
||||
var data map[string]any
|
||||
if err := json.Unmarshal(readRec.Body.Bytes(), &data); err != nil {
|
||||
t.Fatalf("decode config response: %v", err)
|
||||
}
|
||||
accounts, _ := data["accounts"].([]any)
|
||||
if len(accounts) != 1 {
|
||||
t.Fatalf("expected one account, got %d", len(accounts))
|
||||
}
|
||||
accountMap, _ := accounts[0].(map[string]any)
|
||||
if hasToken, _ := accountMap["has_token"].(bool); hasToken {
|
||||
t.Fatalf("expected imported token to be ignored, account=%#v", accountMap)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountTestRefreshesRuntimeTokenButExportOmitsToken(t *testing.T) {
|
||||
ds := &testingDSMock{}
|
||||
router := newHTTPAdminHarness(t, `{
|
||||
"accounts":[{"email":"batch@example.com","password":"pwd","token":"stale-token"}]
|
||||
}`, ds)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, adminReq(http.MethodPost, "/accounts/test", []byte(`{"identifier":"batch@example.com"}`)))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("test account status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var testResp map[string]any
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &testResp); err != nil {
|
||||
t.Fatalf("decode test response: %v", err)
|
||||
}
|
||||
if ok, _ := testResp["success"].(bool); !ok {
|
||||
t.Fatalf("expected test success, got %#v", testResp)
|
||||
}
|
||||
if ds.loginCalls < 1 {
|
||||
t.Fatalf("expected login to be called at least once, got %d", ds.loginCalls)
|
||||
}
|
||||
|
||||
exportRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(exportRec, adminReq(http.MethodGet, "/config/export", nil))
|
||||
if exportRec.Code != http.StatusOK {
|
||||
t.Fatalf("export status=%d body=%s", exportRec.Code, exportRec.Body.String())
|
||||
}
|
||||
var exportResp map[string]any
|
||||
if err := json.Unmarshal(exportRec.Body.Bytes(), &exportResp); err != nil {
|
||||
t.Fatalf("decode export response: %v", err)
|
||||
}
|
||||
exportJSON, _ := exportResp["json"].(string)
|
||||
if strings.Contains(exportJSON, `"token"`) {
|
||||
t.Fatalf("expected export json to omit tokens, got %s", exportJSON)
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func TestDetermineWithXAPIKeyManagedKeyAcquiresAccount(t *testing.T) {
|
||||
if auth.AccountID != "acc@example.com" {
|
||||
t.Fatalf("unexpected account id: %q", auth.AccountID)
|
||||
}
|
||||
if auth.DeepSeekToken != "account-token" {
|
||||
if auth.DeepSeekToken != "fresh-token" {
|
||||
t.Fatalf("unexpected account token: %q", auth.DeepSeekToken)
|
||||
}
|
||||
if auth.CallerID == "" {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
)
|
||||
import "strings"
|
||||
|
||||
func (a Account) Identifier() string {
|
||||
if strings.TrimSpace(a.Email) != "" {
|
||||
@@ -13,12 +9,5 @@ func (a Account) Identifier() string {
|
||||
if mobile := NormalizeMobileForStorage(a.Mobile); mobile != "" {
|
||||
return mobile
|
||||
}
|
||||
// Backward compatibility: old configs may contain token-only accounts.
|
||||
// Use a stable non-sensitive synthetic id so they can still join the pool.
|
||||
token := strings.TrimSpace(a.Token)
|
||||
if token == "" {
|
||||
return ""
|
||||
}
|
||||
sum := sha256.Sum256([]byte(token))
|
||||
return "token:" + hex.EncodeToString(sum[:8])
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ type Config struct {
|
||||
Toolcall ToolcallConfig `json:"toolcall,omitempty"`
|
||||
Responses ResponsesConfig `json:"responses,omitempty"`
|
||||
Embeddings EmbeddingsConfig `json:"embeddings,omitempty"`
|
||||
AutoDelete AutoDeleteConfig `json:"auto_delete"`
|
||||
VercelSyncHash string `json:"_vercel_sync_hash,omitempty"`
|
||||
AutoDelete AutoDeleteConfig `json:"auto_delete"`
|
||||
VercelSyncHash string `json:"_vercel_sync_hash,omitempty"`
|
||||
VercelSyncTime int64 `json:"_vercel_sync_time,omitempty"`
|
||||
AdditionalFields map[string]any `json:"-"`
|
||||
}
|
||||
@@ -26,6 +26,15 @@ type Account struct {
|
||||
TestStatus string `json:"test_status,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Config) ClearAccountTokens() {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
for i := range c.Accounts {
|
||||
c.Accounts[i].Token = ""
|
||||
}
|
||||
}
|
||||
|
||||
type CompatConfig struct {
|
||||
WideInputStrictOutput *bool `json:"wide_input_strict_output,omitempty"`
|
||||
}
|
||||
|
||||
@@ -2,25 +2,21 @@ package config
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAccountIdentifierFallsBackToTokenHash(t *testing.T) {
|
||||
func TestAccountIdentifierRequiresEmailOrMobile(t *testing.T) {
|
||||
acc := Account{Token: "example-token-value"}
|
||||
id := acc.Identifier()
|
||||
if !strings.HasPrefix(id, "token:") {
|
||||
t.Fatalf("expected token-prefixed identifier, got %q", id)
|
||||
}
|
||||
if len(id) != len("token:")+16 {
|
||||
t.Fatalf("unexpected identifier length: %d (%q)", len(id), id)
|
||||
if id != "" {
|
||||
t.Fatalf("expected empty identifier when only token is present, got %q", id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreFindAccountWithTokenOnlyIdentifier(t *testing.T) {
|
||||
func TestLoadStoreClearsTokensFromConfigInput(t *testing.T) {
|
||||
t.Setenv("DS2API_CONFIG_JSON", `{
|
||||
"keys":["k1"],
|
||||
"accounts":[{"token":"token-only-account"}]
|
||||
"accounts":[{"email":"u@example.com","password":"p","token":"token-only-account"}]
|
||||
}`)
|
||||
|
||||
store := LoadStore()
|
||||
@@ -28,22 +24,14 @@ func TestStoreFindAccountWithTokenOnlyIdentifier(t *testing.T) {
|
||||
if len(accounts) != 1 {
|
||||
t.Fatalf("expected 1 account, got %d", len(accounts))
|
||||
}
|
||||
id := accounts[0].Identifier()
|
||||
if id == "" {
|
||||
t.Fatalf("expected synthetic identifier for token-only account")
|
||||
}
|
||||
found, ok := store.FindAccount(id)
|
||||
if !ok {
|
||||
t.Fatalf("expected FindAccount to locate token-only account by synthetic id")
|
||||
}
|
||||
if found.Token != "token-only-account" {
|
||||
t.Fatalf("unexpected token value: %q", found.Token)
|
||||
if accounts[0].Token != "" {
|
||||
t.Fatalf("expected token to be cleared after loading, got %q", accounts[0].Token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreUpdateAccountTokenKeepsOldAndNewIdentifierResolvable(t *testing.T) {
|
||||
func TestStoreUpdateAccountTokenKeepsIdentifierResolvable(t *testing.T) {
|
||||
t.Setenv("DS2API_CONFIG_JSON", `{
|
||||
"accounts":[{"token":"old-token"}]
|
||||
"accounts":[{"email":"user@example.com","password":"p"}]
|
||||
}`)
|
||||
|
||||
store := LoadStore()
|
||||
@@ -52,23 +40,12 @@ func TestStoreUpdateAccountTokenKeepsOldAndNewIdentifierResolvable(t *testing.T)
|
||||
t.Fatalf("expected 1 account, got %d", len(before))
|
||||
}
|
||||
oldID := before[0].Identifier()
|
||||
if oldID == "" {
|
||||
t.Fatal("expected old identifier")
|
||||
}
|
||||
if err := store.UpdateAccountToken(oldID, "new-token"); err != nil {
|
||||
t.Fatalf("update token failed: %v", err)
|
||||
}
|
||||
|
||||
after := store.Accounts()
|
||||
newID := after[0].Identifier()
|
||||
if newID == "" || newID == oldID {
|
||||
t.Fatalf("expected changed identifier, old=%q new=%q", oldID, newID)
|
||||
}
|
||||
if got, ok := store.FindAccount(newID); !ok || got.Token != "new-token" {
|
||||
t.Fatalf("expected find by new identifier")
|
||||
}
|
||||
if got, ok := store.FindAccount(oldID); !ok || got.Token != "new-token" {
|
||||
t.Fatalf("expected find by old identifier alias")
|
||||
t.Fatalf("expected find by stable account identifier")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ func loadConfig() (Config, bool, error) {
|
||||
}
|
||||
if rawCfg != "" {
|
||||
cfg, err := parseConfigString(rawCfg)
|
||||
cfg.ClearAccountTokens()
|
||||
return cfg, true, err
|
||||
}
|
||||
|
||||
@@ -55,6 +56,7 @@ func loadConfig() (Config, bool, error) {
|
||||
if err := json.Unmarshal(content, &cfg); err != nil {
|
||||
return Config{}, false, err
|
||||
}
|
||||
cfg.ClearAccountTokens()
|
||||
if IsVercel() {
|
||||
// Vercel filesystem is ephemeral/read-only for runtime writes; avoid save errors.
|
||||
return cfg, true, nil
|
||||
@@ -161,7 +163,9 @@ func (s *Store) Save() error {
|
||||
Logger.Info("[save_config] source from env, skip write")
|
||||
return nil
|
||||
}
|
||||
b, err := json.MarshalIndent(s.cfg, "", " ")
|
||||
persistCfg := s.cfg.Clone()
|
||||
persistCfg.ClearAccountTokens()
|
||||
b, err := json.MarshalIndent(persistCfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -173,7 +177,9 @@ func (s *Store) saveLocked() error {
|
||||
Logger.Info("[save_config] source from env, skip write")
|
||||
return nil
|
||||
}
|
||||
b, err := json.MarshalIndent(s.cfg, "", " ")
|
||||
persistCfg := s.cfg.Clone()
|
||||
persistCfg.ClearAccountTokens()
|
||||
b, err := json.MarshalIndent(persistCfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -197,7 +203,9 @@ func (s *Store) SetVercelSync(hash string, ts int64) error {
|
||||
func (s *Store) ExportJSONAndBase64() (string, string, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
b, err := json.Marshal(s.cfg)
|
||||
exportCfg := s.cfg.Clone()
|
||||
exportCfg.ClearAccountTokens()
|
||||
b, err := json.Marshal(exportCfg)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@
|
||||
"delete": "Delete",
|
||||
"copy": "Copy",
|
||||
"generate": "Generate",
|
||||
"test": "Test",
|
||||
"testing": "Testing...",
|
||||
"test": "Refresh token",
|
||||
"testing": "Refreshing...",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"messages": {
|
||||
@@ -93,8 +93,8 @@
|
||||
"deleteKeyConfirm": "Are you sure you want to delete this API key?",
|
||||
"deleteAccountConfirm": "Are you sure you want to delete this account?",
|
||||
"invalidIdentifier": "Invalid account identifier. Operation aborted.",
|
||||
"testAllConfirm": "Test API connectivity for all accounts?",
|
||||
"testAllCompleted": "Completed: {success}/{total} available",
|
||||
"testAllConfirm": "Refresh all account tokens and verify login?",
|
||||
"testAllCompleted": "Completed: {success}/{total} refreshed",
|
||||
"testFailed": "Test failed: {error}",
|
||||
"available": "Available",
|
||||
"inUse": "In use",
|
||||
@@ -110,9 +110,9 @@
|
||||
"noApiKeys": "No API keys found.",
|
||||
"accountsTitle": "DeepSeek Accounts",
|
||||
"accountsDesc": "Manage the DeepSeek account pool",
|
||||
"testAll": "Test all",
|
||||
"testAll": "Refresh all tokens",
|
||||
"addAccount": "Add account",
|
||||
"testingAllAccounts": "Testing all accounts...",
|
||||
"testingAllAccounts": "Refreshing tokens for all accounts...",
|
||||
"sessionActive": "Session active",
|
||||
"reauthRequired": "Re-auth required",
|
||||
"testStatusFailed": "Last test failed",
|
||||
@@ -150,7 +150,7 @@
|
||||
"missingApiKey": "Please provide an API key.",
|
||||
"requestFailed": "Request failed.",
|
||||
"networkError": "Network error: {error}",
|
||||
"testSuccess": "{account}: Test successful ({time}ms)",
|
||||
"testSuccess": "{account}: Token refresh successful ({time}ms)",
|
||||
"config": "Configuration",
|
||||
"modelLabel": "Model",
|
||||
"streamMode": "Streaming",
|
||||
|
||||
@@ -49,8 +49,8 @@
|
||||
"delete": "删除",
|
||||
"copy": "复制",
|
||||
"generate": "生成",
|
||||
"test": "测试",
|
||||
"testing": "正在测试...",
|
||||
"test": "刷新 Token",
|
||||
"testing": "正在刷新...",
|
||||
"loading": "加载中..."
|
||||
},
|
||||
"messages": {
|
||||
@@ -93,8 +93,8 @@
|
||||
"deleteKeyConfirm": "确定要删除此 API 密钥吗?",
|
||||
"deleteAccountConfirm": "确定要删除此账号吗?",
|
||||
"invalidIdentifier": "账号标识无效,无法执行操作",
|
||||
"testAllConfirm": "测试所有账号的 API 连通性?",
|
||||
"testAllCompleted": "完成:{success}/{total} 可用",
|
||||
"testAllConfirm": "刷新所有账号 Token 并验证登录?",
|
||||
"testAllCompleted": "完成:{success}/{total} 刷新成功",
|
||||
"testFailed": "测试失败: {error}",
|
||||
"available": "可用",
|
||||
"inUse": "正在使用",
|
||||
@@ -110,9 +110,9 @@
|
||||
"noApiKeys": "未找到 API 密钥",
|
||||
"accountsTitle": "DeepSeek 账号",
|
||||
"accountsDesc": "管理 DeepSeek 账号池",
|
||||
"testAll": "测试全部",
|
||||
"testAll": "刷新全部 Token",
|
||||
"addAccount": "添加账号",
|
||||
"testingAllAccounts": "正在测试所有账号...",
|
||||
"testingAllAccounts": "正在刷新所有账号 Token...",
|
||||
"sessionActive": "已建立会话",
|
||||
"reauthRequired": "需重新登录",
|
||||
"testStatusFailed": "上次测试失败",
|
||||
@@ -150,7 +150,7 @@
|
||||
"missingApiKey": "请提供 API 密钥",
|
||||
"requestFailed": "请求失败",
|
||||
"networkError": "网络错误: {error}",
|
||||
"testSuccess": "{account}: 测试成功 ({time}ms)",
|
||||
"testSuccess": "{account}: Token 刷新成功 ({time}ms)",
|
||||
"config": "配置",
|
||||
"modelLabel": "模型",
|
||||
"streamMode": "流式模式",
|
||||
|
||||
Reference in New Issue
Block a user