Files
dsfree2api/internal/admin/login_test.go
T
nyoungo fed260559c feat(admin): 登录改用 bcrypt 并对失败登录限速;bump 0.6.4
- 管理台密码改为 bcrypt 校验(已配置的 bcrypt 值直接使用,明文先 SHA-256 再哈希,避开 72 字节上限并保持常数时间比较)
- 同一客户端 5 次失败登录锁定 5 分钟,429 响应带 Retry-After;成功登录清零
- 版本号提升到 0.6.4(Makefile / internal/admin.Version)
2026-10-09 00:50:36 +08:00

71 lines
2.0 KiB
Go

package admin
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"golang.org/x/crypto/bcrypt"
)
func loginOnce(t *testing.T, s *Server, password string) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(http.MethodPost, "/api/login", strings.NewReader(`{"password":"`+password+`"}`))
req.RemoteAddr = "192.0.2.10:12345"
w := httptest.NewRecorder()
s.handleLogin(w, req)
return w
}
func TestLoginAcceptsBcryptPassword(t *testing.T) {
cfg := testConfig(t)
cfg.Admin.Password = "correct-horse"
s := newActionServer(t, cfg)
if s.passwordMatches("wrong") {
t.Fatal("wrong password must not match")
}
if !s.passwordMatches("correct-horse") {
t.Fatal("configured password must match")
}
if w := loginOnce(t, s, "correct-horse"); w.Code != http.StatusOK {
t.Fatalf("login status = %d, body = %s", w.Code, w.Body.String())
}
}
func TestLoginAcceptsPreHashedPassword(t *testing.T) {
hash, err := bcrypt.GenerateFromPassword([]byte("plaintext-secret"), bcrypt.MinCost)
if err != nil {
t.Fatalf("hash: %v", err)
}
cfg := testConfig(t)
cfg.Admin.Password = string(hash)
s := newActionServer(t, cfg)
if !s.passwordPreHashed {
t.Fatal("a bcrypt hash in config must be used verbatim")
}
if w := loginOnce(t, s, "plaintext-secret"); w.Code != http.StatusOK {
t.Fatalf("login status = %d, body = %s", w.Code, w.Body.String())
}
}
func TestLoginLocksOutAfterFiveFailures(t *testing.T) {
cfg := testConfig(t)
cfg.Admin.Password = "secret"
s := newActionServer(t, cfg)
for i := 0; i < adminMaxFails; i++ {
if w := loginOnce(t, s, "nope"); w.Code != http.StatusUnauthorized {
t.Fatalf("attempt %d status = %d, want 401", i+1, w.Code)
}
}
// The next attempt — even with the right password — is locked out.
w := loginOnce(t, s, "secret")
if w.Code != http.StatusTooManyRequests {
t.Fatalf("locked status = %d, want 429", w.Code)
}
if w.Header().Get("Retry-After") == "" {
t.Fatal("locked response should carry Retry-After")
}
}