package role import ( "errors" "fmt" "testing" "time" permModel "perms-system-server/internal/model/perm" roleModel "perms-system-server/internal/model/role" "perms-system-server/internal/model/roleperm" "perms-system-server/internal/model/userrole" "perms-system-server/internal/response" "perms-system-server/internal/svc" "perms-system-server/internal/testutil" "perms-system-server/internal/testutil/ctxhelper" "perms-system-server/internal/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zeromicro/go-zero/core/stores/redis" ) // TC-0126: 正常删除+级联 func TestDeleteRole_WithCascading(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() now := time.Now().Unix() pc := testutil.UniqueId() roleRes, err := svcCtx.SysRoleModel.Insert(ctx, &roleModel.SysRole{ ProductCode: pc, Name: testutil.UniqueId(), Status: 1, PermsLevel: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) roleId, _ := roleRes.LastInsertId() pRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{ ProductCode: pc, Name: testutil.UniqueId(), Code: testutil.UniqueId(), Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) permId, _ := pRes.LastInsertId() rpRes, err := svcCtx.SysRolePermModel.Insert(ctx, &roleperm.SysRolePerm{ RoleId: roleId, PermId: permId, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) rpId, _ := rpRes.LastInsertId() urRes, err := svcCtx.SysUserRoleModel.Insert(ctx, &userrole.SysUserRole{ UserId: 888888, RoleId: roleId, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) urId, _ := urRes.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_role_perm`", rpId) testutil.CleanTable(ctx, conn, "`sys_user_role`", urId) testutil.CleanTable(ctx, conn, "`sys_role`", roleId) testutil.CleanTable(ctx, conn, "`sys_perm`", permId) }) logic := NewDeleteRoleLogic(ctx, svcCtx) err = logic.DeleteRole(&types.DeleteRoleReq{Id: roleId}) require.NoError(t, err) _, err = svcCtx.SysRoleModel.FindOne(ctx, roleId) assert.Error(t, err) permIds, err := svcCtx.SysRolePermModel.FindPermIdsByRoleId(ctx, roleId) require.NoError(t, err) assert.Empty(t, permIds) roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, 888888) require.NoError(t, err) assert.NotContains(t, roleIds, roleId) } // TC-0128: 无关联数据 func TestDeleteRole_NoAssociations(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() now := time.Now().Unix() pc := testutil.UniqueId() roleRes, err := svcCtx.SysRoleModel.Insert(ctx, &roleModel.SysRole{ ProductCode: pc, Name: testutil.UniqueId(), Status: 1, PermsLevel: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) roleId, _ := roleRes.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_role`", roleId) }) logic := NewDeleteRoleLogic(ctx, svcCtx) err = logic.DeleteRole(&types.DeleteRoleReq{Id: roleId}) require.NoError(t, err) _, err = svcCtx.SysRoleModel.FindOne(ctx, roleId) assert.Error(t, err) } // TC-1120: L-R14-1 非超管 DeleteRole 针对别产品的 roleId 必须 404 "角色不存在", // 与 RoleDetail 的 M-N3 / UpdateRole 的 TC-1119 口径一致。 func TestDeleteRole_L_R14_1_CrossProductReturns404(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() now := time.Now().Unix() otherProduct := "l_r14_1_del_" + testutil.UniqueId() res, err := svcCtx.SysRoleModel.Insert(ctx, &roleModel.SysRole{ ProductCode: otherProduct, Name: testutil.UniqueId(), Status: 1, PermsLevel: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) roleId, _ := res.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_role`", roleId) }) adminCtx := ctxhelper.AdminCtx("test_product") err = NewDeleteRoleLogic(adminCtx, svcCtx).DeleteRole(&types.DeleteRoleReq{Id: roleId}) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 404, ce.Code(), "跨产品 roleId 必须 404") assert.Equal(t, "角色不存在", ce.Error(), "文案必须与 id 不存在完全一致") // DB 必须保持原状 still, err := svcCtx.SysRoleModel.FindOne(ctx, roleId) require.NoError(t, err, "跨产品拒绝的 DeleteRole 不得真的删角色") assert.Equal(t, roleId, still.Id) } // TC-1201: H-R17-2 —— DeleteRole 成功后必须在 post-commit 上显式失效 sysRole 的两把缓存键 // (id key + productCode:name key),与 DeleteDept 的 H-R17-2 同构。 // // 失效窗口:DeleteWithTx 里 sqlc.CachedConn.ExecCtx 的 "exec → DelCache" 钩子先于外层 // TransactCtx commit 执行。commit 前并发 FindOne / FindOneByProductCodeName 会把旧行 // 回填回 Redis,commit 后 DB 没了 role 但 Redis 还挂着"幽灵 role 快照"到 TTL。叠加 // 角色树权限模型,后续 ResolveOwnRoleOr404 命中幽灵 snapshot 会产生谜之 409(CAS // 命中但 DB 无行),排障代价巨大——post-commit 必须再补一次 InvalidateRoleCache。 func TestDeleteRole_H_R17_2_InvalidatesCacheAfterCommit(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() cfg := testutil.GetTestConfig() svcCtx := svc.NewServiceContext(cfg) conn := testutil.GetTestSqlConn() rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf) now := time.Now().Unix() pc := "h_r17_2_" + testutil.UniqueId() roleName := "h_r17_2_role_" + testutil.UniqueId() res, err := svcCtx.SysRoleModel.Insert(ctx, &roleModel.SysRole{ ProductCode: pc, Name: roleName, Status: 1, PermsLevel: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) roleId, _ := res.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_role`", roleId) }) idKey := fmt.Sprintf("%s:cache:sysRole:id:%d", cfg.CacheRedis.KeyPrefix, roleId) nameKey := fmt.Sprintf("%s:cache:sysRole:productCode:name:%s:%s", cfg.CacheRedis.KeyPrefix, pc, roleName) t.Cleanup(func() { _, _ = rds.Del(idKey, nameKey) }) require.NoError(t, rds.Set(idKey, fmt.Sprintf(`{"Id":%d,"ProductCode":%q,"Name":%q,"PermsLevel":1,"Status":1}`, roleId, pc, roleName)), "预置 ghost idKey 失败") require.NoError(t, rds.Set(nameKey, fmt.Sprintf(`{"Id":%d,"ProductCode":%q,"Name":%q,"PermsLevel":1,"Status":1}`, roleId, pc, roleName)), "预置 ghost nameKey 失败") require.NoError(t, NewDeleteRoleLogic(ctx, svcCtx).DeleteRole(&types.DeleteRoleReq{Id: roleId})) existsId, err := rds.Exists(idKey) require.NoError(t, err) assert.False(t, existsId, "H-R17-2:commit 之后 InvalidateRoleCache 必须 DEL sysRoleIdKey,"+ "否则后续 FindOne(roleId) 命中 ghost snapshot → CAS 撞不到行回 409") existsName, err := rds.Exists(nameKey) require.NoError(t, err) assert.False(t, existsName, "H-R17-2:InvalidateRoleCache 必须同时 DEL sysRoleProductCodeNameKey,"+ "因为 FindOneByProductCodeName 是 CreateRole 前的唯一性校验路径,"+ "残留 ghost 会让同名 role 在删除后反复被 409 误判为冲突") } // TC-0540: deleteRole非管理员拒绝 func TestDeleteRole_MemberRejected(t *testing.T) { pc := "test_product" ctx := ctxhelper.MemberCtx(pc) svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() now := time.Now().Unix() roleRes, err := svcCtx.SysRoleModel.Insert(ctx, &roleModel.SysRole{ ProductCode: pc, Name: testutil.UniqueId(), Status: 1, PermsLevel: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) roleId, _ := roleRes.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_role`", roleId) }) logic := NewDeleteRoleLogic(ctx, svcCtx) err = logic.DeleteRole(&types.DeleteRoleReq{Id: roleId}) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) }