Files
cmautobuy/admin/service/auth_test.go
T

172 lines
5.6 KiB
Go
Raw 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 service
import (
"errors"
"strings"
"sync"
"testing"
"time"
"golang.org/x/crypto/bcrypt"
"cmautobuy/admin/repository"
)
func TestSetupInitialAdmin_保存哈希且入口永久关闭(t *testing.T) {
db := newSyncTestDB(t)
now := time.Date(2026, 8, 9, 6, 0, 0, 0, time.UTC)
if err := SetupInitialAdmin(db, "admin", "safe-password", "safe-password", now); err != nil {
t.Fatalf("初始化管理员失败: %v", err)
}
user, err := repository.FindUserByUsername(db, "ADMIN") // username 是 NOCASE
if err != nil {
t.Fatalf("读取管理员失败: %v", err)
}
if user.PasswordHash == "safe-password" || strings.Contains(user.PasswordHash, "safe-password") {
t.Fatal("数据库不能保存或包含明文密码")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte("safe-password")); err != nil {
t.Fatalf("密码哈希不能验证原密码: %v", err)
}
if err := SetupInitialAdmin(db, "second", "other-password", "other-password", now); !errors.Is(err, repository.ErrUsersAlreadyExist) {
t.Fatalf("已有用户后初始化入口应永久关闭,实际 %v", err)
}
}
func TestSetupInitialAdmin_校验密码字符数和Bcrypt字节上限(t *testing.T) {
for _, test := range []struct {
name string
password string
want string
}{
{"少于六个字符", "12345", "至少需要 6 个字符"},
{"超过bcrypt字节上限", strings.Repeat("密", 25), "不能超过 72 个字节"},
} {
t.Run(test.name, func(t *testing.T) {
db := newSyncTestDB(t)
err := SetupInitialAdmin(db, "admin", test.password, test.password, time.Now())
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("校验错误 = %v,期望包含 %q", err, test.want)
}
})
}
}
func TestSetupInitialAdmin_六个字符密码可用(t *testing.T) {
db := newSyncTestDB(t)
if err := SetupInitialAdmin(db, "admin", "123456", "123456", time.Now()); err != nil {
t.Fatalf("六个字符应达到最小密码长度,实际 %v", err)
}
}
func TestSetupInitialAdmin_并发最多一个成功(t *testing.T) {
db := newSyncTestDB(t)
start := make(chan struct{})
errorsOut := make(chan error, 2)
var wg sync.WaitGroup
for _, username := range []string{"admin-a", "admin-b"} {
wg.Add(1)
go func(name string) {
defer wg.Done()
<-start
errorsOut <- SetupInitialAdmin(db, name, "safe-password", "safe-password", time.Now())
}(username)
}
close(start)
wg.Wait()
close(errorsOut)
successes := 0
for err := range errorsOut {
if err == nil {
successes++
continue
}
if !errors.Is(err, repository.ErrUsersAlreadyExist) {
t.Errorf("失败请求应明确提示已初始化,实际 %v", err)
}
}
if successes != 1 {
t.Fatalf("并发初始化应恰好一个成功,实际 %d", successes)
}
count, _ := repository.CountUsers(db)
if count != 1 {
t.Fatalf("并发初始化后数据库应只有一个用户,实际 %d", count)
}
}
func TestLoginAuthenticateLogout_数据库只存Token哈希(t *testing.T) {
db := newSyncTestDB(t)
now := time.Date(2026, 8, 9, 6, 0, 0, 0, time.UTC)
if err := SetupInitialAdmin(db, "admin", "safe-password", "safe-password", now); err != nil {
t.Fatal(err)
}
token, user, expiresAt, err := Login(db, "admin", "safe-password", now)
if err != nil {
t.Fatalf("登录失败: %v", err)
}
if len(token) < 40 || expiresAt.Sub(now) != WebSessionDuration {
t.Fatalf("Session Token 或 12 小时过期时间不正确")
}
var stored string
if err := db.QueryRow(`SELECT session_hash FROM web_sessions`).Scan(&stored); err != nil {
t.Fatal(err)
}
if stored == token || stored != SessionTokenHash(token) || len(stored) != 64 {
t.Fatalf("数据库应只保存 64 位 SHA-256,stored=%q token=%q", stored, token)
}
got, err := Authenticate(db, token, now.Add(time.Hour))
if err != nil || got.UserID != user.UserID {
t.Fatalf("有效 Session 认证失败: user=%+v err=%v", got, err)
}
if err := Logout(db, token); err != nil {
t.Fatalf("退出失败: %v", err)
}
if _, err := Authenticate(db, token, now.Add(time.Hour)); !errors.Is(err, ErrUnauthenticated) {
t.Fatalf("退出后旧 Token 不应继续有效,实际 %v", err)
}
}
func TestLogin_错误凭据和禁用账号统一提示(t *testing.T) {
db := newSyncTestDB(t)
if err := SetupInitialAdmin(db, "admin", "safe-password", "safe-password", time.Now()); err != nil {
t.Fatal(err)
}
for name, password := range map[string]string{
"missing": "safe-password",
"admin": "wrong-password",
} {
if _, _, _, err := Login(db, name, password, time.Now()); !errors.Is(err, ErrInvalidCredentials) {
t.Errorf("%s 登录错误应统一,实际 %v", name, err)
}
}
if _, err := db.Exec(`UPDATE users SET status = 'disabled' WHERE username = 'admin'`); err != nil {
t.Fatal(err)
}
if _, _, _, err := Login(db, "admin", "safe-password", time.Now()); !errors.Is(err, ErrInvalidCredentials) {
t.Fatalf("禁用账号也应使用统一提示,实际 %v", err)
}
}
func TestAuthenticate_过期Session被删除(t *testing.T) {
db := newSyncTestDB(t)
now := time.Date(2026, 8, 9, 6, 0, 0, 0, time.UTC)
if err := SetupInitialAdmin(db, "admin", "safe-password", "safe-password", now); err != nil {
t.Fatal(err)
}
token, _, _, err := Login(db, "admin", "safe-password", now)
if err != nil {
t.Fatal(err)
}
if _, err := Authenticate(db, token, now.Add(WebSessionDuration)); !errors.Is(err, ErrUnauthenticated) {
t.Fatalf("到期时刻 Session 必须失效,实际 %v", err)
}
var count int
if err := db.QueryRow(`SELECT COUNT(*) FROM web_sessions`).Scan(&count); err != nil {
t.Fatal(err)
}
if count != 0 {
t.Fatalf("过期 Session 应被清理,实际剩 %d", count)
}
}