package models import ( "errors" "strings" "testing" ) func TestAccountRole_Valid(t *testing.T) { valid := []AccountRole{RoleMember, RoleOwner, RoleAgent, RoleAdmin, ""} invalid := []AccountRole{"superuser", "MEMBER", "member ", "0"} for _, role := range valid { if got := role.Valid(); role != "" && !got { t.Errorf("AccountRole(%q).Valid() = false, want true", role) } } for _, role := range invalid { if role.Valid() { t.Errorf("AccountRole(%q).Valid() = true, want false", role) } } if !RoleMember.Valid() { t.Error("RoleMember.Valid() = false, want true") } } func TestAccount_SetPassword(t *testing.T) { tests := []struct { name string password string wantErr error }{ {name: "合法密碼", password: "s3cret!pass"}, {name: "剛好 8 字元", password: "12345678"}, {name: "太短", password: "1234567", wantErr: ErrPasswordTooShort}, {name: "空白", password: "", wantErr: ErrPasswordTooShort}, {name: "太長", password: strings.Repeat("a", MaxPasswordLength+1), wantErr: ErrPasswordTooLong}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { acct := &Account{} err := acct.SetPassword(tt.password) if !errors.Is(err, tt.wantErr) { t.Fatalf("SetPassword() error = %v, want %v", err, tt.wantErr) } if tt.wantErr == nil { if acct.PasswordHash == "" { t.Fatal("SetPassword() 後 PasswordHash 為空") } if acct.PasswordHash == tt.password { t.Fatal("PasswordHash 不應儲存明文密碼") } } }) } } func TestAccount_VerifyPassword(t *testing.T) { acct := &Account{} if err := acct.SetPassword("s3cret!pass"); err != nil { t.Fatalf("SetPassword() error = %v", err) } if !acct.VerifyPassword("s3cret!pass") { t.Error("VerifyPassword(正確密碼) = false, want true") } if acct.VerifyPassword("wrong-pass") { t.Error("VerifyPassword(錯誤密碼) = true, want false") } if acct.VerifyPassword("") { t.Error("VerifyPassword(空字串) = true, want false") } } func TestAccount_VerifyPassword_每次雜湊不同(t *testing.T) { a1, a2 := &Account{}, &Account{} if err := a1.SetPassword("same-password"); err != nil { t.Fatalf("a1.SetPassword() error = %v", err) } if err := a2.SetPassword("same-password"); err != nil { t.Fatalf("a2.SetPassword() error = %v", err) } if a1.PasswordHash == a2.PasswordHash { t.Error("相同密碼的兩次雜湊應不同(bcrypt 應加鹽)") } if !a1.VerifyPassword("same-password") || !a2.VerifyPassword("same-password") { t.Error("兩個帳號都應能以原始密碼通過驗證") } } func TestAccount_Validate(t *testing.T) { valid := func() *Account { acct := &Account{Email: "dan@example.com", Name: "Dan"} if err := acct.SetPassword("s3cret!pass"); err != nil { t.Fatalf("SetPassword() error = %v", err) } return acct } tests := []struct { name string mutate func(*Account) wantErr error }{ {name: "合法帳號", mutate: func(*Account) {}}, { name: "缺 email", mutate: func(a *Account) { a.Email = "" }, wantErr: ErrEmailRequired, }, { name: "email 格式錯誤", mutate: func(a *Account) { a.Email = "not-an-email" }, wantErr: ErrEmailInvalid, }, { name: "email 過長", mutate: func(a *Account) { a.Email = strings.Repeat("a", 250) + "@example.com" }, wantErr: ErrEmailInvalid, }, { name: "缺 name", mutate: func(a *Account) { a.Name = "" }, wantErr: ErrNameRequired, }, { name: "name 過長", mutate: func(a *Account) { a.Name = strings.Repeat("名", 101) }, wantErr: ErrNameTooLong, }, { name: "role 不合法", mutate: func(a *Account) { a.Role = "hacker" }, wantErr: ErrRoleInvalid, }, { name: "phone 過長", mutate: func(a *Account) { a.Phone = strings.Repeat("0", 31) }, wantErr: ErrPhoneTooLong, }, { name: "avatar_url 過長", mutate: func(a *Account) { a.AvatarURL = strings.Repeat("x", 513) }, wantErr: ErrAvatarURLTooLong, }, { name: "未設定密碼", mutate: func(a *Account) { a.PasswordHash = "" }, wantErr: ErrPasswordRequired, }, { name: "所有合法角色可通過", mutate: func(a *Account) { a.Role = RoleAdmin a.Phone = "0912345678" a.AvatarURL = "https://example.com/a.png" }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { acct := valid() tt.mutate(acct) err := acct.Validate() if !errors.Is(err, tt.wantErr) { t.Fatalf("Validate() error = %v, want %v", err, tt.wantErr) } }) } } func TestAccount_Normalize(t *testing.T) { acct := &Account{ Email: " Dan@Example.COM ", Name: " 陳大同 ", Phone: " 0912345678 ", AvatarURL: " https://example.com/a.png ", } acct.Normalize() if acct.Email != "dan@example.com" { t.Errorf("Email = %q, want %q", acct.Email, "dan@example.com") } if acct.Name != "陳大同" { t.Errorf("Name = %q, want %q", acct.Name, "陳大同") } if acct.Phone != "0912345678" { t.Errorf("Phone = %q, want %q", acct.Phone, "0912345678") } if acct.AvatarURL != "https://example.com/a.png" { t.Errorf("AvatarURL = %q, want %q", acct.AvatarURL, "https://example.com/a.png") } if acct.Role != RoleMember { t.Errorf("空角色應預設為 RoleMember, got %q", acct.Role) } } func TestAccount_Normalize_不覆寫已設角色(t *testing.T) { acct := &Account{Role: RoleAgent} acct.Normalize() if acct.Role != RoleAgent { t.Errorf("Role = %q, want %q", acct.Role, RoleAgent) } } func TestAccount_IsAdmin(t *testing.T) { admin := &Account{Role: RoleAdmin} member := &Account{Role: RoleMember} if !admin.IsAdmin() { t.Error("admin.IsAdmin() = false, want true") } if member.IsAdmin() { t.Error("member.IsAdmin() = true, want false") } }