59eb34976a
iotlcm-admin-svr:管理后台服务。 - Go module:iotlcm → git.5iot.com/iotlcm-public/iotlcm-admin-svr(729 文件) - 凭据与地址脱敏:JWT signingKey(含代码里的 MustGet 默认值)、 MySQL DSN、Redis 口令、告警邮件发件人邮箱全部改为占位; 私网 IP → 文档地址段;库名 xiujie_admin → iotlcm - 真实手机号/邮箱 → 合法 11 位编造号 / example.com - 移除内部资产:CI 触发器、过时 DDL(sql/db_iotlcm.sql)、 私有依赖 go get 行、明文 registry 登录口令 - 启动 banner 统一 IOTLCM 方块字 + 社区惯例署名 - Dockerfile 改用公共基础镜像 - 拼错的 RADEME.md → README.md,README 重写 - 需要真实数据库的用例改为可用 IOTLCM_TEST_WITH_DB=1 显式启用 - 补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes 数据库 schema 不随本仓发布(生产 DDL 已过时),README 已说明。
186 lines
9.3 KiB
Go
186 lines
9.3 KiB
Go
package agent
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||
"github.com/gogf/gf/v2/database/gdb"
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
"github.com/gogf/gf/v2/os/gcfg"
|
||
|
||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||
|
||
_ "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/memorydb"
|
||
_ "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/system"
|
||
)
|
||
|
||
// 复现生产形态:模板角色已配置(role 18 @ dept 100,is_pass=1),
|
||
// 一级代理的默认角色是创建代理时复制下来的(is_pass=0)。
|
||
// 二级代理(dept 1862, parent 128)要再创建下级时,必须能从本机构默认角色拿到权限。
|
||
func TestPassRoleByDeptWorksForChildAgentWithoutTemplateFlag(t *testing.T) {
|
||
setupPassRoleTestDB(t)
|
||
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
|
||
ID: 1,
|
||
TenantId: "000000",
|
||
DeptId: 128, // 一级代理,要继续创建二级代理
|
||
}}})
|
||
|
||
// 三级结构:平台 100(模板 18,is_pass=1)→ 一级代理 128(默认角色 2321,is_pass=0)→ 二级代理 1862(默认角色 1779,is_pass=0)
|
||
mustExec(t, ctx, `INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES
|
||
(100,'000000',0,1,'0',1),
|
||
(128,'000000',100,1,'0',2321),
|
||
(1862,'000000',128,1,'0',1779)`)
|
||
mustExec(t, ctx, `INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES
|
||
(18,'000000',100,'代理商','dl','0','4',1,100),
|
||
(2321,'000000',128,'代理商','dl','0','4',0,128),
|
||
(1779,'000000',1862,'代理商','dl','0','4',0,100)`)
|
||
mustExec(t, ctx, `INSERT INTO sys_role_menu (tenant_id, role_id, menu_id, data_scope) VALUES
|
||
('000000',18,100,'0'),('000000',2321,100,'0'),('000000',1779,100,'0')`)
|
||
mustExec(t, ctx, `INSERT INTO sys_config (config_id, tenant_id, config_key, config_value) VALUES (1,'000000','agent.role.template.role_id','18')`)
|
||
|
||
service := NewAgentDept()
|
||
role, menus, err := service.currentDeptPassRole(ctx)
|
||
if err != nil {
|
||
t.Fatalf("一级代理创建下级时应能取到本机构默认角色权限,实际报错: %v", err)
|
||
}
|
||
if role == nil || role.RoleId != 2321 {
|
||
t.Fatalf("应以本机构默认角色 2321 作为下发来源,实际: %+v", role)
|
||
}
|
||
if len(menus) != 1 || menus[0].MenuId != 100 {
|
||
t.Fatalf("应复制本机构默认角色的菜单,实际: %+v", menus)
|
||
}
|
||
}
|
||
|
||
func setupPassRoleTestDB(t *testing.T) {
|
||
t.Helper()
|
||
if err := gdb.SetConfig(gdb.Config{
|
||
"default": gdb.ConfigGroup{
|
||
{Type: "sqlite", Link: "sqlite::@file(:memory:)", Debug: false},
|
||
},
|
||
}); err != nil {
|
||
t.Fatalf("configure sqlite failed: %v", err)
|
||
}
|
||
// 走真实部署形态:内存库指向主库(config.yaml 里 link 为空时回落主库)
|
||
if err := g.Cfg().GetAdapter().(*gcfg.AdapterFile).Set("database.memorydb.link", ""); err != nil {
|
||
t.Fatalf("override memorydb link failed: %v", err)
|
||
}
|
||
ctx := context.Background()
|
||
t.Cleanup(func() { _ = g.DB().Close(ctx) })
|
||
mustExec(t, ctx, `CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, tenant_id TEXT, parent_id INTEGER, dept_type INTEGER, status TEXT DEFAULT '0', dept_role INTEGER, deleted_at DATETIME)`)
|
||
mustExec(t, ctx, `CREATE TABLE sys_config (config_id INTEGER PRIMARY KEY, tenant_id TEXT, config_name TEXT, config_key TEXT, config_value TEXT, config_type TEXT, created_dept INTEGER, created_by INTEGER, created_at DATETIME, updated_by INTEGER, updated_at DATETIME, remark TEXT)`)
|
||
mustExec(t, ctx, `CREATE TABLE sys_role (
|
||
role_id INTEGER PRIMARY KEY AUTOINCREMENT, tenant_id TEXT, dept_id INTEGER, role_name TEXT, role_key TEXT, role_sort INTEGER, data_scope TEXT,
|
||
menu_check_strictly INTEGER, dept_check_strictly INTEGER, status TEXT, is_pass INTEGER, created_dept INTEGER, created_by INTEGER, created_at DATETIME, updated_by INTEGER, updated_at DATETIME, deleted_by INTEGER, deleted_at DATETIME, remark TEXT)`)
|
||
mustExec(t, ctx, `CREATE TABLE sys_role_menu (tenant_id TEXT, role_id INTEGER, menu_id INTEGER, data_scope TEXT)`)
|
||
mustExec(t, ctx, `CREATE TABLE sys_role_dept (tenant_id TEXT, role_id INTEGER, dept_id INTEGER)`)
|
||
mustExec(t, ctx, `CREATE TABLE sys_user_role (tenant_id TEXT DEFAULT '000000', user_id INTEGER, role_id INTEGER)`)
|
||
_ = consts.SysRoleDataScopeDeptAndBelow
|
||
}
|
||
|
||
func mustExec(t *testing.T, ctx context.Context, sql string) {
|
||
t.Helper()
|
||
if _, err := g.DB().Exec(ctx, sql); err != nil {
|
||
t.Fatalf("execute sql failed: %v\nsql=%s", err, sql)
|
||
}
|
||
}
|
||
|
||
// 平台自身(dept 100 = 模板所属机构)仍以模板角色为下发来源。
|
||
func TestPassRoleByDeptTemplateOwnerUsesTemplateRole(t *testing.T) {
|
||
setupPassRoleTestDB(t)
|
||
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
|
||
ID: 1, TenantId: "000000", DeptId: 100,
|
||
}}})
|
||
mustExec(t, ctx, `INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (100,'000000',0,1,'0',1)`)
|
||
mustExec(t, ctx, `INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES
|
||
(1,'000000',100,'超级管理员','admin','0','1',0,100),
|
||
(18,'000000',100,'代理商','dl','0','4',1,100)`)
|
||
mustExec(t, ctx, `INSERT INTO sys_role_menu (tenant_id, role_id, menu_id, data_scope) VALUES ('000000',18,100,'0')`)
|
||
mustExec(t, ctx, `INSERT INTO sys_config (config_id, tenant_id, config_key, config_value) VALUES (1,'000000','agent.role.template.role_id','18')`)
|
||
|
||
role, menus, err := NewAgentDept().currentDeptPassRole(ctx)
|
||
if err != nil {
|
||
t.Fatalf("平台创建一级代理应成功,实际: %v", err)
|
||
}
|
||
if role.RoleId != 18 {
|
||
t.Fatalf("平台下发来源应为模板角色 18,实际: %d", role.RoleId)
|
||
}
|
||
if len(menus) != 1 || menus[0].MenuId != 100 {
|
||
t.Fatalf("应复制模板菜单,实际: %+v", menus)
|
||
}
|
||
}
|
||
|
||
// 默认角色缺失/停用/归属不符时必须明确报错,不能静默 0 权限。
|
||
func TestPassRoleByDeptRejectsBrokenDefaultRole(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
sql []string
|
||
}{
|
||
{
|
||
name: "默认角色不存在",
|
||
sql: []string{
|
||
`INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (128,'000000',100,1,'0',9999)`,
|
||
},
|
||
},
|
||
{
|
||
name: "默认角色已停用",
|
||
sql: []string{
|
||
`INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (128,'000000',100,1,'0',2321)`,
|
||
`INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES (2321,'000000',128,'代理商','dl','1','4',0,128)`,
|
||
},
|
||
},
|
||
{
|
||
name: "默认角色归属其它机构",
|
||
sql: []string{
|
||
`INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (128,'000000',100,1,'0',2321)`,
|
||
`INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES (2321,'000000',129,'代理商','dl','0','4',0,100)`,
|
||
},
|
||
},
|
||
{
|
||
name: "本机构未绑定默认角色",
|
||
sql: []string{
|
||
`INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (128,'000000',100,1,'0',0)`,
|
||
},
|
||
},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
setupPassRoleTestDB(t)
|
||
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
|
||
ID: 1, TenantId: "000000", DeptId: 128,
|
||
}}})
|
||
mustExec(t, ctx, `INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (100,'000000',0,1,'0',1)`)
|
||
mustExec(t, ctx, `INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES (18,'000000',100,'代理商','dl','0','4',1,100)`)
|
||
mustExec(t, ctx, `INSERT INTO sys_role_menu (tenant_id, role_id, menu_id, data_scope) VALUES ('000000',18,100,'0')`)
|
||
mustExec(t, ctx, `INSERT INTO sys_config (config_id, tenant_id, config_key, config_value) VALUES (1,'000000','agent.role.template.role_id','18')`)
|
||
for _, sql := range tc.sql {
|
||
mustExec(t, ctx, sql)
|
||
}
|
||
if _, _, err := NewAgentDept().currentDeptPassRole(ctx); err == nil {
|
||
t.Fatal("默认角色异常时必须报错,不能静默下发空权限")
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// 未配置平台模板时,保留原 is_pass=1 唯一角色的下发流程。
|
||
func TestPassRoleByDeptKeepsLegacyFlowWithoutTemplate(t *testing.T) {
|
||
setupPassRoleTestDB(t)
|
||
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
|
||
ID: 1, TenantId: "000000", DeptId: 128,
|
||
}}})
|
||
mustExec(t, ctx, `INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (128,'000000',100,1,'0',2321)`)
|
||
mustExec(t, ctx, `INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES (2321,'000000',128,'代理商','dl','0','4',1,128)`)
|
||
mustExec(t, ctx, `INSERT INTO sys_role_menu (tenant_id, role_id, menu_id, data_scope) VALUES ('000000',2321,100,'0')`)
|
||
|
||
role, menus, err := NewAgentDept().currentDeptPassRole(ctx)
|
||
if err != nil {
|
||
t.Fatalf("未配模板时应走原有 is_pass 流程,实际: %v", err)
|
||
}
|
||
if role.RoleId != 2321 || len(menus) != 1 {
|
||
t.Fatalf("旧流程结果不符: role=%+v menus=%+v", role, menus)
|
||
}
|
||
}
|