Files
zkxw/backend/tests/test_market_field_scope.py
T

710 lines
22 KiB
Python

import re
from pathlib import Path
import pytest
from fastapi import HTTPException
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.models.base import Base
from app.models.models import (
BizCollectionProtocol,
BizCollectionRegionStaff,
BizPresignOrder,
BizProtocolCollectionInfo,
SysPost,
SysPostRole,
SysRole,
SysRoleDataScopeDetail,
SysUserPost,
SysUserRole,
)
from app.models.research_donation import (
BizResearchDonation,
BizResearchDonationCollection,
)
def _session():
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(
engine,
tables=[
BizCollectionRegionStaff.__table__,
BizPresignOrder.__table__,
BizCollectionProtocol.__table__,
BizProtocolCollectionInfo.__table__,
BizResearchDonation.__table__,
BizResearchDonationCollection.__table__,
SysRole.__table__,
SysRoleDataScopeDetail.__table__,
SysUserRole.__table__,
SysPost.__table__,
SysPostRole.__table__,
SysUserPost.__table__,
],
)
return sessionmaker(bind=engine)()
def _presign(
order_id,
*,
tenant_no="tenant-a",
region_id="region-a",
sales_user_id="sales-a",
manager_user_id="manager-a",
create_by="creator-a",
del_flag=0,
):
return BizPresignOrder(
id=order_id,
tenant_no=tenant_no,
region_id=region_id,
sales_user_id=sales_user_id,
region_manager_user_id=manager_user_id,
create_by=create_by,
create_dept_id="dept-a",
del_flag=del_flag,
)
def _cell_protocol(
protocol_id,
*,
tenant_no="tenant-a",
collection_tenant_no=None,
region_id="region-a",
sales_user_id="sales-a",
manager_user_id="manager-a",
parent_del_flag=0,
collection_del_flag=0,
):
collection_tenant_no = collection_tenant_no or tenant_no
return [
BizCollectionProtocol(
id=protocol_id,
protocol_no=f"CP-{protocol_id}",
tenant_no=tenant_no,
create_by=sales_user_id,
create_dept_id="dept-a",
del_flag=parent_del_flag,
),
BizProtocolCollectionInfo(
id=f"ci-{protocol_id}",
collection_protocol_id=protocol_id,
tenant_no=collection_tenant_no,
region_id=region_id,
sales_user_id=sales_user_id,
region_manager_user_id=manager_user_id,
create_dept_id="dept-a",
del_flag=collection_del_flag,
),
]
def _donation(
donation_id,
*,
tenant_no="tenant-a",
collection_tenant_no=None,
region_id="region-a",
sales_user_id="sales-a",
manager_user_id="manager-a",
parent_del_flag=0,
collection_del_flag=0,
):
collection_tenant_no = collection_tenant_no or tenant_no
return [
BizResearchDonation(
id=donation_id,
donation_no=f"RD-{donation_id}",
tenant_no=tenant_no,
create_by=sales_user_id,
create_dept_id="dept-a",
del_flag=parent_del_flag,
),
BizResearchDonationCollection(
id=f"dc-{donation_id}",
research_donation_id=donation_id,
tenant_no=collection_tenant_no,
region_id=region_id,
sales_user_id=sales_user_id,
region_manager_user_id=manager_user_id,
create_dept_id="dept-a",
del_flag=collection_del_flag,
),
]
def _ids(rows):
return {row.id for row in rows}
def _user_info(user_id="u1", role="super_admin"):
return {"user_id": user_id, "role": role, "dept_id": "dept-a"}
def test_specialist_matches_sales_user_only():
from app.services.market_field_scope import (
MarketFieldContext,
apply_presign_scope,
)
db = _session()
db.add_all(
[
_presign("own", sales_user_id="u1", manager_user_id="other"),
_presign("managed", sales_user_id="other", manager_user_id="u1"),
]
)
db.commit()
ctx = MarketFieldContext("tenant-a", "u1", "specialist")
rows = apply_presign_scope(
db.query(BizPresignOrder), db, ctx, user_info=_user_info()
).all()
assert _ids(rows) == {"own"}
def test_manager_matches_leader_region_or_document_manager():
from app.services.market_field_scope import (
MarketFieldContext,
apply_presign_scope,
)
db = _session()
db.add(
BizCollectionRegionStaff(
id="leader",
region_id="region-led",
user_id="u1",
is_leader=1,
tenant_no="tenant-a",
create_dept_id="dept-a",
del_flag=0,
)
)
db.add_all(
[
_presign("by-region", region_id="region-led", manager_user_id="other"),
_presign("by-manager", region_id="region-other", manager_user_id="u1"),
_presign("outside", region_id="region-other", manager_user_id="other"),
]
)
db.commit()
ctx = MarketFieldContext("tenant-a", "u1", "manager")
rows = apply_presign_scope(
db.query(BizPresignOrder), db, ctx, user_info=_user_info()
).all()
assert _ids(rows) == {"by-region", "by-manager"}
def test_manager_ignores_non_leader_staff_rows():
from app.services.market_field_scope import (
MarketFieldContext,
apply_presign_scope,
)
db = _session()
db.add(
BizCollectionRegionStaff(
id="staff",
region_id="region-staff",
user_id="u1",
is_leader=0,
tenant_no="tenant-a",
create_dept_id="dept-a",
del_flag=0,
)
)
db.add(_presign("staff-region", region_id="region-staff", manager_user_id="other"))
db.commit()
ctx = MarketFieldContext("tenant-a", "u1", "manager")
assert (
apply_presign_scope(
db.query(BizPresignOrder), db, ctx, user_info=_user_info()
).all()
== []
)
def test_soft_deleted_or_cross_tenant_region_never_matches():
from app.services.market_field_scope import (
MarketFieldContext,
apply_presign_scope,
)
db = _session()
db.add_all(
[
BizCollectionRegionStaff(
id="deleted-leader",
region_id="region-deleted",
user_id="u1",
is_leader=1,
tenant_no="tenant-a",
create_dept_id="dept-a",
del_flag=1,
),
BizCollectionRegionStaff(
id="other-tenant-leader",
region_id="region-cross",
user_id="u1",
is_leader=1,
tenant_no="tenant-b",
create_dept_id="dept-a",
del_flag=0,
),
_presign("deleted-region", region_id="region-deleted", manager_user_id="other"),
_presign("cross-region", region_id="region-cross", manager_user_id="other"),
_presign("soft-deleted-doc", manager_user_id="u1", del_flag=1),
_presign("cross-tenant-doc", tenant_no="tenant-b", manager_user_id="u1"),
]
)
db.commit()
ctx = MarketFieldContext("tenant-a", "u1", "manager")
assert (
apply_presign_scope(
db.query(BizPresignOrder), db, ctx, user_info=_user_info()
).all()
== []
)
def test_dual_role_contexts_do_not_union_specialist_and_manager_scope():
from app.services.market_field_scope import (
MarketFieldContext,
apply_presign_scope,
)
db = _session()
db.add_all(
[
_presign("specialist-only", sales_user_id="u1", manager_user_id="other"),
_presign("manager-only", sales_user_id="other", manager_user_id="u1"),
]
)
db.commit()
specialist = MarketFieldContext("tenant-a", "u1", "specialist")
manager = MarketFieldContext("tenant-a", "u1", "manager")
assert _ids(apply_presign_scope(
db.query(BizPresignOrder), db, specialist, user_info=_user_info()
).all()) == {
"specialist-only"
}
assert _ids(apply_presign_scope(
db.query(BizPresignOrder), db, manager, user_info=_user_info()
).all()) == {
"manager-only"
}
def test_apply_scope_requires_general_data_scope_context():
from app.services.market_field_scope import (
MarketFieldContext,
apply_presign_scope,
)
db = _session()
ctx = MarketFieldContext("tenant-a", "u1", "specialist")
with pytest.raises(TypeError):
apply_presign_scope(db.query(BizPresignOrder), db, ctx)
@pytest.mark.parametrize(
("apply_name", "model"),
[
("apply_presign_scope", BizPresignOrder),
("apply_cell_protocol_scope", BizCollectionProtocol),
("apply_donation_scope", BizResearchDonation),
],
)
@pytest.mark.parametrize(
"user_info",
[{}, {"user_id": ""}, {"user_id": "other", "role": "super_admin"}],
)
def test_apply_scopes_reject_missing_or_mismatched_user_identity(
apply_name, model, user_info
):
from app.services import market_field_scope
db = _session()
ctx = market_field_scope.MarketFieldContext("tenant-a", "u1", "specialist")
apply_scope = getattr(market_field_scope, apply_name)
with pytest.raises(PermissionError):
apply_scope(db.query(model), db, ctx, user_info=user_info)
@pytest.mark.parametrize(
"assert_name",
[
"assert_presign_access",
"assert_cell_protocol_access",
"assert_donation_access",
],
)
@pytest.mark.parametrize(
"user_info",
[{}, {"user_id": ""}, {"user_id": "other", "role": "super_admin"}],
)
def test_assert_access_rejects_missing_or_mismatched_user_identity(
assert_name, user_info
):
from app.services import market_field_scope
db = _session()
ctx = market_field_scope.MarketFieldContext("tenant-a", "u1", "specialist")
assert_access = getattr(market_field_scope, assert_name)
with pytest.raises(PermissionError):
assert_access(db, "any-id", ctx, user_info=user_info)
def test_protocol_and_donation_queries_use_their_collection_info_scope():
from app.services.market_field_scope import (
MarketFieldContext,
apply_cell_protocol_scope,
apply_donation_scope,
)
db = _session()
db.add_all(
_cell_protocol("cell-own", sales_user_id="u1")
+ _cell_protocol("cell-other", sales_user_id="other")
+ _cell_protocol("cell-deleted-info", sales_user_id="u1", collection_del_flag=1)
+ _donation("donation-own", sales_user_id="u1")
+ _donation("donation-other", sales_user_id="other")
+ _donation("donation-deleted-info", sales_user_id="u1", collection_del_flag=1)
)
db.commit()
ctx = MarketFieldContext("tenant-a", "u1", "specialist")
assert _ids(
apply_cell_protocol_scope(
db.query(BizCollectionProtocol), db, ctx, user_info=_user_info()
).all()
) == {"cell-own"}
assert _ids(
apply_donation_scope(
db.query(BizResearchDonation), db, ctx, user_info=_user_info()
).all()
) == {"donation-own"}
def test_cell_and_donation_manager_scope_uses_leader_region_or_document_manager():
from app.services.market_field_scope import (
MarketFieldContext,
apply_cell_protocol_scope,
apply_donation_scope,
)
db = _session()
db.add_all(
[
BizCollectionRegionStaff(
id="leader",
region_id="region-led",
user_id="u1",
is_leader=1,
tenant_no="tenant-a",
create_dept_id="dept-a",
del_flag=0,
),
BizCollectionRegionStaff(
id="non-leader",
region_id="region-staff",
user_id="u1",
is_leader=0,
tenant_no="tenant-a",
create_dept_id="dept-a",
del_flag=0,
),
]
+ _cell_protocol("cell-region", region_id="region-led", manager_user_id="other")
+ _cell_protocol("cell-manager", region_id="other", manager_user_id="u1")
+ _cell_protocol("cell-non-leader", region_id="region-staff", manager_user_id="other")
+ _cell_protocol(
"cell-cross-info",
collection_tenant_no="tenant-b",
region_id="region-led",
manager_user_id="u1",
)
+ _cell_protocol("cell-deleted-parent", manager_user_id="u1", parent_del_flag=1)
+ _cell_protocol("cell-cross-parent", tenant_no="tenant-b", manager_user_id="u1")
+ _donation("donation-region", region_id="region-led", manager_user_id="other")
+ _donation("donation-manager", region_id="other", manager_user_id="u1")
+ _donation("donation-non-leader", region_id="region-staff", manager_user_id="other")
+ _donation(
"donation-cross-info",
collection_tenant_no="tenant-b",
region_id="region-led",
manager_user_id="u1",
)
+ _donation("donation-deleted-parent", manager_user_id="u1", parent_del_flag=1)
+ _donation("donation-cross-parent", tenant_no="tenant-b", manager_user_id="u1")
)
db.commit()
ctx = MarketFieldContext("tenant-a", "u1", "manager")
assert _ids(
apply_cell_protocol_scope(
db.query(BizCollectionProtocol), db, ctx, user_info=_user_info()
).all()
) == {"cell-region", "cell-manager"}
assert _ids(
apply_donation_scope(
db.query(BizResearchDonation), db, ctx, user_info=_user_info()
).all()
) == {"donation-region", "donation-manager"}
def test_assert_access_uses_the_same_scope_for_all_document_types():
from app.services.market_field_scope import (
MarketFieldContext,
assert_cell_protocol_access,
assert_donation_access,
assert_presign_access,
)
db = _session()
db.add_all(
[
_presign("presign-own", sales_user_id="u1"),
_presign("presign-outside", sales_user_id="other"),
_presign("presign-cross", tenant_no="tenant-b", sales_user_id="u1"),
_presign("presign-deleted", sales_user_id="u1", del_flag=1),
]
)
db.add_all(_cell_protocol("cell-own", sales_user_id="u1"))
db.add_all(_cell_protocol("cell-outside", sales_user_id="other"))
db.add_all(_cell_protocol("cell-cross", tenant_no="tenant-b", sales_user_id="u1"))
db.add_all(_cell_protocol("cell-deleted", sales_user_id="u1", parent_del_flag=1))
db.add_all(_donation("donation-own", sales_user_id="u1"))
db.add_all(_donation("donation-outside", sales_user_id="other"))
db.add_all(_donation("donation-cross", tenant_no="tenant-b", sales_user_id="u1"))
db.add_all(_donation("donation-deleted", sales_user_id="u1", parent_del_flag=1))
db.commit()
ctx = MarketFieldContext("tenant-a", "u1", "specialist")
assert assert_presign_access(
db, "presign-own", ctx, user_info=_user_info()
).id == "presign-own"
assert assert_cell_protocol_access(
db, "cell-own", ctx, user_info=_user_info()
).id == "cell-own"
assert assert_donation_access(
db, "donation-own", ctx, user_info=_user_info()
).id == "donation-own"
rejected = {
assert_presign_access: [
"presign-outside",
"presign-cross",
"presign-deleted",
],
assert_cell_protocol_access: [
"cell-outside",
"cell-cross",
"cell-deleted",
],
assert_donation_access: [
"donation-outside",
"donation-cross",
"donation-deleted",
],
}
for checker, record_ids in rejected.items():
for record_id in record_ids:
with pytest.raises(HTTPException) as exc_info:
checker(db, record_id, ctx, user_info=_user_info())
assert exc_info.value.status_code == 404
def test_general_data_scope_and_field_scope_are_intersected():
from app.services.market_field_scope import (
MarketFieldContext,
apply_presign_scope,
)
db = _session()
db.add_all(
[
SysRole(
id="self-role",
role_name="仅本人",
data_scope="4",
status="1",
tenant_no="tenant-a",
create_dept_id="dept-a",
del_flag=0,
),
SysUserRole(
id="user-role",
user_id="u1",
role_id="self-role",
tenant_no="tenant-a",
create_dept_id="dept-a",
del_flag=0,
),
_presign("both", sales_user_id="u1", create_by="u1"),
_presign("field-only", sales_user_id="u1", create_by="other"),
_presign("data-only", sales_user_id="other", create_by="u1"),
]
)
db.commit()
ctx = MarketFieldContext("tenant-a", "u1", "specialist")
rows = apply_presign_scope(
db.query(BizPresignOrder),
db,
ctx,
user_info={"user_id": "u1", "role": "admin", "dept_id": "dept-a"},
).all()
assert _ids(rows) == {"both"}
def test_multi_role_union_is_still_intersected_with_field_scope():
from app.services.market_field_scope import (
MarketFieldContext,
apply_presign_scope,
)
db = _session()
db.add_all(
[
SysRole(
id="self-role",
role_name="仅本人",
data_scope="4",
status="1",
tenant_no="tenant-a",
create_dept_id="dept-a",
del_flag=0,
),
SysRole(
id="all-role",
role_name="全部数据",
data_scope="1",
status="1",
tenant_no="tenant-a",
create_dept_id="dept-a",
del_flag=0,
),
SysUserRole(
id="self-user-role",
user_id="u1",
role_id="self-role",
tenant_no="tenant-a",
create_dept_id="dept-a",
del_flag=0,
),
SysUserRole(
id="all-user-role",
user_id="u1",
role_id="all-role",
tenant_no="tenant-a",
create_dept_id="dept-a",
del_flag=0,
),
_presign("both", sales_user_id="u1", create_by="u1"),
_presign("field-only", sales_user_id="u1", create_by="other"),
_presign("data-only", sales_user_id="other", create_by="u1"),
]
)
db.commit()
ctx = MarketFieldContext("tenant-a", "u1", "specialist")
rows = apply_presign_scope(
db.query(BizPresignOrder),
db,
ctx,
user_info=_user_info(role="admin"),
).all()
assert _ids(rows) == {"both", "field-only"}
def test_market_field_scope_index_migration_is_idempotent_and_complete():
migration = (
Path(__file__).resolve().parents[2]
/ "docs/sql/migrations/2026-10-10-market-field-scope-indexes.sql"
)
sql = migration.read_text(encoding="utf-8")
compact = re.sub(r"\s+", " ", re.sub(r"--[^\n]*", "", sql)).lower()
expected = {
"idx_crs_market_leader_scope": (
"biz_collection_region_staff",
"tenant_no,user_id,is_leader,del_flag,region_id",
),
"idx_po_market_sales_scope": (
"biz_presign_order",
"tenant_no,del_flag,sales_user_id",
),
"idx_po_market_region_scope": (
"biz_presign_order",
"tenant_no,del_flag,region_id",
),
"idx_po_market_manager_scope": (
"biz_presign_order",
"tenant_no,del_flag,region_manager_user_id",
),
"idx_pci_market_parent_scope": (
"biz_protocol_collection_info",
"tenant_no,del_flag,collection_protocol_id",
),
"idx_pci_market_sales_scope": (
"biz_protocol_collection_info",
"tenant_no,del_flag,sales_user_id,collection_protocol_id",
),
"idx_pci_market_region_scope": (
"biz_protocol_collection_info",
"tenant_no,del_flag,region_id,collection_protocol_id",
),
"idx_pci_market_manager_scope": (
"biz_protocol_collection_info",
"tenant_no,del_flag,region_manager_user_id,collection_protocol_id",
),
"idx_rdc_market_parent_scope": (
"biz_research_donation_collection",
"tenant_no,del_flag,research_donation_id",
),
"idx_rdc_market_sales_scope": (
"biz_research_donation_collection",
"tenant_no,del_flag,sales_user_id,research_donation_id",
),
"idx_rdc_market_region_scope": (
"biz_research_donation_collection",
"tenant_no,del_flag,region_id,research_donation_id",
),
"idx_rdc_market_manager_scope": (
"biz_research_donation_collection",
"tenant_no,del_flag,region_manager_user_id,research_donation_id",
),
}
assert compact.count("information_schema.statistics") == len(expected)
assert compact.count("prepare stmt from @sql") == len(expected)
assert compact.count("deallocate prepare stmt") == len(expected)
for index_name, (table_name, columns) in expected.items():
assert f"table_name = '{table_name}'" in compact
assert f"index_name = '{index_name}'" in compact
column_sql = ",".join(f"`{column}`" for column in columns.split(","))
assert re.search(
rf"alter table `{table_name}` add index `{index_name}` \({column_sql}\)",
compact,
)