用户发现未登录也显示「推荐 & 踩雷(我的结论)」,访客没法解读「谁的我的」。
顺着把访客可见模板与 static/js 全过了一遍,同病共四处——都是**写模板时站在站长视角下笔、
而那一块又对访客公开**:
1. stats.html:标题 → {{if .IsAdmin}}我的结论{{else}}站长结论{{end}};
副标题「只统计你自己…」→ 站长「你自己」/ 访客「站长」;两列「我推荐/我踩雷的地点」同理。
2. stats.html 访客块:「与我的结论互不影响:你标你的,访客投访客的」——对访客说不通,
改成对两边都成立的「与上面的站长结论互不影响:各存各的表,谁也不会覆盖谁」。
3. home.html:「我的标签」→「足迹标签」(同级卡片全是中性的:足迹地图/城市足迹/天气足迹…)。
4. report.html:「这一年里,你第一次踏足的这些城市」→ 去掉人称(年报是站长的回顾)。
已经写对的照它学:place.html 的投票区早就是 {{if .IsAdmin}} 分支(站长「我的结论」、
访客「大家怎么说」),rond-map.js 的气泡同理;「含家/工作」chip 与一串「只有你自己能看到」
本来就在站长分支里。
门禁 TestVisitorPagesUseThirdPersonCopy:以**访客 ctx(无 user)**渲染 /、/stats、/report,
逐个禁用词断言,同时断言站长那份**仍旧说「我的」**(别把站长自己的视角也改掉);
前置需打开 marks_public(访客才看得到那一块),用完还原。
⚠️ 变异验证时踩了老坑:直接 `go test -run …` 因缺 ROND_TEST_CONF/ROND_SRC 会**静默 skip 并报 ok**
(0.17s 就 ok,一眼假绿)。改用 ./scripts/verify.sh 后变异被正确抓住
(visitor_copy_test.go:85 报「我的结论」),还原后转绿。
全量验收 PASS=339 FAIL=0 SKIP=0。
110 lines
4.6 KiB
Go
110 lines
4.6 KiB
Go
package web
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// ---------- 措辞要分清「站长视角」与「访客视角」 ----------
|
||
//
|
||
// 用户实测反馈:未登录时统计页也显示「推荐 & 踩雷(我的结论)」,访客会困惑「谁的我的」。
|
||
// 这类毛病都出在同一处:**写模板时站在站长视角下笔**(我/你自己),而那一块又对访客公开。
|
||
// 地点页的投票区早就是按身份分支的(站长看「我的结论」、访客看「大家怎么说」),
|
||
// 这里把它做成回归用例:以**访客身份**取回页面,逐个钉死那些只有站长读得懂的说法。
|
||
|
||
// renderAsVisitor 以访客身份取回页面(ctx 里没有登录态)。
|
||
func renderAsVisitor(t *testing.T, f *movementFixture, h http.HandlerFunc, path string) string {
|
||
t.Helper()
|
||
rec := httptest.NewRecorder()
|
||
// 访客就是「没有 user 的 ctx」:IsAdmin() 看的是 p.User 是否为 nil
|
||
req := httptest.NewRequest("GET", path, nil).WithContext(context.Background())
|
||
h(rec, req)
|
||
if rec.Code != http.StatusOK {
|
||
t.Fatalf("访客取 %s 状态码 %d:%s", path, rec.Code, rec.Body.String())
|
||
}
|
||
body := rec.Body.String()
|
||
if !strings.HasSuffix(strings.TrimSpace(body), "</html>") {
|
||
t.Fatalf("访客取 %s 没渲染到底:…%s", path, body[len(body)-300:])
|
||
}
|
||
return body
|
||
}
|
||
|
||
// renderAsAdmin 以站长身份取回页面。
|
||
func renderAsAdmin(t *testing.T, f *movementFixture, h http.HandlerFunc, path string) string {
|
||
t.Helper()
|
||
rec := httptest.NewRecorder()
|
||
req := httptest.NewRequest("GET", path, nil).WithContext(f.ctx)
|
||
h(rec, req)
|
||
if rec.Code != http.StatusOK {
|
||
t.Fatalf("站长取 %s 状态码 %d:%s", path, rec.Code, rec.Body.String())
|
||
}
|
||
return rec.Body.String()
|
||
}
|
||
|
||
// forceMarksPublic 把「推荐 / 踩雷对访客公开」打开——访客版页面里那一块才会渲染出来。
|
||
// 用例结束后还原设置(站点设置是全局一行,别把后面的用例带偏)。
|
||
func forceMarksPublic(t *testing.T, f *movementFixture) {
|
||
t.Helper()
|
||
var old sql.NullString
|
||
if err := f.pg.QueryRowContext(f.ctx, `SELECT value::text FROM settings WHERE key='site'`).Scan(&old); err != nil && err != sql.ErrNoRows {
|
||
t.Fatalf("读站点设置失败: %v", err)
|
||
}
|
||
if _, err := f.pg.ExecContext(f.ctx, `INSERT INTO settings (key, value)
|
||
VALUES ('site', '{"marks_public":true}'::jsonb)
|
||
ON CONFLICT (key) DO UPDATE SET value = jsonb_set(settings.value, '{marks_public}', 'true'::jsonb, true)`); err != nil {
|
||
t.Fatalf("打开「对访客公开」失败: %v", err)
|
||
}
|
||
t.Cleanup(func() {
|
||
if old.Valid {
|
||
f.pg.ExecContext(f.ctx, `UPDATE settings SET value=$1::jsonb WHERE key='site'`, old.String)
|
||
return
|
||
}
|
||
f.pg.ExecContext(f.ctx, `DELETE FROM settings WHERE key='site'`)
|
||
})
|
||
}
|
||
|
||
// visitorCopyForbidden 是**只有站长读得懂**的说法:访客看到「我 / 你自己」会一头雾水。
|
||
var visitorCopyForbidden = []string{"我的结论", "你自己", "我推荐的地点", "我踩雷的地点", "你标你的", "我的标签"}
|
||
|
||
func TestVisitorPagesUseThirdPersonCopy(t *testing.T) {
|
||
f := newMovementFixture(t)
|
||
forceMarksPublic(t, f)
|
||
|
||
// ① 统计页:站长仍旧说「我的」,访客一律第三人称
|
||
adminStats := renderAsAdmin(t, f, f.srv.statsPage, "/stats")
|
||
if !strings.Contains(adminStats, "我的结论") {
|
||
t.Error("站长自己看时应当说「我的结论」——按身份分支,不该把站长那份也改成第三人称")
|
||
}
|
||
visitorStats := renderAsVisitor(t, f, f.srv.statsPage, "/stats")
|
||
for _, bad := range visitorCopyForbidden {
|
||
if strings.Contains(visitorStats, bad) {
|
||
t.Errorf("访客版统计页出现了只有站长读得懂的说法:%q", bad)
|
||
}
|
||
}
|
||
if !strings.Contains(visitorStats, "站长结论") {
|
||
t.Error("访客版统计页应当用「站长结论」这类第三人称说法")
|
||
}
|
||
// 访客那一块自己得说清楚「谁投的」
|
||
if !strings.Contains(visitorStats, "访客点赞") {
|
||
t.Error("访客版统计页应当有独立的「访客点赞」一块")
|
||
}
|
||
|
||
// ② 首页:标签卡原先写「我的标签」,访客看不出是谁的
|
||
visitorHome := renderAsVisitor(t, f, f.srv.home, "/")
|
||
for _, bad := range visitorCopyForbidden {
|
||
if strings.Contains(visitorHome, bad) {
|
||
t.Errorf("访客版首页出现了只有站长读得懂的说法:%q", bad)
|
||
}
|
||
}
|
||
|
||
// ③ 年报:那是站长的年度回顾,不该对访客说「你这一年…」
|
||
visitorReport := renderAsVisitor(t, f, f.srv.reportPage, "/report")
|
||
if strings.Contains(visitorReport, "你第一次踏足") {
|
||
t.Error("访客版年报里把站长的一年说成了「你」的")
|
||
}
|
||
}
|