Files
Agent-System/internal/auth/admin_reset_password_test.go
T
weichen 39f8c1d134 feat(auth): admin password reset API and credential guards
Implement T20-F4-B reset-password endpoint with one-time temp password,
audit table, frontend wiring, login error envelope parsing, and
CreateUser fail-closed for empty password hashes. Recovered test chenwei
via env write (password out of band).
2026-09-24 04:18:58 +00:00

103 lines
3.3 KiB
Go

package auth_test
import (
"strings"
"testing"
"chat-agent-system/internal/auth"
"chat-agent-system/internal/config"
)
func TestGenerateTempPassword(t *testing.T) {
pw, err := auth.GenerateTempPassword(16)
if err != nil {
t.Fatal(err)
}
if len(pw) != 16 {
t.Fatalf("len=%d", len(pw))
}
for _, r := range pw {
if !strings.ContainsRune("ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789", r) {
t.Fatalf("unexpected rune %q", r)
}
}
}
func TestCreateUserRejectsEmptyPasswordHash(t *testing.T) {
store := auth.NewMemoryStore(&config.Config{Env: "dev", AllowInMemory: true})
_, err := store.CreateUser(t.Context(), &auth.UserAccount{
Username: "nohash",
Name: "无哈希",
Status: auth.StatusActive,
RoleID: "role-guest",
})
if err == nil || !strings.Contains(err.Error(), "password hash required") {
t.Fatalf("expected password hash required, got %v", err)
}
}
func TestAdminResetPassword_Service(t *testing.T) {
cfg := &config.Config{Env: "dev", AllowInMemory: true, AdminUsername: "admin", AdminPassword: "admin123"}
store := auth.NewMemoryStore(cfg)
svc := auth.NewService(store, nil, cfg)
created, err := svc.CreateUserByAdmin(t.Context(), auth.CreateUserRequest{
Username: "svc_reset",
Name: "服务重置",
InitialPassword: "InitPass123",
RoleID: "role-engineer",
Position: "方案工程师",
})
if err != nil {
t.Fatal(err)
}
result, err := svc.AdminResetPassword(t.Context(), created.ID, 1, "req_test")
if err != nil {
t.Fatal(err)
}
if result.TempPassword == "" || !result.MustChangePassword {
t.Fatalf("bad result %+v", result)
}
_, err = svc.Login(t.Context(), "svc_reset", "InitPass123")
if err == nil {
t.Fatal("old password should fail")
}
session, err := svc.Login(t.Context(), "svc_reset", result.TempPassword)
if err != nil {
t.Fatal(err)
}
if !session.MustChangePassword {
t.Fatal("expected must_change_password")
}
}
func TestLoginMissingPasswordHash(t *testing.T) {
cfg := &config.Config{Env: "dev", AllowInMemory: true, AdminUsername: "admin", AdminPassword: "admin123"}
store := auth.NewMemoryStore(cfg)
// Bypass CreateUser guard by mutating seeded user hash after create via UpdateUserPassword empty is not allowed;
// plant via direct store map is not exported — use CreateUser with hash then clear via UpdateUserPassword with invalid.
// Instead: create with hash then overwrite stored hash through UpdateUserPassword with a sentinel that CheckPassword rejects,
// and separately verify empty-hash path by creating through internal seed is hard.
// Use Login against unknown user for 401 uniformity, and empty-hash via GetUserByID mutation on MemoryStore UpdateUserPassword
// with hash "" is allowed by UpdateUserPassword — plant empty hash that way.
created, err := store.CreateUser(t.Context(), &auth.UserAccount{
Username: "emptyhash",
Name: "空哈希",
PasswordHash: "placeholder",
Status: auth.StatusActive,
RoleID: "role-guest",
})
if err != nil {
t.Fatal(err)
}
if _, err := store.UpdateUserPassword(t.Context(), created.ID, "", false); err != nil {
t.Fatal(err)
}
svc := auth.NewService(store, nil, cfg)
_, err = svc.Login(t.Context(), "emptyhash", "anything")
if err == nil || err.Error() != "用户名或密码错误" {
t.Fatalf("expected unified credential error, got %v", err)
}
}