Files
nestly/internal/models/account_test.go
T
2026-10-05 07:40:34 +08:00

219 lines
5.7 KiB
Go

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")
}
}