Files
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

82 lines
3.2 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 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)
}
}
}