Files
rond-with-you/internal/web/server_error_test.go
tamakyi b4c2a8452f 审计修复:后端(安全 / 数据一致性 / 性能)
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 再用应用侧读查询真打一遍,
迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
2026-09-24 20:37:16 +08:00

94 lines
3.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package web
import (
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"rond-with-you/internal/auth"
)
// TestServerErrorHidesDetailFromVisitor 断言 500 响应体不带内部错误详情——
// 公开访客页把 SQL 错误、磁盘路径原样回显,等于免费送出表结构与目录布局。
// 站长自己看得到详情(排查要用),别人只能看到一句人话。
func TestServerErrorHidesDetailFromVisitor(t *testing.T) {
s := &Server{}
const secret = `pq: relation "zzz_table" does not exist (SQLSTATE 42P01)`
anon := httptest.NewRecorder()
s.serverError(anon, httptest.NewRequest("GET", "/", nil), errors.New(secret))
if anon.Code != http.StatusInternalServerError {
t.Fatalf("状态码应为 500,实际 %d", anon.Code)
}
if body := anon.Body.String(); strings.Contains(body, "42P01") || strings.Contains(body, "zzz_table") {
t.Errorf("访客页泄漏了内部错误详情: %s", body)
}
owner := httptest.NewRecorder()
s.serverError(owner, httptest.NewRequest("GET", "/", nil).WithContext(ctxWithUser(&auth.User{ID: 1})), errors.New(secret))
if body := owner.Body.String(); !strings.Contains(body, "42P01") {
t.Errorf("站长应能看到错误详情,实际: %s", body)
}
}
// 半路 panic(首部已提交):不能再写错误页。旧写法无条件 http.Error,
// 会二次 WriteHeader(Go 记一条 superfluous response.WriteHeader)并把错误体
// 追加到半个页面后面 —— 用户看到的是「半页正文 + 一句服务器内部错误」。
func TestRecoverLeavesPartialResponseAlone(t *testing.T) {
s := &Server{}
h := s.recover(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "<p>已经写出去的半页</p>")
panic("boom")
}))
w := httptest.NewRecorder()
h.ServeHTTP(w, httptest.NewRequest("GET", "/", nil))
if w.Code != http.StatusOK {
t.Errorf("首部已提交就不该再改状态码,实际 %d", w.Code)
}
body := w.Body.String()
if !strings.Contains(body, "已经写出去的半页") {
t.Errorf("原有的半页被覆盖了: %q", body)
}
if strings.Contains(body, "服务器内部错误") {
t.Errorf("错误体被追加到了已提交的响应后面: %q", body)
}
}
// 还没写出任何东西就 panic:必须照旧给 500 + 一句人话。
func TestRecoverWritesErrorWhenNothingSent(t *testing.T) {
s := &Server{}
h := s.recover(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { panic("boom") }))
w := httptest.NewRecorder()
h.ServeHTTP(w, httptest.NewRequest("GET", "/", nil))
if w.Code != http.StatusInternalServerError {
t.Errorf("尚未写出时应回 500,实际 %d", w.Code)
}
if !strings.Contains(w.Body.String(), "服务器内部错误") {
t.Errorf("缺少错误提示: %q", w.Body.String())
}
}
// 包装层必须实现 Unwrap():否则 http.NewResponseController 在它这里拿不到
// SetWriteDeadline / Flush / Hijack,导出那条路由的 10 分钟写死线会静默失效。
func TestRecoverWrapperUnwraps(t *testing.T) {
rec := &deadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
s := &Server{}
var got error
s.recover(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
got = http.NewResponseController(w).SetWriteDeadline(time.Now().Add(time.Minute))
})).ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
if got != nil {
t.Fatalf("ResponseController 没能穿过 recover 的包装: %v", got)
}
if !rec.got {
t.Error("SetWriteDeadline 没有落到真正的 ResponseWriter 上")
}
}