75 lines
3.7 KiB
Python
75 lines
3.7 KiB
Python
|
|
import asyncio
|
||
|
|
from contextlib import contextmanager
|
||
|
|
from unittest.mock import AsyncMock
|
||
|
|
import pytest
|
||
|
|
from fastapi import HTTPException
|
||
|
|
from app.workflows import export_guard as guard
|
||
|
|
|
||
|
|
def setup(monkeypatch,status='sending',result=None):
|
||
|
|
records=[]
|
||
|
|
monkeypatch.setattr(guard,'is_workflow_draft',lambda draft_id:True)
|
||
|
|
monkeypatch.setattr(guard,'claim',lambda draft_id,request:{'draft_id':draft_id,'reference':'stable','status':status,'result':result})
|
||
|
|
monkeypatch.setattr(guard,'record',lambda *args,**kwargs:records.append((args,kwargs)))
|
||
|
|
return records
|
||
|
|
|
||
|
|
def test_success_is_saved_before_caller_metadata(monkeypatch):
|
||
|
|
records=setup(monkeypatch)
|
||
|
|
exporter=AsyncMock(return_value={'success':True,'dry_run':False,'economic_draft_id':33})
|
||
|
|
result=asyncio.run(guard.guarded_export(1,exporter,{'lines':[{'line_key':'a','selected':True}]}))
|
||
|
|
assert records[0][0][1]=='done'
|
||
|
|
assert result['exported_line_keys']==['a']
|
||
|
|
assert exporter.call_args.kwargs['export_reference']=='stable'
|
||
|
|
|
||
|
|
def test_completed_export_replays_without_external_call(monkeypatch):
|
||
|
|
result={'economic_draft_id':33,'dry_run':False,'exported_line_keys':['old']}
|
||
|
|
records=setup(monkeypatch,'done',result)
|
||
|
|
exporter=AsyncMock()
|
||
|
|
replay=asyncio.run(guard.guarded_export(1,exporter,{'lines':[{'line_key':'new'}]}))
|
||
|
|
assert replay==result
|
||
|
|
assert replay is not result
|
||
|
|
exporter.assert_not_called()
|
||
|
|
assert records==[]
|
||
|
|
|
||
|
|
def test_timeout_is_uncertain_not_retryable(monkeypatch):
|
||
|
|
records=setup(monkeypatch)
|
||
|
|
exporter=AsyncMock(side_effect=asyncio.TimeoutError())
|
||
|
|
with pytest.raises(asyncio.TimeoutError): asyncio.run(guard.guarded_export(1,exporter,{}))
|
||
|
|
assert records[0][0][1]=='uncertain'
|
||
|
|
|
||
|
|
def test_definitive_preflight_failure_can_be_corrected(monkeypatch):
|
||
|
|
records=setup(monkeypatch)
|
||
|
|
exporter=AsyncMock(side_effect=HTTPException(422,'Invalid mapping before POST'))
|
||
|
|
with pytest.raises(HTTPException): asyncio.run(guard.guarded_export(1,exporter,{}))
|
||
|
|
assert records[0][0][1]=='pending'
|
||
|
|
|
||
|
|
def test_dry_run_never_completes_real_export(monkeypatch):
|
||
|
|
records=setup(monkeypatch)
|
||
|
|
exporter=AsyncMock(return_value={'success':True,'dry_run':True})
|
||
|
|
asyncio.run(guard.guarded_export(1,exporter,{}))
|
||
|
|
assert records[0][0][1]=='pending'
|
||
|
|
|
||
|
|
def test_legacy_draft_uses_existing_export_unchanged(monkeypatch):
|
||
|
|
monkeypatch.setattr(guard,'is_workflow_draft',lambda draft_id:False)
|
||
|
|
exporter=AsyncMock(return_value={'dry_run':True})
|
||
|
|
request={'customer_id':24,'export_reference':'original'}
|
||
|
|
asyncio.run(guard.guarded_export(1,exporter,request))
|
||
|
|
assert exporter.call_args.kwargs==request
|
||
|
|
|
||
|
|
@pytest.mark.parametrize('status',['sending','uncertain'])
|
||
|
|
def test_claim_blocks_concurrent_or_unknown_result_even_force(status,monkeypatch):
|
||
|
|
class Cursor:
|
||
|
|
def execute(self,*args): pass
|
||
|
|
def fetchone(self): return {'draft_id':1,'reference':'stable','status':status}
|
||
|
|
@contextmanager
|
||
|
|
def transaction(): yield Cursor()
|
||
|
|
monkeypatch.setattr(guard,'transaction',transaction)
|
||
|
|
with pytest.raises(HTTPException) as error: guard.claim(1,{'force_export':True})
|
||
|
|
assert error.value.status_code==409
|
||
|
|
|
||
|
|
def test_reconciliation_requires_exact_external_reference_and_line_values():
|
||
|
|
attempt={'draft_id':1,'request':{'currency':'DKK','lines':[{'description':'Support','quantity':1,'unit_price':500,'selected':True}]}}
|
||
|
|
remote={'references':{'other':'1'},'customer':{'customerNumber':24},'currency':'DKK','lines':[{'description':'Support','quantity':1,'unitNetPrice':500,'product':{'productNumber':'TIME'}}]}
|
||
|
|
guard.verify_remote(attempt,remote,24,['TIME'])
|
||
|
|
remote['references']['other']='different'
|
||
|
|
with pytest.raises(HTTPException): guard.verify_remote(attempt,remote,24,['TIME'])
|