Files
rond-with-you/internal/web/pagelink_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

134 lines
5.2 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 (
"html/template"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
// 数据编辑页的翻页链接必须「带条件、不双重转义」。这两件事都踩过坑:
//
// - 等号被 URL 上下文转义成 %3d:`mt%3d4` 在服务端解出来是「键为 mt=4」的参数,
// 筛选条件翻页时静默失效 —— 所以 PageLink 返回 template.URL;
// - & 被手工写成 &:template.URL 在 href 里仍会过一次 urlNormalizer,它自己会做
// HTML 属性转义,手工写一遍的结果是 `&`,浏览器解出来是「字面的 &」,
// 参数名变成 `amp;vpage`,表现为「带筛选时翻页没反应、筛选还丢了」(实测确认过)。
func TestEditPageLink(t *testing.T) {
d := EditData{Page: Page{Back: template.URL("/places?city=河池市")}}
d.Query = "家"
d.VisitQuery = "咖啡"
got := string(d.PageLink("vpage", 2))
if strings.Contains(got, "&") {
t.Errorf("含 & 会被二次转义成 &:%s", got)
}
for _, want := range []string{"q=%E5%AE%B6", "vq=", "back=%2Fplaces", "vpage=2"} {
if !strings.Contains(got, want) {
t.Errorf("翻页链接丢了 %q:%s", want, got)
}
}
// 键值之间的等号必须是裸的:写成 %3d 时服务端解出来是「键为 vpage=2」的参数,
// 翻页静默失效。上面逐项断言 "vpage=2" 就是在守这条。
// (back 的值里出现 %3D 是正常的 —— 那是内层地址自己的编码,必须保持。)
if !strings.HasPrefix(got, "/admin/edit?") {
t.Errorf("链接开头不对:%s", got)
}
// 分隔符必须是裸的 &(交给模板那层做属性转义)
if strings.Count(got, "&") != 3 {
t.Errorf("参数分隔符数量不对(期望 3 个裸 &):%s", got)
}
// 没有 back 时不该凭空多出参数
bare := string(EditData{}.PageLink("page", 3))
if bare != "/admin/edit?page=3" {
t.Errorf("无筛选时链接不对:%s", bare)
}
}
// 「在地图中查看」把地点名当查询参数拼进地址,名字里的 & / # / 空格都必须编码:
// 直接内插的话 & 会把参数拆断、# 会把后面整段当 fragment 吃掉。
func TestMapLink(t *testing.T) {
got := string(Page{}.MapLink("A&B #咖啡 100%"))
want := "/map?q=A%26B+%23%E5%92%96%E5%95%A1+100%25"
if got != want {
t.Errorf("MapLink = %q,期望 %q", got, want)
}
if got := string(Page{}.MapLink(" 家 ")); got != "/map?q=%E5%AE%B6" {
t.Errorf("首尾空白应去掉:%q", got)
}
}
// EditPlaceLink 是「在数据编辑页打开这个地点」,它把返回地址挂在 ?back= 上。
//
// 这里只允许编码**一次**。退回当前页时以前用的是 BackQS(已经 QueryEscape 过),
// 再转一次就成了 %252F…,服务端解一次拿到 %2F… 不以 / 开头,被 backFromRequest
// 当站外地址丢掉 —— 直接打开 /places/1 再点「编辑」,编辑页的返回按钮就凭空消失。
func TestEditPlaceLinkBackIsEncodedOnce(t *testing.T) {
const self = "/places/1?city=河池市&sort=dwell"
cases := []struct {
name string
p Page
want string // back 参数解码一次后的原值
}{
{"列表页给的 back", Page{
Back: template.URL("/places?city=河池市"),
SelfPath: self,
BackQS: template.URL(url.QueryEscape(self)),
}, "/places?city=河池市"},
{"back 为空时退回当前页", Page{
SelfPath: self,
BackQS: template.URL(url.QueryEscape(self)),
}, self},
}
for _, c := range cases {
got := string(c.p.EditPlaceLink(2141))
i := strings.Index(got, "&back=")
if i < 0 {
t.Fatalf("%s:链接里没有 back 参数:%s", c.name, got)
}
encoded := got[i+len("&back="):]
raw, err := url.QueryUnescape(encoded)
if err != nil {
t.Fatalf("%s:back 值解不出来:%v", c.name, err)
}
if raw != c.want {
t.Errorf("%s:back 解码一次 = %q,期望 %q(链接 %s)", c.name, raw, c.want, got)
}
// 服务端真的会收这一份:解出来的必须还能通过站内校验,否则返回按钮是空的
r := httptest.NewRequest("GET", "/admin/edit?open=2141&back="+encoded, nil)
if bk := backFromRequest(r); string(bk) != c.want {
t.Errorf("%s:服务端解出 back=%q,期望 %q —— 返回按钮会消失", c.name, bk, c.want)
}
}
}
// 返回按钮的文案要从 back 里认出来源。踩过的坑:先取「第一个路径段」但没剥查询串,
// 于是 `/places?city=X` 这种「带筛选、没有下级路径」的整串都是第一段,谁都匹配不上,
// 按钮退化成光秃秃的「返回」。
func TestBackLabel(t *testing.T) {
cases := []struct{ back, want string }{
{"/places?city=河池市&sort=dwell", "返回地点库"},
{"/places/2141", "返回地点库"},
{"/timeline?from=2026-01-01", "返回时间线"},
{"/map?q=家", "返回地图"},
{"/stats", "返回数据统计"},
{"/topics/3", "返回专题"},
{"/admin/edit?q=家", "返回数据编辑"},
{"/admin", "返回后台"},
{"/", "返回首页"},
{"/something-else", "返回"},
{"", ""},
}
for _, c := range cases {
p := Page{}
if c.back != "" {
p.Back = template.URL(c.back)
}
if got := p.BackLabel(); got != c.want {
t.Errorf("BackLabel(%q) = %q,期望 %q", c.back, got, c.want)
}
}
}