Files
杭州明婳科技 6595e94677 fix: 多模型评审终审收口 — P0安全 + 主线A并发治理 + 文档对齐
经 deepseek/GLM/Claude 三轮对抗性评审 + 交叉核验 + 终审(见
last_report.md、cross_review_assessment.md),逐条回读源码核实并修复。
go build / vet / test -race ./... 全绿。

安全(P0):
- JWT alg confusion:WithValidMethods 固定 HMAC + keyfunc 方法断言
- JWT 空密钥 / 不支持算法 fail-closed(ErrEmptySecret / ErrUnsupportedAlgorithm)
- 上传大小实测封顶(enforceUploadSize),不信任客户端 file.Size
- HTTP headers/cookies map 竞态(写锁+快照)+ SSRF 防护(opt-in dialer.Control)

并发/资源(主线A 闭合):
- 包级可变全局一律 atomic.Pointer:DefaultRedis / DefaultStorage /
  DefaultManager / Validator / DefaultCache 等,消除裸指针数据竞争
- trace.Close 去 sync.Once(防 exporter 泄漏);app OnReady 失败走 Shutdown
- ws.Hub.Stop stopOnce 防 double-close;cron 接入 App 生命周期(WithCron)
- ratelimit failClosed atomic.Bool;Recover Written() 守卫
- cron checkAndRun 锁内收集锁外 spawn(wg.Add 在锁内防 Stop 竞态)

终审剩余项收口(H-A~M-H + L 系列):
- 副本连接池 MaxOpenConns/2 截断修复(replicaMaxOpenConns)
- JWT 黑名单 1s 超时 + 显式错误;ratelimit GetRedis 取一次复用
- GetPage 深分页上限;HashFile 流式;ReadFile 去 TOCTOU
- config Clone() 深拷贝;cache 锁操作返 ErrRedisNotReady
- compress 解压残留清理;正则预编译;EqualsIgnoreCase→EqualFold
- 删 redisLimiters 死代码;CLI 输入校验 + 回滚

文档:
- README/GUIDE/CHANGELOG 对齐当前 API(含破坏性变更迁移说明)
- docs/README 索引修正;config 注释修正(RS256 已不支持)

破坏性变更详见 CHANGELOG.md [Unreleased]。项目处于研发初期、无下游用户。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:16:03 +08:00

68 lines
2.4 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 response_test
import (
"sync"
"testing"
"github.com/EthanCodeCraft/xlgo-core/response"
)
// TestWithDetailDoesNotMutateOriginal H-10 回归:WithDetail 必须返回新拷贝,
// 不得 mutate 共享的预定义 Err*。修复前 WithDetail 会写 e.Detail 污染全局。
func TestWithDetailDoesNotMutateOriginal(t *testing.T) {
if response.ErrNotFound.Detail != "" {
t.Fatalf("预置条件:ErrNotFound.Detail 应为空,实际 %q", response.ErrNotFound.Detail)
}
detailed := response.ErrNotFound.WithDetail("用户 123 不存在")
// 原共享对象不得被污染
if response.ErrNotFound.Detail != "" {
t.Fatalf("WithDetail 污染了共享 ErrNotFoundDetail=%q", response.ErrNotFound.Detail)
}
// 新拷贝携带 detail
if detailed.Detail != "用户 123 不存在" {
t.Fatalf("新 Error 应携带 detail,实际 %q", detailed.Detail)
}
if detailed.Code != response.ErrNotFound.Code || detailed.Message != response.ErrNotFound.Message {
t.Fatalf("新 Error 应继承 Code/Message,实际 Code=%d Message=%q", detailed.Code, detailed.Message)
}
}
// TestWithDetailConcurrentOnSharedError H-10 回归:并发在共享 Err* 上调 WithDetail
// 不应触发数据竞争(修复前 mutate 共享对象,-race 必采)。
func TestWithDetailConcurrentOnSharedError(t *testing.T) {
var wg sync.WaitGroup
for i := 0; i < 50; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
e := response.ErrNotFound.WithDetail("concurrent")
if e.Detail != "concurrent" {
t.Errorf("Detail 应为 concurrent,实际 %q", e.Detail)
}
}(i)
}
wg.Wait()
}
// TestExposeDetailGating P1 #15SetExposeDetail(false) 时 ToResponse 不得输出 Detail
// true 时输出。默认(未设置)为 true,保持存量行为。
func TestExposeDetailGating(t *testing.T) {
t.Cleanup(func() { response.SetExposeDetail(true) }) // 还原默认
err := response.NewErrorWithDetail(response.CodeServerError, "服务器错误", "pq: relation \"users\" does not exist")
// 暴露开启(默认/开发):Detail 出现在 Data 中
response.SetExposeDetail(true)
if resp := err.ToResponse(); resp.Data == nil {
t.Error("SetExposeDetail(true): ToResponse 应包含 detail")
}
// 暴露关闭(生产):Detail 不得出现,防内部错误泄露
response.SetExposeDetail(false)
if resp := err.ToResponse(); resp.Data != nil {
t.Errorf("SetExposeDetail(false): ToResponse 不得输出 detail,实际 Data=%v", resp.Data)
}
}