Files
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

148 lines
4.1 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 auth
import (
"strings"
"testing"
"time"
)
// TestGuardLocksAfterThreshold 前几次手滑不该被罚,到阈值才开始锁。
func TestGuardLocksAfterThreshold(t *testing.T) {
g := NewGuard()
base := time.Date(2026, 9, 21, 10, 0, 0, 0, time.UTC)
g.now = func() time.Time { return base }
for i := 1; i < guardFreeTries; i++ {
if wait := g.Fail("admin"); wait != 0 {
t.Fatalf("第 %d 次失败就被锁了 %v,阈值应当是 %d", i, wait, guardFreeTries)
}
if d := g.RetryAfter("admin"); d != 0 {
t.Fatalf("第 %d 次失败后不该有等待时间,实际 %v", i, d)
}
}
wait := g.Fail("admin")
if wait <= 0 {
t.Fatalf("第 %d 次失败应当上锁", guardFreeTries)
}
if wait > guardBaseLock {
t.Errorf("首次锁定时长 %v 超过了基准 %v", wait, guardBaseLock)
}
if d := g.RetryAfter("admin"); d != wait {
t.Errorf("RetryAfter=%v 与 Fail 返回的 %v 不一致", d, wait)
}
t.Logf("第 %d 次失败锁 %v", guardFreeTries, wait)
}
// TestGuardBackoffGrowsAndCaps 继续失败要退避得更久,但封顶。
func TestGuardBackoffGrowsAndCaps(t *testing.T) {
g := NewGuard()
base := time.Date(2026, 9, 21, 10, 0, 0, 0, time.UTC)
g.now = func() time.Time { return base }
var prev time.Duration
for i := 0; i < guardFreeTries+12; i++ {
wait := g.Fail("admin")
if wait < prev {
t.Fatalf("第 %d 次失败的等待时间 %v 比上一次 %v 还短", i+1, wait, prev)
}
if wait > guardMaxLock {
t.Fatalf("第 %d 次失败锁了 %v,超过上限 %v", i+1, wait, guardMaxLock)
}
prev = wait
}
if prev != guardMaxLock {
t.Errorf("连续失败后应当封顶在 %v,实际 %v", guardMaxLock, prev)
}
}
// TestGuardReleasesAfterWait 时间走够之后要放行,否则就是永久锁死自己。
func TestGuardReleasesAfterWait(t *testing.T) {
g := NewGuard()
base := time.Date(2026, 9, 21, 10, 0, 0, 0, time.UTC)
now := base
g.now = func() time.Time { return now }
var wait time.Duration
for i := 0; i < guardFreeTries; i++ {
wait = g.Fail("admin")
}
if wait <= 0 {
t.Fatal("到阈值却没上锁")
}
now = base.Add(wait - time.Millisecond)
if d := g.RetryAfter("admin"); d <= 0 {
t.Errorf("还差 1ms 到期,应当仍在锁内,实际 %v", d)
}
now = base.Add(wait)
if d := g.RetryAfter("admin"); d != 0 {
t.Errorf("到期后应当放行,实际还要等 %v", d)
}
}
// TestGuardSucceedClears 登录成功要把计数清零,别让几天前的失败把下次正常登录挡住。
func TestGuardSucceedClears(t *testing.T) {
g := NewGuard()
base := time.Date(2026, 9, 21, 10, 0, 0, 0, time.UTC)
g.now = func() time.Time { return base }
for i := 0; i < guardFreeTries; i++ {
g.Fail("admin")
}
if g.RetryAfter("admin") <= 0 {
t.Fatal("应当处于锁定状态")
}
g.Succeed("admin")
if d := g.RetryAfter("admin"); d != 0 {
t.Errorf("成功后不该还有等待时间,实际 %v", d)
}
// 清零之后又要重新攒满阈值才会锁
for i := 1; i < guardFreeTries; i++ {
if wait := g.Fail("admin"); wait != 0 {
t.Errorf("清零后第 %d 次失败不该上锁,实际 %v", i, wait)
}
}
}
// TestGuardSeparatesKeys 不同用户名各算各的。
func TestGuardSeparatesKeys(t *testing.T) {
g := NewGuard()
base := time.Date(2026, 9, 21, 10, 0, 0, 0, time.UTC)
g.now = func() time.Time { return base }
for i := 0; i < guardFreeTries; i++ {
g.Fail("admin")
}
if g.RetryAfter("admin") <= 0 {
t.Fatal("admin 应当被锁")
}
if d := g.RetryAfter("someone-else"); d != 0 {
t.Errorf("另一个用户名不该受影响,实际 %v", d)
}
}
// TestRandomPassword 确认初始口令有足够熵、字符集不含易混字符。
func TestRandomPassword(t *testing.T) {
const n = 16
a, err := RandomPassword(n)
if err != nil {
t.Fatal(err)
}
b, err := RandomPassword(n)
if err != nil {
t.Fatal(err)
}
if len(a) != n || len(b) != n {
t.Fatalf("长度不对: %d / %d(应为 %d)", len(a), len(b), n)
}
if a == b {
t.Error("两次生成的随机口令相同")
}
if strings.ContainsAny(a, "0O1Il") {
t.Errorf("口令含易混字符: %s", a)
}
if _, err := RandomPassword(0); err != nil {
t.Errorf("长度 0 不该报错: %v", err)
}
}