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 再用应用侧读查询真打一遍, 迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
82 lines
3.2 KiB
Go
82 lines
3.2 KiB
Go
package web
|
||
|
||
import (
|
||
"context"
|
||
"os"
|
||
"regexp"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// TestAggHelpersPropagateErrors 是 D1 的行为守卫:四个「重算聚合 / 写标签」的辅助函数
|
||
// 必须把数据库错误返回给调用方。
|
||
//
|
||
// 它们原先的签名是 `(isHome, isWork bool)` / 无返回值,Scan 的错误被直接丢掉——
|
||
// 于是「查询炸了」和「查到了 false/false」在调用方看来一模一样:到访会被写得没有归属活动,
|
||
// 计数会静默地停在旧值上。用一个已取消的 context 就能逼出这一层错误。
|
||
func TestAggHelpersPropagateErrors(t *testing.T) {
|
||
f := newMovementFixture(t)
|
||
|
||
// 已取消的 context:任何一次 Query/Exec 都会立刻以 context.Canceled 返回。
|
||
ctx, cancel := context.WithCancel(f.ctx)
|
||
cancel()
|
||
|
||
if err := f.srv.refreshDatasetAgg(ctx, f.pg, f.dsID); err == nil {
|
||
t.Error("refreshDatasetAgg 吞掉了错误:ctx 已取消却返回 nil")
|
||
}
|
||
if err := f.srv.refreshPlaceAgg(ctx, f.pg, 1); err == nil {
|
||
t.Error("refreshPlaceAgg 吞掉了错误:ctx 已取消却返回 nil")
|
||
}
|
||
if err := f.srv.saveVisitTags(ctx, f.pg, f.dsID, 1, nil); err == nil {
|
||
t.Error("saveVisitTags 吞掉了错误:ctx 已取消却返回 nil")
|
||
}
|
||
if _, _, err := f.srv.activityFlags(ctx, f.pg, 1); err == nil {
|
||
t.Error("activityFlags 吞掉了错误:ctx 已取消却返回 nil")
|
||
}
|
||
}
|
||
|
||
// TestNoAggCallSiteSwallowsError 是 D1 的静态守卫:这三个辅助函数「返回 error」只有在
|
||
// 每个调用点都接住它时才有意义。裸调用(语句形式、丢掉返回值)会随第一个新加的调用点回归,
|
||
// 而且编译器不会拦——这里按行扫描源码,把裸调用钉死。
|
||
func TestNoAggCallSiteSwallowsError(t *testing.T) {
|
||
// 只匹配「整行就是一次调用」的裸语句,合法写法前面一定还有 `if err := `、`return `、
|
||
// `logAggFailure(` 等前缀,因此不会被误伤。
|
||
bare := regexp.MustCompile(`^\s*s\.(refreshDatasetAgg|refreshPlaceAgg|saveVisitTags)\(`)
|
||
|
||
for _, name := range []string{
|
||
"handlers_edit.go",
|
||
"handlers_photos.go",
|
||
"fillup.go",
|
||
} {
|
||
path := pkgFile(t, name)
|
||
data, err := os.ReadFile(path)
|
||
if err != nil {
|
||
t.Fatalf("读 %s 失败: %v", path, err)
|
||
}
|
||
for i, line := range strings.Split(string(data), "\n") {
|
||
if bare.MatchString(line) {
|
||
t.Errorf("%s:%d 裸调用吞掉了 error:%s", name, i+1, strings.TrimSpace(line))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestAggHelpersHaveErrorReturn 钉住签名本身:如果谁把它们改回无返回值,
|
||
// 上面两条用例会失去意义(编译期就不会再有 error 可断言)。
|
||
func TestAggHelpersHaveErrorReturn(t *testing.T) {
|
||
data, err := os.ReadFile(pkgFile(t, "handlers_edit.go"))
|
||
if err != nil {
|
||
t.Fatalf("读 handlers_edit.go 失败: %v", err)
|
||
}
|
||
src := string(data)
|
||
for _, sig := range []string{
|
||
"func (s *Server) saveVisitTags(ctx context.Context, ex execer, datasetID int64, visitSrc int, tagIDs []string) error",
|
||
"func (s *Server) refreshPlaceAgg(ctx context.Context, ex execer, placeID int64) error",
|
||
"func (s *Server) refreshDatasetAgg(ctx context.Context, ex execer, datasetID int64) error",
|
||
} {
|
||
if !strings.Contains(src, sig) {
|
||
t.Errorf("签名被改掉了(不再返回 error):%s", sig)
|
||
}
|
||
}
|
||
}
|