审计修复:后端(安全 / 数据一致性 / 性能)
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 再用应用侧读查询真打一遍, 迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user