136 lines
6.8 KiB
Python
136 lines
6.8 KiB
Python
"""真实路由+内存数据库,验证鉴权/写事务/字段校验边界。"""
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
from sqlalchemy.pool import StaticPool
|
|
|
|
|
|
@pytest.fixture
|
|
def api(monkeypatch):
|
|
from app.api.pl.quota_cert import router, legal_router
|
|
from app.core.auth import get_current_user
|
|
from app.core.database import get_db
|
|
from app.core import menu_guard, data_scope
|
|
from app.models import pl_quota_models as m
|
|
from app.models.crm_models import CrmContract, CrmContractProduct
|
|
engine = create_engine('sqlite://', connect_args={'check_same_thread': False}, poolclass=StaticPool)
|
|
for table in m.Base.metadata.sorted_tables:
|
|
if table.name.startswith(('pl_purchase_cert', 'pl_transport_cert', 'pl_contract_cert', 'pl_plan_cert', 'pl_cert_')):
|
|
table.create(engine)
|
|
for model in (CrmContract, CrmContractProduct):
|
|
model.__table__.create(engine)
|
|
factory = sessionmaker(bind=engine)
|
|
def database():
|
|
with factory() as db:
|
|
yield db
|
|
app = FastAPI()
|
|
app.include_router(router, prefix='/api')
|
|
app.include_router(legal_router, prefix='/api')
|
|
app.dependency_overrides[get_db] = database
|
|
permissions = set()
|
|
monkeypatch.setattr(menu_guard, 'get_user_menu_codes', lambda *a: permissions)
|
|
monkeypatch.setattr(data_scope, '_resolve_user_role_scopes', lambda *a: [{'data_scope': '1', 'details': []}])
|
|
with TestClient(app) as client:
|
|
yield client, app, get_current_user, permissions
|
|
engine.dispose()
|
|
|
|
|
|
def payload():
|
|
return {'certNo': 'G1', 'productNameOnCert': '硫酸', 'totalQty': '30', 'quantityUnit': '吨',
|
|
'validUseText': '多次有效', 'buyerName': '买方', 'sellerName': '卖方'}
|
|
|
|
|
|
def test_permission_guards_validation_and_atomic_creation(api):
|
|
client, app, auth, permissions = api
|
|
path = '/api/transport/quota-certs/purchase'
|
|
assert client.get(path).status_code == 401
|
|
app.dependency_overrides[auth] = lambda: {'role': 'admin', 'user_id': 'u', 'tenant_no': 't'}
|
|
assert client.get(path).status_code == 403
|
|
permissions.add('pl:quota-cert:list')
|
|
assert client.get(path).status_code == 200
|
|
assert client.post(path, json=payload()).status_code == 403
|
|
permissions.add('pl:quota-cert:edit')
|
|
assert client.post(path, json={**payload(), 'tenantNo': 'other'}).status_code == 422
|
|
assert client.get(path).json() == []
|
|
saved = client.post(path, json=payload())
|
|
assert saved.status_code == 200
|
|
row = saved.json()
|
|
assert row['tenantNo'] == 't' and row['status'] == 'draft' and row['openingConfirmed'] is False
|
|
assert client.put(path + '/' + row['id'], json={'totalQty': '-1'}).status_code == 422
|
|
assert client.get(path + '/' + row['id']).json()['totalQty'] == '30.000000'
|
|
app.dependency_overrides[auth] = lambda: {'role': 'admin', 'user_id': 'u', 'tenant_no': 'other'}
|
|
assert client.get(path + '/' + row['id']).status_code == 404
|
|
|
|
|
|
def test_contract_static_routes_are_not_interpreted_as_certificate_type(api):
|
|
client, app, auth, permissions = api
|
|
app.dependency_overrides[auth] = lambda: {'role': 'super_admin', 'user_id': 'u', 'tenant_no': 't'}
|
|
assert client.get('/api/transport/quota-certs/contracts').json() == []
|
|
assert client.get('/api/transport/quota-certs/contracts/missing/products').status_code == 404
|
|
assert client.get('/api/transport/quota-certs/invalid').status_code == 422
|
|
|
|
|
|
def test_decimal_wire_values_are_strings_without_float_roundtrip(api):
|
|
from app.api.pl.quota_cert import _wire
|
|
from decimal import Decimal
|
|
assert _wire({'totalQty': Decimal('999999999999.999999')})['totalQty'] == '999999999999.999999'
|
|
|
|
|
|
def test_plan_overview_accepts_web_funnel_aliases_and_validates_dates(api, monkeypatch):
|
|
from datetime import date
|
|
from app.services import pl_quota_overview
|
|
client, app, auth, permissions = api
|
|
app.dependency_overrides[auth] = lambda: {'role': 'super_admin', 'user_id': 'u', 'tenant_no': 't'}
|
|
received = []
|
|
def capture(*args, **kwargs):
|
|
received.append((args, kwargs))
|
|
return {'total': 0, 'list': []}
|
|
monkeypatch.setattr(pl_quota_overview, 'list_plans', capture)
|
|
response = client.get('/api/transport/quota-certs/plans', params={
|
|
'keyword': 'PLAN', 'page': 2, 'pageSize': 10, 'planNo': '001', 'licensePlate': '皖A',
|
|
'planDateFrom': '2026-11-01', 'planDateTo': '2026-11-03', 'quotaStatusIn': 'reserved,settled'})
|
|
assert response.status_code == 200
|
|
args, filters = received.pop()
|
|
assert args[2:] == ('PLAN', None, 2, 10)
|
|
assert filters == {'plan_no': '001', 'license_plate': '皖A',
|
|
'plan_date_from': date(2026, 11, 1), 'plan_date_to': date(2026, 11, 3),
|
|
'quota_status_in': 'reserved,settled'}
|
|
assert client.get('/api/transport/quota-certs/plans', params={'planDateFrom': 'wrong'}).status_code == 422
|
|
assert received == []
|
|
|
|
|
|
def test_preview_returns_structured_insufficiency_without_claiming_availability(api, monkeypatch):
|
|
from app.services.pl_quota_allocation import InsufficientQuota
|
|
from app.services import pl_quota_planning
|
|
client, app, auth, permissions = api
|
|
app.dependency_overrides[auth] = lambda: {'role':'super_admin','user_id':'u','tenant_no':'t'}
|
|
error = InsufficientQuota('引用约束不可行')
|
|
error.diagnostics = [{'certType':'purchase','demand':'30','unit':'吨',
|
|
'availableUpperBound':'20','gapLowerBound':'10'}]
|
|
def fail(*args):
|
|
raise error
|
|
monkeypatch.setattr(pl_quota_planning, 'preview_plan', fail)
|
|
response = client.post('/api/transport/quota-certs/plans/plan/preview', json={})
|
|
assert response.status_code == 200
|
|
assert response.json()['status'] == 'insufficient'
|
|
assert response.json()['diagnostics'] == error.diagnostics
|
|
|
|
|
|
def test_total_adjust_requires_provenance_and_replays_without_duplicate_log(api):
|
|
client, app, auth, permissions = api
|
|
app.dependency_overrides[auth] = lambda: {'role':'super_admin','user_id':'u','tenant_no':'t'}
|
|
base = '/api/transport/quota-certs/purchase'
|
|
row = client.post(base, json=payload()).json()
|
|
path = base + '/' + row['id'] + '/total-adjust'
|
|
body = {'operationId':'adjust-1','totalQty':'35','reason':'按发证机关更正','sourceNote':'更正函核对'}
|
|
response = client.post(path, json=body)
|
|
assert response.status_code == 200
|
|
assert client.post(path, json=body).status_code == 200
|
|
logs = client.get(base + '/' + row['id'] + '/logs').json()
|
|
assert len(logs) == 1 and logs[0]['totalDelta'] == '5.000000'
|
|
assert client.post(path, json={**body,'totalQty':'40'}).status_code == 409
|
|
assert client.post(path, json={**body,'operationId':'adjust-2','sourceNote':''}).status_code == 409
|
|
assert client.get(base + '/' + row['id']).json()['totalQty'] == '35.000000'
|