377 lines
21 KiB
Python
377 lines
21 KiB
Python
"""额度账本内存事务回归。"""
|
|
from datetime import date, datetime
|
|
from decimal import Decimal as D
|
|
from types import SimpleNamespace
|
|
import pytest
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import Session
|
|
|
|
@pytest.fixture(params=[True, False], ids=["autoflush", "explicit_flush"])
|
|
def case(request):
|
|
from app.models import pl_quota_models as m
|
|
from app.models.pl_models import PlPlan
|
|
from app.models.crm_models import CrmContract, CrmContractProduct
|
|
engine = create_engine('sqlite://')
|
|
for model in [PlPlan, CrmContract, CrmContractProduct] + [getattr(m,n) for n in dir(m) if n.startswith('Pl')]:
|
|
model.__table__.create(engine)
|
|
db = Session(engine, autoflush=request.param)
|
|
user = SimpleNamespace(id='u',dept_id='d',tenant_no='t',to_user_info=lambda: {'role':'super_admin','user_id':'u'})
|
|
for pid in ['plan','other']:
|
|
db.add(PlPlan(id=pid,plan_no=pid,create_date=date.today(),belongs_company='shanxia',plan_type='direct',tenant_no='t'))
|
|
db.add(CrmContract(id='c',contract_no='c',contract_name='c',customer_id='customer',contract_type='sale',status='approved',tenant_no='t'))
|
|
db.add(CrmContractProduct(id='cp',contract_id='c',product_id='product',product_name='酸',quantity=100,tenant_no='t'))
|
|
for kind,model,pid in [('purchase',m.PlPurchaseCert,'p'),('transport',m.PlTransportCert,'t')]:
|
|
extra = dict(buyer_name='买',seller_name='卖') if kind=='purchase' else dict(shipper_name='货主',vehicle_numbers_json=['CAR'])
|
|
db.add(model(id=pid,cert_no=pid,product_name_on_cert='酸',total_qty=100,quantity_unit='吨',valid_use_text='多次有效',valid_from=date(2020,1,1),valid_to=date(2099,12,31),status='active',verified_by='u',verified_time=datetime.now(),tenant_no='t',**extra))
|
|
db.add(m.PlCertQtyLog(id='opening'+pid,operation_id='opening'+pid,request_hash='opening',cert_type=kind,cert_id=pid,event_type='opening',quantity_unit='吨',before_total_qty=100,after_total_qty=100,before_reserved_qty=0,after_reserved_qty=0,before_used_qty=0,after_used_qty=0,reason='期初',tenant_no='t'))
|
|
db.add(m.PlContractCertLink(id='l'+pid,contract_id='c',contract_no_snapshot='c',cert_type=kind,cert_id=pid,tenant_no='t'))
|
|
db.add(m.PlContractCertProduct(id='s'+pid,contract_cert_link_id='l'+pid,contract_product_id='cp',tenant_no='t'))
|
|
db.add(m.PlTransportCertPurchaseRef(id='ref',transport_cert_id='t',reference_type='purchase_permit',reference_no='p',purchase_cert_id='p',tenant_no='t'))
|
|
db.commit()
|
|
yield SimpleNamespace(db=db,user=user,m=m)
|
|
db.close(); engine.dispose()
|
|
|
|
def request(**kw):
|
|
return dict(operation_id='op',action='reserve',plan_id='plan',purchase_cert_ids=['p'],transport_cert_ids=['t'],vehicle_no='CAR',sources=[dict(source_stop_type='load',source_stop_id='stop',source_stop_product_id='sp',plan_product_id='pp',contract_id='c',contract_product_id='cp',product_id='product',quantity='29',unit='吨',fingerprint='fp')],**kw)
|
|
|
|
def run(c, **kw):
|
|
from app.services.pl_quota_usage_service import execute_usage
|
|
body=request(); body.update(kw)
|
|
return execute_usage(c.db,c.user,body)
|
|
|
|
def test_retry_and_conflict(case):
|
|
c=case
|
|
first=run(c)
|
|
assert run(c)==first
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty==29
|
|
assert c.db.query(c.m.PlCertQtyLog).filter_by(event_type='reserve').count()==2
|
|
for kw in [dict(plan_id='other'),dict(action='resize'),dict(purchase_cert_ids=[])]:
|
|
with pytest.raises(ValueError): run(c,**kw)
|
|
|
|
def test_resize_atomic_and_identity(case):
|
|
c=case; run(c)
|
|
ids=[x.id for x in c.db.query(c.m.PlPlanCertUsageItem).all()]
|
|
sources=request()['sources']; sources[0]['quantity']='101'
|
|
with pytest.raises(ValueError): run(c,operation_id='bad',action='resize',sources=sources)
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty==29
|
|
assert c.db.query(c.m.PlCertOperation).count()==1
|
|
sources[0]['quantity']='30'
|
|
run(c,operation_id='resize',action='resize',sources=sources)
|
|
assert [x.id for x in c.db.query(c.m.PlPlanCertUsageItem).all()]==ids
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty==30
|
|
|
|
def test_settle_and_adjust_delta(case):
|
|
c=case; run(c)
|
|
run(c,operation_id='settle',action='settle',actual_by_source={'load:sp':'18'})
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').used_qty==18
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty==0
|
|
run(c,operation_id='adjust',action='actual_adjust',actual_by_source={'load:sp':'20'},reason='更正',source_note='磅单核对')
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').used_qty==20
|
|
assert c.db.query(c.m.PlCertQtyLog).filter_by(operation_id='adjust',cert_type='purchase').one().used_delta==2
|
|
|
|
def test_once_zero_and_opening(case):
|
|
c=case; c.db.get(c.m.PlPurchaseCert,'p').valid_use_text='一次有效'; c.db.flush()
|
|
run(c)
|
|
run(c,operation_id='settle',action='settle',actual_by_source={'load:sp':'0'})
|
|
with pytest.raises(ValueError, match='已执行'):
|
|
run(c,operation_id='release',action='release')
|
|
assert all(u.usage_status == 'settled' for u in c.db.query(c.m.PlPlanCertUsage).all())
|
|
with pytest.raises(ValueError): run(c,operation_id='other',plan_id='other')
|
|
|
|
def test_shared_pool_and_scope(case):
|
|
c=case; run(c)
|
|
sources=request()['sources']; sources[0]['quantity']='80'
|
|
with pytest.raises(ValueError): run(c,operation_id='other',plan_id='other',sources=sources)
|
|
c.db.get(c.m.PlContractCertProduct,'st').scope_status='inactive'; c.db.flush()
|
|
with pytest.raises(ValueError): run(c,operation_id='invalid',action='resize')
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty==29
|
|
|
|
def test_opening_required_and_consumed(case):
|
|
c=case
|
|
c.db.get(c.m.PlCertQtyLog,'openingp').del_flag=1; c.db.flush()
|
|
with pytest.raises(ValueError,match='期初'): run(c)
|
|
|
|
def test_zero_reservation_consumes_once_slot(case):
|
|
c=case; c.db.get(c.m.PlPurchaseCert,'p').valid_use_text='一次有效'; c.db.flush()
|
|
sources=request()['sources']; sources[0]['quantity']='0'
|
|
run(c,sources=sources)
|
|
assert c.db.query(c.m.PlPlanCertUsage).count()==2
|
|
with pytest.raises(ValueError): run(c,operation_id='other',plan_id='other',sources=sources)
|
|
|
|
def test_double_cert_failure_keeps_all_balances(case):
|
|
c=case; c.db.get(c.m.PlTransportCert,'t').total_qty=10; c.db.flush()
|
|
with pytest.raises(ValueError): run(c)
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty==0
|
|
assert c.db.query(c.m.PlPlanCertUsage).count()==0
|
|
assert c.db.query(c.m.PlCertOperation).count()==0
|
|
|
|
def test_mult_unit_requires_new_manual_actuals(case):
|
|
c=case; c.db.get(c.m.PlTransportCert,'t').quantity_unit='kg'; c.db.flush()
|
|
manual=[dict(permit_kind='transport',source_id='load:sp',permit_id='t',source_quantity='29',permit_quantity='58',note='人工核实')]
|
|
run(c,manual_allocations=manual)
|
|
with pytest.raises(ValueError): run(c,operation_id='fail',action='settle',actual_by_source={'load:sp':'18'})
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty==29
|
|
actual=[dict(permit_kind='transport',source_id='load:sp',permit_id='t',source_quantity='18',permit_quantity='35',note='归档重新核实')]
|
|
run(c,operation_id='settle',action='settle',actual_by_source={'load:sp':'18'},manual_actuals=actual)
|
|
assert c.db.get(c.m.PlTransportCert,'t').used_qty==35
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').used_qty==18
|
|
|
|
def test_actual_missing_and_overage_are_atomic(case):
|
|
c=case; run(c)
|
|
for actual in [{},{'load:sp':'30'}]:
|
|
with pytest.raises(ValueError): run(c,operation_id='fail',action='settle',actual_by_source=actual)
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty==29
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').used_qty==0
|
|
|
|
def test_total_adjust_floor_and_audit(case):
|
|
c=case; run(c)
|
|
from app.services.pl_quota_usage_service import execute_usage
|
|
data=dict(action='total_adjust',operation_id='total',cert_type='purchase',cert_id='p',total_qty='28',reason='修订',source_note='发证机关核实')
|
|
with pytest.raises(ValueError): execute_usage(c.db,c.user,data)
|
|
data['total_qty']='120'
|
|
result=execute_usage(c.db,c.user,data)
|
|
assert execute_usage(c.db,c.user,data)==result
|
|
log=c.db.query(c.m.PlCertQtyLog).filter_by(event_type='total_adjust').one()
|
|
assert log.total_delta==20 and log.reserved_delta==0
|
|
|
|
def test_tenant_and_contract_product_identity(case):
|
|
c=case
|
|
from fastapi import HTTPException
|
|
from app.services.pl_quota_usage_service import execute_usage
|
|
other=SimpleNamespace(**{**c.user.__dict__,'tenant_no':'foreign'})
|
|
with pytest.raises(HTTPException): execute_usage(c.db,other,request())
|
|
sources=request()['sources']; sources[0]['product_id']='fake'
|
|
with pytest.raises(ValueError): run(c,sources=sources)
|
|
assert c.db.query(c.m.PlCertOperation).count()==0
|
|
|
|
def test_opening_consumed_and_expiry_vehicle_block(case):
|
|
c=case; p=c.db.get(c.m.PlPurchaseCert,'p')
|
|
p.valid_use_text='一次有效'; p.opening_single_use_consumed=1; c.db.flush()
|
|
with pytest.raises(ValueError): run(c)
|
|
p.opening_single_use_consumed=0; p.valid_to=date(2000,1,1); c.db.flush()
|
|
with pytest.raises(ValueError): run(c)
|
|
p.valid_to=date(2099,12,31); c.db.flush()
|
|
with pytest.raises(ValueError): run(c,vehicle_no='unknown')
|
|
|
|
def test_no_cert_operation_is_still_idempotent(case):
|
|
c=case
|
|
first=run(c,sources=[],purchase_cert_ids=[],transport_cert_ids=[])
|
|
assert run(c,sources=[],purchase_cert_ids=[],transport_cert_ids=[])==first
|
|
assert c.db.query(c.m.PlCertOperation).count()==1
|
|
|
|
def test_removal_and_restore_preserve_item_id(case):
|
|
c=case; run(c)
|
|
ids={i.id for i in c.db.query(c.m.PlPlanCertUsageItem).all()}
|
|
run(c,action='resize',operation_id='clear',sources=[],purchase_cert_ids=[],transport_cert_ids=[])
|
|
assert all(i.del_flag==1 for i in c.db.query(c.m.PlPlanCertUsageItem).all())
|
|
run(c,action='resize',operation_id='restore')
|
|
assert {i.id for i in c.db.query(c.m.PlPlanCertUsageItem).all()}==ids
|
|
assert all(i.del_flag==0 for i in c.db.query(c.m.PlPlanCertUsageItem).all())
|
|
|
|
def _second_pair(c, cross=False):
|
|
for kind,model,old_id,new_id,total in [('purchase',c.m.PlPurchaseCert,'p','p2',100),('transport',c.m.PlTransportCert,'t','t2',100)]:
|
|
original=c.db.get(model,old_id)
|
|
values={col.name:getattr(original,col.name) for col in model.__table__.columns}
|
|
values.update(id=new_id,cert_no=new_id,total_qty=total)
|
|
c.db.add(model(**values))
|
|
c.db.add(c.m.PlContractCertLink(id='l'+new_id,contract_id='c',contract_no_snapshot='c',cert_type=kind,cert_id=new_id,tenant_no='t'))
|
|
c.db.add(c.m.PlContractCertProduct(id='s'+new_id,contract_cert_link_id='l'+new_id,contract_product_id='cp',tenant_no='t'))
|
|
log=c.db.get(c.m.PlCertQtyLog,'opening'+old_id)
|
|
lv={col.name:getattr(log,col.name) for col in c.m.PlCertQtyLog.__table__.columns}
|
|
lv.update(id='opening'+new_id,operation_id='opening'+new_id,cert_id=new_id)
|
|
c.db.add(c.m.PlCertQtyLog(**lv))
|
|
c.db.add(c.m.PlTransportCertPurchaseRef(id='ref2',transport_cert_id='t2',reference_type='purchase_permit',reference_no='p2' if not cross else 'p',purchase_cert_id='p2' if not cross else 'p',tenant_no='t'))
|
|
if cross:
|
|
c.db.get(c.m.PlTransportCertPurchaseRef,'ref').purchase_cert_id='p2'
|
|
c.db.get(c.m.PlPurchaseCert,'p').total_qty=20
|
|
c.db.get(c.m.PlTransportCert,'t').total_qty=9 if cross else 20
|
|
c.db.flush()
|
|
|
|
@pytest.mark.parametrize('actual,expected', [('29',[D('20'),D('9')]),('18',[D('18'),D('0')])])
|
|
def test_split_settlement_order(case,actual,expected):
|
|
c=case; _second_pair(c)
|
|
run(c,purchase_cert_ids=['p','p2'],transport_cert_ids=['t','t2'])
|
|
run(c,operation_id='settle',action='settle',actual_by_source={'load:sp':actual})
|
|
assert [c.db.get(c.m.PlPurchaseCert,p).used_qty for p in ['p','p2']]==expected
|
|
assert all(u.reserved_qty==0 for u in c.db.query(c.m.PlPlanCertUsage).all())
|
|
|
|
def test_split_pairing_failure_does_not_partially_settle(case):
|
|
c=case; _second_pair(c,cross=True)
|
|
run(c,purchase_cert_ids=['p','p2'],transport_cert_ids=['t','t2'])
|
|
with pytest.raises(ValueError): run(c,operation_id='settle',action='settle',actual_by_source={'load:sp':'18'})
|
|
assert sum(u.reserved_qty for u in c.db.query(c.m.PlPlanCertUsage).all())==58
|
|
assert all(u.used_qty==0 for u in c.db.query(c.m.PlPlanCertUsage).all())
|
|
|
|
def test_zero_foreign_unit_still_requires_manual_pair(case):
|
|
c=case; c.db.get(c.m.PlTransportCert,'t').quantity_unit='kg'; c.db.flush()
|
|
sources=request()['sources']; sources[0]['quantity']='0'
|
|
manual=[dict(permit_kind='transport',source_id='load:sp',permit_id='t',source_quantity='0',permit_quantity='0',note='零量人工核实')]
|
|
run(c,sources=sources,manual_allocations=manual)
|
|
assert c.db.query(c.m.PlPlanCertUsage).count()==2
|
|
|
|
|
|
@pytest.mark.parametrize('marker', ['request', 'in_transit', 'settled', 'snapshot'])
|
|
def test_executed_release_cannot_free_multi_use_quota(case, marker):
|
|
c=case; run(c)
|
|
for u in c.db.query(c.m.PlPlanCertUsage).all():
|
|
if marker in ('in_transit','settled'): u.usage_status=marker
|
|
if marker == 'snapshot': u.confirmation_snapshot={'executed':True}
|
|
c.db.flush()
|
|
with pytest.raises(ValueError, match='已执行'):
|
|
run(c, operation_id='release', action='release', executed=marker=='request')
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty==29
|
|
assert all(u.reserved_qty==29 for u in c.db.query(c.m.PlPlanCertUsage).all())
|
|
assert all(i.reserved_qty==29 for i in c.db.query(c.m.PlPlanCertUsageItem).all())
|
|
assert c.db.query(c.m.PlCertOperation).count()==1
|
|
|
|
@pytest.mark.parametrize('field,value', [('fingerprint','changed'),('source_stop_id','new-stop'),
|
|
('source_stop_product_id','new-product'),('plan_product_id','other-pp'),('contract_id','other-contract'),
|
|
('contract_product_id','other-cp'),('product_id','other-product'),('quantity','30'),('unit','kg'),
|
|
('related_stop_products',['new-delivery'])])
|
|
@pytest.mark.parametrize('action', ['settle','actual_adjust'])
|
|
def test_settle_refuses_changed_authoritative_source(case, field, value, action):
|
|
c=case; run(c)
|
|
if action=='actual_adjust': run(c,operation_id='settle',action='settle',actual_by_source={'load:sp':'18'})
|
|
sources=request()['sources']; sources[0][field]=value
|
|
with pytest.raises(ValueError, match='来源'):
|
|
run(c,operation_id='changed',action=action,sources=sources,actual_by_source={'load:sp':'19'},reason='纠错',source_note='磅单')
|
|
cert=c.db.get(c.m.PlPurchaseCert,'p')
|
|
assert (cert.reserved_qty,cert.used_qty)==((0,18) if action=='actual_adjust' else (29,0))
|
|
|
|
@pytest.mark.parametrize('source_mode', ['missing', 'duplicate', 'extra'])
|
|
def test_settle_requires_exact_source_set(case, source_mode):
|
|
c=case; run(c); sources=request()['sources']
|
|
if source_mode=='missing': sources=[]
|
|
elif source_mode=='duplicate': sources=sources+sources
|
|
else: sources=sources+[{**sources[0], 'source_stop_product_id':'extra'}]
|
|
with pytest.raises(ValueError, match='来源'):
|
|
run(c,operation_id='settle',action='settle',sources=sources,actual_by_source={'load:sp':'18'})
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty==29
|
|
|
|
@pytest.mark.parametrize('field', ['valid_from','valid_to'])
|
|
@pytest.mark.parametrize('action', ['reserve','resize'])
|
|
def test_incomplete_dates_block_reservation(case,field,action):
|
|
c=case
|
|
if action=='resize': run(c)
|
|
setattr(c.db.get(c.m.PlPurchaseCert,'p'),field,None); c.db.flush()
|
|
with pytest.raises(ValueError,match='有效期'):
|
|
run(c,operation_id='date',action=action)
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty==(29 if action=='resize' else 0)
|
|
|
|
@pytest.mark.parametrize('on_date', [date(2020,1,1), date(2099,12,31)])
|
|
def test_date_boundaries_are_inclusive(case,on_date):
|
|
run(case,on_date=on_date)
|
|
assert case.db.get(case.m.PlPurchaseCert,'p').reserved_qty==29
|
|
|
|
def test_preview_uses_own_reservation_without_writes(case):
|
|
from app.services.pl_quota_usage_service import preview_usage
|
|
c=case; run(c)
|
|
before=(c.db.query(c.m.PlCertOperation).count(),c.db.query(c.m.PlCertQtyLog).count())
|
|
data=request(); data['action']='resize'; data['sources'][0]['quantity']='100'
|
|
result=preview_usage(c.db,c.user,data)
|
|
assert result['purchaseAllocations'][0]['permitQuantity']=='100'
|
|
assert result['purchaseAllocations'][0]['ownReservedQty']=='29'
|
|
assert result['purchaseAllocations'][0]['contractCertProductId']=='sp'
|
|
assert result['transportAllocations'][0]['certRevision']==1
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty==29
|
|
assert (c.db.query(c.m.PlCertOperation).count(),c.db.query(c.m.PlCertQtyLog).count())==before
|
|
assert not c.db.new and not c.db.dirty
|
|
|
|
def test_preview_uses_same_eligibility_and_never_records_operation(case):
|
|
from app.services.pl_quota_usage_service import preview_usage
|
|
c=case; c.db.get(c.m.PlPurchaseCert,'p').valid_to=None; c.db.flush()
|
|
with pytest.raises(ValueError,match='有效期'): preview_usage(c.db,c.user,request())
|
|
assert c.db.query(c.m.PlCertOperation).count()==0
|
|
assert c.db.query(c.m.PlPlanCertUsage).count()==0
|
|
|
|
|
|
def test_preview_insufficiency_includes_current_shared_capacity(case):
|
|
from app.services.pl_quota_usage_service import preview_usage
|
|
from app.services.pl_quota_allocation import InsufficientQuota
|
|
c = case
|
|
c.db.get(c.m.PlPurchaseCert, 'p').total_qty = D('20')
|
|
c.db.flush()
|
|
with pytest.raises(InsufficientQuota) as raised:
|
|
preview_usage(c.db, c.user, request())
|
|
row = raised.value.diagnostics[0]
|
|
assert row['certType'] == 'purchase' and D(row['demand']) == 29
|
|
assert D(row['availableUpperBound']) == 20 and D(row['gapLowerBound']) == 9
|
|
assert c.db.query(c.m.PlCertOperation).count() == 0
|
|
assert c.db.query(c.m.PlPlanCertUsage).count()==0
|
|
|
|
@pytest.mark.parametrize('action', ['reserve','resize'])
|
|
@pytest.mark.parametrize('field,value', [('status','void'),('status','draft'),('contract_type','other')])
|
|
def test_current_contract_must_remain_executable(case,action,field,value):
|
|
from app.models.crm_models import CrmContract
|
|
c=case
|
|
if action=='resize': run(c)
|
|
setattr(c.db.get(CrmContract,'c'),field,value); c.db.flush()
|
|
with pytest.raises(ValueError,match='合同'):
|
|
run(c,operation_id='contract-changed',action=action)
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty==(29 if action=='resize' else 0)
|
|
|
|
|
|
def test_initial_operation_lookup_does_not_lock_missing_key(case,monkeypatch):
|
|
from sqlalchemy.orm import Query
|
|
from sqlalchemy.dialects import mysql
|
|
c=case; reads=[]; original=Query.first
|
|
def observed(query):
|
|
if query.column_descriptions[0].get('entity') is c.m.PlCertOperation:
|
|
reads.append(str(query.statement.compile(dialect=mysql.dialect())))
|
|
return original(query)
|
|
monkeypatch.setattr(Query,'first',observed)
|
|
run(c)
|
|
assert reads and 'FOR UPDATE' not in reads[0]
|
|
|
|
@pytest.mark.parametrize('conflict', [False,True])
|
|
def test_operation_unique_insert_arbitrates_stale_snapshot(case,monkeypatch,conflict):
|
|
"""真实SQLite唯一冲突;只隐藏初次快照读以模拟另一事务已提交。"""
|
|
from sqlalchemy.orm import Query
|
|
from sqlalchemy.dialects import mysql
|
|
c=case; result=run(c); reads=[]; first=Query.first; one=Query.one
|
|
def stale(query):
|
|
if query.column_descriptions[0].get('entity') is c.m.PlCertOperation and not reads:
|
|
reads.append((str(query.statement.compile(dialect=mysql.dialect())),False))
|
|
return None
|
|
return first(query)
|
|
def current(query):
|
|
if query.column_descriptions[0].get('entity') is c.m.PlCertOperation:
|
|
reads.append((str(query.statement.compile(dialect=mysql.dialect())),query.load_options._populate_existing))
|
|
return one(query)
|
|
monkeypatch.setattr(Query,'first',stale); monkeypatch.setattr(Query,'one',current)
|
|
if conflict:
|
|
with pytest.raises(ValueError,match='操作号'): run(c,plan_id='other')
|
|
else:
|
|
assert run(c)==result
|
|
assert len(reads)==2
|
|
assert 'FOR UPDATE' not in reads[0][0]
|
|
assert 'FOR UPDATE' in reads[1][0] and reads[1][1]
|
|
assert c.db.query(c.m.PlCertOperation).count()==1
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty==29
|
|
assert c.db.query(c.m.PlCertQtyLog).filter_by(event_type='reserve').count()==2
|
|
|
|
|
|
def test_new_current_usage_rejects_without_out_of_order_cert_lock(case,monkeypatch):
|
|
"""仅隐藏usage快照读;当前读必须发现未锁证件,原子409而非临时补锁。"""
|
|
from sqlalchemy.orm import Query
|
|
from fastapi import HTTPException
|
|
from app.services import pl_quota_usage_service as svc
|
|
c=case; run(c); original=Query.all; cert_get=svc._get_row; hidden=[]; locked=[]
|
|
def stale(query):
|
|
if (query.column_descriptions[0].get('entity') is c.m.PlPlanCertUsage
|
|
and query._for_update_arg is None and not hidden):
|
|
hidden.append(True)
|
|
return []
|
|
return original(query)
|
|
def tracked(db,user,kind,cert_id,lock=False):
|
|
if lock: locked.append((kind,cert_id))
|
|
return cert_get(db,user,kind,cert_id,lock=lock)
|
|
monkeypatch.setattr(Query,'all',stale); monkeypatch.setattr(svc,'_get_row',tracked)
|
|
with pytest.raises(HTTPException) as exc:
|
|
run(c,operation_id='release-race',action='release',purchase_cert_ids=[],transport_cert_ids=[],sources=[])
|
|
assert exc.value.status_code==409
|
|
assert locked==[]
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty==29
|
|
assert all(u.reserved_qty==29 for u in c.db.query(c.m.PlPlanCertUsage).all())
|
|
assert c.db.query(c.m.PlCertOperation).count()==1
|