"""删除级联防护:存在外键引用时禁止删除(dept→user、category→article、city→子级、dict/config→item)。""" import json import pytest from core import R from core.base_service import BaseService from utils.request import parse_id_list from modules.system.dept.repository import dept_repo from modules.system.dept.service import delete_dept from modules.system.role.models import Role, RoleMenu from modules.system.user.repository import user_repo from modules.category.repository import category_repo from modules.category.service import delete_category from modules.article.repository import article_repo from modules.city.repository import city_repo from modules.city.service import delete_city from modules.dictionary.dict_item.repository import dictitem_repo from modules.dictionary.dict.service import dict_service from modules.configuration.config_item.repository import configitem_repo from modules.configuration.config.service import config_service class _FakeQuery: """模拟 SQLAlchemy Query:仅支持 first(),返回行或 None。""" def __init__(self, has_row): self._has_row = has_row def first(self): return object() if self._has_row else None def _body(result): return json.loads(result.body) def _allow_delete(monkeypatch): """放行批量删除,捕获传入的 ids 以便断言走到了该步骤。""" captured = {} def fake_batch_delete(repo, ids): captured['ids'] = ids return R.ok(msg="本次共删除{0}条数据".format(len(parse_id_list(str(ids))))) monkeypatch.setattr(BaseService, 'batch_delete_with_r', staticmethod(fake_batch_delete)) return captured # ============ dept → user ============ def test_delete_dept_blocks_when_users_reference(monkeypatch): monkeypatch.setattr(dept_repo, 'filter', lambda *a, **k: _FakeQuery(False)) # 无下级部门 monkeypatch.setattr(user_repo, 'filter', lambda *a, **k: _FakeQuery(True)) # 有用户引用 result = delete_dept('1') assert _body(result)['ok'] is False assert '用户' in _body(result)['msg'] def test_delete_dept_allows_when_no_references(monkeypatch): monkeypatch.setattr(dept_repo, 'filter', lambda *a, **k: _FakeQuery(False)) monkeypatch.setattr(user_repo, 'filter', lambda *a, **k: _FakeQuery(False)) captured = _allow_delete(monkeypatch) result = delete_dept('1,2') assert _body(result)['ok'] is True assert captured['ids'] == '1,2' # ============ category → article ============ def test_delete_category_blocks_when_articles_reference(monkeypatch): monkeypatch.setattr(category_repo, 'filter', lambda *a, **k: _FakeQuery(False)) # 无下级分类 monkeypatch.setattr(article_repo, 'filter', lambda *a, **k: _FakeQuery(True)) # 有文章引用 result = delete_category('1') assert _body(result)['ok'] is False assert '文章' in _body(result)['msg'] def test_delete_category_allows_when_no_references(monkeypatch): monkeypatch.setattr(category_repo, 'filter', lambda *a, **k: _FakeQuery(False)) monkeypatch.setattr(article_repo, 'filter', lambda *a, **k: _FakeQuery(False)) captured = _allow_delete(monkeypatch) result = delete_category('1,2') assert _body(result)['ok'] is True assert captured['ids'] == '1,2' # ============ city → 子级 ============ def test_delete_city_blocks_when_child_exists(monkeypatch): monkeypatch.setattr(city_repo, 'filter', lambda *a, **k: _FakeQuery(True)) result = delete_city('1') assert _body(result)['ok'] is False assert '下级' in _body(result)['msg'] def test_delete_city_allows_when_no_children(monkeypatch): monkeypatch.setattr(city_repo, 'filter', lambda *a, **k: _FakeQuery(False)) captured = _allow_delete(monkeypatch) result = delete_city('1,2') assert _body(result)['ok'] is True assert captured['ids'] == '1,2' # ============ dict → dict_item ============ def test_dict_before_delete_blocks_when_items_exist(monkeypatch): monkeypatch.setattr(dictitem_repo, 'filter', lambda *a, **k: _FakeQuery(True)) err = dict_service._before_delete('1,2') assert err is not None assert '字典项' in err def test_dict_before_delete_allows_when_no_items(monkeypatch): monkeypatch.setattr(dictitem_repo, 'filter', lambda *a, **k: _FakeQuery(False)) assert dict_service._before_delete('1,2') is None # ============ config → config_item ============ def test_config_before_delete_blocks_when_items_exist(monkeypatch): monkeypatch.setattr(configitem_repo, 'filter', lambda *a, **k: _FakeQuery(True)) err = config_service._before_delete('1,2') assert err is not None assert '配置项' in err def test_config_before_delete_allows_when_no_items(monkeypatch): monkeypatch.setattr(configitem_repo, 'filter', lambda *a, **k: _FakeQuery(False)) assert config_service._before_delete('1,2') is None # ============ role → user_role / role_menu 残留清理 ============ @pytest.mark.asyncio async def test_delete_role_cleans_user_role_and_role_menu(sqlite_db, monkeypatch): from modules.system.role import service as role_service from modules.system.role.models import Role from modules.system.role_menu.models import RoleMenu from modules.system.user_role.models import UserRole from core.database import get_db_session async def _noop(*args, **kwargs): return None monkeypatch.setattr(role_service, 'invalidate_role_perms', _noop) # 显式非内置 id(内置角色 1-4 禁止删除) role = Role(id=100, name='普通角色', code='R1', status=1, sort=1) role.save() db = get_db_session() db.add_all([ UserRole(user_id=5, role_id=role.id), RoleMenu(role_id=role.id, menu_id=10), ]) db.commit() resp = await role_service.delete_role(str(role.id)) assert _body(resp)['code'] == 0 # 关联残留已清理,角色本身软删 assert db.query(UserRole).filter(UserRole.role_id == role.id).count() == 0 assert db.query(RoleMenu).filter(RoleMenu.role_id == role.id).count() == 0 assert role.is_delete == 1 # ============ position / level → user 引用(与部门一致) ============ def test_position_before_delete_blocks_when_users_reference(monkeypatch): from modules.system.position.service import position_service monkeypatch.setattr(user_repo, 'filter', lambda *a, **k: _FakeQuery(True)) err = position_service._before_delete('1') assert err is not None assert '岗位' in err def test_position_before_delete_allows_when_no_references(monkeypatch): from modules.system.position.service import position_service monkeypatch.setattr(user_repo, 'filter', lambda *a, **k: _FakeQuery(False)) assert position_service._before_delete('1,2') is None def test_level_before_delete_blocks_when_users_reference(monkeypatch): from modules.system.level.service import level_service monkeypatch.setattr(user_repo, 'filter', lambda *a, **k: _FakeQuery(True)) err = level_service._before_delete('1') assert err is not None assert '职级' in err def test_level_before_delete_allows_when_no_references(monkeypatch): from modules.system.level.service import level_service monkeypatch.setattr(user_repo, 'filter', lambda *a, **k: _FakeQuery(False)) assert level_service._before_delete('1,2') is None