Files
rond-with-you/internal/web/frontend_fixes_test.go
T
tamakyi d5f6dfa381 审计修复:前端(模板 / CSS / JS 与无障碍)
- 地图脚本:localStorage 读写走 lsGet/lsSet 兜底(隐私模式下裸访问会打断整段脚本);
  preview() 在 /api/map-config 还没回来时不丢点(previewQueue 在 if (live) 之前排队);
  异步回包一律发序号作废,取消后回来的包不再画
- photo-exif.js:EXIF 时间全 0 不再变成 1899-11-30 写进到访时间;读 count 前先按版本卡宽度
- 数据编辑页:三个筛选表单都把其余条件用隐藏域原样带上,提交不再把别的列表的筛选清掉
- 补全页:sessionStorage 的读写一律包 try(脏数据/隐私模式都会抛)
- 窄屏:删掉被后面同特异性规则盖掉的死内边距;宽表堆叠类名成对、判据扩到所有 tb 表
- 无障碍:图片删除按钮补 aria-label,访客画廊图片补 alt
- report.html 补上漏掉的 </tr>,fog-mask 的宽表带 tb-stack

本批不碰数据库与导出路径:往返包 823/323/8/30/791/557/2246 全等、
坐标偏差 0.000000000、时间偏差 0s、迷雾 8701 块一致。
2026-09-24 20:37:57 +08:00

