Files
affiliate_dash/backend/internal/service/merchant_test.go
T

227 lines
8.0 KiB
Go

package service
import (
"fmt"
"strings"
"testing"
"affiliate_dash/internal/model"
"golang.org/x/crypto/bcrypt"
)
func TestCreateAPIClientEnforcesPerMerchantLimit(t *testing.T) {
db := newServiceTestDB(t)
codec, err := NewSecretCodec("test-master-key")
if err != nil {
t.Fatalf("codec: %v", err)
}
svc := NewMerchantService(db, codec, NewTenantService(db))
merchant := model.Merchant{Code: "merchant-api-limit", Name: "限数商户", Status: model.MerchantStatusActive}
if err := db.Create(&merchant).Error; err != nil {
t.Fatalf("create merchant: %v", err)
}
for i := 0; i < MaxAPIClientsPerMerchant; i++ {
if _, err := svc.CreateAPIClient(merchant.ID, CreateAPIClientInput{
Name: fmt.Sprintf("key-%d", i),
Scopes: "products:read",
}, 1); err != nil {
t.Fatalf("create client %d: %v", i, err)
}
}
_, err = svc.CreateAPIClient(merchant.ID, CreateAPIClientInput{
Name: "overflow",
Scopes: "products:read",
}, 1)
if err == nil || !strings.Contains(err.Error(), fmt.Sprintf("最多可创建 %d 个", MaxAPIClientsPerMerchant)) {
t.Fatalf("expected per-merchant limit error, got %v", err)
}
}
func TestMerchantCustomRoleIsScopedAndCanCreateEmployee(t *testing.T) {
db := newServiceTestDB(t)
merchantA := model.Merchant{Code: "team-merchant-a", Name: "团队商户 A", Status: model.MerchantStatusActive}
merchantB := model.Merchant{Code: "team-merchant-b", Name: "团队商户 B", Status: model.MerchantStatusActive}
actor := model.User{Username: "team-owner", PasswordHash: "hash", Role: model.RoleMerchant, Status: 1}
if err := db.Create(&merchantA).Error; err != nil {
t.Fatalf("create merchant A: %v", err)
}
if err := db.Create(&merchantB).Error; err != nil {
t.Fatalf("create merchant B: %v", err)
}
if err := db.Create(&actor).Error; err != nil {
t.Fatalf("create actor: %v", err)
}
svc := NewMerchantService(db, nil, NewTenantService(db))
role, err := svc.CreateRole(merchantA.ID, MerchantRoleInput{
Code: "after-sales",
Name: "客服",
Permissions: []string{model.PermissionOrdersManage},
}, actor.ID)
if err != nil {
t.Fatalf("create custom role: %v", err)
}
member, err := svc.AddMember(merchantA.ID, AddMemberInput{
Username: "merchant-a-support",
Password: "password123",
Nickname: "客服小李",
Role: role.Code,
}, actor.ID)
if err != nil {
t.Fatalf("create employee: %v", err)
}
if member.User == nil || member.User.Username != "merchant-a-support" || member.Role != "after-sales" {
t.Fatalf("unexpected member: %+v", member)
}
if !member.IsDefault {
t.Fatal("new employee should default to the merchant that created it")
}
if _, err := svc.AddMember(merchantB.ID, AddMemberInput{
Username: "merchant-b-support",
Password: "password123",
Role: role.Code,
}, actor.ID); err == nil {
t.Fatal("expected merchant A role to be rejected for merchant B")
}
}
func TestDefaultMerchantRolesCanBeEdited(t *testing.T) {
db := newServiceTestDB(t)
merchant := model.Merchant{Code: "default-roles", Name: "默认角色商户", Status: model.MerchantStatusActive}
actor := model.User{Username: "default-role-owner", PasswordHash: "hash", Role: model.RoleMerchant, Status: 1}
if err := db.Create(&merchant).Error; err != nil {
t.Fatalf("create merchant: %v", err)
}
if err := db.Create(&actor).Error; err != nil {
t.Fatalf("create actor: %v", err)
}
if err := createDefaultMerchantRoles(db, merchant.ID); err != nil {
t.Fatalf("create default roles: %v", err)
}
svc := NewMerchantService(db, nil, NewTenantService(db))
roles, err := svc.ListRoles(merchant.ID)
if err != nil {
t.Fatalf("list roles: %v", err)
}
var finance model.MerchantRole
var hasSupport bool
for _, role := range roles {
if role.Code == model.MemberRoleFinance {
finance = role
}
if role.Code == "support" && role.Name == "客服" {
hasSupport = true
}
}
if finance.ID == 0 || !hasSupport {
t.Fatalf("expected editable finance and support roles, got %+v", roles)
}
updated, err := svc.UpdateRole(merchant.ID, finance.ID, MerchantRoleInput{
Name: "财务主管",
Permissions: []string{model.PermissionWalletView},
}, actor.ID)
if err != nil {
t.Fatalf("update finance role: %v", err)
}
if updated.Name != "财务主管" || updated.Permissions != model.PermissionWalletView {
t.Fatalf("unexpected updated role: %+v", updated)
}
}
func TestMemberCanBeUpdatedOrRemovedWithoutDeletingAccount(t *testing.T) {
db := newServiceTestDB(t)
merchant := model.Merchant{Code: "member-crud", Name: "成员管理商户", Status: model.MerchantStatusActive}
actor := model.User{Username: "member-crud-owner", PasswordHash: "hash", Role: model.RoleMerchant, Status: 1}
employee := model.User{Username: "member-crud-employee", PasswordHash: "hash", Role: model.RoleMerchant, Status: 1}
if err := db.Create(&merchant).Error; err != nil {
t.Fatalf("create merchant: %v", err)
}
if err := db.Create(&actor).Error; err != nil {
t.Fatalf("create actor: %v", err)
}
if err := db.Create(&employee).Error; err != nil {
t.Fatalf("create employee: %v", err)
}
if err := createDefaultMerchantRoles(db, merchant.ID); err != nil {
t.Fatalf("create default roles: %v", err)
}
member := model.MerchantMember{MerchantID: merchant.ID, UserID: employee.ID, Role: "support", Status: 1}
if err := db.Create(&member).Error; err != nil {
t.Fatalf("create member: %v", err)
}
svc := NewMerchantService(db, nil, NewTenantService(db))
updated, err := svc.UpdateMember(merchant.ID, member.ID, UpdateMemberInput{
Role: model.MemberRoleFinance, Status: 0, IsDefault: true,
}, actor.ID)
if err != nil {
t.Fatalf("update member: %v", err)
}
if updated.Role != model.MemberRoleFinance || updated.Status != 0 || !updated.IsDefault {
t.Fatalf("unexpected updated member: %+v", updated)
}
if err := svc.RemoveMember(merchant.ID, member.ID, actor.ID); err != nil {
t.Fatalf("remove member: %v", err)
}
var user model.User
if err := db.First(&user, employee.ID).Error; err != nil {
t.Fatalf("employee account should remain: %v", err)
}
var count int64
if err := db.Model(&model.MerchantMember{}).Where("id = ?", member.ID).Count(&count).Error; err != nil || count != 0 {
t.Fatalf("member relationship should be removed, count=%d err=%v", count, err)
}
}
func TestAuthServiceChangePasswordVerifiesCurrentPassword(t *testing.T) {
db := newServiceTestDB(t)
tenant := NewTenantService(db)
auth := NewAuthService(db, nil, tenant)
user, err := NewUserService(db, tenant).Create("password-owner", "old-password", "密码管理员", model.RoleAdmin, 0)
if err != nil {
t.Fatalf("create user: %v", err)
}
if err := auth.ChangePassword(user.ID, "wrong-password", "new-password"); err == nil {
t.Fatal("expected current password validation error")
}
if err := auth.ChangePassword(user.ID, "old-password", "new-password"); err != nil {
t.Fatalf("change password: %v", err)
}
var saved model.User
if err := db.First(&saved, user.ID).Error; err != nil {
t.Fatalf("load updated user: %v", err)
}
if err := bcrypt.CompareHashAndPassword([]byte(saved.PasswordHash), []byte("new-password")); err != nil {
t.Fatalf("new password should match: %v", err)
}
if err := bcrypt.CompareHashAndPassword([]byte(saved.PasswordHash), []byte("old-password")); err == nil {
t.Fatal("old password should no longer match")
}
}
func TestUserServiceCannotDeleteOrDisableLastAdmin(t *testing.T) {
db := newServiceTestDB(t)
svc := NewUserService(db, nil)
admin, err := svc.Create("only-admin", "password123", "唯一管理员", model.RoleAdmin, 0)
if err != nil {
t.Fatalf("create admin: %v", err)
}
if err := svc.UpdateStatus(admin.ID, 0); err == nil {
t.Fatal("should not disable the last enabled admin")
}
other, err := svc.Create("other-admin", "password123", "另一管理员", model.RoleAdmin, 0)
if err != nil {
t.Fatalf("create second admin: %v", err)
}
if err := svc.Delete(other.ID, admin.ID); err != nil {
t.Fatalf("delete non-current admin: %v", err)
}
if err := svc.Delete(admin.ID, other.ID); err == nil {
t.Fatal("should not delete the last enabled admin")
}
}