199 lines
7.2 KiB
Python
199 lines
7.2 KiB
Python
"""删除级联防护:存在外键引用时禁止删除(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
|