audit-2026-09-21.md 里的后端条目落地。每条都配了回归守卫,并逐条做过变异验证 (注入原 bug → 确认按正确理由变红 → 还原 → 变绿)。 安全 - CSRF 只加同源 Origin/Referer 守卫(csrf.go),不动模板 - 会话 cookie 的 Secure 按请求判断;两步验证两闸(loginGuard + 验证码一次性) - 跳转收口到 safeInternalPath;clientIP 只认 XFF 最后一段 - GPX 按属主圈定且查 RowsAffected;下载名走 downloadDisposition - 上传口统一 MaxBytesReader;落盘后的失败路径清掉半成品 数据一致性 - 区域判定两端各过一遍再 AND(apiTrack 与 movementsWhere 对齐) - Filter.Verdict 全域一致(下推到 visitsWhere);聚合刷新必须返回 error - 附属数据改挂坐标键,Go 与 SQL 两套表达式逐位一致 - 脏备份的伪时间/越界坐标不再进领域模型;rows 循环统一 drainRows - 合并与导出:writeRawRow 上抛错误、悬空引用清理、原生列补全 性能 - placeCols 相关子查询带 dataset_id 前导列;findSourceArchive 先按大小筛 - 批量勾选改 ANY($3::bigint[]);fillupCache 加上限;同一查询不再查两遍 另:新增「全新库」冒烟测试 —— 空库跑完 Migrate 再用应用侧读查询真打一遍, 迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
128 lines
3.7 KiB
Go
128 lines
3.7 KiB
Go
package web
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"strings"
|
||
"sync/atomic"
|
||
"testing"
|
||
"time"
|
||
|
||
"rond-with-you/internal/config"
|
||
)
|
||
|
||
func testServer() *Server { return &Server{cfg: &config.Config{}} }
|
||
|
||
// TestRunJobsRecoversPanic 钉住「任务 panic 不拖垮进程」。
|
||
// runJobs 的任务跑在子 goroutine 里,而 HTTP 层的 recover 只覆盖请求 goroutine,
|
||
// 没有这层兜底时,任何一次 nil 解引用都会让**整个进程**退出(不是这一页报错)。
|
||
func TestRunJobsRecoversPanic(t *testing.T) {
|
||
s := testServer()
|
||
err := s.runJobs(context.Background(),
|
||
pageJob{"ok", func(context.Context) error { return nil }},
|
||
pageJob{"boom", func(context.Context) error { panic("查询炸了") }},
|
||
pageJob{"after", func(context.Context) error { return nil }},
|
||
)
|
||
if err == nil {
|
||
t.Fatal("任务 panic 时应返回错误,而不是让调用方以为这页查成功了")
|
||
}
|
||
if !strings.Contains(err.Error(), "boom") {
|
||
t.Errorf("错误里应带上出错任务的名字,实际 %v", err)
|
||
}
|
||
}
|
||
|
||
// TestRunJobsRecoversPanicInline 单任务走的是同步分支,也必须兜住 panic。
|
||
func TestRunJobsRecoversPanicInline(t *testing.T) {
|
||
s := testServer()
|
||
err := s.runJobs(context.Background(), pageJob{"boom", func(context.Context) error {
|
||
panic("查询炸了")
|
||
}})
|
||
if err == nil {
|
||
t.Fatal("单任务 panic 时应返回错误")
|
||
}
|
||
}
|
||
|
||
func TestRunJobsRunsConcurrently(t *testing.T) {
|
||
s := testServer()
|
||
const n = 8
|
||
const each = 60 * time.Millisecond
|
||
var done int32
|
||
jobs := make([]pageJob, 0, n)
|
||
for i := 0; i < n; i++ {
|
||
jobs = append(jobs, pageJob{"job", func(context.Context) error {
|
||
time.Sleep(each)
|
||
atomic.AddInt32(&done, 1)
|
||
return nil
|
||
}})
|
||
}
|
||
start := time.Now()
|
||
if err := s.runJobs(context.Background(), jobs...); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
elapsed := time.Since(start)
|
||
if int(done) != n {
|
||
t.Fatalf("只完成了 %d 个任务", done)
|
||
}
|
||
// 并发执行:总耗时应接近单个任务,而不是 n 倍。给足余量避免机器慢时抖动。
|
||
if elapsed > each*n/2 {
|
||
t.Errorf("8 个 %v 的任务耗时 %v,看起来没并发(串行约 %v)", each, elapsed, each*n)
|
||
}
|
||
}
|
||
|
||
func TestRunJobsReturnsFirstErrorInOrder(t *testing.T) {
|
||
s := testServer()
|
||
errA, errB := errors.New("A 错"), errors.New("B 错")
|
||
// 故意让靠后的任务先失败,返回值仍应是传入顺序里的第一个错误
|
||
err := s.runJobs(context.Background(),
|
||
pageJob{"slow-fail", func(context.Context) error {
|
||
time.Sleep(50 * time.Millisecond)
|
||
return errA
|
||
}},
|
||
pageJob{"fast-fail", func(context.Context) error { return errB }},
|
||
pageJob{"ok", func(context.Context) error { return nil }},
|
||
)
|
||
if !errors.Is(err, errA) {
|
||
t.Errorf("返回 %v,期望传入顺序里的第一个错误 %v", err, errA)
|
||
}
|
||
}
|
||
|
||
func TestRunJobsSingleJobRunsInline(t *testing.T) {
|
||
s := testServer()
|
||
ran := false
|
||
if err := s.runJobs(context.Background(), pageJob{"only", func(context.Context) error {
|
||
ran = true
|
||
return nil
|
||
}}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !ran {
|
||
t.Error("单个任务也应执行")
|
||
}
|
||
}
|
||
|
||
func TestRunJobsStopsOnCancelledContext(t *testing.T) {
|
||
s := testServer()
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
cancel()
|
||
var ran int32
|
||
jobs := make([]pageJob, 0, 4)
|
||
for i := 0; i < 4; i++ {
|
||
jobs = append(jobs, pageJob{"job", func(ctx context.Context) error {
|
||
// 真实查询在 ctx 取消后就是这样返回的
|
||
select {
|
||
case <-ctx.Done():
|
||
return ctx.Err()
|
||
default:
|
||
}
|
||
atomic.AddInt32(&ran, 1)
|
||
return nil
|
||
}})
|
||
}
|
||
if err := s.runJobs(ctx, jobs...); !errors.Is(err, context.Canceled) {
|
||
t.Errorf("ctx 已取消时应返回 context.Canceled,实际 %v", err)
|
||
}
|
||
if n := atomic.LoadInt32(&ran); n != 0 {
|
||
t.Errorf("ctx 一开始就已取消,不该有任务真正执行(实际 %d 个)", n)
|
||
}
|
||
}
|