syncPerms404_audit_test.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. package pub
  2. import (
  3. "context"
  4. "testing"
  5. productModel "perms-system-server/internal/model/product"
  6. "perms-system-server/internal/response"
  7. "perms-system-server/internal/testutil/mocks"
  8. "perms-system-server/internal/types"
  9. "github.com/stretchr/testify/assert"
  10. "github.com/stretchr/testify/require"
  11. "github.com/zeromicro/go-zero/core/stores/sqlx"
  12. "go.uber.org/mock/gomock"
  13. "golang.org/x/crypto/bcrypt"
  14. )
  15. // ---------------------------------------------------------------------------
  16. // 覆盖目标:审计 M-2(第 8 轮)—— SyncPermsError{Code: 404} 必须被 REST 侧映射为 HTTP 404
  17. // "产品不存在"(ErrNotFound),而不是 default 分支里 err 的原文。
  18. //
  19. // 404 的触发条件:
  20. // 前置 FindOneByAppKey 已经拉到产品行(走通 401/403 校验),但事务内 LockByCodeTx 再查
  21. // 同一产品码时 sqlx.ErrNotFound —— 即事务打开前后并发有人把产品删了。目前仓库里没有
  22. // DeleteProduct Logic,该分支在生产里还到不了;但契约必须稳,否则将来加 DeleteProduct
  23. // 时前端/SDK 分类会错乱。
  24. //
  25. // 命名规则:TC-0979
  26. // ---------------------------------------------------------------------------
  27. // TC-0979: REST SyncPerms —— tx 内 LockByCodeTx ErrNotFound → HTTP 404
  28. func TestSyncPerms_LockByCodeTxNotFound_MapsToHTTP404(t *testing.T) {
  29. ctrl := gomock.NewController(t)
  30. defer ctrl.Finish()
  31. hashedSecret, err := bcrypt.GenerateFromPassword([]byte("m2_secret"), bcrypt.MinCost)
  32. require.NoError(t, err)
  33. mockProduct := mocks.NewMockSysProductModel(ctrl)
  34. mockProduct.EXPECT().FindOneByAppKey(gomock.Any(), "m2_key").
  35. Return(&productModel.SysProduct{
  36. Id: 1, Code: "m2_prod", AppKey: "m2_key",
  37. AppSecret: string(hashedSecret), Status: 1,
  38. }, nil)
  39. // 关键:tx 内 LockByCodeTx 拿到 ErrNotFound → service 返回 SyncPermsError{Code:404, "产品不存在"}。
  40. mockProduct.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "m2_prod").
  41. Return((*productModel.SysProduct)(nil), sqlx.ErrNotFound)
  42. mockPerm := mocks.NewMockSysPermModel(ctrl)
  43. mockPerm.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
  44. DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
  45. return fn(ctx, nil)
  46. })
  47. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Product: mockProduct, Perm: mockPerm})
  48. logic := NewSyncPermsLogic(context.Background(), svcCtx)
  49. resp, err := logic.SyncPerms(&types.SyncPermsReq{
  50. AppKey: "m2_key", AppSecret: "m2_secret",
  51. Perms: []types.SyncPermItem{{Code: "p1", Name: "P1"}},
  52. })
  53. assert.Nil(t, resp)
  54. require.Error(t, err, "M-2:tx 内产品消失必须返回错误")
  55. var ce *response.CodeError
  56. require.ErrorAs(t, err, &ce,
  57. "M-2:必须映射成 response.CodeError 结构化错误,不能透传 SyncPermsError 原文")
  58. assert.Equal(t, 404, ce.Code(),
  59. "M-2:SyncPermsError{Code:404} 必须落到 HTTP 404 分支;若仍是 500 说明 syncPermsLogic 的 switch 缺少 404 case")
  60. assert.Equal(t, "产品不存在", ce.Error(), "M-2:保留原始语义文案")
  61. }
  62. // TC-0980(负值域对称):未映射的 se.Code(例如 500)依旧走 default,原样透传,不得被误收进 404。
  63. // 防御未来有人想"把所有 SyncPermsError 都按 404 处理"的随手改动。
  64. func TestSyncPerms_UnmappedSyncPermsErrCode_StillFallsThroughDefault(t *testing.T) {
  65. ctrl := gomock.NewController(t)
  66. defer ctrl.Finish()
  67. hashedSecret, err := bcrypt.GenerateFromPassword([]byte("m2_secret"), bcrypt.MinCost)
  68. require.NoError(t, err)
  69. mockProduct := mocks.NewMockSysProductModel(ctrl)
  70. mockProduct.EXPECT().FindOneByAppKey(gomock.Any(), "m2_key2").
  71. Return(&productModel.SysProduct{
  72. Id: 1, Code: "m2_prod2", AppKey: "m2_key2",
  73. AppSecret: string(hashedSecret), Status: 1,
  74. }, nil)
  75. mockProduct.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "m2_prod2").
  76. Return(&productModel.SysProduct{Id: 1, Code: "m2_prod2"}, nil)
  77. mockPerm := mocks.NewMockSysPermModel(ctrl)
  78. mockPerm.EXPECT().FindMapByProductCodeWithTx(gomock.Any(), gomock.Any(), "m2_prod2").
  79. Return(nil, assertAnyErr("internal storage bug"))
  80. mockPerm.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
  81. DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
  82. return fn(ctx, nil)
  83. })
  84. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Product: mockProduct, Perm: mockPerm})
  85. logic := NewSyncPermsLogic(context.Background(), svcCtx)
  86. _, err = logic.SyncPerms(&types.SyncPermsReq{
  87. AppKey: "m2_key2", AppSecret: "m2_secret",
  88. Perms: []types.SyncPermItem{{Code: "p1", Name: "P1"}},
  89. })
  90. require.Error(t, err)
  91. var se *SyncPermsError
  92. require.ErrorAs(t, err, &se, "M-2:未映射 code 走 default,原 SyncPermsError 被原样透传")
  93. assert.Equal(t, 500, se.Code, "M-2:500 必须保持 500 原语义,不得被误归类为 404")
  94. var ce *response.CodeError
  95. assert.False(t, assert.ObjectsAreEqual(err, ce),
  96. "M-2:500 分支绝不能被映射成 response.CodeError{Code:404}")
  97. }
  98. // assertAnyErr 构造任意错误,用来模拟 tx 内非业务分支错误。
  99. func assertAnyErr(msg string) error {
  100. return &localErr{s: msg}
  101. }
  102. type localErr struct{ s string }
  103. func (e *localErr) Error() string { return e.s }