changePasswordConflict_audit_test.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. package auth
  2. import (
  3. "errors"
  4. "testing"
  5. "perms-system-server/internal/consts"
  6. "perms-system-server/internal/loaders"
  7. "perms-system-server/internal/middleware"
  8. userModel "perms-system-server/internal/model/user"
  9. "perms-system-server/internal/response"
  10. "perms-system-server/internal/testutil/mocks"
  11. "perms-system-server/internal/types"
  12. "github.com/stretchr/testify/assert"
  13. "github.com/stretchr/testify/require"
  14. "go.uber.org/mock/gomock"
  15. "golang.org/x/crypto/bcrypt"
  16. )
  17. // ---------------------------------------------------------------------------
  18. // 覆盖目标:审计 M-R10-4 —— ChangePassword 必须把底层 `userModel.ErrUpdateConflict`
  19. // 显式映射为 409 "密码已被其他会话修改...";修复前 raw error 会被 rest 兜成 500,
  20. // 导致前端把"并发冲突"误判为系统故障,也会把告警归到 5xx 噪声池。
  21. //
  22. // 口径与 UpdateUserLogic / UpdateUserStatusLogic / UpdateRoleLogic 完全对齐。
  23. // ---------------------------------------------------------------------------
  24. // TC-1015: M-R10-4 —— UpdatePassword 返回 ErrUpdateConflict 时,ChangePassword 必须回 409。
  25. func TestChangePassword_UpdateConflict_Maps409(t *testing.T) {
  26. ctrl := gomock.NewController(t)
  27. t.Cleanup(ctrl.Finish)
  28. const userId = int64(777)
  29. oldPwd := "Oldpass123"
  30. newPwd := "Newpass456"
  31. hashed, err := bcrypt.GenerateFromPassword([]byte(oldPwd), bcrypt.DefaultCost)
  32. require.NoError(t, err)
  33. mockUser := mocks.NewMockSysUserModel(ctrl)
  34. mockUser.EXPECT().FindOne(gomock.Any(), userId).
  35. Return(&userModel.SysUser{
  36. Id: userId,
  37. Username: "m_r10_4_subject",
  38. Password: string(hashed),
  39. Status: consts.StatusEnabled,
  40. UpdateTime: 1000,
  41. }, nil)
  42. // 关键:强制底层返回 ErrUpdateConflict。
  43. // H-R11-1:签名增加 username 与 expectedUpdateTime 两个透传参数。
  44. mockUser.EXPECT().
  45. UpdatePassword(gomock.Any(), userId, "m_r10_4_subject", gomock.Any(), int64(consts.MustChangePasswordNo), int64(1000)).
  46. Return(userModel.ErrUpdateConflict)
  47. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{User: mockUser})
  48. ctx := middleware.WithUserDetails(t.Context(), &loaders.UserDetails{UserId: userId})
  49. logic := NewChangePasswordLogic(ctx, svcCtx)
  50. err = logic.ChangePassword(&types.ChangePasswordReq{
  51. OldPassword: oldPwd,
  52. NewPassword: newPwd,
  53. })
  54. var codeErr *response.CodeError
  55. require.True(t, errors.As(err, &codeErr), "审计 M-R10-4:必须是 *response.CodeError,否则会被 rest 兜成 500")
  56. assert.Equal(t, 409, codeErr.Code(), "审计 M-R10-4:ErrUpdateConflict 必须映射为 409 Conflict")
  57. assert.Contains(t, codeErr.Error(), "密码已被其他会话修改", "审计 M-R10-4:文案与业务契约对齐")
  58. }
  59. // TC-1016: M-R10-4 —— 非 ErrUpdateConflict 的原生错误仍应透传(500 由 rest 兜底),
  60. // 防止修复把所有底层错误都误吞为 409。
  61. func TestChangePassword_GenericUpdateError_StillPropagates(t *testing.T) {
  62. ctrl := gomock.NewController(t)
  63. t.Cleanup(ctrl.Finish)
  64. const userId = int64(778)
  65. oldPwd := "Oldpass123"
  66. newPwd := "Newpass456"
  67. hashed, err := bcrypt.GenerateFromPassword([]byte(oldPwd), bcrypt.DefaultCost)
  68. require.NoError(t, err)
  69. mockUser := mocks.NewMockSysUserModel(ctrl)
  70. mockUser.EXPECT().FindOne(gomock.Any(), userId).
  71. Return(&userModel.SysUser{
  72. Id: userId,
  73. Username: "m_r10_4_subject2",
  74. Password: string(hashed),
  75. Status: consts.StatusEnabled,
  76. UpdateTime: 2000,
  77. }, nil)
  78. genericErr := errors.New("driver: bad connection")
  79. mockUser.EXPECT().
  80. UpdatePassword(gomock.Any(), userId, "m_r10_4_subject2", gomock.Any(), int64(consts.MustChangePasswordNo), int64(2000)).
  81. Return(genericErr)
  82. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{User: mockUser})
  83. ctx := middleware.WithUserDetails(t.Context(), &loaders.UserDetails{UserId: userId})
  84. logic := NewChangePasswordLogic(ctx, svcCtx)
  85. err = logic.ChangePassword(&types.ChangePasswordReq{
  86. OldPassword: oldPwd,
  87. NewPassword: newPwd,
  88. })
  89. require.Error(t, err)
  90. assert.ErrorIs(t, err, genericErr, "审计 M-R10-4:只把 ErrUpdateConflict 映射 409,其余错误原样透传(由 rest 兜 500)")
  91. var codeErr *response.CodeError
  92. assert.False(t, errors.As(err, &codeErr), "审计 M-R10-4:非冲突错误不得伪装成 CodeError")
  93. }