package auth import ( "errors" "testing" "perms-system-server/internal/consts" "perms-system-server/internal/loaders" "perms-system-server/internal/middleware" userModel "perms-system-server/internal/model/user" "perms-system-server/internal/response" "perms-system-server/internal/testutil/mocks" "perms-system-server/internal/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "golang.org/x/crypto/bcrypt" ) // --------------------------------------------------------------------------- // 覆盖目标:审计 M-R10-4 —— ChangePassword 必须把底层 `userModel.ErrUpdateConflict` // 显式映射为 409 "密码已被其他会话修改...";修复前 raw error 会被 rest 兜成 500, // 导致前端把"并发冲突"误判为系统故障,也会把告警归到 5xx 噪声池。 // // 口径与 UpdateUserLogic / UpdateUserStatusLogic / UpdateRoleLogic 完全对齐。 // --------------------------------------------------------------------------- // TC-1015: M-R10-4 —— UpdatePassword 返回 ErrUpdateConflict 时,ChangePassword 必须回 409。 func TestChangePassword_UpdateConflict_Maps409(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) const userId = int64(777) oldPwd := "Oldpass123" newPwd := "Newpass456" hashed, err := bcrypt.GenerateFromPassword([]byte(oldPwd), bcrypt.DefaultCost) require.NoError(t, err) mockUser := mocks.NewMockSysUserModel(ctrl) mockUser.EXPECT().FindOne(gomock.Any(), userId). Return(&userModel.SysUser{ Id: userId, Username: "m_r10_4_subject", Password: string(hashed), Status: consts.StatusEnabled, UpdateTime: 1000, }, nil) // 关键:强制底层返回 ErrUpdateConflict。 mockUser.EXPECT(). UpdatePassword(gomock.Any(), userId, gomock.Any(), int64(consts.MustChangePasswordNo)). Return(userModel.ErrUpdateConflict) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{User: mockUser}) ctx := middleware.WithUserDetails(t.Context(), &loaders.UserDetails{UserId: userId}) logic := NewChangePasswordLogic(ctx, svcCtx) err = logic.ChangePassword(&types.ChangePasswordReq{ OldPassword: oldPwd, NewPassword: newPwd, }) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr), "审计 M-R10-4:必须是 *response.CodeError,否则会被 rest 兜成 500") assert.Equal(t, 409, codeErr.Code(), "审计 M-R10-4:ErrUpdateConflict 必须映射为 409 Conflict") assert.Contains(t, codeErr.Error(), "密码已被其他会话修改", "审计 M-R10-4:文案与业务契约对齐") } // TC-1016: M-R10-4 —— 非 ErrUpdateConflict 的原生错误仍应透传(500 由 rest 兜底), // 防止修复把所有底层错误都误吞为 409。 func TestChangePassword_GenericUpdateError_StillPropagates(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) const userId = int64(778) oldPwd := "Oldpass123" newPwd := "Newpass456" hashed, err := bcrypt.GenerateFromPassword([]byte(oldPwd), bcrypt.DefaultCost) require.NoError(t, err) mockUser := mocks.NewMockSysUserModel(ctrl) mockUser.EXPECT().FindOne(gomock.Any(), userId). Return(&userModel.SysUser{ Id: userId, Username: "m_r10_4_subject2", Password: string(hashed), Status: consts.StatusEnabled, UpdateTime: 2000, }, nil) genericErr := errors.New("driver: bad connection") mockUser.EXPECT(). UpdatePassword(gomock.Any(), userId, gomock.Any(), int64(consts.MustChangePasswordNo)). Return(genericErr) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{User: mockUser}) ctx := middleware.WithUserDetails(t.Context(), &loaders.UserDetails{UserId: userId}) logic := NewChangePasswordLogic(ctx, svcCtx) err = logic.ChangePassword(&types.ChangePasswordReq{ OldPassword: oldPwd, NewPassword: newPwd, }) require.Error(t, err) assert.ErrorIs(t, err, genericErr, "审计 M-R10-4:只把 ErrUpdateConflict 映射 409,其余错误原样透传(由 rest 兜 500)") var codeErr *response.CodeError assert.False(t, errors.As(err, &codeErr), "审计 M-R10-4:非冲突错误不得伪装成 CodeError") }