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

100 lines
3.2 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 turnstile
import (
"context"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"github.com/nyoungo/dsfree2api/internal/config"
"github.com/nyoungo/dsfree2api/internal/httpx"
)
// 回归:retries=0 时求解循环必须至少跑一次,不能直接返回 ("", nil) ——
// 那会让 refreshLocked 以为求解成功,把未经验证的 cookie 缓存进池子。
func TestSolveTokenAPIRetriesZeroStillAttemptsOnce(t *testing.T) {
var calls atomic.Int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"token":"tok-1","elapsed":0.1}`))
}))
defer srv.Close()
solver := New(config.Turnstile{
Enabled: true, Provider: config.ProviderAPI, APIStyle: "ezsolver",
APIURL: srv.URL, TimeoutSeconds: 5, Retries: 0,
}, httpx.DefaultUserAgent)
token, err := solver.solveTokenAPI(context.Background(), "https://deepseek.de/", "0xabc", solver.coreFor(""))
if err != nil {
t.Fatalf("solve: %v", err)
}
if calls.Load() != 1 {
t.Fatalf("solve API 被调用 %d 次,want 1", calls.Load())
}
if token == "" {
t.Fatal("retries=0 返回空 token 且无错误 —— 上游根本没被调用")
}
}
// 回归:retries=0 时校验循环也必须至少跑一次,否则会跳过 verify 直接成功。
func TestVerifyTokenRetriesZeroStillVerifiesOnce(t *testing.T) {
var calls atomic.Int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"ok":true}`))
}))
defer srv.Close()
solver := New(config.Turnstile{
Enabled: true, Provider: config.ProviderAPI,
APIURL: srv.URL, TimeoutSeconds: 5, Retries: 0,
}, httpx.DefaultUserAgent)
sess, err := httpx.NewSession(httpx.Options{Timeout: 5 * time.Second})
if err != nil {
t.Fatalf("session: %v", err)
}
defer sess.Close()
site := &config.Site{
Code: "t", BaseURL: srv.URL, AJAXURL: srv.URL,
VerifyAction: "verify", Language: "en",
}
if err := solver.verifyToken(context.Background(), sess, site, "tok"); err != nil {
t.Fatalf("verify: %v", err)
}
if calls.Load() != 1 {
t.Fatalf("verify 被调用 %d 次,want 1 —— 空 token 会被判定为校验成功", calls.Load())
}
}
// 回归:coreFor 拿到非法代理 URL 时不能吞掉 StdClient 的错误并缓存 nil client,
// 否则 solveTokenAPI 里 client.Do 会 nil panic(warmer 后台协程没有 recover,
// 一次就能拖垮整个进程)。
func TestCoreForInvalidProxyDoesNotPanic(t *testing.T) {
solver := New(config.Turnstile{
Enabled: true, Provider: config.ProviderAPI, APIStyle: "ezsolver",
APIURL: "https://solve.example.invalid/", TimeoutSeconds: 5, Retries: 1,
}, httpx.DefaultUserAgent)
c := solver.coreFor("://bad proxy")
if c == nil {
t.Fatal("coreFor returned nil")
}
defer func() {
if r := recover(); r != nil {
t.Fatalf("coreFor 留下 nil client 导致 panic: %v", r)
}
}()
token, err := solver.solveTokenAPI(context.Background(), "https://deepseek.de/", "0xabc", c)
if err == nil {
t.Fatalf("非法代理应当报错,却返回 token=%q", token)
}
}