"""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'