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 再用应用侧读查询真打一遍, 迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
139 lines
4.8 KiB
Go
139 lines
4.8 KiB
Go
package web
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http/httptest"
|
||
"net/url"
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// sliceFunc 取出 src 里某个函数的函数体(从签名到下一个顶层 func 之前)。
|
||
// 找不到直接 Fatal——否则静态守卫会因为「切片切空了」而假绿。
|
||
func sliceFunc(t *testing.T, src, sig string) string {
|
||
t.Helper()
|
||
i := strings.Index(src, sig)
|
||
if i < 0 {
|
||
t.Fatalf("源码里找不到 %s,静态守卫需要同步更新", sig)
|
||
}
|
||
body := src[i:]
|
||
if j := strings.Index(body[len(sig):], "\nfunc "); j >= 0 {
|
||
body = body[:len(sig)+j]
|
||
}
|
||
if len(body) < 200 {
|
||
t.Fatalf("%s 的函数体只切到 %d 字节,切片判据失效", sig, len(body))
|
||
}
|
||
return body
|
||
}
|
||
|
||
// D5 回归(静态):收藏切换必须是「一条语句翻转并回读」。
|
||
//
|
||
// 以前是 UPDATE 之后再单独 SELECT 回读,那次回读一旦失败就落进 else 报「已取消收藏」——
|
||
// 状态根本没读出来,这句话有一半是错的。换成 UPDATE ... RETURNING 后没有第二次读,
|
||
// 也就没有「读失败只能猜」这个分支。
|
||
//
|
||
// 为什么只能静态守:要让那条独立回读失败得先有故障注入(本项目还没有这套设施),
|
||
// 而正常路径下新旧写法的输出完全一样,行为测试分辨不出来。
|
||
func TestVisitBookmarkTogglesInOneStatement(t *testing.T) {
|
||
src, err := os.ReadFile(pkgFile(t, "handlers_edit.go"))
|
||
if err != nil {
|
||
t.Fatalf("读 handlers_edit.go 失败: %v", err)
|
||
}
|
||
body := sliceFunc(t, string(src), "func (s *Server) adminVisitBookmark(")
|
||
|
||
if !strings.Contains(body, "RETURNING bookmarked") {
|
||
t.Error("收藏切换应当用 UPDATE ... RETURNING 一条语句翻转并回读")
|
||
}
|
||
if strings.Contains(body, "SELECT bookmarked") {
|
||
t.Error("不该再有独立的回读语句:它失败时无法知道状态,只能猜")
|
||
}
|
||
if strings.Contains(body, "err == nil && on") {
|
||
t.Error("不许把「读失败」和「读到 false」合成一个分支——那正是误报「已取消收藏」的写法")
|
||
}
|
||
if !strings.Contains(body, "s.serverError(w, r, err)") {
|
||
t.Error("语句失败必须按错误上报,不能伪装成成功")
|
||
}
|
||
}
|
||
|
||
// D5 回归(行为):连点两次星标要真的来回翻,且提示必须与库里的状态一致。
|
||
// 「翻过来但提示说反了」这种错,只有把提示和库内实况对起来才看得出来。
|
||
func TestVisitBookmarkFlashMatchesRow(t *testing.T) {
|
||
f := newMovementFixture(t)
|
||
|
||
var src int
|
||
if err := f.pg.QueryRowContext(f.ctx, `SELECT src_pk FROM visits
|
||
WHERE dataset_id=$1 ORDER BY src_pk LIMIT 1`, f.dsID).Scan(&src); err != nil {
|
||
t.Fatalf("找不到到访记录: %v", err)
|
||
}
|
||
t.Cleanup(func() {
|
||
f.pg.ExecContext(f.ctx, `UPDATE visits SET bookmarked=FALSE WHERE dataset_id=$1 AND src_pk=$2`,
|
||
f.dsID, src)
|
||
})
|
||
|
||
click := func() (*httptest.ResponseRecorder, bool) {
|
||
t.Helper()
|
||
form := url.Values{"src_pk": {fmt.Sprint(src)}}
|
||
r := httptest.NewRequest("POST", "/admin/edit/visit/bookmark", strings.NewReader(form.Encode()))
|
||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||
r = r.WithContext(f.ctx)
|
||
rec := httptest.NewRecorder()
|
||
f.srv.adminVisitBookmark(rec, r)
|
||
|
||
var on bool
|
||
if err := f.pg.QueryRowContext(f.ctx, `SELECT bookmarked FROM visits
|
||
WHERE dataset_id=$1 AND src_pk=$2`, f.dsID, src).Scan(&on); err != nil {
|
||
t.Fatalf("读库内收藏状态失败: %v", err)
|
||
}
|
||
return rec, on
|
||
}
|
||
|
||
rec, on := click()
|
||
if !on {
|
||
t.Fatalf("点一次应当把收藏打开,库里仍是 %v", on)
|
||
}
|
||
if msg := flashMsg(t, rec); msg != "已标记为收藏" {
|
||
t.Errorf("提示 %q 与库内状态(已收藏)不一致", msg)
|
||
}
|
||
|
||
rec, on = click()
|
||
if on {
|
||
t.Fatalf("再点一次应当取消收藏,库里仍是 %v", on)
|
||
}
|
||
if msg := flashMsg(t, rec); msg != "已取消收藏" {
|
||
t.Errorf("提示 %q 与库内状态(未收藏)不一致", msg)
|
||
}
|
||
|
||
// 不存在的记录要报错,不能假装成功
|
||
form := url.Values{"src_pk": {"2147483000"}}
|
||
r := httptest.NewRequest("POST", "/admin/edit/visit/bookmark", strings.NewReader(form.Encode()))
|
||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||
r = r.WithContext(f.ctx)
|
||
rec = httptest.NewRecorder()
|
||
f.srv.adminVisitBookmark(rec, r)
|
||
if msg := flashMsg(t, rec); strings.Contains(msg, "已") {
|
||
t.Errorf("对不存在的记录不该回一句成功提示: %q", msg)
|
||
}
|
||
}
|
||
|
||
// flashMsg 从重定向的 Location 里取出 flash 文案(redirectEdit 把提示挂在查询串上)。
|
||
func flashMsg(t *testing.T, rec *httptest.ResponseRecorder) string {
|
||
t.Helper()
|
||
loc := rec.Header().Get("Location")
|
||
if loc == "" {
|
||
t.Fatalf("没有重定向,实际状态 %d:%s", rec.Code, firstLine(rec.Body.String()))
|
||
}
|
||
u, err := url.Parse(loc)
|
||
if err != nil {
|
||
t.Fatalf("解析 Location 失败: %v", err)
|
||
}
|
||
msg := u.Query().Get("ok")
|
||
if msg == "" {
|
||
msg = u.Query().Get("err")
|
||
}
|
||
if msg == "" {
|
||
t.Fatalf("Location 里没有提示文案: %s", loc)
|
||
}
|
||
return msg
|
||
}
|