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 再用应用侧读查询真打一遍, 迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
105 lines
4.7 KiB
Go
105 lines
4.7 KiB
Go
package web
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"fmt"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"net/url"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// TestEditVisitUpdateAtomicOnClampFailure 证明「改到访」整组写入是一个事务:
|
||
// 其中夹行程那步失败时,前面已经写进去的到访时间必须一并回滚。
|
||
//
|
||
// 手法是给 movements 挂一个必然报错的 BEFORE UPDATE 触发器——夹行程(clampVisitMovements)
|
||
// 就是靠 UPDATE movements 落地的,触发器一炸它就返回错误。
|
||
// 回滚生效 ⇒ 到访时间保持原值;回滚失效 ⇒ 到访时间已被改掉(旧代码各写各的、没有事务,
|
||
// 到访那条 UPDATE 早就提交了)。
|
||
func TestEditVisitUpdateAtomicOnClampFailure(t *testing.T) {
|
||
f := newMovementFixture(t)
|
||
ctx, pg, dsID := f.ctx, f.pg, f.dsID
|
||
|
||
// 挑一条被行程引用的到访,连同它那条行程的开始时间
|
||
var visitSrc, movePK int
|
||
var mvSt, mvEn time.Time
|
||
if err := pg.QueryRowContext(ctx, `SELECT m.from_visit_src, m.src_pk, m.started_at, m.ended_at
|
||
FROM movements m WHERE m.dataset_id=$1 AND m.from_visit_src IS NOT NULL
|
||
AND m.started_at IS NOT NULL AND m.ended_at IS NOT NULL
|
||
ORDER BY m.src_pk LIMIT 1`, dsID).Scan(&visitSrc, &movePK, &mvSt, &mvEn); err != nil {
|
||
t.Fatalf("挑不出「被行程引用的到访」: %v", err)
|
||
}
|
||
var origArrival time.Time
|
||
var origDeparture sql.NullTime
|
||
if err := pg.QueryRowContext(ctx, `SELECT arrival, departure FROM visits
|
||
WHERE dataset_id=$1 AND src_pk=$2`, dsID, visitSrc).Scan(&origArrival, &origDeparture); err != nil {
|
||
t.Fatalf("读取到访失败: %v", err)
|
||
}
|
||
|
||
// 把离开时间推到行程开始之后,夹行程就必然要动手改这条行程
|
||
newArrival := mvSt.Add(-2 * time.Hour).Truncate(time.Minute)
|
||
newDeparture := mvSt.Add(time.Hour).Truncate(time.Minute)
|
||
|
||
installMovementUpdateFailTrigger(t, ctx, pg)
|
||
|
||
form := url.Values{
|
||
"src_pk": {fmt.Sprint(visitSrc)},
|
||
"arrival": {newArrival.Local().Format("2006-01-02T15:04")},
|
||
"departure": {newDeparture.Local().Format("2006-01-02T15:04")},
|
||
}
|
||
r := httptest.NewRequest("POST", "/admin/edit/visit/update", strings.NewReader(form.Encode()))
|
||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||
w := httptest.NewRecorder()
|
||
f.srv.editVisitUpdate(w, r.WithContext(f.ctx))
|
||
if w.Code != http.StatusInternalServerError {
|
||
t.Fatalf("夹行程失败应报 500,实际 code=%d body=%s", w.Code, w.Body.String())
|
||
}
|
||
|
||
var gotArrival time.Time
|
||
var gotDeparture sql.NullTime
|
||
if err := pg.QueryRowContext(ctx, `SELECT arrival, departure FROM visits
|
||
WHERE dataset_id=$1 AND src_pk=$2`, dsID, visitSrc).Scan(&gotArrival, &gotDeparture); err != nil {
|
||
t.Fatalf("回读到访失败: %v", err)
|
||
}
|
||
same := gotArrival.Equal(origArrival) && gotDeparture.Valid == origDeparture.Valid &&
|
||
(!gotDeparture.Valid || gotDeparture.Time.Equal(origDeparture.Time))
|
||
if !same {
|
||
t.Errorf("夹行程失败后到访 #%d 的时间被改掉了:%v/%v → %v/%v(整组写入应当回滚)",
|
||
visitSrc, origArrival, origDeparture, gotArrival, gotDeparture)
|
||
}
|
||
// 行程那一端也不能被改
|
||
var gotSt, gotEn time.Time
|
||
if err := pg.QueryRowContext(ctx, `SELECT started_at, ended_at FROM movements
|
||
WHERE dataset_id=$1 AND src_pk=$2`, dsID, movePK).Scan(&gotSt, &gotEn); err != nil {
|
||
t.Fatalf("回读行程失败: %v", err)
|
||
}
|
||
if !gotSt.Equal(mvSt) || !gotEn.Equal(mvEn) {
|
||
t.Errorf("行程 #%d 的时间被改掉了:%v/%v → %v/%v", movePK, mvSt, mvEn, gotSt, gotEn)
|
||
}
|
||
t.Logf("夹行程失败后到访 #%d(%v ~ %v)与行程 #%d 均保持原值 ✓", visitSrc, origArrival, gotDeparture, movePK)
|
||
}
|
||
|
||
// installMovementUpdateFailTrigger 给 movements 挂一个必然报错的 BEFORE UPDATE 触发器。
|
||
// 名字带 e2e_ 前缀以免撞上别的对象;用完即弃(t.Cleanup 里 drop,且在连接关闭之前跑)。
|
||
func installMovementUpdateFailTrigger(t *testing.T, ctx context.Context, pg *sql.DB) {
|
||
t.Helper()
|
||
if _, err := pg.ExecContext(ctx, `CREATE OR REPLACE FUNCTION e2e_fail_movement_update() RETURNS trigger
|
||
LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'e2e: 事务原子性用例故意失败'; END $$`); err != nil {
|
||
t.Fatalf("建触发器函数失败: %v", err)
|
||
}
|
||
if _, err := pg.ExecContext(ctx, `DROP TRIGGER IF EXISTS e2e_fail_movement_update ON movements`); err != nil {
|
||
t.Fatalf("清理旧触发器失败: %v", err)
|
||
}
|
||
if _, err := pg.ExecContext(ctx, `CREATE TRIGGER e2e_fail_movement_update
|
||
BEFORE UPDATE ON movements FOR EACH ROW EXECUTE FUNCTION e2e_fail_movement_update()`); err != nil {
|
||
t.Fatalf("建触发器失败: %v", err)
|
||
}
|
||
t.Cleanup(func() {
|
||
pg.ExecContext(ctx, `DROP TRIGGER IF EXISTS e2e_fail_movement_update ON movements`)
|
||
pg.ExecContext(ctx, `DROP FUNCTION IF EXISTS e2e_fail_movement_update()`)
|
||
})
|
||
}
|