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), "") { 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("访客版年报里把站长的一年说成了「你」的") } }