优化商户管理, 增加商户名重置密码等等
This commit is contained in:
@@ -8,8 +8,143 @@ import (
|
||||
"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")
|
||||
|
||||
Reference in New Issue
Block a user