安全:补齐业务批次归属校验
This commit is contained in:
@@ -54,6 +54,7 @@ from module_business.entity.vo.operation_vo import (
|
||||
from module_business.service import inbound_service as inbound_service_module
|
||||
from module_business.service.access_service import AccessService
|
||||
from module_business.service.inbound_service import InboundService
|
||||
from tests.module_business.test_batch_fixtures import make_test_scope
|
||||
|
||||
|
||||
def integrity_error() -> IntegrityError:
|
||||
@@ -75,6 +76,7 @@ def make_pallet(pallet_id: int, *, asset_status: str = '0') -> BizPallet:
|
||||
qr_token=f'qr-{pallet_id}',
|
||||
capacity_packages=20,
|
||||
asset_status=asset_status,
|
||||
test_batch_id=None,
|
||||
)
|
||||
|
||||
|
||||
@@ -88,6 +90,7 @@ def make_inventory(
|
||||
active_filling_id: int | None = None,
|
||||
remaining_packages: int = 0,
|
||||
version: int = 3,
|
||||
test_batch_id: int | None = None,
|
||||
) -> BizPalletInventory:
|
||||
return BizPalletInventory(
|
||||
pallet_id=pallet_id,
|
||||
@@ -98,6 +101,7 @@ def make_inventory(
|
||||
active_filling_id=active_filling_id,
|
||||
remaining_packages=remaining_packages,
|
||||
version=version,
|
||||
test_batch_id=test_batch_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -109,6 +113,7 @@ def make_inbound(
|
||||
status: str = 'DRAFT',
|
||||
idempotency_key: str | None = None,
|
||||
request_hash: str | None = None,
|
||||
test_batch_id: int | None = None,
|
||||
) -> BizInboundOrder:
|
||||
inbound = BizInboundOrder(
|
||||
inbound_id=inbound_id,
|
||||
@@ -117,12 +122,14 @@ def make_inbound(
|
||||
status=status,
|
||||
confirm_idempotency_key=idempotency_key,
|
||||
confirm_request_hash=request_hash,
|
||||
test_batch_id=test_batch_id,
|
||||
)
|
||||
inbound.items = [
|
||||
BizInboundItem(
|
||||
inbound_item_id=index,
|
||||
inbound_id=inbound_id,
|
||||
pallet_id=pallet_id,
|
||||
test_batch_id=test_batch_id,
|
||||
)
|
||||
for index, pallet_id in enumerate(pallet_ids, start=1)
|
||||
]
|
||||
@@ -228,7 +235,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||
),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock(side_effect=access)),
|
||||
patch.object(
|
||||
@@ -275,6 +282,32 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertTrue(result.is_success)
|
||||
self.db.rollback.assert_not_awaited()
|
||||
|
||||
async def test_add_rejects_cross_batch_locked_inventory_and_rolls_back(self) -> None:
|
||||
inventory = make_inventory(2)
|
||||
inventory.test_batch_id = None
|
||||
pallet = make_pallet(2)
|
||||
pallet.test_batch_id = 4
|
||||
with (
|
||||
patch.object(inbound_service_module.TestBatchAccessService, 'lock_active_batch', new=AsyncMock()),
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(status='0', test_batch_id=4)),
|
||||
),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||
patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock(return_value=[inventory])),
|
||||
patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(return_value=[pallet])),
|
||||
self.assertRaisesRegex(ServiceException, '不属于当前数据模式或测试批次'),
|
||||
):
|
||||
await InboundService.add_inbound_services(
|
||||
self.db,
|
||||
InboundCreateModel(warehouseId=7, palletIds=[2]),
|
||||
self.user,
|
||||
make_test_scope(4),
|
||||
)
|
||||
self.db.rollback.assert_awaited_once()
|
||||
self.db.commit.assert_not_awaited()
|
||||
|
||||
async def test_add_rejects_invalid_pallet_before_any_order_or_event_write(self) -> None:
|
||||
page = InboundCreateModel(warehouseId=7, palletIds=[2, 5])
|
||||
invalid_inventory = make_inventory(5, warehouse_id=7)
|
||||
@@ -285,7 +318,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||
),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||
patch.object(
|
||||
@@ -316,7 +349,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
||||
lock = AsyncMock()
|
||||
cases = (
|
||||
(None, ServiceException),
|
||||
(SimpleNamespace(warehouse_id=7, status='1'), ServiceException),
|
||||
(SimpleNamespace(warehouse_id=7, status='1', test_batch_id=None), ServiceException),
|
||||
)
|
||||
for warehouse, exception_type in cases:
|
||||
with self.subTest(warehouse=warehouse):
|
||||
@@ -338,7 +371,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||
),
|
||||
patch.object(
|
||||
AccessService,
|
||||
@@ -386,7 +419,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
||||
replace_items = AsyncMock()
|
||||
warehouse_mapping = AsyncMock(side_effect=warehouse_access)
|
||||
warehouse_read = AsyncMock(
|
||||
return_value=SimpleNamespace(warehouse_id=7, status='0'),
|
||||
return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None),
|
||||
)
|
||||
with (
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
|
||||
@@ -425,7 +458,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=8, status='0')),
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=8, status='0', test_batch_id=None)),
|
||||
),
|
||||
patch.object(
|
||||
InventoryDao,
|
||||
@@ -497,6 +530,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
||||
side_effect=lambda _db, warehouse_id, _scope, **_kwargs: SimpleNamespace(
|
||||
warehouse_id=warehouse_id,
|
||||
status='0',
|
||||
test_batch_id=None,
|
||||
)
|
||||
),
|
||||
),
|
||||
@@ -655,7 +689,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||
),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||
patch.object(
|
||||
@@ -680,7 +714,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||
),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||
patch.object(OperationDao, 'delete_inbound', new=AsyncMock(side_effect=integrity_error())),
|
||||
@@ -830,9 +864,9 @@ class TestInboundReadService(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
async def test_warehouse_options_filter_disabled_and_unauthorized_records(self) -> None:
|
||||
warehouses = [
|
||||
SimpleNamespace(warehouse_id=2, warehouse_code='W2', warehouse_name='二仓', status='0'),
|
||||
SimpleNamespace(warehouse_id=3, warehouse_code='W3', warehouse_name='三仓', status='1'),
|
||||
SimpleNamespace(warehouse_id=5, warehouse_code='W5', warehouse_name='五仓', status='0'),
|
||||
SimpleNamespace(warehouse_id=2, warehouse_code='W2', warehouse_name='二仓', status='0', test_batch_id=None),
|
||||
SimpleNamespace(warehouse_id=3, warehouse_code='W3', warehouse_name='三仓', status='1', test_batch_id=None),
|
||||
SimpleNamespace(warehouse_id=5, warehouse_code='W5', warehouse_name='五仓', status='0', test_batch_id=None),
|
||||
]
|
||||
with (
|
||||
patch.object(MasterDao, 'list_warehouse', new=AsyncMock(return_value=warehouses)) as list_warehouse,
|
||||
@@ -849,8 +883,8 @@ class TestInboundReadService(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
async def test_admin_warehouse_options_include_all_active_records(self) -> None:
|
||||
warehouses = [
|
||||
SimpleNamespace(warehouse_id=2, warehouse_code='W2', warehouse_name='二仓', status='0'),
|
||||
SimpleNamespace(warehouse_id=5, warehouse_code='W5', warehouse_name='五仓', status='0'),
|
||||
SimpleNamespace(warehouse_id=2, warehouse_code='W2', warehouse_name='二仓', status='0', test_batch_id=None),
|
||||
SimpleNamespace(warehouse_id=5, warehouse_code='W5', warehouse_name='五仓', status='0', test_batch_id=None),
|
||||
]
|
||||
scope = AsyncMock()
|
||||
with (
|
||||
@@ -894,7 +928,7 @@ class TestInboundScanService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||
),
|
||||
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=pallet)) as get_pallet,
|
||||
patch.object(MasterDao, 'get_pallet_inventory', new=AsyncMock(return_value=inventory)) as get_inventory,
|
||||
@@ -938,7 +972,7 @@ class TestInboundScanService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||
),
|
||||
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=pallet)),
|
||||
patch.object(MasterDao, 'get_pallet_inventory', new=AsyncMock(return_value=inventory)),
|
||||
@@ -969,7 +1003,7 @@ class TestInboundScanService(unittest.IsolatedAsyncioTestCase):
|
||||
)
|
||||
get_pallet.assert_not_awaited()
|
||||
|
||||
for warehouse in (None, SimpleNamespace(warehouse_id=7, status='1')):
|
||||
for warehouse in (None, SimpleNamespace(warehouse_id=7, status='1', test_batch_id=None)):
|
||||
with self.subTest(warehouse=warehouse):
|
||||
get_pallet.reset_mock()
|
||||
with (
|
||||
@@ -1079,7 +1113,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
async def lock_warehouse(*_args: Any, **_kwargs: Any) -> SimpleNamespace:
|
||||
actions.append('warehouse-lock')
|
||||
return SimpleNamespace(warehouse_id=7, status='0')
|
||||
return SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)
|
||||
|
||||
def transition(current: PalletFlowState, target: PalletFlowState) -> None:
|
||||
actions.append(f'transition-{current.value}-{target.value}')
|
||||
@@ -1282,7 +1316,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||
) as warehouse_lock,
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
|
||||
patch.object(OperationDao, 'update_inbound', new=update),
|
||||
@@ -1343,7 +1377,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||
),
|
||||
patch.object(PalletStateMachine, 'ensure_transition', side_effect=ValueError('invalid')),
|
||||
patch.object(OperationDao, 'update_inbound', new=update),
|
||||
@@ -1378,7 +1412,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||
) as warehouse_lock,
|
||||
):
|
||||
result = await InboundService.confirm_inbound_services(
|
||||
@@ -1439,7 +1473,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||
),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||
):
|
||||
@@ -1486,7 +1520,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='1')),
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='1', test_batch_id=None)),
|
||||
) as warehouse_lock,
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
|
||||
self.assertRaises(PermissionException) as raised,
|
||||
@@ -1564,7 +1598,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||
),
|
||||
patch.object(OperationDao, 'update_inbound', new=AsyncMock()),
|
||||
patch.object(InventoryDao, 'add_event', new=AsyncMock()),
|
||||
@@ -1600,7 +1634,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||
),
|
||||
patch.object(OperationDao, 'update_inbound', new=AsyncMock()),
|
||||
patch.object(InventoryDao, 'add_event', new=AsyncMock()),
|
||||
@@ -1674,7 +1708,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||
),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||
):
|
||||
@@ -1737,7 +1771,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||
) as warehouse_lock,
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
|
||||
patch.object(OperationDao, 'update_inbound', new=AsyncMock()),
|
||||
@@ -1777,7 +1811,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||
),
|
||||
patch.object(OperationDao, 'update_inbound', new=AsyncMock()),
|
||||
patch.object(InventoryDao, 'add_event', new=AsyncMock()),
|
||||
|
||||
Reference in New Issue
Block a user