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 再用应用侧读查询真打一遍, 迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
60 lines
2.0 KiB
Go
60 lines
2.0 KiB
Go
package web
|
||
|
||
import (
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"strconv"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// deadlineRecorder 冒充 http.response:只有实现了 SetWriteDeadline,
|
||
// 才看得出 http.NewResponseController 有没有被中间件挡住。
|
||
type deadlineRecorder struct {
|
||
*httptest.ResponseRecorder
|
||
deadline time.Time
|
||
got bool
|
||
}
|
||
|
||
func (d *deadlineRecorder) SetWriteDeadline(t time.Time) error {
|
||
d.deadline, d.got = t, true
|
||
return nil
|
||
}
|
||
|
||
// P4 回归:gzipWriter 必须实现 Unwrap()。少了它,被 gzip 中间件包住的请求调
|
||
// http.NewResponseController(...).SetWriteDeadline/Flush 只会拿到 ErrNotSupported。
|
||
func TestGzipWriterUnwrapsToUnderlyingWriter(t *testing.T) {
|
||
rec := &deadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
|
||
gw := &gzipWriter{ResponseWriter: rec}
|
||
|
||
if err := http.NewResponseController(gw).SetWriteDeadline(time.Now().Add(time.Minute)); err != nil {
|
||
t.Fatalf("ResponseController 没能穿过 gzipWriter: %v", err)
|
||
}
|
||
if !rec.got {
|
||
t.Error("SetWriteDeadline 没有落到真正的 ResponseWriter 上")
|
||
}
|
||
}
|
||
|
||
// P4 回归:导出是「一次性大响应」,得带上 Content-Length(下载进度条、
|
||
// 也顺带锁住「不再依赖隐式 200」),并把写死线放宽到全站 120s 之外。
|
||
func TestExportSetsContentLengthAndExtendsWriteDeadline(t *testing.T) {
|
||
f := newMovementFixture(t)
|
||
|
||
rec := &deadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
|
||
req := httptest.NewRequest("GET", "/admin/export/rondbackup", nil).WithContext(f.ctx)
|
||
f.srv.exportRondbackup(rec, req)
|
||
|
||
if rec.Code != http.StatusOK {
|
||
t.Fatalf("导出返回 %d:%s", rec.Code, firstLine(rec.Body.String()))
|
||
}
|
||
if cl := rec.Header().Get("Content-Length"); cl != strconv.Itoa(rec.Body.Len()) {
|
||
t.Errorf("Content-Length=%q,实际写出 %d 字节", cl, rec.Body.Len())
|
||
}
|
||
if ct := rec.Header().Get("Content-Type"); ct != "application/octet-stream" {
|
||
t.Errorf("Content-Type 不对: %q", ct)
|
||
}
|
||
if !rec.got {
|
||
t.Error("导出没有延长写死线(慢链路下会被全站 120s 截断)")
|
||
}
|
||
}
|