安全:补齐业务批次归属校验
This commit is contained in:
@@ -55,6 +55,8 @@ from module_business.entity.vo.operation_vo import (
|
||||
from module_business.service import shipment_service as shipment_service_module
|
||||
from module_business.service.access_service import AccessService
|
||||
from module_business.service.shipment_service import ShipmentService
|
||||
from module_business.service.test_batch_access_service import TestBatchAccessService
|
||||
from tests.module_business.test_batch_fixtures import make_test_scope
|
||||
|
||||
|
||||
def make_user(user_id: int = 7) -> CurrentUserModel:
|
||||
@@ -69,13 +71,20 @@ def integrity_error() -> IntegrityError:
|
||||
return IntegrityError('statement', {}, RuntimeError('constraint'))
|
||||
|
||||
|
||||
def make_pallet(pallet_id: int, *, capacity: int = 20, asset_status: str = '0') -> BizPallet:
|
||||
def make_pallet(
|
||||
pallet_id: int,
|
||||
*,
|
||||
capacity: int = 20,
|
||||
asset_status: str = '0',
|
||||
test_batch_id: int | None = None,
|
||||
) -> BizPallet:
|
||||
return BizPallet(
|
||||
pallet_id=pallet_id,
|
||||
pallet_code=f'P-{pallet_id:03d}',
|
||||
qr_token=f'qr-{pallet_id}',
|
||||
capacity_packages=capacity,
|
||||
asset_status=asset_status,
|
||||
test_batch_id=test_batch_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -89,6 +98,7 @@ def make_inventory(
|
||||
remaining_packages: int = 8,
|
||||
flow_state: str = 'FILLED_IN_WAREHOUSE',
|
||||
version: int = 3,
|
||||
test_batch_id: int | None = None,
|
||||
) -> BizPalletInventory:
|
||||
return BizPalletInventory(
|
||||
pallet_id=pallet_id,
|
||||
@@ -99,10 +109,16 @@ def make_inventory(
|
||||
remaining_packages=remaining_packages,
|
||||
flow_state=flow_state,
|
||||
version=version,
|
||||
test_batch_id=test_batch_id,
|
||||
)
|
||||
|
||||
|
||||
def make_filling(pallet_id: int, *, warehouse_id: int = 7) -> BizFillingRecord:
|
||||
def make_filling(
|
||||
pallet_id: int,
|
||||
*,
|
||||
warehouse_id: int = 7,
|
||||
test_batch_id: int | None = None,
|
||||
) -> BizFillingRecord:
|
||||
return BizFillingRecord(
|
||||
filling_id=100 + pallet_id,
|
||||
filling_no=f'FI-{pallet_id}',
|
||||
@@ -119,6 +135,7 @@ def make_filling(pallet_id: int, *, warehouse_id: int = 7) -> BizFillingRecord:
|
||||
request_hash='f' * 64,
|
||||
operator_id=7,
|
||||
operated_at=SimpleNamespace(),
|
||||
test_batch_id=test_batch_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -131,6 +148,7 @@ def make_shipment(
|
||||
status: str = 'DRAFT',
|
||||
idempotency_key: str | None = None,
|
||||
request_hash: str | None = None,
|
||||
test_batch_id: int | None = None,
|
||||
) -> BizShipmentOrder:
|
||||
shipment = BizShipmentOrder(
|
||||
shipment_id=shipment_id,
|
||||
@@ -140,12 +158,14 @@ def make_shipment(
|
||||
status=status,
|
||||
confirm_idempotency_key=idempotency_key,
|
||||
confirm_request_hash=request_hash,
|
||||
test_batch_id=test_batch_id,
|
||||
)
|
||||
shipment.items = [
|
||||
BizShipmentItem(
|
||||
shipment_item_id=index,
|
||||
shipment_id=shipment_id,
|
||||
pallet_id=pallet_id,
|
||||
test_batch_id=test_batch_id,
|
||||
)
|
||||
for index, pallet_id in enumerate(pallet_ids, start=1)
|
||||
]
|
||||
@@ -756,6 +776,58 @@ class ShipmentReadServiceTest(unittest.IsolatedAsyncioTestCase):
|
||||
)
|
||||
self.assertEqual('无权访问该托盘', raised.exception.message)
|
||||
|
||||
async def test_test_scope_scan_rejects_cross_batch_pallet(self) -> None:
|
||||
scope = make_test_scope(4)
|
||||
query = ShipmentScanQueryModel(sourceWarehouseId=7, targetSiteId=9)
|
||||
with (
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(
|
||||
return_value=BizWarehouse(
|
||||
warehouse_id=7,
|
||||
warehouse_code='W-7',
|
||||
warehouse_name='七号仓',
|
||||
status='0',
|
||||
test_batch_id=4,
|
||||
)
|
||||
),
|
||||
),
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_site',
|
||||
new=AsyncMock(
|
||||
return_value=BizCustomerSite(
|
||||
site_id=9,
|
||||
customer_id=1,
|
||||
site_code='S-9',
|
||||
site_name='九号现场',
|
||||
status='0',
|
||||
test_batch_id=4,
|
||||
)
|
||||
),
|
||||
),
|
||||
patch.object(
|
||||
InventoryDao,
|
||||
'get_pallet_by_qr_token',
|
||||
new=AsyncMock(return_value=make_pallet(2, test_batch_id=5)),
|
||||
),
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_pallet_inventory',
|
||||
new=AsyncMock(return_value=make_inventory(2, test_batch_id=4)),
|
||||
),
|
||||
patch.object(
|
||||
OperationDao,
|
||||
'get_filling_by_id',
|
||||
new=AsyncMock(return_value=make_filling(2, test_batch_id=4)),
|
||||
),
|
||||
self.assertRaisesRegex(ServiceException, '不属于当前数据模式或测试批次'),
|
||||
):
|
||||
await ShipmentService.scan_shipment_pallet_services(self.db, 'qr-2', query, self.user, scope)
|
||||
|
||||
|
||||
class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
|
||||
def setUp(self) -> None:
|
||||
@@ -792,6 +864,58 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual([pallet], pallets)
|
||||
self.assertEqual([origin_filling], fillings)
|
||||
|
||||
async def test_test_scope_write_rejects_cross_batch_inventory_and_rolls_back(self) -> None:
|
||||
scope = make_test_scope(4)
|
||||
page = ShipmentCreateModel(sourceWarehouseId=7, targetSiteId=9, palletIds=[2])
|
||||
add_shipment = AsyncMock()
|
||||
with (
|
||||
patch.object(TestBatchAccessService, 'lock_active_batch', new=AsyncMock()),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_warehouse',
|
||||
new=AsyncMock(
|
||||
return_value=BizWarehouse(
|
||||
warehouse_id=7,
|
||||
warehouse_code='W-7',
|
||||
warehouse_name='七号仓',
|
||||
status='0',
|
||||
test_batch_id=4,
|
||||
)
|
||||
),
|
||||
),
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_site',
|
||||
new=AsyncMock(
|
||||
return_value=BizCustomerSite(
|
||||
site_id=9,
|
||||
customer_id=1,
|
||||
site_code='S-9',
|
||||
site_name='九号现场',
|
||||
status='0',
|
||||
test_batch_id=4,
|
||||
)
|
||||
),
|
||||
),
|
||||
patch.object(
|
||||
InventoryDao,
|
||||
'get_inventories_for_update',
|
||||
new=AsyncMock(return_value=[make_inventory(2, test_batch_id=5)]),
|
||||
),
|
||||
patch.object(
|
||||
InventoryDao, 'get_pallets_by_ids', new=AsyncMock(return_value=[make_pallet(2, test_batch_id=4)])
|
||||
),
|
||||
patch.object(OperationDao, 'add_shipment', new=add_shipment),
|
||||
self.assertRaisesRegex(ServiceException, '不属于当前数据模式或测试批次'),
|
||||
):
|
||||
await ShipmentService.add_shipment_services(self.db, page, self.user, scope)
|
||||
|
||||
add_shipment.assert_not_awaited()
|
||||
self.db.commit.assert_not_awaited()
|
||||
self.db.rollback.assert_awaited_once()
|
||||
|
||||
async def test_add_only_persists_pallet_ids_with_null_snapshots(self) -> None: # noqa: PLR0915
|
||||
page = ShipmentCreateModel(sourceWarehouseId=7, targetSiteId=9, palletIds=[5, 2])
|
||||
inventories = [make_inventory(2), make_inventory(5)]
|
||||
|
||||
Reference in New Issue
Block a user