修复(#4): 抽奖 8 宫格默认值 + 100500 msg 脱敏

Closes part of HIF-4 (Stage 2 前端集成反馈 F8 + F10)

F8 (P1): Activity.GridSize 默认值 9 → 8
- migration 02160 ALTER DEFAULT 8(幂等)
- 02156 up.sql 注释同步更新
- admin/lottery.go 兜底值 9 → 8
- 布局 A:3x3 挖中心,中心是抽奖按钮不是奖品

F10 (P1): wrapInternal msg 脱敏
- 内部错误 err.Error() 只写日志,不外传
- 用户端 msg 只带通用文案 (xerr.MapErrMsg lookup)
- 回归护栏:TestWrapInternal_ScrubsErrorDetailsFromMsg + TestWrapInternal_PreservesCodeErrors

CI 全绿;无 API 契约变更;F9 独立在 PR #45 处理
This commit is contained in:
2026-07-12 19:55:25 -07:00
committed by GitHub
parent 980e5adb90
commit cce147108c
7 changed files with 82 additions and 5 deletions
@@ -514,6 +514,52 @@ func errAsCode(err error) (uint32, bool) {
return 0, false
}
// TestWrapInternal_ScrubsErrorDetailsFromMsg is the HIF-4 F10 regression guard.
//
// wrapInternal 之前把 err.Error() 直接塞 xerr.CodeError.Msg,导致对外响应
// {"code":100500,"msg":"insert lottery_claim for draw 15: Error 3140 ..."}
// 把 DB 结构/表名/内部包路径全部泄露给 app 端。F10 修法:细节走日志,msg 只
// 带通用文案。此测试锁死:"wrapInternal(任意非 CodeError 的原生错误) 返回的
// CodeError.Msg 不能等于原始 err.Error()"。
func TestWrapInternal_ScrubsErrorDetailsFromMsg(t *testing.T) {
sensitive := errors.New("insert lottery_claim for draw 15: Error 3140 (22032): Invalid JSON text: The document is empty")
wrapped := wrapInternal(sensitive)
if wrapped == nil {
t.Fatal("wrapInternal returned nil for non-nil error")
}
var ce *xerr.CodeError
if !errors.As(wrapped, &ce) {
t.Fatalf("expected *xerr.CodeError, got %T", wrapped)
}
if ce.GetErrCode() != xerr.LotteryInternalError {
t.Fatalf("expected code=%d, got %d", xerr.LotteryInternalError, ce.GetErrCode())
}
if strings.Contains(ce.GetErrMsg(), "lottery_claim") ||
strings.Contains(ce.GetErrMsg(), "3140") ||
strings.Contains(ce.GetErrMsg(), "Invalid JSON") {
t.Fatalf("F10 regression: internal error detail leaked to msg: %q", ce.GetErrMsg())
}
// 反过来断言:msg 应该是标准文案(xerr.MapErrMsg 查表得到)
if ce.GetErrMsg() != xerr.MapErrMsg(xerr.LotteryInternalError) {
t.Fatalf("expected generic msg %q, got %q", xerr.MapErrMsg(xerr.LotteryInternalError), ce.GetErrMsg())
}
}
// TestWrapInternal_PreservesCodeErrors 副断言:已经是 xerr.CodeError 的错误
// 不能被 wrap 掉(它们的 msg 是设计过的对外文案,比如 4001/4002/4009)。
func TestWrapInternal_PreservesCodeErrors(t *testing.T) {
coded := xerr.NewErrCode(xerr.LotteryNoChances)
wrapped := wrapInternal(coded)
var ce *xerr.CodeError
if !errors.As(wrapped, &ce) {
t.Fatalf("expected *xerr.CodeError, got %T", wrapped)
}
if ce.GetErrCode() != xerr.LotteryNoChances {
t.Fatalf("F10 side-effect: coded error was rewrapped; got code %d instead of %d",
ce.GetErrCode(), xerr.LotteryNoChances)
}
}
// TestInsertSnapshots_UnmetReasonsIsValidJSON is the F4 regression guard
// (kept from PR E — must survive Stage 2 rebase).
//