修复:限定幂等键精确业务域
This commit is contained in:
@@ -9,7 +9,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.routing import APIRoute
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.exc import IntegrityError, MultipleResultsFound
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from common.aspect.interface_auth import CheckUserInterfaceAuth
|
||||
@@ -1407,6 +1407,65 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
self.db.rollback.assert_awaited_once()
|
||||
self.assertTrue(result.is_success)
|
||||
|
||||
async def test_same_key_retry_uses_exact_warehouse_when_user_can_access_duplicate_keys(self) -> None:
|
||||
target = make_inbound(
|
||||
status='CONFIRMED',
|
||||
idempotency_key='confirm-key',
|
||||
request_hash=self.request_hash,
|
||||
)
|
||||
other_domain = make_inbound(
|
||||
inbound_id=12,
|
||||
warehouse_id=8,
|
||||
status='CONFIRMED',
|
||||
idempotency_key='confirm-key',
|
||||
request_hash=InboundService._build_confirm_request_hash(12),
|
||||
)
|
||||
|
||||
async def read_key(
|
||||
_db: AsyncSession,
|
||||
_key: str,
|
||||
*,
|
||||
warehouse_ids: list[int] | None = None,
|
||||
) -> BizInboundOrder | None:
|
||||
matches = [
|
||||
inbound
|
||||
for inbound in (target, other_domain)
|
||||
if warehouse_ids is None or inbound.warehouse_id in warehouse_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(OperationDao, 'get_inbound_by_id', new=get_detail),
|
||||
patch.object(OperationDao, 'get_inbound_by_confirm_idempotency_key', new=key_read),
|
||||
patch.object(OperationDao, 'get_inbound_for_update', new=AsyncMock(return_value=target)),
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
||||
),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||
):
|
||||
result = await InboundService.confirm_inbound_services(
|
||||
self.db,
|
||||
11,
|
||||
self.command,
|
||||
self.user,
|
||||
)
|
||||
|
||||
self.assertTrue(result.is_success)
|
||||
get_detail.assert_awaited_once_with(self.db, 11, warehouse_ids=[7, 8])
|
||||
key_read.assert_awaited_once_with(
|
||||
self.db,
|
||||
'confirm-key',
|
||||
warehouse_ids=[7],
|
||||
)
|
||||
self.db.rollback.assert_awaited_once()
|
||||
|
||||
async def test_same_key_retry_hides_document_when_current_mapping_is_revoked(self) -> None:
|
||||
confirmed = make_inbound(
|
||||
status='CONFIRMED',
|
||||
@@ -1569,6 +1628,76 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(2, self.db.rollback.await_count)
|
||||
self.assertTrue(result.is_success)
|
||||
|
||||
async def test_integrity_recovery_uses_exact_warehouse_with_duplicate_visible_keys(self) -> None:
|
||||
initial = make_inbound(status='DRAFT')
|
||||
recovered = make_inbound(
|
||||
status='CONFIRMED',
|
||||
idempotency_key='confirm-key',
|
||||
request_hash=self.request_hash,
|
||||
)
|
||||
other_domain = make_inbound(
|
||||
inbound_id=12,
|
||||
warehouse_id=8,
|
||||
status='CONFIRMED',
|
||||
idempotency_key='confirm-key',
|
||||
request_hash=InboundService._build_confirm_request_hash(12),
|
||||
)
|
||||
read_count = 0
|
||||
|
||||
async def read_key(
|
||||
_db: AsyncSession,
|
||||
_key: str,
|
||||
*,
|
||||
warehouse_ids: list[int] | None = None,
|
||||
) -> BizInboundOrder | None:
|
||||
nonlocal read_count
|
||||
read_count += 1
|
||||
if read_count == 1:
|
||||
return None
|
||||
matches = [
|
||||
inbound
|
||||
for inbound in (recovered, other_domain)
|
||||
if warehouse_ids is None or inbound.warehouse_id in warehouse_ids
|
||||
]
|
||||
if len(matches) > 1:
|
||||
raise MultipleResultsFound('广域幂等键恢复查询返回多条入库单')
|
||||
return matches[0] if matches else None
|
||||
|
||||
warehouse_scope = AsyncMock(side_effect=[[7, 8], [7, 8]])
|
||||
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(OperationDao, 'get_inbound_by_id', new=AsyncMock(return_value=initial)),
|
||||
patch.object(OperationDao, 'get_inbound_by_confirm_idempotency_key', new=key_read),
|
||||
patch.object(OperationDao, 'get_inbound_for_update', new=parent_lock),
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
||||
),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||
):
|
||||
result = await InboundService.confirm_inbound_services(
|
||||
self.db,
|
||||
11,
|
||||
self.command,
|
||||
self.user,
|
||||
)
|
||||
|
||||
self.assertTrue(result.is_success)
|
||||
self.assertEqual(2, warehouse_scope.await_count)
|
||||
self.assertEqual(2, parent_lock.await_count)
|
||||
self.assertEqual(
|
||||
[
|
||||
unittest.mock.call(self.db, 'confirm-key', warehouse_ids=[7]),
|
||||
unittest.mock.call(self.db, 'confirm-key', warehouse_ids=[7]),
|
||||
],
|
||||
key_read.await_args_list,
|
||||
)
|
||||
self.assertEqual(2, self.db.rollback.await_count)
|
||||
self.db.commit.assert_not_awaited()
|
||||
|
||||
async def test_integrity_recovery_locks_parent_and_rechecks_revoked_current_mapping(self) -> None:
|
||||
inbound = make_inbound(pallet_ids=(2,))
|
||||
recovered = make_inbound(
|
||||
|
||||
Reference in New Issue
Block a user