安全:补齐业务批次归属校验
This commit is contained in:
@@ -37,6 +37,8 @@ from module_business.entity.vo.operation_vo import (
|
||||
)
|
||||
from module_business.service.access_service import AccessService
|
||||
from module_business.service.filling_service import FillingService
|
||||
from module_business.service.test_batch_access_service import TestBatchAccessService
|
||||
from tests.module_business.test_batch_fixtures import make_test_scope
|
||||
|
||||
|
||||
def integrity_error() -> IntegrityError:
|
||||
@@ -78,6 +80,7 @@ def make_pallet(
|
||||
qr_token: str = 'qr-2',
|
||||
capacity_packages: int = 20,
|
||||
asset_status: str = '0',
|
||||
test_batch_id: int | None = None,
|
||||
) -> BizPallet:
|
||||
return BizPallet(
|
||||
pallet_id=pallet_id,
|
||||
@@ -85,6 +88,7 @@ def make_pallet(
|
||||
qr_token=qr_token,
|
||||
capacity_packages=capacity_packages,
|
||||
asset_status=asset_status,
|
||||
test_batch_id=test_batch_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -99,6 +103,7 @@ def make_inventory(
|
||||
remaining_packages: int = 0,
|
||||
last_event_id: int | None = 40,
|
||||
version: int = 3,
|
||||
test_batch_id: int | None = None,
|
||||
) -> BizPalletInventory:
|
||||
return BizPalletInventory(
|
||||
pallet_id=pallet_id,
|
||||
@@ -111,6 +116,7 @@ def make_inventory(
|
||||
last_event_id=last_event_id,
|
||||
version=version,
|
||||
updated_at=datetime(2026, 7, 25, 9, 0),
|
||||
test_batch_id=test_batch_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -120,12 +126,14 @@ def make_strain(
|
||||
status: str = '0',
|
||||
code: str = 'S-011',
|
||||
name: str = '香菇 11',
|
||||
test_batch_id: int | None = None,
|
||||
) -> BizStrain:
|
||||
return BizStrain(
|
||||
strain_id=strain_id,
|
||||
strain_code=code,
|
||||
strain_name=name,
|
||||
status=status,
|
||||
test_batch_id=test_batch_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -135,6 +143,7 @@ def make_filling(
|
||||
warehouse_id: int = 7,
|
||||
idempotency_key: str = 'fill-key',
|
||||
request_hash: str | None = None,
|
||||
test_batch_id: int | None = None,
|
||||
) -> BizFillingRecord:
|
||||
page = make_page(warehouse_id=warehouse_id, idempotency_key=idempotency_key)
|
||||
return BizFillingRecord(
|
||||
@@ -157,6 +166,7 @@ def make_filling(
|
||||
create_time=datetime(2026, 7, 26, 10, 30),
|
||||
update_by='user-7',
|
||||
update_time=datetime(2026, 7, 26, 10, 30),
|
||||
test_batch_id=test_batch_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -392,6 +402,24 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
|
||||
await FillingService.scan_filling_pallet_services(self.db, 'qr-2', make_user(1), TestBatchScope.formal())
|
||||
self.assertEqual('托盘库存不存在', raised.exception.message)
|
||||
|
||||
async def test_test_scope_scan_rejects_cross_batch_pallet(self) -> None:
|
||||
scope = make_test_scope(4)
|
||||
with (
|
||||
patch.object(
|
||||
InventoryDao,
|
||||
'get_pallet_by_qr_token',
|
||||
new=AsyncMock(return_value=make_pallet(test_batch_id=5)),
|
||||
),
|
||||
patch.object(
|
||||
MasterDao,
|
||||
'get_pallet_inventory',
|
||||
new=AsyncMock(return_value=make_inventory(test_batch_id=4)),
|
||||
),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||
self.assertRaisesRegex(ServiceException, '不属于当前数据模式或测试批次'),
|
||||
):
|
||||
await FillingService.scan_filling_pallet_services(self.db, 'qr-2', self.user, scope)
|
||||
|
||||
|
||||
class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
|
||||
def setUp(self) -> None:
|
||||
@@ -401,7 +429,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
|
||||
self.pallet = make_pallet()
|
||||
self.inventory = make_inventory()
|
||||
self.strain = make_strain()
|
||||
self.warehouse = SimpleNamespace(warehouse_id=7, status='0')
|
||||
self.warehouse = SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)
|
||||
self.fixed_time = datetime(2026, 7, 26, 10, 30)
|
||||
|
||||
async def test_fill_locks_inventory_then_pallet_and_writes_one_atomic_event( # noqa: PLR0915
|
||||
@@ -549,6 +577,29 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual('装填成功', result.message)
|
||||
self.assertEqual(31, result.result)
|
||||
|
||||
async def test_test_scope_write_rejects_cross_batch_pallet_and_rolls_back(self) -> None:
|
||||
scope = make_test_scope(4)
|
||||
add_filling = AsyncMock()
|
||||
with (
|
||||
patch.object(TestBatchAccessService, 'lock_active_batch', new=AsyncMock()),
|
||||
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7])),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||
patch.object(OperationDao, 'get_filling_by_idempotency_key', new=AsyncMock(return_value=None)),
|
||||
patch.object(
|
||||
InventoryDao,
|
||||
'get_pallet_by_qr_token',
|
||||
new=AsyncMock(return_value=make_pallet(test_batch_id=5)),
|
||||
),
|
||||
patch.object(InventoryDao, 'get_inventory_for_update', new=AsyncMock()),
|
||||
patch.object(OperationDao, 'add_filling_record', new=add_filling),
|
||||
self.assertRaisesRegex(ServiceException, '不属于当前数据模式或测试批次'),
|
||||
):
|
||||
await FillingService.fill_pallet_services(self.db, self.page, self.user, scope)
|
||||
|
||||
add_filling.assert_not_awaited()
|
||||
self.db.commit.assert_not_awaited()
|
||||
self.db.rollback.assert_awaited_once()
|
||||
|
||||
async def _assert_fill_rejected(
|
||||
self,
|
||||
*,
|
||||
@@ -597,7 +648,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
async def lock_warehouse(*_args: Any, **_kwargs: Any) -> SimpleNamespace:
|
||||
actions.append('warehouse-read')
|
||||
return SimpleNamespace(warehouse_id=7, status='1')
|
||||
return SimpleNamespace(warehouse_id=7, status='1', test_batch_id=None)
|
||||
|
||||
warehouse_read = AsyncMock(side_effect=lock_warehouse)
|
||||
strain_read = AsyncMock()
|
||||
|
||||
Reference in New Issue
Block a user