| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836 |
- package pub
- import (
- "context"
- "errors"
- "fmt"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "github.com/zeromicro/go-zero/core/stores/redis"
- "github.com/zeromicro/go-zero/core/stores/sqlx"
- "go.uber.org/mock/gomock"
- "golang.org/x/crypto/bcrypt"
- permModel "perms-system-server/internal/model/perm"
- productModel "perms-system-server/internal/model/product"
- "perms-system-server/internal/response"
- "perms-system-server/internal/testutil"
- "perms-system-server/internal/testutil/mocks"
- "perms-system-server/internal/types"
- "testing"
- "time"
- )
- func insertSyncTestProduct(t *testing.T, ctx context.Context, code, appKey, appSecret string, status int64) (int64, func()) {
- t.Helper()
- svcCtx := newTestSvcCtx()
- conn := testutil.GetTestSqlConn()
- now := time.Now().Unix()
- hashedSecret, err := bcrypt.GenerateFromPassword([]byte(appSecret), bcrypt.MinCost)
- require.NoError(t, err)
- res, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
- Code: code,
- Name: code,
- AppKey: appKey,
- AppSecret: string(hashedSecret),
- Status: status,
- CreateTime: now,
- UpdateTime: now,
- })
- require.NoError(t, err)
- id, _ := res.LastInsertId()
- cleanup := func() {
- testutil.CleanTable(ctx, conn, "`sys_product`", id)
- }
- return id, cleanup
- }
- // TC-0036: 全部新增
- func TestSyncPerms_AllNew(t *testing.T) {
- ctx := context.Background()
- svcCtx := newTestSvcCtx()
- conn := testutil.GetTestSqlConn()
- pc := testutil.UniqueId()
- appKey := testutil.UniqueId()
- appSecret := testutil.UniqueId()
- _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
- t.Cleanup(cleanProduct)
- t.Cleanup(func() { testutil.CleanTableByField(ctx, conn, "`sys_perm`", "productCode", pc) })
- logic := NewSyncPermsLogic(ctx, svcCtx)
- resp, err := logic.SyncPerms(&types.SyncPermsReq{
- AppKey: appKey,
- AppSecret: appSecret,
- Perms: []types.SyncPermItem{
- {Code: "perm_a", Name: "Perm A", Remark: "remark a"},
- {Code: "perm_b", Name: "Perm B"},
- {Code: "perm_c", Name: "Perm C"},
- },
- })
- require.NoError(t, err)
- require.NotNil(t, resp)
- assert.Equal(t, int64(3), resp.Added)
- assert.Equal(t, int64(0), resp.Updated)
- assert.Equal(t, int64(0), resp.Disabled)
- }
- // TC-0037: 更新已有(名称变更)
- func TestSyncPerms_UpdateExisting(t *testing.T) {
- ctx := context.Background()
- svcCtx := newTestSvcCtx()
- conn := testutil.GetTestSqlConn()
- pc := testutil.UniqueId()
- appKey := testutil.UniqueId()
- appSecret := testutil.UniqueId()
- now := time.Now().Unix()
- _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
- t.Cleanup(cleanProduct)
- permRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
- ProductCode: pc, Name: "Old Name", Code: "upd_code", Remark: "old remark", Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- permId, _ := permRes.LastInsertId()
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_perm`", permId) })
- logic := NewSyncPermsLogic(ctx, svcCtx)
- resp, err := logic.SyncPerms(&types.SyncPermsReq{
- AppKey: appKey,
- AppSecret: appSecret,
- Perms: []types.SyncPermItem{
- {Code: "upd_code", Name: "New Name", Remark: "new remark"},
- },
- })
- require.NoError(t, err)
- require.NotNil(t, resp)
- assert.Equal(t, int64(0), resp.Added)
- assert.Equal(t, int64(1), resp.Updated)
- assert.Equal(t, int64(0), resp.Disabled)
- updated, err := svcCtx.SysPermModel.FindOne(ctx, permId)
- require.NoError(t, err)
- assert.Equal(t, "New Name", updated.Name)
- assert.Equal(t, "new remark", updated.Remark)
- assert.Equal(t, int64(1), updated.Status)
- }
- // TC-0038: 无变化
- func TestSyncPerms_NoChanges(t *testing.T) {
- ctx := context.Background()
- svcCtx := newTestSvcCtx()
- conn := testutil.GetTestSqlConn()
- pc := testutil.UniqueId()
- appKey := testutil.UniqueId()
- appSecret := testutil.UniqueId()
- now := time.Now().Unix()
- _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
- t.Cleanup(cleanProduct)
- permRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
- ProductCode: pc, Name: "Same Name", Code: "same_code", Remark: "same remark", Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- permId, _ := permRes.LastInsertId()
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_perm`", permId) })
- logic := NewSyncPermsLogic(ctx, svcCtx)
- resp, err := logic.SyncPerms(&types.SyncPermsReq{
- AppKey: appKey,
- AppSecret: appSecret,
- Perms: []types.SyncPermItem{
- {Code: "same_code", Name: "Same Name", Remark: "same remark"},
- },
- })
- require.NoError(t, err)
- require.NotNil(t, resp)
- assert.Equal(t, int64(0), resp.Added)
- assert.Equal(t, int64(0), resp.Updated)
- assert.Equal(t, int64(0), resp.Disabled)
- }
- // TC-0039: 禁用权限重启
- func TestSyncPerms_ReEnableDisabled(t *testing.T) {
- ctx := context.Background()
- svcCtx := newTestSvcCtx()
- conn := testutil.GetTestSqlConn()
- pc := testutil.UniqueId()
- appKey := testutil.UniqueId()
- appSecret := testutil.UniqueId()
- now := time.Now().Unix()
- _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
- t.Cleanup(cleanProduct)
- permRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
- ProductCode: pc, Name: "Disabled Perm", Code: "dis_code", Remark: "", Status: 2, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- permId, _ := permRes.LastInsertId()
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_perm`", permId) })
- logic := NewSyncPermsLogic(ctx, svcCtx)
- resp, err := logic.SyncPerms(&types.SyncPermsReq{
- AppKey: appKey,
- AppSecret: appSecret,
- Perms: []types.SyncPermItem{
- {Code: "dis_code", Name: "Disabled Perm"},
- },
- })
- require.NoError(t, err)
- require.NotNil(t, resp)
- assert.Equal(t, int64(0), resp.Added)
- assert.Equal(t, int64(1), resp.Updated)
- reEnabled, err := svcCtx.SysPermModel.FindOne(ctx, permId)
- require.NoError(t, err)
- assert.Equal(t, int64(1), reEnabled.Status)
- }
- // TC-0040: 移除不在列表的权限
- func TestSyncPerms_DisableNotInList(t *testing.T) {
- ctx := context.Background()
- svcCtx := newTestSvcCtx()
- conn := testutil.GetTestSqlConn()
- pc := testutil.UniqueId()
- appKey := testutil.UniqueId()
- appSecret := testutil.UniqueId()
- now := time.Now().Unix()
- _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
- t.Cleanup(cleanProduct)
- keepCode := testutil.UniqueId()
- removeCode := testutil.UniqueId()
- keepRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
- ProductCode: pc, Name: "Keep", Code: keepCode, Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- keepId, _ := keepRes.LastInsertId()
- removeRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
- ProductCode: pc, Name: "Remove", Code: removeCode, Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- removeId, _ := removeRes.LastInsertId()
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_perm`", keepId, removeId) })
- logic := NewSyncPermsLogic(ctx, svcCtx)
- resp, err := logic.SyncPerms(&types.SyncPermsReq{
- AppKey: appKey,
- AppSecret: appSecret,
- Perms: []types.SyncPermItem{
- {Code: keepCode, Name: "Keep"},
- },
- })
- require.NoError(t, err)
- require.NotNil(t, resp)
- assert.Equal(t, int64(0), resp.Added)
- assert.Equal(t, int64(1), resp.Disabled)
- disabled, err := svcCtx.SysPermModel.FindOne(ctx, removeId)
- require.NoError(t, err)
- assert.Equal(t, int64(2), disabled.Status)
- kept, err := svcCtx.SysPermModel.FindOne(ctx, keepId)
- require.NoError(t, err)
- assert.Equal(t, int64(1), kept.Status)
- }
- // TC-0041: 空perms数组应被拒绝
- func TestSyncPerms_EmptyPermsRejected(t *testing.T) {
- ctx := context.Background()
- svcCtx := newTestSvcCtx()
- pc := testutil.UniqueId()
- appKey := testutil.UniqueId()
- appSecret := testutil.UniqueId()
- _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
- t.Cleanup(cleanProduct)
- logic := NewSyncPermsLogic(ctx, svcCtx)
- resp, err := logic.SyncPerms(&types.SyncPermsReq{
- AppKey: appKey,
- AppSecret: appSecret,
- Perms: []types.SyncPermItem{},
- })
- require.Nil(t, resp)
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 400, codeErr.Code())
- assert.Contains(t, codeErr.Error(), "权限列表不能为空")
- }
- // TC-0043: appKey无效
- func TestSyncPerms_InvalidAppKey(t *testing.T) {
- ctx := context.Background()
- svcCtx := newTestSvcCtx()
- logic := NewSyncPermsLogic(ctx, svcCtx)
- resp, err := logic.SyncPerms(&types.SyncPermsReq{
- AppKey: "nonexistent_key_" + testutil.UniqueId(),
- AppSecret: "whatever",
- Perms: []types.SyncPermItem{{Code: "x", Name: "x"}},
- })
- require.Nil(t, resp)
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 401, codeErr.Code())
- assert.Equal(t, "无效的appKey", codeErr.Error())
- }
- // TC-0044: appSecret错误
- func TestSyncPerms_WrongAppSecret(t *testing.T) {
- ctx := context.Background()
- svcCtx := newTestSvcCtx()
- pc := testutil.UniqueId()
- appKey := testutil.UniqueId()
- appSecret := testutil.UniqueId()
- _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
- t.Cleanup(cleanProduct)
- logic := NewSyncPermsLogic(ctx, svcCtx)
- resp, err := logic.SyncPerms(&types.SyncPermsReq{
- AppKey: appKey,
- AppSecret: "wrong_secret",
- Perms: []types.SyncPermItem{{Code: "x", Name: "x"}},
- })
- require.Nil(t, resp)
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 401, codeErr.Code())
- assert.Equal(t, "appSecret验证失败", codeErr.Error())
- }
- // TC-0045: 产品已禁用
- func TestSyncPerms_ProductDisabled(t *testing.T) {
- ctx := context.Background()
- svcCtx := newTestSvcCtx()
- pc := testutil.UniqueId()
- appKey := testutil.UniqueId()
- appSecret := testutil.UniqueId()
- _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 2)
- t.Cleanup(cleanProduct)
- logic := NewSyncPermsLogic(ctx, svcCtx)
- resp, err := logic.SyncPerms(&types.SyncPermsReq{
- AppKey: appKey,
- AppSecret: appSecret,
- Perms: []types.SyncPermItem{{Code: "x", Name: "x"}},
- })
- require.Nil(t, resp)
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 403, codeErr.Code())
- assert.Equal(t, "产品已被禁用", codeErr.Error())
- }
- // TC-0046: 大批量(1000条)
- func TestSyncPerms_LargeBatch1000(t *testing.T) {
- ctx := context.Background()
- svcCtx := newTestSvcCtx()
- conn := testutil.GetTestSqlConn()
- pc := testutil.UniqueId()
- appKey := testutil.UniqueId()
- appSecret := testutil.UniqueId()
- _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
- t.Cleanup(cleanProduct)
- t.Cleanup(func() { testutil.CleanTableByField(ctx, conn, "`sys_perm`", "productCode", pc) })
- perms := make([]types.SyncPermItem, 1000)
- for i := 0; i < 1000; i++ {
- perms[i] = types.SyncPermItem{
- Code: fmt.Sprintf("batch_%s_%d", pc, i),
- Name: fmt.Sprintf("Perm_%d", i),
- }
- }
- logic := NewSyncPermsLogic(ctx, svcCtx)
- resp, err := logic.SyncPerms(&types.SyncPermsReq{
- AppKey: appKey,
- AppSecret: appSecret,
- Perms: perms,
- })
- require.NoError(t, err)
- require.NotNil(t, resp)
- assert.Equal(t, int64(1000), resp.Added)
- assert.Equal(t, int64(0), resp.Updated)
- assert.Equal(t, int64(0), resp.Disabled)
- }
- // TC-0047: 重复code去重
- func TestSyncPerms_DeduplicateCodes(t *testing.T) {
- ctx := context.Background()
- svcCtx := newTestSvcCtx()
- conn := testutil.GetTestSqlConn()
- pc := testutil.UniqueId()
- appKey := testutil.UniqueId()
- appSecret := testutil.UniqueId()
- _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
- t.Cleanup(cleanProduct)
- t.Cleanup(func() { testutil.CleanTableByField(ctx, conn, "`sys_perm`", "productCode", pc) })
- logic := NewSyncPermsLogic(ctx, svcCtx)
- resp, err := logic.SyncPerms(&types.SyncPermsReq{
- AppKey: appKey,
- AppSecret: appSecret,
- Perms: []types.SyncPermItem{
- {Code: "dup_code", Name: "Perm First"},
- {Code: "dup_code", Name: "Perm Duplicate"},
- {Code: "unique_code", Name: "Unique"},
- },
- })
- require.NoError(t, err)
- require.NotNil(t, resp)
- assert.Equal(t, int64(2), resp.Added, "重复code应被去重,只添加2条")
- }
- // TC-0042: 验证disabled返回值
- func TestSyncPerms_VerifyDisabledCount(t *testing.T) {
- ctx := context.Background()
- svcCtx := newTestSvcCtx()
- conn := testutil.GetTestSqlConn()
- pc := testutil.UniqueId()
- appKey := testutil.UniqueId()
- appSecret := testutil.UniqueId()
- now := time.Now().Unix()
- _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
- t.Cleanup(cleanProduct)
- var permIds []int64
- for i := 0; i < 5; i++ {
- res, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
- ProductCode: pc, Name: fmt.Sprintf("p%d", i), Code: fmt.Sprintf("code_%s_%d", pc, i),
- Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- id, _ := res.LastInsertId()
- permIds = append(permIds, id)
- }
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_perm`", permIds...) })
- logic := NewSyncPermsLogic(ctx, svcCtx)
- resp, err := logic.SyncPerms(&types.SyncPermsReq{
- AppKey: appKey,
- AppSecret: appSecret,
- Perms: []types.SyncPermItem{
- {Code: fmt.Sprintf("code_%s_0", pc), Name: "p0"},
- {Code: fmt.Sprintf("code_%s_1", pc), Name: "p1"},
- },
- })
- require.NoError(t, err)
- require.NotNil(t, resp)
- assert.Equal(t, int64(0), resp.Added)
- assert.Equal(t, int64(3), resp.Disabled)
- }
- 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, "tx 内产品消失必须返回错误")
- var ce *response.CodeError
- require.ErrorAs(t, err, &ce,
- "必须映射成 response.CodeError 结构化错误,不能透传 SyncPermsError 原文")
- assert.Equal(t, 404, ce.Code(),
- "SyncPermsError{Code:404} 必须落到 HTTP 404 分支;若仍是 500 说明 syncPermsLogic 的 switch 缺少 404 case")
- assert.Equal(t, "产品不存在", ce.Error(), "保留原始语义文案")
- }
- // 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)
- // LockByCodeTx 拿到的行必须 Status=1 才能继续进入 diff 逻辑
- mockProduct.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "m2_prod2").
- Return(&productModel.SysProduct{Id: 1, Code: "m2_prod2", Status: 1}, 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, "未映射 code 走 default,原 SyncPermsError 被原样透传")
- assert.Equal(t, 500, se.Code, "500 必须保持 500 原语义,不得被误归类为 404")
- var ce *response.CodeError
- assert.False(t, assert.ObjectsAreEqual(err, ce),
- "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 }
- func primeProductIndex(t *testing.T, rds *redis.Redis, cachePrefix, productCode string) string {
- t.Helper()
- idxKey := fmt.Sprintf("%s:ud:idx:p:%s", cachePrefix, productCode)
- canary := fmt.Sprintf("%s:ud:probe:%s", cachePrefix, testutil.UniqueId())
- _, err := rds.Sadd(idxKey, canary)
- require.NoError(t, err, "SAdd 到 productIndexKey 失败,Redis 不可用,测试前置条件失败")
- members, err := rds.Smembers(idxKey)
- require.NoError(t, err)
- require.Contains(t, members, canary, "primeProductIndex: canary 必须先出现在集合里")
- return idxKey
- }
- // TC-1064: 纯新增不触发 CleanByProduct
- func TestSyncPerms_PureAddDoesNotTriggerCleanByProduct(t *testing.T) {
- ctx := context.Background()
- svcCtx := newTestSvcCtx()
- cfg := testutil.GetTestConfig()
- rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
- conn := testutil.GetTestSqlConn()
- pc := testutil.UniqueId()
- appKey := testutil.UniqueId()
- appSecret := testutil.UniqueId()
- _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
- t.Cleanup(cleanProduct)
- t.Cleanup(func() { testutil.CleanTableByField(ctx, conn, "`sys_perm`", "productCode", pc) })
- idxKey := primeProductIndex(t, rds, cfg.CacheRedis.KeyPrefix, pc)
- t.Cleanup(func() { _, _ = rds.Del(idxKey) })
- result, err := ExecuteSyncPerms(ctx, svcCtx, appKey, appSecret, []SyncPermItem{
- {Code: "r11_4_add_a", Name: "A"},
- {Code: "r11_4_add_b", Name: "B"},
- {Code: "r11_4_add_c", Name: "C"},
- })
- require.NoError(t, err)
- require.NotNil(t, result)
- assert.Equal(t, int64(3), result.Added)
- assert.Equal(t, int64(0), result.Updated)
- assert.Equal(t, int64(0), result.Disabled)
- // 纯新增路径:CleanByProduct 不得被调用,索引集合必须仍保留 canary。
- exists, err := rds.Exists(idxKey)
- require.NoError(t, err)
- assert.True(t, exists,
- "added=3 / updated=0 / disabled=0 属于纯新增,不得触发 CleanByProduct;"+
- "productIndexKey 若被删除说明 SyncPerms 仍在走全产品清缓存路径,回归:会把该产品"+
- "所有在线用户下一次请求同时打穿回 DB")
- }
- // TC-1065: updated > 0 时必须触发 CleanByProduct
- func TestSyncPerms_UpdateTriggersCleanByProduct(t *testing.T) {
- ctx := context.Background()
- svcCtx := newTestSvcCtx()
- cfg := testutil.GetTestConfig()
- rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
- conn := testutil.GetTestSqlConn()
- pc := testutil.UniqueId()
- appKey := testutil.UniqueId()
- appSecret := testutil.UniqueId()
- _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
- t.Cleanup(cleanProduct)
- t.Cleanup(func() { testutil.CleanTableByField(ctx, conn, "`sys_perm`", "productCode", pc) })
- // 第一次同步:纯新增,为随后的 update 打底;这次不触发 Clean,CleanByProduct 仍然可被后续触发。
- _, err := ExecuteSyncPerms(ctx, svcCtx, appKey, appSecret, []SyncPermItem{
- {Code: "r11_4_upd", Name: "OldName"},
- })
- require.NoError(t, err)
- idxKey := primeProductIndex(t, rds, cfg.CacheRedis.KeyPrefix, pc)
- t.Cleanup(func() { _, _ = rds.Del(idxKey) })
- // 第二次同步:同一 Code 改 Name → updated=1。
- result, err := ExecuteSyncPerms(ctx, svcCtx, appKey, appSecret, []SyncPermItem{
- {Code: "r11_4_upd", Name: "NewName"},
- })
- require.NoError(t, err)
- require.NotNil(t, result)
- assert.Equal(t, int64(1), result.Updated,
- "前置:同名 Code 改 Name 必须 updated=1,否则后续断言失去意义")
- exists, err := rds.Exists(idxKey)
- require.NoError(t, err)
- assert.False(t, exists,
- "updated>0 必须触发 CleanByProduct;若 canary 仍在,说明 Logic 把"+
- "updated 情况也误归入'纯新增'分支,已存在 UD 缓存中的旧 perms 将长期对外返回")
- }
- // TC-1066: disabled > 0 时必须触发 CleanByProduct
- func TestSyncPerms_DisableTriggersCleanByProduct(t *testing.T) {
- ctx := context.Background()
- svcCtx := newTestSvcCtx()
- cfg := testutil.GetTestConfig()
- rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
- conn := testutil.GetTestSqlConn()
- pc := testutil.UniqueId()
- appKey := testutil.UniqueId()
- appSecret := testutil.UniqueId()
- _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
- t.Cleanup(cleanProduct)
- t.Cleanup(func() { testutil.CleanTableByField(ctx, conn, "`sys_perm`", "productCode", pc) })
- // 先注入两个 perm。
- _, err := ExecuteSyncPerms(ctx, svcCtx, appKey, appSecret, []SyncPermItem{
- {Code: "r11_4_keep", Name: "K"},
- {Code: "r11_4_drop", Name: "D"},
- })
- require.NoError(t, err)
- idxKey := primeProductIndex(t, rds, cfg.CacheRedis.KeyPrefix, pc)
- t.Cleanup(func() { _, _ = rds.Del(idxKey) })
- // 第二次只同步 r11_4_keep,r11_4_drop 会被 DisableNotInCodesWithTx 置 disabled。
- result, err := ExecuteSyncPerms(ctx, svcCtx, appKey, appSecret, []SyncPermItem{
- {Code: "r11_4_keep", Name: "K"},
- })
- require.NoError(t, err)
- assert.Equal(t, int64(1), result.Disabled,
- "前置:第二次只同步 keep,drop 必须被 disabled=1")
- exists, err := rds.Exists(idxKey)
- require.NoError(t, err)
- assert.False(t, exists,
- "disabled>0 必须触发 CleanByProduct,否则已缓存的 UD.perms 里仍挂着"+
- "已禁用权限,权限网关会把不再有效的权限判为 allow,产生权限残留")
- }
- func TestExecuteSyncPerms_DeduplicatesRequest(t *testing.T) {
- ctrl := gomock.NewController(t)
- t.Cleanup(ctrl.Finish)
- hashedSecret, err := bcrypt.GenerateFromPassword([]byte("s"), bcrypt.MinCost)
- require.NoError(t, err)
- mockProduct := mocks.NewMockSysProductModel(ctrl)
- mockProduct.EXPECT().FindOneByAppKey(gomock.Any(), "ak").
- Return(&productModel.SysProduct{
- Id: 1, Code: "pc_dedup", AppKey: "ak", AppSecret: string(hashedSecret), Status: 1,
- }, nil)
- // LockByCodeTx 拿到的行必须 Status=1
- mockProduct.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "pc_dedup").
- Return(&productModel.SysProduct{Id: 1, Code: "pc_dedup", Status: 1}, nil)
- mockPerm := mocks.NewMockSysPermModel(ctrl)
- mockPerm.EXPECT().FindMapByProductCodeWithTx(gomock.Any(), gomock.Any(), "pc_dedup").
- Return(map[string]*permModel.SysPerm{}, nil)
- var captured []*permModel.SysPerm
- mockPerm.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
- DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
- return fn(ctx, nil)
- })
- mockPerm.EXPECT().BatchInsertWithTx(gomock.Any(), nil, gomock.Any()).
- DoAndReturn(func(ctx context.Context, s sqlx.Session, items []*permModel.SysPerm) error {
- captured = items
- return nil
- })
- // 去重后 codes 只剩一个,DisableNotInCodesWithTx 用去重后的集合做 NOT IN。
- mockPerm.EXPECT().DisableNotInCodesWithTx(gomock.Any(), nil, "pc_dedup", []string{"dup_code"}, gomock.Any()).
- Return(int64(0), nil)
- svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
- Product: mockProduct, Perm: mockPerm,
- })
- result, err := ExecuteSyncPerms(context.Background(), svcCtx, "ak", "s", []SyncPermItem{
- {Code: "dup_code", Name: "A"},
- {Code: "dup_code", Name: "A-again"},
- {Code: "dup_code", Name: "A-yet-again"},
- })
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Len(t, captured, 1, "入参内 code 重复必须去重为 1 条,避免自撞 1062")
- assert.Equal(t, "dup_code", captured[0].Code)
- assert.Equal(t, "A", captured[0].Name,
- "去重策略应稳定到首次出现,使行为可预测")
- }
- func newBaseProductMock(ctrl *gomock.Controller, code string) *mocks.MockSysProductModel {
- hashed, _ := bcrypt.GenerateFromPassword([]byte("s"), bcrypt.MinCost)
- m := mocks.NewMockSysProductModel(ctrl)
- m.EXPECT().FindOneByAppKey(gomock.Any(), "ak").
- Return(&productModel.SysProduct{
- Id: 1, Code: code, AppKey: "ak", AppSecret: string(hashed), Status: 1,
- }, nil)
- return m
- }
- // TC-0843: 契约 —— 正常路径下 LockByCodeTx 必须先于 FindMapByProductCodeWithTx,
- // 且两者均在同一个 tx session 内被调用。
- func TestExecuteSyncPerms_LockBeforeMapReadInTx(t *testing.T) {
- ctrl := gomock.NewController(t)
- t.Cleanup(ctrl.Finish)
- productMock := newBaseProductMock(ctrl, "pc_tx_order")
- permMock := mocks.NewMockSysPermModel(ctrl)
- // 关键点 1:TransactCtx 必须真的传入一个 tx session,并把所有子调用都发生在其中。
- permMock.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
- DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
- return fn(ctx, nil) // nil session 只是 mock 占位
- })
- // 关键点 2:gomock 的 Call.After 强制 LockByCodeTx 先于 FindMapByProductCodeWithTx 执行。
- // 顺序反过来的话 gomock 会在 Finish 时报错。
- // 事务内复核 Status 必须 Status=1,否则走 403 分支不写 perm
- lockCall := productMock.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "pc_tx_order").
- Return(&productModel.SysProduct{Id: 1, Code: "pc_tx_order", Status: 1}, nil)
- permMock.EXPECT().FindMapByProductCodeWithTx(gomock.Any(), gomock.Any(), "pc_tx_order").
- Return(map[string]*permModel.SysPerm{}, nil).
- After(lockCall)
- // 一条简单的 INSERT + DisableNotIn 让流程走完;非本 TC 的主断言。
- permMock.EXPECT().BatchInsertWithTx(gomock.Any(), nil, gomock.Any()).Return(nil)
- permMock.EXPECT().DisableNotInCodesWithTx(gomock.Any(), nil, "pc_tx_order", []string{"x"}, gomock.Any()).
- Return(int64(0), nil)
- svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Product: productMock, Perm: permMock})
- result, err := ExecuteSyncPerms(context.Background(), svcCtx, "ak", "s",
- []SyncPermItem{{Code: "x", Name: "X"}})
- require.NoError(t, err)
- require.NotNil(t, result)
- assert.Equal(t, int64(1), result.Added, "lock 在 tx 内就位后应当能正常写入")
- }
- // TC-0844: 分支 —— tx 内 LockByCodeTx 返回 sqlx.ErrNotFound(产品在 tx 开启后被删),
- // 必须映射为 SyncPermsError{Code:404, Message:"产品不存在"},而非 500。
- func TestExecuteSyncPerms_LockNotFound_Maps404(t *testing.T) {
- ctrl := gomock.NewController(t)
- t.Cleanup(ctrl.Finish)
- productMock := newBaseProductMock(ctrl, "pc_tx_gone")
- permMock := mocks.NewMockSysPermModel(ctrl)
- permMock.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
- DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
- return fn(ctx, nil)
- })
- productMock.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "pc_tx_gone").
- Return(nil, sqlx.ErrNotFound)
- // 关键:锁失败后绝不能继续走 FindMapByProductCodeWithTx / BatchInsertWithTx。
- // gomock 默认严格模式会在 Finish 时报 "unexpected call",所以不为这些方法登记任何期望即可。
- svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Product: productMock, Perm: permMock})
- result, err := ExecuteSyncPerms(context.Background(), svcCtx, "ak", "s",
- []SyncPermItem{{Code: "x", Name: "X"}})
- assert.Nil(t, result)
- require.Error(t, err)
- var se *SyncPermsError
- require.True(t, errors.As(err, &se), "锁不到产品行必须产出 *SyncPermsError")
- assert.Equal(t, 404, se.Code,
- "tx 开启后 LockByCodeTx=ErrNotFound 意味着产品行在 tx 中不可见,应当返回 404 而非 500")
- assert.Contains(t, se.Message, "产品不存在",
- "文案应当能让调用方人眼秒懂是什么错误")
- }
- // TC-0845: 容错 —— tx 内 LockByCodeTx 冒出非 NotFound 的通用错误(driver/conn 异常),
- // 必须被事务回滚并被外层包裹为 SyncPermsError(500 级),而非原始 driver 错误直接冒出去。
- func TestExecuteSyncPerms_LockGenericError_WrappedAs500(t *testing.T) {
- ctrl := gomock.NewController(t)
- t.Cleanup(ctrl.Finish)
- productMock := newBaseProductMock(ctrl, "pc_tx_boom")
- permMock := mocks.NewMockSysPermModel(ctrl)
- permMock.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
- DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
- return fn(ctx, nil)
- })
- boom := errors.New("driver: connection lost")
- productMock.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "pc_tx_boom").
- Return(nil, boom)
- // 锁失败后同样不应调用后续方法。
- svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Product: productMock, Perm: permMock})
- result, err := ExecuteSyncPerms(context.Background(), svcCtx, "ak", "s",
- []SyncPermItem{{Code: "x", Name: "X"}})
- assert.Nil(t, result)
- require.Error(t, err)
- var se *SyncPermsError
- require.True(t, errors.As(err, &se), "底层错误必须被包成 *SyncPermsError,防止 driver 错误直接上抛")
- assert.Equal(t, 500, se.Code,
- "非 NotFound 的 DB 错误应当 fail-close 为 500,让接入方区别于 404/409")
- assert.NotContains(t, se.Message, "connection lost",
- "对外文案不能泄露原始 driver 错误(避免信息披露)")
- }
|