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) } }