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).
103 lines
3.3 KiB
Go
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)
|
|
}
|
|
} |