签入版本

This commit is contained in:
2026-09-12 18:24:41 +08:00
commit fb2b7d9538
852 changed files with 115466 additions and 0 deletions
+163
View File
@@ -0,0 +1,163 @@
"""system/menu service 单元测试(async 路径,回归删除父级时禁止有子级)。
测试环境无 Redis:monkeypatch 掉 invalidate_menu_change,避免 get_redis() 抛异常。
"""
import json
import pytest
from modules.system.menu import service as menu_service
from modules.system.menu.models import Menu
from modules.system.menu.repository import menu_repo
from modules.system.menu.schemas import MenuForm
@pytest.fixture(autouse=True)
def _stub_perm_cache(monkeypatch):
async def _noop():
return None
monkeypatch.setattr(menu_service, "invalidate_menu_change", _noop)
def _resp_body(resp):
return json.loads(resp.body)
def _form(**overrides):
data = dict(
name="系统管理", icon=None, path="/system", component=None, parentId=0,
type=0, target=0, permission=None, hide=0, status=1, sort=1, note=None,
)
data.update(overrides)
return MenuForm(**data)
@pytest.mark.asyncio
async def test_delete_parent_with_child_blocked(fake_request, sqlite_db):
await menu_service.add_menu(fake_request, _form())
parent = menu_repo.filter(Menu.name == "系统管理").first()
await menu_service.add_menu(
fake_request,
_form(name="用户管理", path="/system/user", parentId=parent.id),
)
child = menu_repo.filter(Menu.name == "用户管理").first()
resp = await menu_service.delete_menu(str(parent.id))
body = _resp_body(resp)
assert body["code"] != 0
assert "下级菜单" in body["msg"]
assert menu_repo.get_by_id(parent.id).is_delete == 0
assert menu_repo.get_by_id(child.id).is_delete == 0
@pytest.mark.asyncio
async def test_delete_leaf_soft_deletes(fake_request, sqlite_db):
await menu_service.add_menu(fake_request, _form(name="单菜单", path="/single"))
leaf = menu_repo.filter(Menu.name == "单菜单").first()
resp = await menu_service.delete_menu(str(leaf.id))
assert _resp_body(resp)["code"] == 0
assert menu_repo.filter(Menu.id == leaf.id).first().is_delete == 1
# ============ 菜单树成环检测:update 的 parent_id 存在性 + 自身/下级 校验 ============
@pytest.mark.asyncio
async def test_update_menu_rejects_missing_parent(fake_request, sqlite_db):
await menu_service.add_menu(fake_request, _form(name="系统管理", path="/system"))
menu = menu_repo.filter(Menu.name == "系统管理").first()
resp = await menu_service.update_menu(
fake_request, _form(name="系统管理", path="/system", id=menu.id, parentId=99999))
body = _resp_body(resp)
assert body["code"] != 0
assert "上级菜单不存在" in body["msg"]
@pytest.mark.asyncio
async def test_update_menu_rejects_self_parent(fake_request, sqlite_db):
await menu_service.add_menu(fake_request, _form(name="系统管理", path="/system"))
menu = menu_repo.filter(Menu.name == "系统管理").first()
resp = await menu_service.update_menu(
fake_request, _form(name="系统管理", path="/system", id=menu.id, parentId=menu.id))
body = _resp_body(resp)
assert body["code"] != 0
assert "自身" in body["msg"]
# 原记录未被改动
assert menu_repo.get_by_id(menu.id).parent_id == 0
@pytest.mark.asyncio
async def test_update_menu_rejects_descendant_parent(fake_request, sqlite_db):
await menu_service.add_menu(fake_request, _form(name="系统管理", path="/system"))
parent = menu_repo.filter(Menu.name == "系统管理").first()
await menu_service.add_menu(
fake_request, _form(name="用户管理", path="/system/user", parentId=parent.id))
child = menu_repo.filter(Menu.name == "用户管理").first()
# 把父级设为自己的子级 → 成环,拒绝
resp = await menu_service.update_menu(
fake_request, _form(name="系统管理", path="/system", id=parent.id, parentId=child.id))
body = _resp_body(resp)
assert body["code"] != 0
assert "下级" in body["msg"]
assert menu_repo.get_by_id(parent.id).parent_id == 0
# ============ 多角色重复授权:权限列表 / 菜单树 distinct 去重 ============
def _seed_user_with_two_roles_same_menu(sqlite_db):
"""造一个同时属于两个角色、且两角色均授予同一权限节点/菜单的用户。
返回的用户 id 必不为 1(先占位超管),否则会走超级管理员全量放行分支。
"""
from modules.system.role.models import Role
from modules.system.role_menu.models import RoleMenu
from modules.system.user.models import User
from modules.system.user_role.models import UserRole
User(realname='超管', nickname='admin', avatar='', mobile='', email='',
province_code='', city_code='', district_code='', street_code='', address='',
username='admin', password='', status=1).save()
user = User(realname='张三', nickname='zhangsan', avatar='', mobile='', email='',
province_code='', city_code='', district_code='', street_code='', address='',
username='zhangsan', password='', status=1)
user.save()
r1 = Role(name='角色一', code='R1', status=1, sort=1); r1.save()
r2 = Role(name='角色二', code='R2', status=1, sort=1); r2.save()
perm = Menu(name='添加用户', parent_id=0, path='/system/user/add', type=1,
permission='sys:user:add', status=0, sort=1)
perm.save()
menu = Menu(name='用户管理', parent_id=0, path='/system/user', component='system/user/index',
type=0, status=0, hide=0, sort=1)
menu.save()
# 两个角色授予同一权限节点与同一菜单 → 多角色 join 会产生重复行
for role in (r1, r2):
RoleMenu(role_id=role.id, menu_id=perm.id).save()
RoleMenu(role_id=role.id, menu_id=menu.id).save()
UserRole(user_id=user.id, role_id=role.id).save()
sqlite_db.commit()
return user
def test_permissions_list_dedup_for_multi_role(sqlite_db):
user = _seed_user_with_two_roles_same_menu(sqlite_db)
perms = menu_service.get_permissions_list_sync(user.id)
# 同一权限节点经两个角色授权只出现一次
assert perms.count('sys:user:add') == 1
assert len(perms) == len(set(perms))
def test_menu_list_dedup_for_multi_role(sqlite_db):
user = _seed_user_with_two_roles_same_menu(sqlite_db)
tree = menu_service.get_permission_menu_list_sync(user.id)
# 菜单树无重复节点
assert len(tree) == 1
assert tree[0]['name'] == '用户管理'