package pub import ( "context" "testing" productModel "perms-system-server/internal/model/product" "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" "github.com/zeromicro/go-zero/core/stores/sqlx" "go.uber.org/mock/gomock" "golang.org/x/crypto/bcrypt" ) // --------------------------------------------------------------------------- // 覆盖目标:审计 M-2(第 8 轮)—— SyncPermsError{Code: 404} 必须被 REST 侧映射为 HTTP 404 // "产品不存在"(ErrNotFound),而不是 default 分支里 err 的原文。 // // 404 的触发条件: // 前置 FindOneByAppKey 已经拉到产品行(走通 401/403 校验),但事务内 LockByCodeTx 再查 // 同一产品码时 sqlx.ErrNotFound —— 即事务打开前后并发有人把产品删了。目前仓库里没有 // DeleteProduct Logic,该分支在生产里还到不了;但契约必须稳,否则将来加 DeleteProduct // 时前端/SDK 分类会错乱。 // // 命名规则:TC-0979 // --------------------------------------------------------------------------- // TC-0979: REST SyncPerms —— tx 内 LockByCodeTx ErrNotFound → HTTP 404 func TestSyncPerms_LockByCodeTxNotFound_MapsToHTTP404(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() hashedSecret, err := bcrypt.GenerateFromPassword([]byte("m2_secret"), bcrypt.MinCost) require.NoError(t, err) mockProduct := mocks.NewMockSysProductModel(ctrl) mockProduct.EXPECT().FindOneByAppKey(gomock.Any(), "m2_key"). Return(&productModel.SysProduct{ Id: 1, Code: "m2_prod", AppKey: "m2_key", AppSecret: string(hashedSecret), Status: 1, }, nil) // 关键:tx 内 LockByCodeTx 拿到 ErrNotFound → service 返回 SyncPermsError{Code:404, "产品不存在"}。 mockProduct.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "m2_prod"). Return((*productModel.SysProduct)(nil), sqlx.ErrNotFound) mockPerm := mocks.NewMockSysPermModel(ctrl) mockPerm.EXPECT().TransactCtx(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error { return fn(ctx, nil) }) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Product: mockProduct, Perm: mockPerm}) logic := NewSyncPermsLogic(context.Background(), svcCtx) resp, err := logic.SyncPerms(&types.SyncPermsReq{ AppKey: "m2_key", AppSecret: "m2_secret", Perms: []types.SyncPermItem{{Code: "p1", Name: "P1"}}, }) assert.Nil(t, resp) require.Error(t, err, "M-2:tx 内产品消失必须返回错误") var ce *response.CodeError require.ErrorAs(t, err, &ce, "M-2:必须映射成 response.CodeError 结构化错误,不能透传 SyncPermsError 原文") assert.Equal(t, 404, ce.Code(), "M-2:SyncPermsError{Code:404} 必须落到 HTTP 404 分支;若仍是 500 说明 syncPermsLogic 的 switch 缺少 404 case") assert.Equal(t, "产品不存在", ce.Error(), "M-2:保留原始语义文案") } // TC-0980(负值域对称):未映射的 se.Code(例如 500)依旧走 default,原样透传,不得被误收进 404。 // 防御未来有人想"把所有 SyncPermsError 都按 404 处理"的随手改动。 func TestSyncPerms_UnmappedSyncPermsErrCode_StillFallsThroughDefault(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() hashedSecret, err := bcrypt.GenerateFromPassword([]byte("m2_secret"), bcrypt.MinCost) require.NoError(t, err) mockProduct := mocks.NewMockSysProductModel(ctrl) mockProduct.EXPECT().FindOneByAppKey(gomock.Any(), "m2_key2"). Return(&productModel.SysProduct{ Id: 1, Code: "m2_prod2", AppKey: "m2_key2", AppSecret: string(hashedSecret), Status: 1, }, nil) mockProduct.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "m2_prod2"). Return(&productModel.SysProduct{Id: 1, Code: "m2_prod2"}, nil) mockPerm := mocks.NewMockSysPermModel(ctrl) mockPerm.EXPECT().FindMapByProductCodeWithTx(gomock.Any(), gomock.Any(), "m2_prod2"). Return(nil, assertAnyErr("internal storage bug")) mockPerm.EXPECT().TransactCtx(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error { return fn(ctx, nil) }) svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Product: mockProduct, Perm: mockPerm}) logic := NewSyncPermsLogic(context.Background(), svcCtx) _, err = logic.SyncPerms(&types.SyncPermsReq{ AppKey: "m2_key2", AppSecret: "m2_secret", Perms: []types.SyncPermItem{{Code: "p1", Name: "P1"}}, }) require.Error(t, err) var se *SyncPermsError require.ErrorAs(t, err, &se, "M-2:未映射 code 走 default,原 SyncPermsError 被原样透传") assert.Equal(t, 500, se.Code, "M-2:500 必须保持 500 原语义,不得被误归类为 404") var ce *response.CodeError assert.False(t, assert.ObjectsAreEqual(err, ce), "M-2:500 分支绝不能被映射成 response.CodeError{Code:404}") } // assertAnyErr 构造任意错误,用来模拟 tx 内非业务分支错误。 func assertAnyErr(msg string) error { return &localErr{s: msg} } type localErr struct{ s string } func (e *localErr) Error() string { return e.s }