534 lines
21 KiB
Go
534 lines
21 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"affiliate_dash/internal/model"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func intPtr(v int) *int { return &v }
|
|
|
|
// seedMerchantWithOwner 直接构造商户 + 负责人登录账号 + owner 成员关系。
|
|
func seedMerchantWithOwner(t *testing.T, db *gorm.DB, merchantCode, username string, ownerStatus int) (uint, uint) {
|
|
t.Helper()
|
|
merchant := model.Merchant{Code: merchantCode, Name: merchantCode, Status: model.MerchantStatusActive}
|
|
if err := db.Create(&merchant).Error; err != nil {
|
|
t.Fatalf("create merchant: %v", err)
|
|
}
|
|
owner := model.User{Username: username, PasswordHash: "old-hash", Nickname: "负责人", Role: model.RoleMerchant, Status: ownerStatus}
|
|
if err := db.Create(&owner).Error; err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
if err := db.Create(&model.MerchantMember{
|
|
MerchantID: merchant.ID, UserID: owner.ID, Role: model.MemberRoleOwner, Status: 1, IsDefault: true,
|
|
}).Error; err != nil {
|
|
t.Fatalf("create owner member: %v", err)
|
|
}
|
|
return merchant.ID, owner.ID
|
|
}
|
|
|
|
func TestUpdateMerchantOwnerAccountResetsPasswordAndUsername(t *testing.T) {
|
|
db := newServiceTestDB(t)
|
|
svc := NewMerchantService(db, nil, NewTenantService(db))
|
|
merchantID, ownerID := seedMerchantWithOwner(t, db, "owner-account-a", "owner-a", 1)
|
|
|
|
updated, err := svc.UpdateMerchantOwnerAccount(merchantID, UpdateMerchantOwnerAccountInput{
|
|
UserID: ownerID,
|
|
Username: "owner-a-new",
|
|
Nickname: "新负责人",
|
|
Password: "newpass123",
|
|
Status: intPtr(1),
|
|
}, 9)
|
|
if err != nil {
|
|
t.Fatalf("update owner account: %v", err)
|
|
}
|
|
if updated.Username != "owner-a-new" || updated.Nickname != "新负责人" || updated.Status != 1 {
|
|
t.Fatalf("unexpected updated account: %+v", updated)
|
|
}
|
|
|
|
var user model.User
|
|
if err := db.First(&user, ownerID).Error; err != nil {
|
|
t.Fatalf("reload user: %v", err)
|
|
}
|
|
if user.Username != "owner-a-new" || user.Nickname != "新负责人" {
|
|
t.Fatalf("user fields not persisted: %+v", user)
|
|
}
|
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte("newpass123")); err != nil {
|
|
t.Fatalf("password should be reset: %v", err)
|
|
}
|
|
|
|
var audit model.AuditLog
|
|
if err := db.Where("action = ? AND entity_id = ?", "merchant.account.update", fmt.Sprint(ownerID)).First(&audit).Error; err != nil {
|
|
t.Fatalf("account update audit missing: %v", err)
|
|
}
|
|
if audit.ActorUserID == nil || *audit.ActorUserID != 9 {
|
|
t.Fatalf("audit should record actor user 9, got %v", audit.ActorUserID)
|
|
}
|
|
|
|
accounts, err := svc.ListOwnerAccounts(merchantID)
|
|
if err != nil {
|
|
t.Fatalf("list owner accounts: %v", err)
|
|
}
|
|
if len(accounts) != 1 || accounts[0].Username != "owner-a-new" || !accounts[0].IsDefault {
|
|
t.Fatalf("unexpected owner accounts: %+v", accounts)
|
|
}
|
|
}
|
|
|
|
func TestUpdateMerchantOwnerAccountGuards(t *testing.T) {
|
|
db := newServiceTestDB(t)
|
|
svc := NewMerchantService(db, nil, NewTenantService(db))
|
|
merchantA, ownerA := seedMerchantWithOwner(t, db, "owner-acc-a", "owner-a1", 1)
|
|
merchantB, ownerB := seedMerchantWithOwner(t, db, "owner-acc-b", "owner-b1", 1)
|
|
|
|
// 用户名全局唯一:改成其他商户已占用的用户名必须被拒绝。
|
|
if _, err := svc.UpdateMerchantOwnerAccount(merchantA, UpdateMerchantOwnerAccountInput{
|
|
UserID: ownerA, Username: "owner-b1",
|
|
}, 9); err == nil || !strings.Contains(err.Error(), "用户名已存在") {
|
|
t.Fatalf("expected username conflict, got %v", err)
|
|
}
|
|
|
|
// 密码过短必须被拒绝。
|
|
if _, err := svc.UpdateMerchantOwnerAccount(merchantA, UpdateMerchantOwnerAccountInput{
|
|
UserID: ownerA, Username: "owner-a1", Password: "123",
|
|
}, 9); err == nil || !strings.Contains(err.Error(), "至少 6 位") {
|
|
t.Fatalf("expected short password rejection, got %v", err)
|
|
}
|
|
|
|
// 非负责人账号不能被当作负责人修改。
|
|
staff := model.User{Username: "owner-staff", PasswordHash: "hash", Role: model.RoleMerchant, Status: 1}
|
|
if err := db.Create(&staff).Error; err != nil {
|
|
t.Fatalf("create staff: %v", err)
|
|
}
|
|
if err := db.Create(&model.MerchantMember{
|
|
MerchantID: merchantA, UserID: staff.ID, Role: model.MemberRoleOperator, Status: 1, IsDefault: false,
|
|
}).Error; err != nil {
|
|
t.Fatalf("create staff member: %v", err)
|
|
}
|
|
if _, err := svc.UpdateMerchantOwnerAccount(merchantA, UpdateMerchantOwnerAccountInput{
|
|
UserID: staff.ID, Username: "owner-staff",
|
|
}, 9); err == nil || !strings.Contains(err.Error(), "不是该商户的负责人") {
|
|
t.Fatalf("expected non-owner rejection, got %v", err)
|
|
}
|
|
|
|
// 停用最后一个启用的负责人必须被拒绝(防锁死)。
|
|
if _, err := svc.UpdateMerchantOwnerAccount(merchantA, UpdateMerchantOwnerAccountInput{
|
|
UserID: ownerA, Username: "owner-a1", Status: intPtr(0),
|
|
}, 9); err == nil || !strings.Contains(err.Error(), "至少保留一个启用的商户负责人") {
|
|
t.Fatalf("expected last-owner protection, got %v", err)
|
|
}
|
|
|
|
// 商户 B 有第二个启用负责人时,可以停用其中一个。
|
|
second := model.User{Username: "owner-b2", PasswordHash: "hash", Role: model.RoleMerchant, Status: 1}
|
|
if err := db.Create(&second).Error; err != nil {
|
|
t.Fatalf("create second owner: %v", err)
|
|
}
|
|
if err := db.Create(&model.MerchantMember{
|
|
MerchantID: merchantB, UserID: second.ID, Role: model.MemberRoleOwner, Status: 1, IsDefault: false,
|
|
}).Error; err != nil {
|
|
t.Fatalf("create second owner member: %v", err)
|
|
}
|
|
if _, err := svc.UpdateMerchantOwnerAccount(merchantB, UpdateMerchantOwnerAccountInput{
|
|
UserID: ownerB, Username: "owner-b1", Status: intPtr(0),
|
|
}, 9); err != nil {
|
|
t.Fatalf("should allow disabling one of two owners: %v", err)
|
|
}
|
|
// 再停用剩下的启用负责人则被拒绝。
|
|
if _, err := svc.UpdateMerchantOwnerAccount(merchantB, UpdateMerchantOwnerAccountInput{
|
|
UserID: second.ID, Username: "owner-b2", Status: intPtr(0),
|
|
}, 9); err == nil || !strings.Contains(err.Error(), "至少保留一个启用的商户负责人") {
|
|
t.Fatalf("expected last-owner protection after disabling first, got %v", err)
|
|
}
|
|
}
|
|
|
|
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.CreatePlatformAdmin("only-admin", "password123", "唯一管理员")
|
|
if err != nil {
|
|
t.Fatalf("create admin: %v", err)
|
|
}
|
|
if _, err := svc.UpdatePlatformAdmin(admin.ID, UpdatePlatformAdminInput{
|
|
Username: admin.Username, Nickname: admin.Nickname, Status: 0,
|
|
}, 0); err == nil {
|
|
t.Fatal("should not disable the last enabled admin")
|
|
}
|
|
other, err := svc.CreatePlatformAdmin("other-admin", "password123", "另一管理员")
|
|
if err != nil {
|
|
t.Fatalf("create second admin: %v", err)
|
|
}
|
|
if err := svc.DeletePlatformAdmin(other.ID, admin.ID); err != nil {
|
|
t.Fatalf("delete non-current admin: %v", err)
|
|
}
|
|
if err := svc.DeletePlatformAdmin(admin.ID, other.ID); err == nil {
|
|
t.Fatal("should not delete the last enabled admin")
|
|
}
|
|
}
|
|
|
|
func TestPlatformAdminCRUDUsesHiddenSelfMerchantContext(t *testing.T) {
|
|
db := newServiceTestDB(t)
|
|
svc := NewUserService(db, NewTenantService(db))
|
|
actor, err := svc.CreatePlatformAdmin("existing-admin", "password123", "现有管理员")
|
|
if err != nil {
|
|
t.Fatalf("create actor: %v", err)
|
|
}
|
|
admin, err := svc.CreatePlatformAdmin("platform-admin", "password123", "平台管理员")
|
|
if err != nil {
|
|
t.Fatalf("create platform admin: %v", err)
|
|
}
|
|
var membershipCount int64
|
|
if err := db.Model(&model.MerchantMember{}).Where("user_id = ?", admin.ID).Count(&membershipCount).Error; err != nil {
|
|
t.Fatalf("count merchant memberships: %v", err)
|
|
}
|
|
if membershipCount != 1 {
|
|
t.Fatalf("platform admin should have one hidden default membership, count=%d", membershipCount)
|
|
}
|
|
var membership model.MerchantMember
|
|
if err := db.Where("user_id = ?", admin.ID).First(&membership).Error; err != nil {
|
|
t.Fatalf("load default membership: %v", err)
|
|
}
|
|
if membership.Role != model.MemberRoleOwner || !membership.IsDefault {
|
|
t.Fatalf("unexpected default membership: %+v", membership)
|
|
}
|
|
updated, err := svc.UpdatePlatformAdmin(admin.ID, UpdatePlatformAdminInput{
|
|
Username: "platform-admin-updated", Nickname: "更新后管理员", Password: "new-password", Status: 1,
|
|
}, actor.ID)
|
|
if err != nil {
|
|
t.Fatalf("update platform admin: %v", err)
|
|
}
|
|
if updated.Username != "platform-admin-updated" || updated.Nickname != "更新后管理员" {
|
|
t.Fatalf("unexpected updated admin: %+v", updated)
|
|
}
|
|
var saved model.User
|
|
if err := db.First(&saved, admin.ID).Error; err != nil {
|
|
t.Fatalf("load updated admin: %v", err)
|
|
}
|
|
if err := bcrypt.CompareHashAndPassword([]byte(saved.PasswordHash), []byte("new-password")); err != nil {
|
|
t.Fatalf("updated password should match: %v", err)
|
|
}
|
|
if err := svc.DeletePlatformAdmin(admin.ID, actor.ID); err != nil {
|
|
t.Fatalf("delete platform admin: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUpdateCurrentAccountCanChangeUsernameNicknameAndPassword(t *testing.T) {
|
|
db := newServiceTestDB(t)
|
|
tenant := NewTenantService(db)
|
|
auth := NewAuthService(db, nil, tenant)
|
|
user, err := NewUserService(db, tenant).CreatePlatformAdmin("current-account", "old-password", "旧昵称")
|
|
if err != nil {
|
|
t.Fatalf("create account: %v", err)
|
|
}
|
|
updated, err := auth.UpdateCurrentAccount(user.ID, UpdateCurrentAccountInput{
|
|
CurrentPassword: "old-password",
|
|
Username: "updated-account",
|
|
Nickname: "新昵称",
|
|
NewPassword: "new-password",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("update current account: %v", err)
|
|
}
|
|
if updated.Username != "updated-account" || updated.Nickname != "新昵称" {
|
|
t.Fatalf("unexpected updated account: %+v", updated)
|
|
}
|
|
if err := bcrypt.CompareHashAndPassword([]byte(updated.PasswordHash), []byte("new-password")); err != nil {
|
|
t.Fatalf("updated password should match: %v", err)
|
|
}
|
|
if _, err := auth.UpdateCurrentAccount(user.ID, UpdateCurrentAccountInput{
|
|
CurrentPassword: "wrong-password", Username: "should-not-save", Nickname: "", NewPassword: "",
|
|
}); err == nil {
|
|
t.Fatal("expected current password verification error")
|
|
}
|
|
}
|
|
|
|
func TestListMerchantAccountGroupsExcludesPlatformAdmins(t *testing.T) {
|
|
db := newServiceTestDB(t)
|
|
merchantA := model.Merchant{Code: "merchant-accounts-a", Name: "商户账号 A", Status: model.MerchantStatusActive}
|
|
merchantB := model.Merchant{Code: "merchant-accounts-b", Name: "商户账号 B", Status: model.MerchantStatusActive}
|
|
employeeA := model.User{Username: "merchant-account-a", PasswordHash: "hash", Role: model.RoleMerchant, Status: 1}
|
|
employeeB := model.User{Username: "merchant-account-b", PasswordHash: "hash", Role: model.RoleMerchant, Status: 1}
|
|
admin := model.User{Username: "merchant-account-admin", PasswordHash: "hash", Role: model.RoleAdmin, Status: 1}
|
|
for _, entity := range []interface{}{&merchantA, &merchantB, &employeeA, &employeeB, &admin} {
|
|
if err := db.Create(entity).Error; err != nil {
|
|
t.Fatalf("create fixture: %v", err)
|
|
}
|
|
}
|
|
for _, member := range []model.MerchantMember{
|
|
{MerchantID: merchantA.ID, UserID: employeeA.ID, Role: model.MemberRoleOperator, Status: 1},
|
|
{MerchantID: merchantA.ID, UserID: employeeB.ID, Role: "support", Status: 1},
|
|
{MerchantID: merchantB.ID, UserID: admin.ID, Role: model.MemberRoleOwner, Status: 1},
|
|
} {
|
|
if err := db.Create(&member).Error; err != nil {
|
|
t.Fatalf("create membership: %v", err)
|
|
}
|
|
}
|
|
|
|
groups, total, err := NewUserService(db, nil).ListMerchantAccountGroups(1, 20)
|
|
if err != nil {
|
|
t.Fatalf("list merchant account groups: %v", err)
|
|
}
|
|
if total != 1 || len(groups) != 1 {
|
|
t.Fatalf("expected one merchant group, total=%d groups=%+v", total, groups)
|
|
}
|
|
if groups[0].MerchantID != merchantA.ID || groups[0].Merchant == nil || len(groups[0].Members) != 2 {
|
|
t.Fatalf("unexpected merchant account group: %+v", groups[0])
|
|
}
|
|
}
|
|
|
|
func TestDeleteMerchantAccountRemovesAllMemberships(t *testing.T) {
|
|
db := newServiceTestDB(t)
|
|
merchantA := model.Merchant{Code: "delete-account-a", Name: "删除账号商户 A", Status: model.MerchantStatusActive}
|
|
merchantB := model.Merchant{Code: "delete-account-b", Name: "删除账号商户 B", Status: model.MerchantStatusActive}
|
|
merchantUser := model.User{Username: "delete-merchant-account", PasswordHash: "hash", Role: model.RoleMerchant, Status: 1}
|
|
platformAdmin := model.User{Username: "delete-account-admin", PasswordHash: "hash", Role: model.RoleAdmin, Status: 1}
|
|
for _, entity := range []interface{}{&merchantA, &merchantB, &merchantUser, &platformAdmin} {
|
|
if err := db.Create(entity).Error; err != nil {
|
|
t.Fatalf("create fixture: %v", err)
|
|
}
|
|
}
|
|
for _, member := range []model.MerchantMember{
|
|
{MerchantID: merchantA.ID, UserID: merchantUser.ID, Role: model.MemberRoleOperator, Status: 1},
|
|
{MerchantID: merchantB.ID, UserID: merchantUser.ID, Role: "support", Status: 1},
|
|
} {
|
|
if err := db.Create(&member).Error; err != nil {
|
|
t.Fatalf("create membership: %v", err)
|
|
}
|
|
}
|
|
|
|
svc := NewUserService(db, nil)
|
|
if err := svc.DeleteMerchantAccount(merchantUser.ID, platformAdmin.ID); err != nil {
|
|
t.Fatalf("delete merchant account: %v", err)
|
|
}
|
|
var memberships int64
|
|
if err := db.Model(&model.MerchantMember{}).Where("user_id = ?", merchantUser.ID).Count(&memberships).Error; err != nil || memberships != 0 {
|
|
t.Fatalf("merchant memberships should be removed, count=%d err=%v", memberships, err)
|
|
}
|
|
var deleted model.User
|
|
if err := db.Unscoped().First(&deleted, merchantUser.ID).Error; err != nil || !deleted.DeletedAt.Valid {
|
|
t.Fatalf("merchant account should be soft deleted, user=%+v err=%v", deleted, err)
|
|
}
|
|
if err := svc.DeleteMerchantAccount(platformAdmin.ID, merchantUser.ID); err == nil {
|
|
t.Fatal("platform administrator should not be deletable from merchant account endpoint")
|
|
}
|
|
}
|
|
|
|
func TestUsernameCanBeReusedAfterMerchantAccountSoftDelete(t *testing.T) {
|
|
db := newServiceTestDB(t)
|
|
svc := NewUserService(db, nil)
|
|
admin := model.User{Username: "reuse-account-admin", PasswordHash: "hash", Role: model.RoleAdmin, Status: 1}
|
|
if err := db.Create(&admin).Error; err != nil {
|
|
t.Fatalf("create admin: %v", err)
|
|
}
|
|
account, err := svc.Create("reusable-username", "password123", "旧账号", model.RoleMerchant, 0)
|
|
if err != nil {
|
|
t.Fatalf("create merchant account: %v", err)
|
|
}
|
|
if err := svc.DeleteMerchantAccount(account.ID, admin.ID); err != nil {
|
|
t.Fatalf("soft delete merchant account: %v", err)
|
|
}
|
|
recreated, err := svc.Create("reusable-username", "password123", "新账号", model.RoleMerchant, 0)
|
|
if err != nil {
|
|
t.Fatalf("recreate account with soft-deleted username: %v", err)
|
|
}
|
|
if recreated.ID == account.ID {
|
|
t.Fatalf("expected a new account record, got id=%d", recreated.ID)
|
|
}
|
|
}
|