修复:限定幂等键精确业务域
This commit is contained in:
@@ -11,7 +11,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from fastapi import FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.exc import IntegrityError, MultipleResultsFound
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from common.aspect.interface_auth import CheckUserInterfaceAuth
|
||||
@@ -2181,6 +2181,96 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
|
||||
parent_lock.assert_not_awaited()
|
||||
self.db.rollback.assert_awaited_once()
|
||||
|
||||
async def test_same_key_retry_uses_exact_route_when_user_can_access_duplicate_keys(self) -> None:
|
||||
target = make_shipment(
|
||||
status='CONFIRMED',
|
||||
idempotency_key='confirm-key',
|
||||
request_hash=self.request_hash,
|
||||
)
|
||||
other_domain = make_shipment(
|
||||
13,
|
||||
source_warehouse_id=8,
|
||||
target_site_id=10,
|
||||
status='CONFIRMED',
|
||||
idempotency_key='confirm-key',
|
||||
request_hash=ShipmentService._build_confirm_request_hash(13),
|
||||
)
|
||||
|
||||
async def read_key(
|
||||
_db: AsyncSession,
|
||||
_key: str,
|
||||
*,
|
||||
warehouse_ids: list[int] | None = None,
|
||||
site_ids: list[int] | None = None,
|
||||
) -> BizShipmentOrder | None:
|
||||
matches = [
|
||||
shipment
|
||||
for shipment in (target, other_domain)
|
||||
if (warehouse_ids is None or shipment.source_warehouse_id in warehouse_ids)
|
||||
and (site_ids is None or shipment.target_site_id in site_ids)
|
||||
]
|
||||
if len(matches) > 1:
|
||||
raise MultipleResultsFound('广域幂等键查询返回多条发货单')
|
||||
return matches[0] if matches else None
|
||||
|
||||
get_detail = AsyncMock(return_value=target)
|
||||
key_read = AsyncMock(side_effect=read_key)
|
||||
with (
|
||||
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7, 8])),
|
||||
patch.object(AccessDao, 'list_user_site_ids', new=AsyncMock(return_value=[9, 10])),
|
||||
patch.object(OperationDao, 'get_shipment_by_id', new=get_detail),
|
||||
patch.object(OperationDao, 'get_shipment_by_confirm_idempotency_key', new=key_read),
|
||||
patch.object(OperationDao, 'get_shipment_for_update', new=AsyncMock(return_value=target)),
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(
|
||||
return_value=BizWarehouse(
|
||||
warehouse_id=7,
|
||||
warehouse_code='W-7',
|
||||
warehouse_name='七号仓',
|
||||
status='0',
|
||||
)
|
||||
),
|
||||
),
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_site',
|
||||
new=AsyncMock(
|
||||
return_value=BizCustomerSite(
|
||||
site_id=9,
|
||||
customer_id=1,
|
||||
site_code='S-9',
|
||||
site_name='九号现场',
|
||||
status='0',
|
||||
)
|
||||
),
|
||||
),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
|
||||
):
|
||||
result = await ShipmentService.confirm_shipment_services(
|
||||
self.db,
|
||||
12,
|
||||
self.command,
|
||||
self.user,
|
||||
)
|
||||
|
||||
self.assertTrue(result.is_success)
|
||||
get_detail.assert_awaited_once_with(
|
||||
self.db,
|
||||
12,
|
||||
warehouse_ids=[7, 8],
|
||||
site_ids=[9, 10],
|
||||
)
|
||||
key_read.assert_awaited_once_with(
|
||||
self.db,
|
||||
'confirm-key',
|
||||
warehouse_ids=[7],
|
||||
site_ids=[9],
|
||||
)
|
||||
self.db.rollback.assert_awaited_once()
|
||||
|
||||
async def test_integrity_recovery_nonexact_result_is_stable_conflict_not_success(self) -> None:
|
||||
initial = make_shipment()
|
||||
nonexact = make_shipment(
|
||||
@@ -2413,6 +2503,113 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(2, self.db.rollback.await_count)
|
||||
self.db.commit.assert_not_awaited()
|
||||
|
||||
async def test_integrity_recovery_uses_exact_route_with_duplicate_visible_keys(self) -> None:
|
||||
initial = make_shipment(status='DRAFT')
|
||||
recovered = make_shipment(
|
||||
status='CONFIRMED',
|
||||
idempotency_key='confirm-key',
|
||||
request_hash=self.request_hash,
|
||||
)
|
||||
other_domain = make_shipment(
|
||||
13,
|
||||
source_warehouse_id=8,
|
||||
target_site_id=10,
|
||||
status='CONFIRMED',
|
||||
idempotency_key='confirm-key',
|
||||
request_hash=ShipmentService._build_confirm_request_hash(13),
|
||||
)
|
||||
read_count = 0
|
||||
|
||||
async def read_key(
|
||||
_db: AsyncSession,
|
||||
_key: str,
|
||||
*,
|
||||
warehouse_ids: list[int] | None = None,
|
||||
site_ids: list[int] | None = None,
|
||||
) -> BizShipmentOrder | None:
|
||||
nonlocal read_count
|
||||
read_count += 1
|
||||
if read_count == 1:
|
||||
return None
|
||||
matches = [
|
||||
shipment
|
||||
for shipment in (recovered, other_domain)
|
||||
if (warehouse_ids is None or shipment.source_warehouse_id in warehouse_ids)
|
||||
and (site_ids is None or shipment.target_site_id in site_ids)
|
||||
]
|
||||
if len(matches) > 1:
|
||||
raise MultipleResultsFound('广域幂等键恢复查询返回多条发货单')
|
||||
return matches[0] if matches else None
|
||||
|
||||
warehouse_scope = AsyncMock(side_effect=[[7, 8], [7, 8]])
|
||||
site_scope = AsyncMock(side_effect=[[9, 10], [9, 10]])
|
||||
key_read = AsyncMock(side_effect=read_key)
|
||||
parent_lock = AsyncMock(side_effect=[integrity_error(), recovered])
|
||||
with (
|
||||
patch.object(AccessDao, 'list_user_warehouse_ids', new=warehouse_scope),
|
||||
patch.object(AccessDao, 'list_user_site_ids', new=site_scope),
|
||||
patch.object(OperationDao, 'get_shipment_by_id', new=AsyncMock(return_value=initial)),
|
||||
patch.object(OperationDao, 'get_shipment_by_confirm_idempotency_key', new=key_read),
|
||||
patch.object(OperationDao, 'get_shipment_for_update', new=parent_lock),
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(
|
||||
return_value=BizWarehouse(
|
||||
warehouse_id=7,
|
||||
warehouse_code='W-7',
|
||||
warehouse_name='七号仓',
|
||||
status='0',
|
||||
)
|
||||
),
|
||||
),
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_site',
|
||||
new=AsyncMock(
|
||||
return_value=BizCustomerSite(
|
||||
site_id=9,
|
||||
customer_id=1,
|
||||
site_code='S-9',
|
||||
site_name='九号现场',
|
||||
status='0',
|
||||
)
|
||||
),
|
||||
),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
|
||||
):
|
||||
result = await ShipmentService.confirm_shipment_services(
|
||||
self.db,
|
||||
12,
|
||||
self.command,
|
||||
self.user,
|
||||
)
|
||||
|
||||
self.assertTrue(result.is_success)
|
||||
self.assertEqual(2, warehouse_scope.await_count)
|
||||
self.assertEqual(2, site_scope.await_count)
|
||||
self.assertEqual(2, parent_lock.await_count)
|
||||
self.assertEqual(
|
||||
[
|
||||
unittest.mock.call(
|
||||
self.db,
|
||||
'confirm-key',
|
||||
warehouse_ids=[7],
|
||||
site_ids=[9],
|
||||
),
|
||||
unittest.mock.call(
|
||||
self.db,
|
||||
'confirm-key',
|
||||
warehouse_ids=[7],
|
||||
site_ids=[9],
|
||||
),
|
||||
],
|
||||
key_read.await_args_list,
|
||||
)
|
||||
self.assertEqual(2, self.db.rollback.await_count)
|
||||
self.db.commit.assert_not_awaited()
|
||||
|
||||
async def test_revoked_mapping_after_locks_hides_document_and_mutates_nothing(self) -> None:
|
||||
shipment = make_shipment(pallet_ids=(2,))
|
||||
inventory = make_inventory(2, version=4)
|
||||
|
||||
Reference in New Issue
Block a user