功能:隔离五类测试业务链路

This commit is contained in:
焦龙言
2026-09-23 08:27:14 +08:00
parent 43fe17cc70
commit f5df2150d8
18 changed files with 1206 additions and 831 deletions
@@ -25,6 +25,7 @@ from module_business.dao.inventory_dao import InventoryDao
from module_business.dao.master_dao import MasterDao
from module_business.dao.operation_dao import OperationDao
from module_business.domain.enums import PalletEventType, PalletFlowState
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.do.master_do import BizPallet, BizStrain
from module_business.entity.do.operation_do import BizFillingRecord, BizPalletEvent, BizPalletInventory
from module_business.entity.vo.operation_vo import (
@@ -160,13 +161,24 @@ def make_filling(
class TestFillingPublicContract(unittest.TestCase):
def test_public_business_methods_require_explicit_test_scope(self) -> None:
for method_name in (
'get_filling_list_services',
'filling_detail_services',
'get_strain_options_services',
'scan_filling_pallet_services',
'fill_pallet_services',
):
with self.subTest(method=method_name):
self.assertIn('test_scope', inspect.signature(getattr(FillingService, method_name)).parameters)
def test_service_and_controller_are_available(self) -> None:
self.assertIsNotNone(FillingService)
self.assertIsNotNone(filling_controller_module.filling_controller)
def test_fill_signature_and_types_are_exact(self) -> None:
signature = inspect.signature(FillingService.fill_pallet_services)
self.assertEqual(('query_db', 'page_object', 'current_user'), tuple(signature.parameters))
self.assertEqual(('query_db', 'page_object', 'current_user', 'test_scope'), tuple(signature.parameters))
hints = get_type_hints(FillingService.fill_pallet_services)
self.assertIs(AsyncSession, hints['query_db'])
self.assertIs(FillingCreateModel, hints['page_object'])
@@ -217,9 +229,15 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[9, 7, 7])),
patch.object(OperationDao, 'get_filling_list', new=get_list),
):
result = await FillingService.get_filling_list_services(self.db, query, self.user)
result = await FillingService.get_filling_list_services(self.db, query, self.user, TestBatchScope.formal())
get_list.assert_awaited_once_with(self.db, query, True, warehouse_ids=[7, 9])
get_list.assert_awaited_once_with(
self.db,
query,
warehouse_ids=[7, 9],
scope=TestBatchScope.formal(),
is_page=True,
)
self.assertIsInstance(result.rows[0], FillingModel)
self.assertEqual(1, result.total)
@@ -231,9 +249,15 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[])),
patch.object(OperationDao, 'get_filling_list', new=get_list),
):
result = await FillingService.get_filling_list_services(self.db, query, self.user)
result = await FillingService.get_filling_list_services(self.db, query, self.user, TestBatchScope.formal())
get_list.assert_awaited_once_with(self.db, query, True, warehouse_ids=[])
get_list.assert_awaited_once_with(
self.db,
query,
warehouse_ids=[],
scope=TestBatchScope.formal(),
is_page=True,
)
self.assertEqual([], result.rows)
self.assertEqual(0, result.total)
@@ -245,7 +269,7 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_filling_list', new=get_list),
self.assertRaises(PermissionException),
):
await FillingService.get_filling_list_services(self.db, query, self.user)
await FillingService.get_filling_list_services(self.db, query, self.user, TestBatchScope.formal())
get_list.assert_not_awaited()
async def test_admin_list_is_unscoped(self) -> None:
@@ -256,9 +280,17 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(AccessDao, 'list_user_warehouse_ids', new=scope),
patch.object(OperationDao, 'get_filling_list', new=get_list),
):
result = await FillingService.get_filling_list_services(self.db, query, make_user(1), is_page=False)
result = await FillingService.get_filling_list_services(
self.db, query, make_user(1), TestBatchScope.formal(), is_page=False
)
scope.assert_not_awaited()
get_list.assert_awaited_once_with(self.db, query, False, warehouse_ids=None)
get_list.assert_awaited_once_with(
self.db,
query,
warehouse_ids=None,
scope=TestBatchScope.formal(),
is_page=False,
)
self.assertEqual([], result)
async def test_detail_is_scoped_before_lookup_and_hides_missing_for_normal_user(self) -> None:
@@ -268,8 +300,8 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_filling_by_id', new=get_detail),
self.assertRaises(PermissionException) as raised,
):
await FillingService.filling_detail_services(self.db, 31, self.user)
get_detail.assert_awaited_once_with(self.db, 31, warehouse_ids=[7])
await FillingService.filling_detail_services(self.db, 31, self.user, TestBatchScope.formal())
get_detail.assert_awaited_once_with(self.db, 31, TestBatchScope.formal(), warehouse_ids=[7])
self.assertEqual('无权访问该装填记录', raised.exception.message)
async def test_admin_detail_keeps_precise_not_found_error(self) -> None:
@@ -278,8 +310,8 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_filling_by_id', new=get_detail),
self.assertRaises(ServiceException) as raised,
):
await FillingService.filling_detail_services(self.db, 31, make_user(1))
get_detail.assert_awaited_once_with(self.db, 31, warehouse_ids=None)
await FillingService.filling_detail_services(self.db, 31, make_user(1), TestBatchScope.formal())
get_detail.assert_awaited_once_with(self.db, 31, TestBatchScope.formal(), warehouse_ids=None)
self.assertEqual('装填记录不存在', raised.exception.message)
async def test_strain_options_return_only_enabled_business_options(self) -> None:
@@ -290,11 +322,12 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
]
list_strain = AsyncMock(return_value=strains)
with patch.object(MasterDao, 'list_strain', new=list_strain):
result = await FillingService.get_strain_options_services(self.db, self.user)
result = await FillingService.get_strain_options_services(self.db, self.user, TestBatchScope.formal())
query = list_strain.await_args.args[1]
self.assertEqual('0', query.status)
self.assertFalse(list_strain.await_args.args[2])
self.assertEqual(TestBatchScope.formal(), list_strain.await_args.args[2])
self.assertFalse(list_strain.await_args.args[3])
self.assertEqual([1, 3], [option.id for option in result])
self.assertTrue(all(isinstance(option, BusinessOptionModel) for option in result))
@@ -311,7 +344,9 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_inventory_for_update', new=inventory_lock),
patch.object(InventoryDao, 'get_pallets_by_ids', new=pallet_lock),
):
result = await FillingService.scan_filling_pallet_services(self.db, 'qr-2', self.user)
result = await FillingService.scan_filling_pallet_services(
self.db, 'qr-2', self.user, TestBatchScope.formal()
)
access.assert_awaited_once_with(self.db, self.user, 7)
inventory_lock.assert_not_awaited()
@@ -334,7 +369,7 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(AccessService, 'assert_warehouse_access', new=access),
self.assertRaises(PermissionException) as raised,
):
await FillingService.scan_filling_pallet_services(self.db, 'qr-2', self.user)
await FillingService.scan_filling_pallet_services(self.db, 'qr-2', self.user, TestBatchScope.formal())
access.assert_awaited_once_with(self.db, self.user, 7)
self.assertEqual('', raised.exception.data)
self.assertEqual('无权访问该托盘', raised.exception.message)
@@ -346,7 +381,7 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=None)),
self.assertRaises(exception_type),
):
await FillingService.scan_filling_pallet_services(self.db, 'missing', user)
await FillingService.scan_filling_pallet_services(self.db, 'missing', user, TestBatchScope.formal())
async def test_scan_admin_distinguishes_missing_inventory(self) -> None:
with (
@@ -354,7 +389,7 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'get_pallet_inventory', new=AsyncMock(return_value=None)),
self.assertRaises(ServiceException) as raised,
):
await FillingService.scan_filling_pallet_services(self.db, 'qr-2', make_user(1))
await FillingService.scan_filling_pallet_services(self.db, 'qr-2', make_user(1), TestBatchScope.formal())
self.assertEqual('托盘库存不存在', raised.exception.message)
@@ -441,7 +476,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch('module_business.service.filling_service.datetime') as mocked_datetime,
):
mocked_datetime.now.return_value = self.fixed_time
result = await FillingService.fill_pallet_services(self.db, self.page, self.user)
result = await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual(
[
@@ -460,8 +495,8 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
[False, False, True], [call.kwargs.get('for_update', False) for call in access.await_args_list]
)
self.assertEqual([7, 7, 7], [call.args[2] for call in access.await_args_list])
warehouse_read.assert_awaited_once_with(self.db, 7, for_update=True)
strain_read.assert_awaited_once_with(self.db, 11, for_update=True)
warehouse_read.assert_awaited_once_with(self.db, 7, TestBatchScope.formal(), for_update=True)
strain_read.assert_awaited_once_with(self.db, 11, TestBatchScope.formal(), for_update=True)
filling = captured['filling']
self.assertTrue(filling.filling_no.startswith('FL-'))
self.assertEqual(2, filling.pallet_id)
@@ -539,7 +574,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=add_event),
self.assertRaises(ServiceException),
):
await FillingService.fill_pallet_services(self.db, page or self.page, self.user)
await FillingService.fill_pallet_services(self.db, page or self.page, self.user, TestBatchScope.formal())
add_filling.assert_not_awaited()
add_event.assert_not_awaited()
self.db.commit.assert_not_awaited()
@@ -580,11 +615,11 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=add_event),
self.assertRaises(ServiceException) as raised,
):
await FillingService.fill_pallet_services(self.db, self.page, self.user)
await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('仓库已停用', raised.exception.message)
self.assertEqual(['inventory-lock', 'pallet-lock', 'warehouse-read'], actions)
warehouse_read.assert_awaited_once_with(self.db, 7, for_update=True)
warehouse_read.assert_awaited_once_with(self.db, 7, TestBatchScope.formal(), for_update=True)
strain_read.assert_not_awaited()
add_filling.assert_not_awaited()
add_event.assert_not_awaited()
@@ -639,7 +674,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=add_event),
self.assertRaises(PermissionException) as raised,
):
await FillingService.fill_pallet_services(self.db, self.page, self.user)
await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('', raised.exception.data)
self.assertEqual('无权访问该托盘', raised.exception.message)
@@ -651,7 +686,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
[False, False, True], [call.kwargs.get('for_update', False) for call in access.await_args_list]
)
self.assertEqual([7, 7, 7], [call.args[2] for call in access.await_args_list])
warehouse_read.assert_awaited_once_with(self.db, 7, for_update=True)
warehouse_read.assert_awaited_once_with(self.db, 7, TestBatchScope.formal(), for_update=True)
strain_read.assert_not_awaited()
add_filling.assert_not_awaited()
add_event.assert_not_awaited()
@@ -676,7 +711,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'add_filling_record', new=add_filling),
self.assertRaises(ServiceException),
):
await FillingService.fill_pallet_services(self.db, self.page, self.user)
await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
add_filling.assert_not_awaited()
self.db.rollback.assert_awaited_once()
@@ -717,9 +752,14 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'get_warehouse', new=warehouse_read),
patch.object(MasterDao, 'get_strain', new=strain_read),
):
result = await FillingService.fill_pallet_services(self.db, self.page, self.user)
result = await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
get_key.assert_awaited_once_with(self.db, 'fill-key', warehouse_ids=[7])
get_key.assert_awaited_once_with(
self.db,
'fill-key',
TestBatchScope.formal(),
warehouse_ids=[7],
)
access.assert_awaited_once_with(self.db, self.user, 7)
inventory_lock.assert_not_awaited()
warehouse_read.assert_not_awaited()
@@ -741,9 +781,16 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
patch.object(OperationDao, 'get_filling_by_idempotency_key', new=get_key),
):
result = await FillingService.fill_pallet_services(self.db, self.page, user)
result = await FillingService.fill_pallet_services(
self.db, self.page, user, TestBatchScope.formal()
)
get_key.assert_awaited_once_with(self.db, 'fill-key', warehouse_ids=[7])
get_key.assert_awaited_once_with(
self.db,
'fill-key',
TestBatchScope.formal(),
warehouse_ids=[7],
)
self.assertEqual(31, result.result)
if user.user and user.user.admin:
scope_read.assert_not_awaited()
@@ -756,7 +803,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_filling_by_idempotency_key', new=AsyncMock(return_value=existing)),
self.assertRaises(ServiceException) as raised,
):
await FillingService.fill_pallet_services(self.db, self.page, self.user)
await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertIn('幂等键', raised.exception.message)
self.db.rollback.assert_awaited_once()
@@ -767,7 +814,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_filling_by_idempotency_key', new=get_key),
self.assertRaises(PermissionException) as raised,
):
await FillingService.fill_pallet_services(self.db, self.page, self.user)
await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('', raised.exception.data)
self.assertEqual('无权访问该仓库', raised.exception.message)
get_key.assert_not_awaited()
@@ -802,7 +849,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=add_event),
self.assertRaises(PermissionException) as raised,
):
await FillingService.fill_pallet_services(self.db, self.page, self.user)
await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual([7, foreign_warehouse_id], [call.args[2] for call in access.await_args_list])
self.assertEqual('', raised.exception.data)
@@ -856,7 +903,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'add_filling_record', new=add_filling),
patch.object(InventoryDao, 'add_event', new=add_event),
):
result = await FillingService.fill_pallet_services(self.db, self.page, self.user)
result = await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual(['inventory-lock', 'pallet-lock', 'filling-lock'], actions)
self.assertEqual(2, get_key.await_count)
@@ -893,7 +940,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'add_filling_record', new=add_filling),
self.assertRaises(ServiceException) as raised,
):
await FillingService.fill_pallet_services(self.db, self.page, self.user)
await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('装填失败,幂等键已被其他请求使用', raised.exception.message)
self.assertEqual(2, get_key.await_count)
@@ -914,7 +961,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'add_filling_record', new=AsyncMock(side_effect=RuntimeError('flush failed'))),
self.assertRaises(RuntimeError),
):
await FillingService.fill_pallet_services(self.db, self.page, self.user)
await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.db.rollback.assert_awaited_once()
self.db.commit.assert_not_awaited()
@@ -923,7 +970,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(FillingService, '_build_request_hash', side_effect=RuntimeError('hash failed')),
self.assertRaises(RuntimeError),
):
await FillingService.fill_pallet_services(self.db, self.page, self.user)
await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.db.rollback.assert_awaited_once()
self.db.commit.assert_not_awaited()
@@ -942,7 +989,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'get_strain', new=AsyncMock(return_value=self.strain)),
patch.object(OperationDao, 'add_filling_record', new=AsyncMock(side_effect=integrity_error())),
):
result = await FillingService.fill_pallet_services(self.db, self.page, self.user)
result = await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual(2, key_reads.await_count)
self.assertEqual(
@@ -968,7 +1015,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'add_filling_record', new=AsyncMock(side_effect=integrity_error())),
self.assertRaises(PermissionException) as raised,
):
await FillingService.fill_pallet_services(self.db, self.page, self.user)
await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('无权访问该仓库', raised.exception.message)
self.assertEqual(2, warehouse_scope.await_count)
@@ -993,7 +1040,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'add_filling_record', new=AsyncMock(side_effect=integrity_error())),
self.assertRaises(ServiceException) as raised,
):
await FillingService.fill_pallet_services(self.db, self.page, self.user)
await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('装填失败,数据冲突', raised.exception.message)
self.assertEqual(2, self.db.rollback.await_count)
self.db.commit.assert_not_awaited()
@@ -1009,7 +1056,13 @@ class TestFillingOperationDaoScope(unittest.IsolatedAsyncioTestCase):
paginate = AsyncMock(return_value=[])
db = MagicMock(spec=AsyncSession)
with patch.object(OperationDao, '_paginate', new=paginate):
await OperationDao.get_filling_list(db, query, False, warehouse_ids=[9, 7, 7])
await OperationDao.get_filling_list(
db,
query,
warehouse_ids=[9, 7, 7],
scope=TestBatchScope.formal(),
is_page=False,
)
sql = self._compiled(paginate.await_args.args[1])
self.assertIn('biz_filling_record.warehouse_id in (7, 9)', sql)
@@ -1018,7 +1071,13 @@ class TestFillingOperationDaoScope(unittest.IsolatedAsyncioTestCase):
paginate = AsyncMock(return_value=[])
db = MagicMock(spec=AsyncSession)
with patch.object(OperationDao, '_paginate', new=paginate):
await OperationDao.get_filling_list(db, query, False, warehouse_ids=[])
await OperationDao.get_filling_list(
db,
query,
warehouse_ids=[],
scope=TestBatchScope.formal(),
is_page=False,
)
sql = self._compiled(paginate.await_args.args[1])
self.assertTrue('false' in sql or '0 = 1' in sql, sql)
@@ -1032,7 +1091,7 @@ class TestFillingOperationDaoScope(unittest.IsolatedAsyncioTestCase):
result.scalar_one_or_none.return_value = None
db = MagicMock(spec=AsyncSession)
db.execute.return_value = result
await method(db, key, warehouse_ids=[7])
await method(db, key, TestBatchScope.formal(), warehouse_ids=[7])
sql = self._compiled(db.execute.await_args.args[0])
self.assertIn('biz_filling_record.warehouse_id in (7)', sql)