Files
DjangoAdmin_FastAPI_EleVue/tests/unit/modules/test_menu_service.py
T
2026-09-12 17:39:13 +08:00

164 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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'] == '用户管理'