Files
rond-with-you/internal/web/visit_bookmark_test.go
T
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

139 lines
4.8 KiB
Go
Raw 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 (
"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
}