Files
nyoung ed51efd8bc fix: 网关与控制台代码审查修复批次(含回归测试)
- Chat 接口拒绝 n>1(此前静默丢弃多余 choice),返回 400 结构化错误
- Responses 接口透传 text.format(json_schema / json_object),三种接口协议对齐
- TLS 指纹统一升级到 Chrome_150(internal/httpx.DefaultProfile),与 UA 匹配
- metrics 落盘失败不再静默:Save() 返回错误并打日志,临时文件失败会清理
- GET /api/config 脱敏 admin.password 与 turnstile.api_key(控制台从不展示,
  不应随 5s 轮询下发);PUT 改走 ReplaceJSON,先校验后替换、失败回滚
- set_limits / set_turnstile 改指针部分更新:缺字段保持现状,不会把并发、
  限流或求解开关按零值清掉
- 读请求体统一 64MB 上限;配置写盘原子替换 + 0600 权限
- main.go 改为 run() 返回退出码:日志落盘、指标落盘、xray 子进程回收全部
  挂 defer,任何退出路径(含启动失败)都会执行,杜绝孤儿 xray
- proxypool / turnstile / upstream / openai / config / httpx / logbuf 等
  逐项补红绿回归测试,新增 14 个测试文件,race 下全绿
2026-10-08 15:13:23 +08:00

103 lines
3.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package admin
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/nyoungo/dsfree2api/internal/config"
"github.com/nyoungo/dsfree2api/internal/turnstile"
)
func testConfig(t *testing.T) *config.Config {
t.Helper()
raw, err := os.ReadFile("../../config.example.toml")
if err != nil {
t.Fatalf("read example config: %v", err)
}
dst := filepath.Join(t.TempDir(), "config.toml")
if err := os.WriteFile(dst, raw, 0o644); err != nil {
t.Fatalf("write temp config: %v", err)
}
cfg, err := config.Load(dst)
if err != nil {
t.Fatalf("load config: %v", err)
}
return cfg
}
func newActionServer(t *testing.T, cfg *config.Config) *Server {
t.Helper()
return New(cfg, nil, nil, nil, turnstile.New(cfg.Turnstile, cfg.Upstream.UserAgent), nil, nil, nil)
}
func postAction(t *testing.T, s *Server, payload string) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(http.MethodPost, "/api/actions", strings.NewReader(payload))
w := httptest.NewRecorder()
s.handleActions(w, req)
return w
}
// 回归:set_limits 是部分更新 —— payload 里没带的字段必须保持原值。旧实现把
// max_concurrent/rate_per_minute 当零值字段,缺字段就把它们写成 0:
// 并发闸门(max<=0 = 不限)和按 Key 限流(limit<=0 = 不限)会被静默关掉,
// slow_start 同样被清零。
func TestSetLimitsPartialPayloadKeepsExistingValues(t *testing.T) {
cfg := testConfig(t)
cfg.Limits.MaxConcurrentPerSite = 4
cfg.Limits.RatePerMinute = 7
cfg.Proxy.SlowStartSeconds = 8
s := newActionServer(t, cfg)
w := postAction(t, s, `{"action":"set_limits","cross_site_failover":false}`)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, body = %s", w.Code, w.Body.String())
}
cfg.RLock()
got := struct {
MaxConcurrent int
Rate float64
SlowStart float64
CrossSite bool
}{cfg.Limits.MaxConcurrentPerSite, cfg.Limits.RatePerMinute, cfg.Proxy.SlowStartSeconds, cfg.Upstream.CrossSiteFailover}
cfg.RUnlock()
if got.MaxConcurrent != 4 {
t.Errorf("max_concurrent = %d, want 4(缺字段被清零会让并发闸门失效)", got.MaxConcurrent)
}
if got.Rate != 7 {
t.Errorf("rate_per_minute = %v, want 7", got.Rate)
}
if got.SlowStart != 8 {
t.Errorf("slow_start = %v, want 8", got.SlowStart)
}
if got.CrossSite {
t.Errorf("cross_site_failover = true, want false(payload 里的值要生效)")
}
}
// 回归:set_turnstile 同样是部分更新 —— 不带 enabled 不能把求解器关掉。
func TestSetTurnstilePartialPayloadKeepsEnabled(t *testing.T) {
cfg := testConfig(t)
cfg.Turnstile.Enabled = true
cfg.Turnstile.Provider = config.ProviderAPI
cfg.Turnstile.APIURL = "https://solve.example.invalid/"
cfg.Turnstile.Retries = 5
s := newActionServer(t, cfg)
w := postAction(t, s, `{"action":"set_turnstile","retries":3}`)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, body = %s", w.Code, w.Body.String())
}
if !cfg.Turnstile.Enabled {
t.Errorf("turnstile.enabled 被部分 payload 关掉了")
}
if cfg.Turnstile.Retries != 3 {
t.Errorf("retries = %d, want 3", cfg.Turnstile.Retries)
}
}