Separate cross-team selection from attribution maintenance and apply the same scope to options, history filters and writes. Add an opt-in tenant permission without granting existing roles. Protect authorization mutation paths against self-grants, validate tenant targets and links, and include menu ancestors atomically. Cover role union, revocation, mobile writes and administration boundaries with regression tests.
202 lines
11 KiB
Python
202 lines
11 KiB
Python
"""Mobile attribution writes and scoped reads against isolated SQLite records."""
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
from app.core.config import settings
|
|
from app.models.project_management import Project, ProjectTeam
|
|
from app.models.models import BizPresignOrder, BizCollectionProtocol
|
|
from app.models.research_donation import BizResearchDonation
|
|
from app.schemas.market_mobile import MarketPresignOrderBody, MarketProtocolBody
|
|
from app.services import market_presign, market_protocol
|
|
from test_market_protocol_api import _session, _masters, _ctx, _user, _body, _cell, _donation
|
|
|
|
|
|
@pytest.fixture
|
|
def db():
|
|
session = _session()
|
|
_masters(session)
|
|
session.add_all([
|
|
ProjectTeam(id='team', tenant_no='tenant-a', team_name='团队', leader_user_id='u1'),
|
|
Project(id='project', tenant_no='tenant-a', project_no='P', project_name='项目', team_id='team', leader_user_id='u1', status='active', biz_types='presign,cell_storage,research_donation'),
|
|
])
|
|
session.commit()
|
|
yield session
|
|
session.close()
|
|
|
|
|
|
@pytest.mark.parametrize('kind', ['presign', 'cell_storage', 'research_donation'])
|
|
def test_mobile_create_persists_and_returns_project(db, kind):
|
|
if kind == 'presign':
|
|
result = market_presign.create_presign_order(db, body=MarketPresignOrderBody(motherName='母亲', projectId='project'), ctx=_ctx('specialist'), user_info=_user())
|
|
basic = result
|
|
else:
|
|
result = market_protocol.create_protocol(db, body=_body(kind, projectId='project', samples=[], attachments={}), ctx=_ctx('specialist'), user_info=_user())
|
|
basic = result['basic']
|
|
model = {'presign': BizPresignOrder, 'cell_storage': BizCollectionProtocol, 'research_donation': BizResearchDonation}[kind]
|
|
db.expire_all()
|
|
assert db.get(model, basic['id']).project_id == 'project'
|
|
assert basic['projectId'] == 'project'
|
|
assert basic['projectName'] == '项目'
|
|
assert basic['teamId'] == 'team'
|
|
assert basic['teamName'] == '团队'
|
|
|
|
|
|
@pytest.mark.parametrize('kind', ['presign', 'cell_storage', 'research_donation'])
|
|
def test_mobile_creation_obeys_required_configuration(db, monkeypatch, kind):
|
|
monkeypatch.setattr(settings, 'PROJECT_ASSIGNMENT_REQUIRED', True)
|
|
with pytest.raises(HTTPException):
|
|
if kind == 'presign':
|
|
market_presign.create_presign_order(db, body=MarketPresignOrderBody(motherName='母亲'), ctx=_ctx('specialist'), user_info=_user())
|
|
else:
|
|
market_protocol.create_protocol(db, body=_body(kind, samples=[], attachments={}), ctx=_ctx('specialist'), user_info=_user())
|
|
|
|
|
|
@pytest.mark.parametrize('kind', ['presign', 'cell_storage', 'research_donation'])
|
|
def test_scoped_lists_and_immutable_edits(db, kind):
|
|
if kind == 'presign':
|
|
own = BizPresignOrder(id='own', tenant_no='tenant-a', sales_user_id='u1', order_status='active', project_id='project')
|
|
hidden = BizPresignOrder(id='hidden', tenant_no='tenant-a', sales_user_id='u2', order_status='active', project_id='project')
|
|
db.add_all([own, hidden])
|
|
else:
|
|
factory = _cell if kind == 'cell_storage' else _donation
|
|
own = factory(db, 'own')
|
|
hidden = factory(db, 'hidden', sales='u2')
|
|
own.project_id = hidden.project_id = 'project'
|
|
db.commit()
|
|
service = market_presign if kind == 'presign' else market_protocol
|
|
listing = service.list_presign_orders if kind == 'presign' else service.list_protocols
|
|
params = {} if kind == 'presign' else {'source': kind}
|
|
rows, total = listing(db, ctx=_ctx('specialist'), user_info=_user(), project_id='project', team_id='team', **params)
|
|
assert total == 1
|
|
assert rows[0]['id'] == 'own'
|
|
assert rows[0]['projectName'] == '项目'
|
|
assert listing(db, ctx=_ctx('specialist'), user_info=_user(), project_id='missing', **params)[1] == 0
|
|
db.get(Project, 'project').status = 'disabled'
|
|
db.commit()
|
|
kwargs = {'order_id': 'own'} if kind == 'presign' else {'protocol_id': 'own', 'source': kind}
|
|
update = service.update_presign_order if kind == 'presign' else service.update_protocol
|
|
body = MarketPresignOrderBody(remark='保留历史') if kind == 'presign' else MarketProtocolBody(source=kind, remark='保留历史')
|
|
update(db, ctx=_ctx('specialist'), user_info=_user(), body=body, **kwargs)
|
|
db.refresh(own)
|
|
assert own.project_id == 'project'
|
|
body = MarketPresignOrderBody(projectId=None) if kind == 'presign' else MarketProtocolBody(source=kind, projectId=None)
|
|
with pytest.raises(HTTPException, match='项目归属维护'):
|
|
update(db, ctx=_ctx('specialist'), user_info=_user(), body=body, **kwargs)
|
|
|
|
|
|
@pytest.mark.parametrize('kind', ['presign', 'cell_storage', 'research_donation'])
|
|
def test_mobile_rejects_foreign_project_and_legacy_backfill(db, kind):
|
|
db.get(Project, 'project').tenant_no = 'tenant-other'
|
|
db.commit()
|
|
with pytest.raises(HTTPException):
|
|
if kind == 'presign':
|
|
market_presign.create_presign_order(db, body=MarketPresignOrderBody(motherName='母亲', projectId='project'), ctx=_ctx('specialist'), user_info=_user())
|
|
else:
|
|
market_protocol.create_protocol(db, body=_body(kind, projectId='project', samples=[], attachments={}), ctx=_ctx('specialist'), user_info=_user())
|
|
if kind == 'presign':
|
|
row = BizPresignOrder(id='legacy', tenant_no='tenant-a', sales_user_id='u1', order_status='active')
|
|
db.add(row)
|
|
else:
|
|
row = (_cell if kind == 'cell_storage' else _donation)(db, 'legacy')
|
|
db.commit()
|
|
with pytest.raises(HTTPException, match='项目归属维护'):
|
|
if kind == 'presign':
|
|
market_presign.update_presign_order(db, order_id='legacy', body=MarketPresignOrderBody(projectId='project'), ctx=_ctx('specialist'), user_info=_user())
|
|
else:
|
|
market_protocol.update_protocol(db, source=kind, protocol_id='legacy', body=MarketProtocolBody(source=kind, projectId='project'), ctx=_ctx('specialist'), user_info=_user())
|
|
db.refresh(row)
|
|
assert row.project_id is None
|
|
|
|
|
|
@pytest.mark.parametrize('role', ['specialist', 'manager'])
|
|
@pytest.mark.parametrize('resource', ['protocols', 'presign-orders'])
|
|
def test_mobile_http_list_binds_project_and_team_aliases(db, role, resource):
|
|
import asyncio
|
|
import httpx
|
|
from fastapi import FastAPI
|
|
from app.api.market_mobile import router
|
|
from app.core.database import get_db
|
|
|
|
row = _cell(db, 'visible')
|
|
row.project_id = 'project'
|
|
hidden = _cell(db, 'hidden', sales='u2', manager='manager-other')
|
|
hidden.project_id = 'project'
|
|
db.add_all([
|
|
BizPresignOrder(id='presign-own', tenant_no='tenant-a', sales_user_id='u1', region_manager_user_id='manager-a', order_status='active', project_id='project'),
|
|
BizPresignOrder(id='presign-hidden', tenant_no='tenant-a', sales_user_id='u2', region_manager_user_id='manager-other', order_status='active', project_id='project'),
|
|
])
|
|
db.commit()
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
|
|
async def database():
|
|
yield db
|
|
|
|
async def user():
|
|
return _user('manager-a' if role == 'manager' else 'u1')
|
|
|
|
app.dependency_overrides[get_db] = database
|
|
for route in router.routes:
|
|
for dependency in route.dependant.dependencies:
|
|
if dependency.name == 'user':
|
|
app.dependency_overrides[dependency.call] = user
|
|
|
|
async def check():
|
|
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url='http://test') as client:
|
|
url = f'/mini/market/{role}/{resource}'
|
|
response = await client.get(url, params={'source': 'cell_storage', 'projectId': 'project', 'teamId': 'team'})
|
|
assert response.status_code == 200
|
|
data = response.json()['data']
|
|
assert data['total'] == 1
|
|
assert data['list'][0]['projectName'] == '项目'
|
|
response = await client.get(url, params={'source': 'cell_storage', 'projectId': 'missing'})
|
|
assert response.json()['data']['total'] == 0
|
|
response = await client.get(url, params={'source': 'cell_storage', 'teamId': 'missing'})
|
|
assert response.json()['data']['total'] == 0
|
|
asyncio.run(check())
|
|
|
|
|
|
@pytest.mark.parametrize('role', ['specialist', 'manager'])
|
|
def test_mobile_history_filter_options_keep_field_scope(db, role):
|
|
from app.services.project_management import filter_options
|
|
from app.services.market_field_scope import apply_presign_scope
|
|
db.get(Project, 'project').status = 'disabled'
|
|
db.add(Project(id='hidden-project', tenant_no='tenant-a', project_no='H', project_name='隐藏', team_id='team', leader_user_id='u2', status='active', biz_types='cell_storage'))
|
|
db.add_all([
|
|
BizPresignOrder(id='own', tenant_no='tenant-a', sales_user_id='u1', region_manager_user_id='manager-a', project_id='project'),
|
|
BizPresignOrder(id='hidden', tenant_no='tenant-a', sales_user_id='u2', region_manager_user_id='manager-other', project_id='hidden-project'),
|
|
])
|
|
db.commit()
|
|
user = _user('manager-a' if role == 'manager' else 'u1')
|
|
query = apply_presign_scope(db.query(BizPresignOrder), db, _ctx(role, user['user_id']), user_info=user)
|
|
result = filter_options(db, 'tenant-a', user, 'presign', business_scope=query)
|
|
assert [row['id'] for row in result['list']] == ['project']
|
|
|
|
@pytest.mark.parametrize('kind', ['presign', 'cell_storage', 'research_donation'])
|
|
def test_mobile_cross_team_assignment_requires_explicit_all_permission(db, kind):
|
|
from app.models.models import SysMenu, SysRole, SysRoleMenu, SysUserRole
|
|
# Becoming a project leader does not make the user a member of its team.
|
|
db.get(ProjectTeam, 'team').leader_user_id = 'u2'
|
|
db.add_all([
|
|
SysRole(id='assignment-role', tenant_no='tenant-a', role_name='归属维护', data_scope='4'),
|
|
SysUserRole(id='assignment-user-role', tenant_no='tenant-a', user_id='u1', role_id='assignment-role'),
|
|
SysMenu(id='assignment-menu', tenant_no='tenant-a', menu_name='归属维护', perms='project:assignment:edit'),
|
|
SysRoleMenu(id='assignment-role-menu', tenant_no='tenant-a', role_id='assignment-role', menu_id='assignment-menu'),
|
|
])
|
|
db.commit()
|
|
|
|
def create():
|
|
if kind == 'presign':
|
|
return market_presign.create_presign_order(db, body=MarketPresignOrderBody(motherName='母亲', projectId='project'), ctx=_ctx('specialist'), user_info={**_user(), 'role': 'admin'})
|
|
return market_protocol.create_protocol(db, body=_body(kind, projectId='project', samples=[], attachments={}), ctx=_ctx('specialist'), user_info={**_user(), 'role': 'admin'})
|
|
|
|
with pytest.raises(HTTPException) as caught:
|
|
create()
|
|
assert caught.value.status_code == 403
|
|
db.add_all([
|
|
SysMenu(id='all-menu', tenant_no='tenant-a', menu_name='全部项目', menu_type='F', visible='0', perms='project:project:all'),
|
|
SysRoleMenu(id='all-role-menu', tenant_no='tenant-a', role_id='assignment-role', menu_id='all-menu'),
|
|
])
|
|
db.commit()
|
|
result = create()
|
|
assert (result if kind == 'presign' else result['basic'])['projectId'] == 'project'
|