201 lines
7.5 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 (
"os"
"strings"
"testing"
)
// ---------- F5:地图脚本里的 localStorage 访问必须走兜底 ----------
//
// 隐私模式 / 禁用 Cookie 的浏览器里 localStorage 读写都会抛异常,裸调用会把整段脚本打断
// (地图白屏、图层不出来,且控制台之外看不出来)。统一走 lsGet/lsSet。
func TestMapLocalStorageGoesThroughGuard(t *testing.T) {
b, err := staticFS.ReadFile("static/js/rond-map.js")
if err != nil {
t.Fatalf("读取 rond-map.js 失败: %v", err)
}
src := string(b)
if !strings.Contains(src, "function lsGet(k)") || !strings.Contains(src, "function lsSet(k, v)") {
t.Fatal("rond-map.js 里找不到 lsGet/lsSet 兜底(写法变了,这条守卫失效)")
}
if n := strings.Count(src, "window.localStorage."); n != 2 {
t.Errorf("裸 localStorage 访问应只剩 lsGet/lsSet 里那 2 处,实际 %d 处"+
"(其余一律要走 lsGet/lsSet,否则隐私模式下整段脚本会抛异常)", n)
}
}
// ---------- F6:配置未就绪时的第二次 preview 不能丢点 ----------
func TestMapPreviewQueuesPendingPoints(t *testing.T) {
b, err := staticFS.ReadFile("static/js/rond-map.js")
if err != nil {
t.Fatalf("读取 rond-map.js 失败: %v", err)
}
src := string(b)
body := funcBody(src, "preview: function (opts)")
if body == "" {
t.Fatal("在 rond-map.js 里找不到 preview(写法变了,这条守卫失效)")
}
queue := strings.Index(body, "previewQueue[opts.id] = points;")
live := strings.Index(body, "if (live) {")
if queue < 0 || live < 0 || queue > live {
t.Error("preview:必须在 `if (live)` 之前就把本次的点记进 previewQueue," +
"否则配置未就绪时第二次 preview 的点会被静默丢掉")
}
if !strings.Contains(body, "if (previewQueue[opts.id]) points = previewQueue[opts.id];") {
t.Error("preview:配置回调里没把 previewQueue 排队的点取回来画")
}
if strings.Contains(src, "previewPending") {
t.Error("rond-map.js 里不该再有 previewPending(已由 previewQueue 取代)")
}
}
// ---------- F8:EXIF 全 0 时间不能变成 1899-11-30 ----------
func TestExifRejectsOutOfRangeDate(t *testing.T) {
b, err := staticFS.ReadFile("static/js/photo-exif.js")
if err != nil {
t.Fatalf("读取 photo-exif.js 失败: %v", err)
}
src := string(b)
guard := strings.Index(src, "Y >= 1900")
mk := strings.Index(src, "res.shot = new Date(Y, Mo - 1, D, H, Mi, S);")
flag := strings.Index(src, "res.hasTime = true;")
if guard < 0 || mk < 0 || flag < 0 || guard > mk || guard > flag {
t.Error("photo-exif.js:拍摄时间必须先过范围校验再赋给 res.shot / hasTime" +
"(否则「0000:00:00」会被 Date 归一成 1899-11-30,被当成到达时间写进库)")
}
}
// ---------- F9:findExifItemID 读 count 前必须卡住宽度 ----------
func TestExifItemIDChecksBoundsBeforeRead(t *testing.T) {
b, err := staticFS.ReadFile("static/js/photo-exif.js")
if err != nil {
t.Fatalf("读取 photo-exif.js 失败: %v", err)
}
body := funcBody(string(b), "function findExifItemID(")
if body == "" {
t.Fatal("在 photo-exif.js 里找不到 findExifItemID(写法变了,这条守卫失效)")
}
bounds := strings.Index(body, "p + 2 > u8.length")
read := strings.Index(body, "dv.getUint16(p)")
if bounds < 0 || read < 0 || bounds > read {
t.Error("findExifItemID:读 count 之前必须按 ver 卡住宽度(p+2 / p+4)——" +
"末尾恰好相等(start+4 == length)时旧守卫会放行,越界会抛 RangeError")
}
}
// ---------- F16:数据编辑页每个筛选表单都要带上全部筛选条件 ----------
//
// 三个表单各自只放自己那几个控件,其余条件靠隐藏域带着;漏一个,用户一提交
// 就会把别的列表的筛选悄悄清掉(原来「到访」表单只带 vq+back)。
func TestEditSearchFormsCarryAllFilters(t *testing.T) {
b, err := tmplFS.ReadFile("templates/admin/edit.html")
if err != nil {
t.Fatalf("读取 edit.html 失败: %v", err)
}
src := string(b)
const open = `<form method="get" action="/admin/edit"`
params := []string{`name="q"`, `name="src"`, `name="sus"`, `name="vq"`,
`name="mt"`, `name="mp"`, `name="mfrom"`, `name="mto"`}
n := 0
rest := src
for {
i := strings.Index(rest, open)
if i < 0 {
break
}
n++
rest = rest[i:]
j := strings.Index(rest, "</form>")
if j < 0 {
t.Fatalf("第 %d 个筛选表单没有收尾", n)
}
body := rest[:j]
for _, p := range params {
if !strings.Contains(body, p) {
t.Errorf("第 %d 个筛选表单少了 %s:一提交就会把别的列表的筛选清掉", n, p)
}
}
rest = rest[j:]
}
if n < 3 {
t.Fatalf("只找到 %d 个 /admin/edit 筛选表单,预期至少 3 个(写法变了,这条守卫失效)", n)
}
}
// ---------- F17:`safe` 这个现成的转义出口不该留着 ----------
func TestNoSafeEscapeFunc(t *testing.T) {
b, err := os.ReadFile(pkgFile(t, "view.go"))
if err != nil {
t.Fatalf("读取 view.go 失败: %v", err)
}
if strings.Contains(string(b), `"safe":`) {
t.Error("view.go 里不该再有 `safe` 模板函数(模板一处都没用,却是个现成的 HTML 注入出口)")
}
}
// ---------- F18:纯图标按钮要有 aria-label,图片要有 alt ----------
func TestPlaceImageControlsAreLabelled(t *testing.T) {
b, err := tmplFS.ReadFile("templates/place.html")
if err != nil {
t.Fatalf("读取 place.html 失败: %v", err)
}
src := string(b)
if !strings.Contains(src, `aria-label="删除这张图片"`) {
t.Error("place.html 模板渲染的删除按钮(×)缺 aria-label")
}
if !strings.Contains(src, `setAttribute('aria-label', '删除这张图片')`) {
t.Error("place.html 用 JS 新建的删除按钮(×)缺 aria-label")
}
if strings.Contains(src, `img.alt = ''`) {
t.Error("place.html 新建缩略图时 alt 不能留空")
}
}
// ---------- F14:窄屏导航链接的内边距别再写死一处(会被后面的触控块盖掉) ----------
func TestNavLinksNarrowRuleHasNoDeadPadding(t *testing.T) {
b, err := os.ReadFile(pkgFile(t, "static/css/app.css"))
if err != nil {
t.Fatalf("读取 app.css 失败: %v", err)
}
src := string(b)
if !strings.Contains(src, ".nav-links a { font-size: 13px; }") {
t.Fatal("找不到窄屏的 `.nav-links a { font-size: 13px; }`(写法变了,这条守卫失效)")
}
if strings.Contains(src, ".nav-links a { padding:") {
t.Error("窄屏 .nav-links a 又写了 padding:同特异性的内边距由后面「窄屏触控」那块给," +
"写在这里会被盖掉(死代码)")
}
}
// ---------- F10:补全页的 sessionStorage 读写都要包在 try 里 ----------
//
// 隐私模式下 sessionStorage 本身就抛,脏数据又会让 JSON.parse 抛;任何一处漏了 try,
// 整段脚本就从那一行断掉——下拉候选、草稿恢复全部哑掉,而且控制台之外看不出来。
func TestFillupSessionStorageIsGuarded(t *testing.T) {
b, err := tmplFS.ReadFile("templates/admin/fillup.html")
if err != nil {
t.Fatalf("读取 fillup.html 失败: %v", err)
}
src := string(b)
needles := []string{"JSON.parse(sessionStorage", "sessionStorage.setItem"}
total := 0
for _, n := range needles {
rest := src
for {
i := strings.Index(rest, n)
if i < 0 {
break
}
total++
if open := strings.LastIndex(rest[:i], "try {"); open < 0 || i-open > 160 {
t.Errorf("fillup.html:`%s` 前面没有紧挨着的 try {(隐私模式 / 脏数据会中断整段脚本)", n)
}
rest = rest[i+len(n):]
}
}
if total == 0 {
t.Fatal("fillup.html 里找不到 sessionStorage 读写(写法变了,这条守卫失效)")
}
}