Add HTTP token-runtime coverage and fix gate tests for tokenless config

This commit is contained in:
CJACK.
2026-03-21 14:27:12 +08:00
parent 708fcb5beb
commit ca08bb66b9
16 changed files with 192 additions and 163 deletions

View File

@@ -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 ""
}

View File

@@ -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"`
}

View File

@@ -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")
}
}

View File

@@ -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
}