first release

This commit is contained in:
tamakyi
2026-09-18 13:05:49 +08:00
commit b327bc1230
123 changed files with 30003 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
package web
import (
"context"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
)
// withVisitorRegions 临时给「访客可见区域」配上一项,跑完还原。
//
// 这类 bug 只在访客视角 + 配了区域时才犯(`visitorRegions` 对站长返回空),
// 站长自己点一遍是好的,所以必须专门造这个组合。
func withVisitorRegions(t *testing.T, f *movementFixture) {
t.Helper()
var prev []byte
if err := f.pg.QueryRowContext(f.ctx, `SELECT value FROM settings WHERE key='site'`).Scan(&prev); err != nil {
t.Fatalf("读站点设置失败: %v", err)
}
write := func(v []byte) {
t.Helper()
if _, err := f.pg.ExecContext(f.ctx,
`UPDATE settings SET value=$1::jsonb WHERE key='site'`, string(v)); err != nil {
t.Fatalf("写站点设置失败: %v", err)
}
}
write([]byte(`{"regions":["河池市"],"regions_mode":"blacklist"}`))
t.Cleanup(func() { write(prev) })
}
// TestVisitorPagesWithRegionFilter 用**访客身份**把公开页面走一遍,只要求不 500。
//
// 专为「某个查询漏了区域条件、或者参数与占位符对不上」准备:这类错误犯过两次——
// 一次是 movements 的里程没跟着区域走,一次是专题的「天数」查询把上一条 SQL 的参数
// 整个传进来(多了区域那几个),pgx 报 expected N arguments, got M;
// 而专题列表会把错误吞掉,表现成「专题内容全变 0、点进去才 500」。
// 两次都只在访客视角复现,站长自己试是好的——所以这个组合必须专门测。
func TestVisitorPagesWithRegionFilter(t *testing.T) {
f := newMovementFixture(t)
withVisitorRegions(t, f)
var placeID, topicID int64
if err := f.pg.QueryRowContext(f.ctx, `SELECT id FROM places WHERE dataset_id=$1 ORDER BY id LIMIT 1`,
f.dsID).Scan(&placeID); err != nil {
t.Fatalf("取地点失败: %v", err)
}
// 专题页正是这次踩坑的地方,没有现成的就建一个覆盖几天到访的
if err := f.pg.QueryRowContext(f.ctx, `SELECT id FROM topics WHERE dataset_id=$1 ORDER BY id LIMIT 1`,
f.dsID).Scan(&topicID); err != nil {
if err := f.pg.QueryRowContext(f.ctx, `INSERT INTO topics
(dataset_id, user_id, title, start_at, end_at, enabled)
VALUES ($1,$2,'访客冒烟','2020-01-01T00:00:00+08:00','2030-01-01T00:00:00+08:00',true)
RETURNING id`, f.dsID, userFrom(f.ctx).ID).Scan(&topicID); err != nil {
t.Fatalf("建测试专题失败: %v", err)
}
t.Cleanup(func() {
f.pg.ExecContext(f.ctx, `DELETE FROM topics WHERE id=$1`, topicID)
})
}
pages := []struct {
name string
path string
handle http.HandlerFunc
param string // PathValue("id") 用真实 id 填充
}{
{"主页", "/", f.srv.home, ""},
{"地点库", "/places", f.srv.placesPage, ""},
{"地点详情", "/places/x", f.srv.placePage, strconv.FormatInt(placeID, 10)},
{"统计", "/stats", f.srv.statsPage, ""},
{"时间线", "/timeline", f.srv.timelinePage, ""},
{"年度报告", "/report", f.srv.reportPage, ""},
{"地图", "/map", f.srv.mapPage, ""},
{"专题列表", "/topics", f.srv.topicsListPage, ""},
{"专题详情", "/topics/x", f.srv.topicPage, strconv.FormatInt(topicID, 10)},
{"点位接口", "/api/points", f.srv.apiPoints, ""},
{"轨迹接口", "/api/track", f.srv.apiTrack, ""},
}
for _, p := range pages {
t.Run(p.name, func(t *testing.T) {
req := httptest.NewRequest("GET", p.path, nil)
// 访客 = 上下文里没有登录态(withUser 中间件没跑)
req = req.WithContext(context.Background())
if p.param != "" {
req.SetPathValue("id", p.param)
}
rec := httptest.NewRecorder()
p.handle(rec, req)
if rec.Code >= 500 {
t.Errorf("%s 访客访问返回 %d:%s", p.path, rec.Code, firstLine(rec.Body.String()))
}
})
}
}
func firstLine(s string) string {
if i := strings.IndexByte(s, '\n'); i >= 0 {
s = s[:i]
}
if len(s) > 200 {
s = s[:200]
}
return s
}