Files
rond-with-you/internal/web/parallel_test.go
T
tamakyi b4c2a8452f 审计修复:后端(安全 / 数据一致性 / 性能)
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 再用应用侧读查询真打一遍,
迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
2026-09-24 20:37:16 +08:00

128 lines
3.7 KiB
Go
Raw 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 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)
}
}