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 再用应用侧读查询真打一遍, 迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
148 lines
4.1 KiB
Go
148 lines
4.1 KiB
Go
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)
|
||
}
|
||
}
|