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 再用应用侧读查询真打一遍, 迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
130 lines
4.9 KiB
Go
130 lines
4.9 KiB
Go
package web
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// mkFile 造一个指定字节数的候选文件。
|
|
func mkFile(t *testing.T, dir, name string, n int) string {
|
|
t.Helper()
|
|
p := filepath.Join(dir, name)
|
|
if err := os.WriteFile(p, bytes.Repeat([]byte("x"), n), 0o644); err != nil {
|
|
t.Fatalf("造 %s 失败: %v", name, err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
// P5 回归:按摘要找回备份包时,必须先按字节数筛一遍再算摘要。
|
|
//
|
|
// 只看「最终返回哪个文件」新旧写法完全一样——真正的差别是**算了几次 sha256**,
|
|
// 而 uploads 里每份备份动辄几百 MB,多算一次就是几百 MB 的读盘。所以这里注入一个
|
|
// 会计数的 hash,把「有没有白算」直接钉住。
|
|
func TestPickArchiveSkipsWrongSizedFilesWithoutHashing(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// 两个诱饵都比正主新(会排在候选前面),大小都和正主不同
|
|
d1 := mkFile(t, dir, "decoy-newest.rondbackup", 10)
|
|
d2 := mkFile(t, dir, "decoy-mid.rondbackup", 20)
|
|
right := mkFile(t, dir, "right.rondbackup", 30)
|
|
|
|
hashed := 0
|
|
got := pickArchiveBySha([]string{d1, d2, right}, "sha-right", 30, func(f string) (string, error) {
|
|
hashed++
|
|
// 按文件名给摘要:这样「选中谁」和「算了几次」互相独立——
|
|
// 去掉预筛时选中的仍然是 right,但白读了两个诱饵(hashed=3)。
|
|
return "sha-" + strings.TrimSuffix(filepath.Base(f), ".rondbackup"), nil
|
|
})
|
|
if got != right {
|
|
t.Errorf("应当选中 %s,实际 %s", filepath.Base(right), filepath.Base(got))
|
|
}
|
|
if hashed != 1 {
|
|
t.Errorf("只该对大小相符的那份算摘要,实际算了 %d 次(白读了 %d 份整包)", hashed, hashed-1)
|
|
}
|
|
}
|
|
|
|
// 反向约束:筛选只许按大小筛,不许把大小恰好相符的候选也一起跳过。
|
|
// 三份大小相同、只有最后一份摘要对得上 ⇒ 必须老老实实算三次。
|
|
func TestPickArchiveHashesEverySameSizedCandidate(t *testing.T) {
|
|
dir := t.TempDir()
|
|
a := mkFile(t, dir, "a.rondbackup", 30)
|
|
b := mkFile(t, dir, "b.rondbackup", 30)
|
|
c := mkFile(t, dir, "c.rondbackup", 30)
|
|
|
|
hashed := 0
|
|
got := pickArchiveBySha([]string{a, b, c}, "sha-c", 30, func(f string) (string, error) {
|
|
hashed++
|
|
return "sha-" + strings.TrimSuffix(filepath.Base(f), ".rondbackup"), nil
|
|
})
|
|
if got != c {
|
|
t.Errorf("应当选中 c.rondbackup,实际 %s", filepath.Base(got))
|
|
}
|
|
if hashed != 3 {
|
|
t.Errorf("大小都相符时不能跳过任何一份,实际只算了 %d 次", hashed)
|
|
}
|
|
}
|
|
|
|
// 退化路径:库里没记 sha(老数据)时不按大小筛,保持「取最新的那份」的旧行为——
|
|
// 否则会因为记不到 size 而突然一份都选不中,导出直接失败。
|
|
func TestPickArchiveKeepsNewestWhenNoSha(t *testing.T) {
|
|
dir := t.TempDir()
|
|
newest := mkFile(t, dir, "newest.rondbackup", 10)
|
|
older := mkFile(t, dir, "older.rondbackup", 999)
|
|
|
|
hashed := 0
|
|
got := pickArchiveBySha([]string{newest, older}, "", 0, func(f string) (string, error) {
|
|
hashed++
|
|
return "whatever", nil
|
|
})
|
|
if got != newest {
|
|
t.Errorf("没有 sha 时应当取第一份可读的,实际 %s", filepath.Base(got))
|
|
}
|
|
if hashed != 1 {
|
|
t.Errorf("没有 sha 时不该多算,实际算了 %d 次", hashed)
|
|
}
|
|
}
|
|
|
|
// 目录与读不到的文件要跳过,不能因为一份坏候选就整段失败。
|
|
func TestPickArchiveSkipsDirsAndUnreadable(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(dir, "subdir"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
missing := filepath.Join(dir, "does-not-exist.rondbackup")
|
|
ok := mkFile(t, dir, "ok.rondbackup", 5)
|
|
|
|
got := pickArchiveBySha([]string{missing, filepath.Join(dir, "subdir"), ok}, "sha-ok", 5, func(f string) (string, error) {
|
|
if f == ok {
|
|
return "sha-ok", nil
|
|
}
|
|
t.Errorf("不该对 %s 算摘要", f)
|
|
return "nope", nil
|
|
})
|
|
if got != ok {
|
|
t.Errorf("应当跳过缺失与目录后选中 ok.rondbackup,实际 %q", got)
|
|
}
|
|
|
|
// 找不到时返回空串(调用方据此报「请重新上传」)
|
|
if s := pickArchiveBySha([]string{ok}, "sha-none", 5, func(string) (string, error) { return "other", nil }); s != "" {
|
|
t.Errorf("没有命中时应返回空串,实际 %q", s)
|
|
}
|
|
}
|
|
|
|
// findSourceArchive 必须把 size_bytes 一起查出来——只查 sha 的话上面那道筛选就永远不生效,
|
|
// 而且编译器不会拦(筛挂掉、越筛越慢,行为测试看不出来)。
|
|
func TestFindSourceArchiveConsultsSizeBytes(t *testing.T) {
|
|
src, err := os.ReadFile(pkgFile(t, "handlers_export.go"))
|
|
if err != nil {
|
|
t.Fatalf("读 handlers_export.go 失败: %v", err)
|
|
}
|
|
body := sliceFunc(t, string(src), "func (s *Server) findSourceArchive(")
|
|
if !strings.Contains(body, "size_bytes") {
|
|
t.Error("findSourceArchive 没有读取 size_bytes:按大小预筛会永远拿不到目标大小")
|
|
}
|
|
if !strings.Contains(body, "pickArchiveBySha(") {
|
|
t.Error("findSourceArchive 没有走 pickArchiveBySha:按大小预筛没有生效")
|
|
}
|
|
}
|