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 再用应用侧读查询真打一遍, 迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
109 lines
3.8 KiB
Go
109 lines
3.8 KiB
Go
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// adminNotesBulk 必须走批量语句。
|
|
//
|
|
// 旧实现是 `for _, id := range ids` 里逐条 Delete/SetNote/SaveVerdict:勾 50 个地点就是
|
|
// 50(清空是 100)次数据库往返,在 1C1G 的机器上体感很明显。这条守卫钉住「不许退回逐条循环」。
|
|
func TestAdminNotesBulkUsesBulkStatements(t *testing.T) {
|
|
b, err := os.ReadFile(pkgFile(t, "handlers_admin.go"))
|
|
if err != nil {
|
|
t.Fatalf("读取 handlers_admin.go 失败: %v", err)
|
|
}
|
|
fn := funcBody(string(b), "func (s *Server) adminNotesBulk(")
|
|
if fn == "" {
|
|
t.Fatal("在 handlers_admin.go 里找不到 adminNotesBulk(写法变了,这条守卫就失效了)")
|
|
}
|
|
for _, banned := range []string{"s.notes.Delete(", "s.notes.SetNote(", "s.votes.SaveVerdict("} {
|
|
if strings.Contains(fn, banned) {
|
|
t.Errorf("adminNotesBulk 不该再逐条调用 %s —— 应改用对应的 *Bulk 方法", banned)
|
|
}
|
|
}
|
|
for _, want := range []string{"s.notes.DeleteBulk(", "s.notes.SetNoteBulk(", "s.votes.SaveVerdictBulk("} {
|
|
if !strings.Contains(fn, want) {
|
|
t.Errorf("adminNotesBulk 未改用批量方法 %s", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 批量勾选必须真的作用到**每一个**被勾的地点(不是只改第一条)。
|
|
func TestAdminNotesBulkAppliesToEveryCheckedPlace(t *testing.T) {
|
|
f := newMovementFixture(t)
|
|
uid := userFrom(f.ctx).ID
|
|
pks := []int64{680001, 680002, 680003}
|
|
|
|
for i, pk := range pks {
|
|
if _, err := f.pg.ExecContext(f.ctx, `INSERT INTO places (dataset_id, src_pk, name, lat, lon)
|
|
VALUES ($1,$2,'批量点',$3,$4)`, f.dsID, pk,
|
|
30.0+float64(i)*0.01, 120.0+float64(i)*0.01); err != nil {
|
|
t.Fatalf("建地点失败: %v", err)
|
|
}
|
|
}
|
|
t.Cleanup(func() {
|
|
f.pg.ExecContext(f.ctx, `DELETE FROM place_notes WHERE user_id=$1`, uid)
|
|
f.pg.ExecContext(f.ctx, `DELETE FROM place_votes WHERE user_id=$1`, uid)
|
|
f.pg.ExecContext(f.ctx, `DELETE FROM places WHERE dataset_id=$1 AND src_pk = ANY($2::bigint[])`,
|
|
f.dsID, pks)
|
|
})
|
|
|
|
post := func(vals url.Values) {
|
|
t.Helper()
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest("POST", "/admin/notes/bulk", strings.NewReader(vals.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
f.srv.adminNotesBulk(rec, req.WithContext(f.ctx))
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("批量操作应跳回页面: code=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
bulk := func(action string, extra ...string) url.Values {
|
|
v := url.Values{"action": {action}}
|
|
for i := 0; i+1 < len(extra); i += 2 {
|
|
v.Set(extra[i], extra[i+1])
|
|
}
|
|
for _, pk := range pks {
|
|
v.Add("src_pk", strconv.FormatInt(pk, 10))
|
|
}
|
|
return v
|
|
}
|
|
|
|
// 先把三个地点都写上别名 + 备注
|
|
for _, pk := range pks {
|
|
if err := f.srv.notes.Save(f.ctx, uid, f.dsID, int(pk), "别名", "备注"); err != nil {
|
|
t.Fatalf("预置备注失败: %v", err)
|
|
}
|
|
}
|
|
|
|
post(bulk("verdict", "verdict", "1"))
|
|
if n := f.scalar(t, `SELECT count(*) FROM place_votes WHERE user_id=$1 AND verdict=1`, uid); n != len(pks) {
|
|
t.Fatalf("三个地点都该设上结论,实际 %d", n)
|
|
}
|
|
|
|
post(bulk("clear"))
|
|
if n := f.scalar(t, `SELECT count(*) FROM place_notes WHERE user_id=$1 AND note <> ''`, uid); n != 0 {
|
|
t.Fatalf("清空后不该还有手写备注,剩 %d 条", n)
|
|
}
|
|
if n := f.scalar(t, `SELECT count(*) FROM place_notes WHERE user_id=$1 AND alias='别名'`, uid); n != len(pks) {
|
|
t.Fatalf("清空备注要保留别名,实际剩 %d 条", n)
|
|
}
|
|
|
|
post(bulk("verdict", "verdict", "0"))
|
|
if n := f.scalar(t, `SELECT count(*) FROM place_votes WHERE user_id=$1`, uid); n != 0 {
|
|
t.Fatalf("取消结论后不该还有记录,剩 %d 条", n)
|
|
}
|
|
|
|
post(bulk("delete"))
|
|
if n := f.scalar(t, `SELECT count(*) FROM place_notes WHERE user_id=$1`, uid); n != 0 {
|
|
t.Fatalf("删除后不该还有备注,剩 %d 条", n)
|
|
}
|
|
}
|