394 lines
19 KiB
Python
394 lines
19 KiB
Python
"""隔离 SQLite 档案回归,不连接配置数据库。"""
|
|
import importlib.util
|
|
from decimal import Decimal
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
from pydantic import ValidationError
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
def test_archive_module_exists():
|
|
assert importlib.util.find_spec('app.services.pl_quota_cert_service') is not None
|
|
|
|
|
|
@pytest.fixture
|
|
def ctx():
|
|
from app.models import pl_quota_models as m
|
|
from app.services import pl_quota_cert_service as svc
|
|
engine = create_engine('sqlite://')
|
|
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)
|
|
db = Session(engine)
|
|
user = SimpleNamespace(id='u', dept_id='d', tenant_no='t', to_user_info=lambda: {'role': 'super_admin', 'user_id': 'u', 'tenant_no': 't'})
|
|
yield db, user, svc, m
|
|
db.close()
|
|
engine.dispose()
|
|
|
|
|
|
def purchase(**kw):
|
|
return dict(certNo='G1', productNameOnCert='硫酸', totalQty='100.123456', quantityUnit='吨', validUseText='多次有效', buyerName='购方', sellerName='销方', **kw)
|
|
|
|
|
|
def transport(**kw):
|
|
data = purchase()
|
|
data.pop('buyerName'); data.pop('sellerName')
|
|
data.update(certNo='Y1', shipperName='货主', vehicleNumbersJson=['皖A1', '皖A2'])
|
|
data.update(kw)
|
|
return data
|
|
|
|
|
|
def test_manual_optional_image_complete_fields(ctx):
|
|
db,u,s,m = ctx
|
|
data = purchase(buyerAddress='地址', buyerPhone='电话', buyerLegalRepresentative='甲', sellerAddress='乙址', sellerPhone='乙电话', sellerLegalRepresentative='乙', purchasePurpose='用途', issueDate='2026-01-01', validFrom='2026-01-01', validTo='2027-01-01')
|
|
row = s.save_cert(db,u,'purchase',data)
|
|
assert row['buyerAddress'] == '地址' and row['attachmentUrls'] is None
|
|
assert row['totalQty'] == Decimal('100.123456')
|
|
assert row['status'] == 'draft'
|
|
assert db.query(m.PlCertQtyLog).count() == 0
|
|
db.rollback()
|
|
assert db.query(m.PlPurchaseCert).count() == 0
|
|
|
|
|
|
def test_opening_confirmation_remains_visible_after_reverification_required(ctx):
|
|
db, user, svc, models = ctx
|
|
row = svc.save_cert(db, user, 'purchase', purchase())
|
|
assert row['openingConfirmed'] is False
|
|
row = svc.confirm_cert(db, user, 'purchase', row['id'], {
|
|
'operationId': 'op-ui', 'usedQty': '0', 'singleUseConsumed': False, 'reason': '全新证'})
|
|
changed = svc.save_cert(db, user, 'purchase', {'buyerAddress': '更正地址'}, row['id'])
|
|
assert changed['verifiedTime'] is None
|
|
assert changed['openingConfirmed'] is True
|
|
|
|
|
|
@pytest.mark.parametrize('qty', ['-1', 'NaN', 'Infinity', '1.0000001', '1000000000000'])
|
|
def test_decimal_validation(qty):
|
|
from app.schemas.pl.quota_cert import PurchaseCertCreate
|
|
data=purchase(); data['totalQty']=qty
|
|
with pytest.raises(ValidationError): PurchaseCertCreate(**data)
|
|
|
|
|
|
def test_reference_delta_omitted_empty_and_invalid_prewrite(ctx):
|
|
db,u,s,m=ctx
|
|
row=s.save_cert(db,u,'transport',transport(purchaseRefs=[{'referenceType':'unknown','referenceNo':'外部1'}]))
|
|
ref=row['purchaseRefs'][0]
|
|
row=s.save_cert(db,u,'transport',{'remark':'补图'},row['id'])
|
|
assert row['purchaseRefs'][0]['id']==ref['id']
|
|
row=s.save_cert(db,u,'transport',{'purchaseRefs':[{'id':ref['id'],'referenceType':'unknown','referenceNo':'外部1','remark':'留痕'}]},row['id'])
|
|
assert row['purchaseRefs'][0]['id']==ref['id']
|
|
with pytest.raises(HTTPException): s.save_cert(db,u,'transport',{'remark':'不可写','purchaseRefs':[{'id':'foreign','referenceType':'unknown','referenceNo':'X'}]},row['id'])
|
|
assert s.get_cert(db,u,'transport',row['id'])['remark']=='补图'
|
|
row=s.save_cert(db,u,'transport',{'purchaseRefs':[]},row['id'])
|
|
assert row['purchaseRefs']==[]
|
|
assert db.query(m.PlTransportCertPurchaseRef).one().del_flag==1
|
|
|
|
|
|
def test_confirmation_and_history_protection(ctx):
|
|
db,u,s,m=ctx
|
|
row=s.save_cert(db,u,'purchase',purchase())
|
|
with pytest.raises((HTTPException,ValidationError)): s.confirm_cert(db,u,'purchase',row['id'],{'usedQty':'0','singleUseConsumed':True,'reason':'核对','operationId':'op'})
|
|
row=s.confirm_cert(db,u,'purchase',row['id'],{'usedQty':'0','singleUseConsumed':True,'reason':'核对','evidenceUrls':'https://minio/evidence','operationId':'op'})
|
|
assert row['openingSingleUseConsumed']==1 and row['status']=='active'
|
|
assert db.query(m.PlCertQtyLog).one().event_type=='opening'
|
|
for data in ({'quantityUnit':'千克'},{'certNo':'new'},{'totalQty':'90'}):
|
|
with pytest.raises(HTTPException): s.save_cert(db,u,'purchase',data,row['id'])
|
|
with pytest.raises(HTTPException): s.delete_cert(db,u,'purchase',row['id'])
|
|
|
|
|
|
def test_tenant_and_scope(ctx,monkeypatch):
|
|
db,u,s,m=ctx
|
|
row=s.save_cert(db,u,'purchase',purchase())
|
|
other=SimpleNamespace(**{**u.__dict__,'tenant_no':'other'})
|
|
with pytest.raises(HTTPException): s.get_cert(db,other,'purchase',row['id'])
|
|
def deny(*args): raise HTTPException(403,'数据范围')
|
|
monkeypatch.setattr(s,'assert_pl_row_in_scope',deny)
|
|
with pytest.raises(HTTPException): s.save_cert(db,u,'purchase',{'remark':'bad'},row['id'])
|
|
|
|
|
|
def opening(**kw):
|
|
data=dict(usedQty='0',singleUseConsumed=False,reason='期初核对',evidenceUrls='https://minio/evidence',operationId='opening')
|
|
data.update(kw)
|
|
return data
|
|
|
|
|
|
def test_used_transport_vehicle_identity_cannot_change(ctx):
|
|
db,u,s,m=ctx
|
|
row=s.save_cert(db,u,'transport',transport())
|
|
s.confirm_cert(db,u,'transport',row['id'],opening())
|
|
with pytest.raises(HTTPException):
|
|
s.save_cert(db,u,'transport',{'vehicleNumbersJson':['皖B1']},row['id'])
|
|
|
|
|
|
def test_opening_retry_preserves_balance_and_operation_identity(ctx):
|
|
db,u,s,m=ctx
|
|
row=s.save_cert(db,u,'purchase',purchase())
|
|
s.confirm_cert(db,u,'purchase',row['id'],opening())
|
|
db.query(m.PlPurchaseCert).one().used_qty=Decimal('5')
|
|
retry=s.confirm_cert(db,u,'purchase',row['id'],opening())
|
|
assert retry['usedQty']==Decimal('5')
|
|
assert db.query(m.PlCertQtyLog).count()==1
|
|
assert db.query(m.PlCertOperation).count()==1
|
|
with pytest.raises(HTTPException): s.confirm_cert(db,u,'purchase',row['id'],opening(usedQty='7'))
|
|
with pytest.raises(HTTPException): s.confirm_cert(db,u,'purchase',row['id'],opening(operationId='second'))
|
|
|
|
|
|
def test_refs_duplicate_cross_owner_and_used_removal(ctx):
|
|
db,u,s,m=ctx
|
|
first=s.save_cert(db,u,'transport',transport(purchaseRefs=[{'referenceType':'unknown','referenceNo':'A'}]))
|
|
data=transport(); data['certNo']='Y2'
|
|
second=s.save_cert(db,u,'transport',data)
|
|
ref=first['purchaseRefs'][0]
|
|
child={k:ref[k] for k in ('id','referenceType','referenceNo')}
|
|
for cert_id,refs in ((first['id'],[child,child]),(second['id'],[child])):
|
|
with pytest.raises(HTTPException): s.save_cert(db,u,'transport',{'purchaseRefs':refs},cert_id)
|
|
s.confirm_cert(db,u,'transport',first['id'],opening())
|
|
with pytest.raises(HTTPException): s.save_cert(db,u,'transport',{'purchaseRefs':[]},first['id'])
|
|
assert len(s.get_cert(db,u,'transport',first['id'])['purchaseRefs'])==1
|
|
|
|
|
|
def test_readonly_cache_and_tenant_cannot_be_written(ctx):
|
|
db,u,s,m=ctx
|
|
for field,value in [('tenantNo','other'),('usedQty','1'),('reservedQty','1'),('openingSingleUseConsumed',True)]:
|
|
with pytest.raises(ValidationError): s.save_cert(db,u,'purchase',{**purchase(),field:value})
|
|
|
|
|
|
def test_transport_all_fields_and_scope_filtered_list(ctx):
|
|
db,u,s,m=ctx
|
|
data=transport(shipperAddress='地址',shipperPhone='电话',shipperLegalRepresentative='甲',packagingText='槽罐',carrierName='运方',carrierAddressPhone='住址/电话',transportMode='汽车',routeText='路线',consigneeName='收方',applicantName='申请方')
|
|
row=s.save_cert(db,u,'transport',data)
|
|
for key,value in data.items():
|
|
assert row[key] == (Decimal(value) if key=='totalQty' else value)
|
|
assert s.list_certs(db,u,'transport')[0]['id']==row['id']
|
|
|
|
|
|
def test_reverify_after_correction_keeps_opening_and_balances(ctx):
|
|
db,u,s,m=ctx
|
|
row=s.save_cert(db,u,'purchase',purchase())
|
|
s.confirm_cert(db,u,'purchase',row['id'],opening(usedQty='5'))
|
|
corrected=s.save_cert(db,u,'purchase',{'buyerAddress':'更正地址'},row['id'])
|
|
assert corrected['status']=='suspended' and corrected['verifiedBy'] is None
|
|
result=s.verify_cert(db,u,'purchase',row['id'],{'operationId':'verify-1','reason':'地址更正核对','evidenceUrls':'https://minio/evidence'})
|
|
assert result['status']=='active' and result['usedQty']==Decimal('5')
|
|
assert db.query(m.PlCertQtyLog).count()==1
|
|
assert db.query(m.PlCertOperation).count()==2
|
|
result=s.verify_cert(db,u,'purchase',row['id'],{'operationId':'verify-1','reason':'地址更正核对','evidenceUrls':'https://minio/evidence'})
|
|
assert db.query(m.PlCertOperation).count()==2
|
|
|
|
|
|
def test_actual_scope_denies_user_without_roles(ctx):
|
|
from app.models.models import SysRole, SysUserRole
|
|
db,u,s,m=ctx
|
|
for model in (SysRole, SysUserRole): model.__table__.create(db.get_bind())
|
|
row=s.save_cert(db,u,'purchase',purchase())
|
|
restricted=SimpleNamespace(id='restricted',dept_id='d',tenant_no='t',to_user_info=lambda:{'role':'admin','user_id':'restricted','tenant_no':'t'})
|
|
assert s.list_certs(db,restricted,'purchase') == []
|
|
with pytest.raises(HTTPException) as error: s.get_cert(db,restricted,'purchase',row['id'])
|
|
assert error.value.status_code==403
|
|
|
|
|
|
def test_sql_and_models_have_same_columns(ctx):
|
|
import re
|
|
from pathlib import Path
|
|
db,u,s,m=ctx
|
|
sql=(Path(__file__).resolve().parents[2]/'docs/sql/2026-11-01-pl-contract-cert-quota.sql').read_text()
|
|
tables=re.findall(r'CREATE TABLE `(\w+)` \((.*?)\) ENGINE=',sql,re.S)
|
|
assert len(tables)==9
|
|
for table,body in tables:
|
|
names=re.findall(r'^ `(\w+)` ',body,re.M)
|
|
assert list(m.Base.metadata.tables[table].columns.keys())==names
|
|
|
|
|
|
def test_no_change_save_preserves_verification(ctx):
|
|
db,u,s,m=ctx
|
|
row=s.save_cert(db,u,'purchase',purchase())
|
|
row=s.confirm_cert(db,u,'purchase',row['id'],opening())
|
|
saved=s.save_cert(db,u,'purchase',purchase(),row['id'])
|
|
assert saved['status']=='active'
|
|
assert saved['revision']==row['revision']
|
|
assert saved['verifiedTime']==row['verifiedTime']
|
|
|
|
|
|
def test_zero_opening_can_activate_without_image(ctx):
|
|
db,u,s,m=ctx
|
|
row=s.save_cert(db,u,'purchase',purchase())
|
|
result=s.confirm_cert(db,u,'purchase',row['id'],{'operationId':'no-image','usedQty':'0','singleUseConsumed':False,'reason':'核实全新证,尚未使用'})
|
|
assert result['status']=='active' and result['attachmentUrls'] is None
|
|
assert result['verifiedBy']=='u' and result['verifiedTime'] is not None
|
|
|
|
|
|
def test_historical_opening_accepts_written_source_without_image(ctx):
|
|
db,u,s,m=ctx
|
|
row=s.save_cert(db,u,'purchase',purchase())
|
|
result=s.confirm_cert(db,u,'purchase',row['id'],{'operationId':'written-source','usedQty':'5','singleUseConsumed':True,'reason':'历史核对','sourceNote':'2026年10月纸质台账第3页,经张三核实'})
|
|
assert result['usedQty']==Decimal('5') and result['status']=='active'
|
|
assert db.query(m.PlCertQtyLog).one().event_snapshot['sourceNote'].startswith('2026')
|
|
|
|
|
|
def test_unknown_use_text_is_preserved_and_requires_confirmation(ctx):
|
|
db,u,s,m=ctx
|
|
data=purchase(); data['validUseText']=' 按核发机关规定使用 '
|
|
row=s.save_cert(db,u,'purchase',data)
|
|
assert row['validUseText']==data['validUseText']
|
|
assert row['usagePolicy']=='needs_confirmation'
|
|
with pytest.raises(HTTPException) as error:
|
|
s.confirm_cert(db,u,'purchase',row['id'],opening())
|
|
assert '次数' in error.value.detail
|
|
assert s.get_cert(db,u,'purchase',row['id'])['status']=='draft'
|
|
assert db.query(m.PlCertQtyLog).count()==0
|
|
|
|
|
|
def test_known_use_policy_is_derived_and_readonly(ctx):
|
|
db,u,s,m=ctx
|
|
for number,text,policy in [('G1','一次有效','once'),('G2','多次有效','multiple')]:
|
|
data=purchase(); data.update(certNo=number,validUseText=text)
|
|
assert s.save_cert(db,u,'purchase',data)['usagePolicy']==policy
|
|
with pytest.raises(ValidationError):
|
|
s.save_cert(db,u,'purchase',{**purchase(),'usagePolicy':'once'})
|
|
|
|
|
|
def test_verify_refuses_unknown_use_policy(ctx):
|
|
db,u,s,m=ctx
|
|
row=s.save_cert(db,u,'purchase',purchase())
|
|
s.confirm_cert(db,u,'purchase',row['id'],opening())
|
|
# 模拟迁移导入的原文,不能被复核入口猜测放行。
|
|
stored=db.query(m.PlPurchaseCert).one()
|
|
stored.valid_use_text='另有规定'
|
|
stored.status='suspended'
|
|
with pytest.raises(HTTPException) as error:
|
|
s.verify_cert(db,u,'purchase',row['id'],{'operationId':'verify-unknown','reason':'复核'})
|
|
assert '次数' in error.value.detail
|
|
assert stored.status=='suspended'
|
|
|
|
|
|
def test_reference_partial_update_validates_preserved_purchase_identity(ctx):
|
|
db,u,s,m=ctx
|
|
purchase_row=s.save_cert(db,u,'purchase',purchase())
|
|
row=s.save_cert(db,u,'transport',transport(purchaseRefs=[{'referenceType':'purchase_permit','referenceNo':'G1','purchaseCertId':purchase_row['id']}]))
|
|
reference=row['purchaseRefs'][0]
|
|
with pytest.raises(HTTPException):
|
|
s.save_cert(db,u,'transport',{'remark':'不应保存','purchaseRefs':[{'id':reference['id'],'referenceType':'purchase_permit','referenceNo':'G2'}]},row['id'])
|
|
unchanged=s.get_cert(db,u,'transport',row['id'])
|
|
assert unchanged['remark'] is None
|
|
assert unchanged['purchaseRefs'][0]['referenceNo']=='G1'
|
|
assert unchanged['purchaseRefs'][0]['purchaseCertId']==purchase_row['id']
|
|
|
|
|
|
def test_purchase_number_change_cannot_break_active_transport_reference(ctx):
|
|
db,u,s,m=ctx
|
|
purchase_row=s.save_cert(db,u,'purchase',purchase())
|
|
transport_row=s.save_cert(db,u,'transport',transport(purchaseRefs=[{'referenceType':'purchase_permit','referenceNo':'G1','purchaseCertId':purchase_row['id']}]))
|
|
with pytest.raises(HTTPException):
|
|
s.save_cert(db,u,'purchase',{'certNo':'G3','remark':'不应写入'},purchase_row['id'])
|
|
unchanged=s.get_cert(db,u,'purchase',purchase_row['id'])
|
|
assert unchanged['certNo']=='G1' and unchanged['remark'] is None
|
|
assert s.get_cert(db,u,'transport',transport_row['id'])['purchaseRefs'][0]['referenceNo']=='G1'
|
|
|
|
|
|
def test_draft_reference_can_restore_same_business_key_without_deleted_id(ctx):
|
|
db,u,s,m=ctx
|
|
row=s.save_cert(db,u,'transport',transport(purchaseRefs=[{'referenceType':'unknown','referenceNo':'A','remark':'原备注'}]))
|
|
ref=db.query(m.PlTransportCertPurchaseRef).one()
|
|
original=(ref.id,ref.create_by,ref.create_time)
|
|
s.save_cert(db,u,'transport',{'purchaseRefs':[]},row['id'])
|
|
assert ref.del_flag==1
|
|
with pytest.raises(HTTPException):
|
|
s.save_cert(db,u,'transport',{'purchaseRefs':[{'id':ref.id,'referenceType':'unknown','referenceNo':'A'}]},row['id'])
|
|
restored=s.save_cert(db,u,'transport',{'purchaseRefs':[{'referenceType':'unknown','referenceNo':'A'}]},row['id'])
|
|
assert restored['purchaseRefs'][0]['id']==original[0]
|
|
assert (ref.id,ref.create_by,ref.create_time)==original
|
|
assert ref.remark=='原备注' and ref.del_flag==0
|
|
assert db.query(m.PlTransportCertPurchaseRef).count()==1
|
|
|
|
|
|
def test_historical_transport_cannot_restore_deleted_reference(ctx):
|
|
db,u,s,m=ctx
|
|
row=s.save_cert(db,u,'transport',transport(purchaseRefs=[{'referenceType':'unknown','referenceNo':'A'}]))
|
|
s.save_cert(db,u,'transport',{'purchaseRefs':[]},row['id'])
|
|
s.confirm_cert(db,u,'transport',row['id'],opening())
|
|
with pytest.raises(HTTPException):
|
|
s.save_cert(db,u,'transport',{'purchaseRefs':[{'referenceType':'unknown','referenceNo':'A'}]},row['id'])
|
|
assert db.query(m.PlTransportCertPurchaseRef).one().del_flag==1
|
|
|
|
|
|
def test_transport_edit_locks_referenced_purchases_before_transport(ctx, monkeypatch):
|
|
db,u,s,m=ctx
|
|
p=s.save_cert(db,u,'purchase',purchase())
|
|
t=s.save_cert(db,u,'transport',transport(purchaseRefs=[{'referenceType':'purchase_permit','referenceNo':'G1','purchaseCertId':p['id']}]))
|
|
acquired=[]
|
|
original=s._get_row
|
|
def traced(db,user,kind,cert_id,lock=False):
|
|
if lock: acquired.append((kind,cert_id))
|
|
return original(db,user,kind,cert_id,lock)
|
|
monkeypatch.setattr(s,'_get_row',traced)
|
|
s.save_cert(db,u,'transport',{'purchaseRefs':[{k:v for k,v in t['purchaseRefs'][0].items() if k in ('id','referenceType','referenceNo','purchaseCertId')}]},t['id'])
|
|
assert acquired[:2]==[('purchase',p['id']),('transport',t['id'])]
|
|
|
|
|
|
def test_deleted_old_purchase_does_not_block_unrelated_reference_save(ctx):
|
|
db,u,s,m=ctx
|
|
p=s.save_cert(db,u,'purchase',purchase())
|
|
t=s.save_cert(db,u,'transport',transport(purchaseRefs=[{'referenceType':'purchase_permit','referenceNo':'G1','purchaseCertId':p['id']}]))
|
|
s.save_cert(db,u,'transport',{'purchaseRefs':[]},t['id'])
|
|
s.delete_cert(db,u,'purchase',p['id'])
|
|
result=s.save_cert(db,u,'transport',{'purchaseRefs':[]},t['id'])
|
|
assert result['purchaseRefs']==[]
|
|
|
|
|
|
@pytest.mark.parametrize('action',['opening','verify'])
|
|
def test_opening_verify_stale_operation_snapshot_uses_unique_insert_arbitration(ctx,monkeypatch,action):
|
|
from sqlalchemy.orm import Query
|
|
db,u,s,m=ctx
|
|
p=s.save_cert(db,u,'purchase',purchase())
|
|
initial=opening()
|
|
s.confirm_cert(db,u,'purchase',p['id'],initial)
|
|
if action=='verify':
|
|
body={'operationId':'verify-retry','reason':'重新核对'}
|
|
fn=s.verify_cert
|
|
fn(db,u,'purchase',p['id'],body)
|
|
else:
|
|
body=initial;fn=s.confirm_cert
|
|
count=db.query(m.PlCertOperation).count()
|
|
original=Query.first
|
|
missed=[]
|
|
def stale_once(query):
|
|
if query.column_descriptions[0].get('entity') is m.PlCertOperation and not missed:
|
|
missed.append(True)
|
|
return None
|
|
return original(query)
|
|
monkeypatch.setattr(Query,'first',stale_once)
|
|
result=fn(db,u,'purchase',p['id'],body)
|
|
assert missed and result['id']==p['id']
|
|
assert db.query(m.PlCertOperation).count()==count
|
|
assert db.query(m.PlCertQtyLog).count()==1
|
|
|
|
|
|
def test_rejected_opening_rolls_back_claimed_operation(ctx):
|
|
db,u,s,m=ctx
|
|
p=s.save_cert(db,u,'purchase',purchase())
|
|
with pytest.raises(HTTPException):
|
|
s.confirm_cert(db,u,'purchase',p['id'],{**opening(),'reason':' '})
|
|
assert db.query(m.PlCertOperation).count()==0
|
|
assert db.query(m.PlCertQtyLog).count()==0
|
|
|
|
|
|
def test_replay_reads_current_certificate_and_opening_not_old_snapshot(ctx,monkeypatch):
|
|
from sqlalchemy.orm import Query
|
|
db,u,s,m=ctx
|
|
p=s.save_cert(db,u,'purchase',purchase())
|
|
row=db.get(m.PlPurchaseCert,p['id'])
|
|
stale=m.PlPurchaseCert(**{c.name:getattr(row,c.name) for c in row.__table__.columns})
|
|
s.confirm_cert(db,u,'purchase',p['id'],opening())
|
|
original=Query.first
|
|
def stale_reads(query):
|
|
entity=query.column_descriptions[0].get('entity')
|
|
if query._for_update_arg is None:
|
|
if entity is m.PlPurchaseCert: return stale
|
|
if entity is m.PlCertQtyLog: return None
|
|
return original(query)
|
|
monkeypatch.setattr(Query,'first',stale_reads)
|
|
replay=s.confirm_cert(db,u,'purchase',p['id'],opening())
|
|
assert replay['status']=='active' and replay['openingConfirmed'] is True
|