"""隔离 MySQL 验证角色配置、用户分配、旧会话即时生效与工资操作解耦。""" from uuid import uuid4 import pytest from tests.integration.test_tplus_workbench import workspace as workspace BASE = "/api/v1/system" pytestmark = pytest.mark.integration def current(client, headers): response = client.get("/api/v1/auth/me", headers=headers) assert response.status_code == 200, response.text return response.json()["data"] def authorization(client, admin, user_id): response = client.get(f"{BASE}/users/{user_id}/authorization", headers=admin["headers"]) assert response.status_code == 200, response.text return response.json()["data"] def save(client, admin, user_id, **changes): before = authorization(client, admin, user_id) data = { "role_codes": before["role_codes"], "expected_revision": before["revision"], "reason": "隔离角色分配验收", } | changes return client.put(f"{BASE}/users/{user_id}/authorization", headers=admin["headers"], json=data) def role(client, admin, codes): response = client.post( BASE + "/roles", headers=admin["headers"], json={"role_code": "t_rbac_" + uuid4().hex[:8], "role_name": "t_权限验收角色"}, ) assert response.status_code == 200, response.text item = response.json()["data"] before = client.get(f"{BASE}/roles/{item['id']}/permissions", headers=admin["headers"]).json()[ "data" ] response = client.put( f"{BASE}/roles/{item['id']}/permissions", headers=admin["headers"], json={ "permission_codes": codes, "expected_revision": before["revision"], "reason": "配置测试角色", }, ) assert response.status_code == 200, response.text return item def test_管理员无需绑定制作人即可维护资料和获得工资权限(client, admin): from sqlalchemy import select from app.core.db import get_session_factory from app.models import AuthorizationSetting, BusinessRole, User from app.services.authorization_service import require_permission with get_session_factory()() as session: session.get(AuthorizationSetting, 1).maker_user_id = None session.commit() require_permission(session, session.get(User, admin["id"]), "settlements:generate") business_role_id = session.scalar( select(BusinessRole.id).where(BusinessRole.role_code == "salesperson") ) assert business_role_id is not None info = current(client, admin["headers"]) assert info["can_write_payroll"] and info["maker_required_permissions"] == [] response = client.post( "/api/v1/base/personnel", headers=admin["headers"], json={"personnel_name": "t_角色授权人员", "business_role_id": business_role_id}, ) assert response.status_code == 200, response.text person = response.json()["data"]["id"] for path, data in (("regions", {"regions": []}), ("plans", {"plans": []})): response = client.put( f"/api/v1/base/personnel/{person}/{path}", headers=admin["headers"], json=data ) assert response.status_code == 200, response.text def test_角色配置后分配给用户并在原会话即时生效(client, admin, normal): assert "base:personnel:regions" not in current(client, normal["headers"])["permissions"] item = role(client, admin, ["base:personnel:view", "base:personnel:regions"]) assert ( save(client, admin, normal["id"], role_codes=["user", item["role_code"]]).status_code == 200 ) assert "base:personnel:regions" in current(client, normal["headers"])["permissions"] assert save(client, admin, normal["id"], role_codes=["user"]).status_code == 200 assert "base:personnel:regions" not in current(client, normal["headers"])["permissions"] def test_来源销售关系核对需要业务更新和核算准备双权限(client, admin, normal): business_only = role(client, admin, ["business:businesses:view", "business:businesses:update"]) payroll_only = role(client, admin, ["payroll:view", "payroll:prepare"]) payload = {"item_id": 1, "expected_version": 1, "evidence": "权限回归"} url = "/api/v1/business/businesses/1/sales-relation/release" for item in (business_only, payroll_only): assert save(client, admin, normal["id"], role_codes=["user", item["role_code"]]).status_code == 200 assert client.put(url, headers=normal["headers"], json=payload).status_code == 403 assert save(client, admin, normal["id"], role_codes=["user", business_only["role_code"], payroll_only["role_code"]]).status_code == 200 response = client.put(url, headers=normal["headers"], json=payload) assert response.status_code == 400 assert "来源业务" in response.json()["message"] or "核对结论" in response.json()["message"] assert save(client, admin, normal["id"], role_codes=["user"]).status_code == 200 def test_旧个人覆盖写入拒绝且不影响既有角色(client, admin, normal): response = save( client, admin, normal["id"], overrides=[{"permission_code": "base:personnel:regions", "effect": "ALLOW"}], ) assert response.status_code == 400 assert current(client, normal["headers"])["roles"] == ["user"] assert "base:personnel:regions" not in current(client, normal["headers"])["permissions"] def test_普通用户不能配置角色或读取权限目录(client, normal): assert client.get(BASE + "/permissions/catalog", headers=normal["headers"]).status_code == 403 assert client.get(BASE + "/roles", headers=normal["headers"]).status_code == 403 def test_过期角色分配版本返回409且不覆盖前次结果(client, admin, normal): before = authorization(client, admin, normal["id"]) assert save(client, admin, normal["id"]).status_code == 200 response = save( client, admin, normal["id"], expected_revision=before["revision"], role_codes=[] ) assert response.status_code == 409 assert "user" in current(client, normal["headers"])["roles"] def test_工资角色不再需要绑定且旧绑定接口不再写入(client, admin, normal): assert save(client, admin, normal["id"], role_codes=["payroll_maker"]).status_code == 200 info = current(client, normal["headers"]) assert info["can_write_payroll"] and "payroll:calculate" in info["permissions"] settings = client.get(BASE + "/authorization-settings", headers=admin["headers"]).json()["data"] assert settings["mode"] == "roles" and settings["maker_user_id"] is None response = client.put( BASE + "/authorization-settings", headers=admin["headers"], json={ "maker_user_id": normal["id"], "expected_revision": settings["revision"], "reason": "旧客户端兼容验收", }, ) assert response.status_code == 400 and "角色" in response.json()["message"] def test_不能撤销当前管理员自身管理身份(client, admin): assert save(client, admin, admin["id"], role_codes=["user"]).status_code == 403 assert "admin" in current(client, admin["headers"])["roles"] def test_角色停用撤销旧会话授权且恢复后重新生效(client, admin, normal): item = role(client, admin, ["base:personnel:view", "base:personnel:regions"]) assert ( save(client, admin, normal["id"], role_codes=["user", item["role_code"]]).status_code == 200 ) for status in (0, 1): response = client.put( f"{BASE}/roles/{item['id']}/status", headers=admin["headers"], json={"status": status} ) assert response.status_code == 200, response.text assert ( "base:personnel:regions" in current(client, normal["headers"])["permissions"] ) == bool(status) def test_编辑角色拒绝停用权限且保留原授权(client, admin): from sqlalchemy import select from app.core.db import get_session_factory from app.models import Permission item = role(client, admin, ["base:personnel:view"]) url = f"{BASE}/roles/{item['id']}/permissions" before = client.get(url, headers=admin["headers"]).json()["data"] with get_session_factory()() as session: permission = session.scalar(select(Permission).where( Permission.perm_code == "base:personnel:regions" )) old_status = permission.status permission.status = 0 session.commit() try: response = client.put(url, headers=admin["headers"], json={ "permission_codes": ["base:personnel:view", "base:personnel:regions"], "expected_revision": before["revision"], "reason": "验证停用权限", }) assert response.status_code == 400 after = client.get(url, headers=admin["headers"]).json()["data"] assert after["permission_codes"] == before["permission_codes"] assert after["revision"] == before["revision"] finally: with get_session_factory()() as session: permission = session.scalar(select(Permission).where( Permission.perm_code == "base:personnel:regions" )) permission.status = old_status session.commit() def test_编辑角色拒绝缺失权限且保留原授权(client, admin, monkeypatch): from app.services.permission_catalog import CATALOG item = role(client, admin, ["base:personnel:view"]) url = f"{BASE}/roles/{item['id']}/permissions" before = client.get(url, headers=admin["headers"]).json()["data"] code = "test:missing:view" monkeypatch.setitem(CATALOG, code, {"admin_only": False, "view_code": code}) response = client.put(url, headers=admin["headers"], json={ "permission_codes": [code], "expected_revision": before["revision"], "reason": "验证缺失权限", }) assert response.status_code == 400 after = client.get(url, headers=admin["headers"]).json()["data"] assert after["permission_codes"] == before["permission_codes"] assert after["revision"] == before["revision"] def test_角色权限编辑自动影响全部绑定用户(client, admin, normal, make_user): other = make_user(["user"]) item = role(client, admin, ["base:personnel:view"]) for user_id in (normal["id"], other["id"]): assert save(client, admin, user_id, role_codes=[item["role_code"]]).status_code == 200 before = client.get(f"{BASE}/roles/{item['id']}/permissions", headers=admin["headers"]).json()[ "data" ] response = client.put( f"{BASE}/roles/{item['id']}/permissions", headers=admin["headers"], json={ "permission_codes": ["base:personnel:view", "base:personnel:regions"], "expected_revision": before["revision"], "reason": "角色同步授权验收", }, ) assert response.status_code == 200 from app.core.db import get_session_factory from app.models import User from app.services.authorization_service import calculate with get_session_factory()() as session: for user_id in (normal["id"], other["id"]): assert ( "base:personnel:regions" in calculate(session, session.get(User, user_id))["permissions"] ) def test_表单参照可使用但不能借此读取完整资料(client, admin, normal): item = role(client, admin, ["business:businesses:view"]) assert save(client, admin, normal["id"], role_codes=[item["role_code"]]).status_code == 200 response = client.get("/api/v1/meta/options?resource=personnel", headers=normal["headers"]) assert response.status_code == 200, response.text assert all( not {"salary_standard", "phone", "password"} & row.keys() for row in response.json()["data"] ) assert client.get("/api/v1/base/personnel", headers=normal["headers"]).status_code == 403 def test_排队后撤销角色任务执行前重新检查(client, admin, normal, workspace): from sqlalchemy import select from app.models import TPlusWorkRun from app.services.tplus import workbench session, _ = workspace item = role(client, admin, ["tplus:view", "tplus:preview"]) assert ( save(client, admin, normal["id"], role_codes=["user", item["role_code"]]).status_code == 200 ) response = client.post( BASE + "/tplus/work-runs", headers=normal["headers"], json={"kind": "MASTER_PREVIEW", "request_key": str(uuid4()), "entity_types": ["personnel"]}, ) assert response.status_code == 200, response.text number = response.json()["data"]["run_no"] assert save(client, admin, normal["id"], role_codes=["user"]).status_code == 200 assert workbench.execute_next(session) == number run = session.scalar(select(TPlusWorkRun).where(TPlusWorkRun.run_no == number)) assert run.status == "FAILED" and run.error_code == "TASK_AUTHORIZATION_REVOKED" def test_并发角色分配仅一个版本成功(client, admin, normal): from concurrent.futures import ThreadPoolExecutor choices = [ role(client, admin, ["base:personnel:view", code]) for code in ("base:personnel:regions", "base:personnel:plans") ] before = authorization(client, admin, normal["id"]) def change(item): return save( client, admin, normal["id"], role_codes=["user", item["role_code"]], expected_revision=before["revision"], ).status_code with ThreadPoolExecutor(max_workers=2) as pool: result = list(pool.map(change, choices)) assert sorted(result) == [200, 409] effective = current(client, normal["headers"])["permissions"] assert ( sum(code in effective for code in ("base:personnel:regions", "base:personnel:plans")) == 1 )