修复:收紧入库数据范围校验
This commit is contained in:
@@ -7,13 +7,14 @@ from types import SimpleNamespace
|
||||
from typing import Any, get_type_hints
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.routing import APIRoute
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from common.aspect.interface_auth import CheckUserInterfaceAuth
|
||||
from common.aspect.pre_auth import PreAuth
|
||||
from common.context import RequestContext
|
||||
from common.vo import (
|
||||
CrudResponseModel,
|
||||
DataResponseModel,
|
||||
@@ -23,6 +24,7 @@ from common.vo import (
|
||||
)
|
||||
from exceptions.exception import PermissionException, ServiceException
|
||||
from module_admin.entity.vo.user_vo import CurrentUserModel, UserInfoModel
|
||||
from module_admin.service.log_service import LogQueueService
|
||||
from module_admin.service.login_service import LoginService
|
||||
from module_business.controller import inbound_controller as inbound_controller_module
|
||||
from module_business.dao.access_dao import AccessDao
|
||||
@@ -161,6 +163,13 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
||||
self.db = MagicMock(spec=AsyncSession)
|
||||
self.user = make_user()
|
||||
self.fixed_time = datetime(2026, 7, 26, 10, 30)
|
||||
self.scope_patcher = patch.object(
|
||||
AccessDao,
|
||||
'list_user_warehouse_ids',
|
||||
new=AsyncMock(return_value=[2, 5, 7, 8, 11]),
|
||||
)
|
||||
self.default_scope = self.scope_patcher.start()
|
||||
self.addCleanup(self.scope_patcher.stop)
|
||||
|
||||
async def test_add_validates_and_locks_pallets_before_writing_draft(self) -> None:
|
||||
page = InboundCreateModel(warehouseId=7, palletIds=[5, 2])
|
||||
@@ -320,14 +329,38 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
||||
lock.assert_not_awaited()
|
||||
self.db.rollback.assert_awaited_once()
|
||||
|
||||
async def test_edit_checks_original_and_target_access_then_replaces_items(self) -> None:
|
||||
async def test_add_checks_warehouse_access_before_revealing_existence(self) -> None:
|
||||
page = InboundCreateModel(warehouseId=404, palletIds=[2])
|
||||
get_warehouse = AsyncMock(return_value=None)
|
||||
access = AsyncMock(
|
||||
side_effect=PermissionException(data='', message='无权访问该仓库'),
|
||||
)
|
||||
pallet_lock = AsyncMock()
|
||||
|
||||
with (
|
||||
patch.object(MasterDao, 'get_warehouse', new=get_warehouse),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=access),
|
||||
patch.object(InventoryDao, 'get_inventories_for_update', new=pallet_lock),
|
||||
self.assertRaises(Exception) as raised,
|
||||
):
|
||||
await InboundService.add_inbound_services(self.db, page, self.user)
|
||||
|
||||
self.assertIsInstance(raised.exception, PermissionException)
|
||||
self.assertEqual('无权访问该仓库', raised.exception.message)
|
||||
access.assert_awaited_once_with(self.db, self.user, 404)
|
||||
get_warehouse.assert_not_awaited()
|
||||
pallet_lock.assert_not_awaited()
|
||||
self.db.rollback.assert_awaited_once()
|
||||
|
||||
async def test_edit_scopes_original_and_checks_target_access_then_replaces_items(self) -> None:
|
||||
page = InboundUpdateModel(inboundId=11, warehouseId=8, palletIds=[5, 2])
|
||||
inbound = make_inbound(warehouse_id=7, pallet_ids=(3,))
|
||||
lock_order = AsyncMock(return_value=inbound)
|
||||
access = AsyncMock()
|
||||
update = AsyncMock()
|
||||
replace = AsyncMock()
|
||||
with (
|
||||
patch.object(OperationDao, 'get_inbound_for_update', new=AsyncMock(return_value=inbound)),
|
||||
patch.object(OperationDao, 'get_inbound_for_update', new=lock_order),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=access),
|
||||
patch.object(
|
||||
MasterDao,
|
||||
@@ -351,10 +384,13 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
||||
service_datetime.now.return_value = self.fixed_time
|
||||
result = await InboundService.edit_inbound_services(self.db, page, self.user)
|
||||
|
||||
self.assertEqual(
|
||||
[unittest.mock.call(self.db, self.user, 7), unittest.mock.call(self.db, self.user, 8)],
|
||||
access.await_args_list,
|
||||
self.default_scope.assert_awaited_once_with(self.db, 7)
|
||||
lock_order.assert_awaited_once_with(
|
||||
self.db,
|
||||
11,
|
||||
warehouse_ids=[2, 5, 7, 8, 11],
|
||||
)
|
||||
access.assert_awaited_once_with(self.db, self.user, 8)
|
||||
inventory_lock.assert_awaited_once_with(self.db, [2, 5])
|
||||
pallet_lock.assert_awaited_once_with(self.db, [2, 5])
|
||||
self.assertEqual(8, inbound.warehouse_id)
|
||||
@@ -388,16 +424,56 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
||||
update.assert_not_awaited()
|
||||
self.db.rollback.assert_awaited_once()
|
||||
|
||||
async def test_edit_scopes_parent_lock_and_hides_unmatched_document(self) -> None:
|
||||
page = InboundUpdateModel(inboundId=404, warehouseId=8, palletIds=[2])
|
||||
lock_order = AsyncMock(return_value=None)
|
||||
get_warehouse = AsyncMock()
|
||||
inventory_lock = AsyncMock()
|
||||
with (
|
||||
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7])) as scope,
|
||||
patch.object(OperationDao, 'get_inbound_for_update', new=lock_order),
|
||||
patch.object(MasterDao, 'get_warehouse', new=get_warehouse),
|
||||
patch.object(InventoryDao, 'get_inventories_for_update', new=inventory_lock),
|
||||
self.assertRaises(Exception) as raised,
|
||||
):
|
||||
await InboundService.edit_inbound_services(self.db, page, self.user)
|
||||
|
||||
self.assertIsInstance(raised.exception, PermissionException)
|
||||
self.assertEqual('无权访问该入库单', raised.exception.message)
|
||||
scope.assert_awaited_once_with(self.db, 7)
|
||||
lock_order.assert_awaited_once_with(self.db, 404, warehouse_ids=[7])
|
||||
get_warehouse.assert_not_awaited()
|
||||
inventory_lock.assert_not_awaited()
|
||||
self.db.rollback.assert_awaited_once()
|
||||
|
||||
async def test_admin_missing_edit_keeps_precise_not_found(self) -> None:
|
||||
page = InboundUpdateModel(inboundId=404, warehouseId=8, palletIds=[2])
|
||||
scope = AsyncMock()
|
||||
lock_order = AsyncMock(return_value=None)
|
||||
with (
|
||||
patch.object(AccessDao, 'list_user_warehouse_ids', new=scope),
|
||||
patch.object(OperationDao, 'get_inbound_for_update', new=lock_order),
|
||||
self.assertRaises(ServiceException) as raised,
|
||||
):
|
||||
await InboundService.edit_inbound_services(self.db, page, make_user(1))
|
||||
|
||||
self.assertEqual('入库单不存在', raised.exception.message)
|
||||
scope.assert_not_awaited()
|
||||
lock_order.assert_awaited_once_with(self.db, 404, warehouse_ids=None)
|
||||
self.db.rollback.assert_awaited_once()
|
||||
|
||||
async def test_batch_delete_locks_and_preflights_all_before_ordered_delete(self) -> None:
|
||||
actions: list[str] = []
|
||||
|
||||
async def lock(_db: AsyncSession, inbound_id: int) -> BizInboundOrder:
|
||||
async def lock(
|
||||
_db: AsyncSession,
|
||||
inbound_id: int,
|
||||
warehouse_ids: list[int] | None = None,
|
||||
) -> BizInboundOrder:
|
||||
self.assertEqual([2, 5, 7, 8, 11], warehouse_ids)
|
||||
actions.append(f'lock-{inbound_id}')
|
||||
return make_inbound(inbound_id=inbound_id, warehouse_id=inbound_id)
|
||||
|
||||
async def access(_db: AsyncSession, _user: CurrentUserModel, warehouse_id: int) -> None:
|
||||
actions.append(f'access-{warehouse_id}')
|
||||
|
||||
async def delete(_db: AsyncSession, inbound_id: int) -> None:
|
||||
actions.append(f'delete-{inbound_id}')
|
||||
|
||||
@@ -405,17 +481,20 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
||||
actions.append('commit')
|
||||
|
||||
self.db.commit.side_effect = commit
|
||||
access = AsyncMock()
|
||||
with (
|
||||
patch.object(OperationDao, 'get_inbound_for_update', new=AsyncMock(side_effect=lock)),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock(side_effect=access)),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=access),
|
||||
patch.object(OperationDao, 'delete_inbound', new=AsyncMock(side_effect=delete)),
|
||||
):
|
||||
result = await InboundService.delete_inbound_services(self.db, [5, 2], self.user)
|
||||
|
||||
self.assertEqual(
|
||||
['lock-2', 'access-2', 'lock-5', 'access-5', 'delete-2', 'delete-5', 'commit'],
|
||||
['lock-2', 'lock-5', 'delete-2', 'delete-5', 'commit'],
|
||||
actions,
|
||||
)
|
||||
self.default_scope.assert_awaited_once_with(self.db, 7)
|
||||
access.assert_not_awaited()
|
||||
self.assertTrue(result.is_success)
|
||||
self.db.rollback.assert_not_awaited()
|
||||
|
||||
@@ -429,7 +508,9 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(
|
||||
OperationDao,
|
||||
'get_inbound_for_update',
|
||||
new=AsyncMock(side_effect=lambda _db, inbound_id: records[inbound_id]),
|
||||
new=AsyncMock(
|
||||
side_effect=lambda _db, inbound_id, warehouse_ids=None: records[inbound_id],
|
||||
),
|
||||
),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||
patch.object(OperationDao, 'delete_inbound', new=delete),
|
||||
@@ -441,6 +522,39 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
||||
self.db.commit.assert_not_awaited()
|
||||
self.db.rollback.assert_awaited_once()
|
||||
|
||||
async def test_delete_scopes_parent_lock_and_hides_unmatched_document(self) -> None:
|
||||
lock_order = AsyncMock(return_value=None)
|
||||
delete = AsyncMock()
|
||||
with (
|
||||
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7])) as scope,
|
||||
patch.object(OperationDao, 'get_inbound_for_update', new=lock_order),
|
||||
patch.object(OperationDao, 'delete_inbound', new=delete),
|
||||
self.assertRaises(Exception) as raised,
|
||||
):
|
||||
await InboundService.delete_inbound_services(self.db, [404], self.user)
|
||||
|
||||
self.assertIsInstance(raised.exception, PermissionException)
|
||||
self.assertEqual('无权访问该入库单', raised.exception.message)
|
||||
scope.assert_awaited_once_with(self.db, 7)
|
||||
lock_order.assert_awaited_once_with(self.db, 404, warehouse_ids=[7])
|
||||
delete.assert_not_awaited()
|
||||
self.db.rollback.assert_awaited_once()
|
||||
|
||||
async def test_admin_missing_delete_keeps_precise_not_found(self) -> None:
|
||||
scope = AsyncMock()
|
||||
lock_order = AsyncMock(return_value=None)
|
||||
with (
|
||||
patch.object(AccessDao, 'list_user_warehouse_ids', new=scope),
|
||||
patch.object(OperationDao, 'get_inbound_for_update', new=lock_order),
|
||||
self.assertRaises(ServiceException) as raised,
|
||||
):
|
||||
await InboundService.delete_inbound_services(self.db, [404], make_user(1))
|
||||
|
||||
self.assertEqual('入库单不存在', raised.exception.message)
|
||||
scope.assert_not_awaited()
|
||||
lock_order.assert_awaited_once_with(self.db, 404, warehouse_ids=None)
|
||||
self.db.rollback.assert_awaited_once()
|
||||
|
||||
async def test_all_draft_mutations_convert_integrity_errors(self) -> None:
|
||||
add_page = InboundCreateModel(warehouseId=7, palletIds=[2])
|
||||
with (
|
||||
@@ -543,28 +657,52 @@ class TestInboundReadService(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(0, actual.total)
|
||||
self.assertFalse(actual.has_next)
|
||||
|
||||
async def test_detail_loads_then_checks_warehouse_scope(self) -> None:
|
||||
async def test_detail_pushes_warehouse_scope_into_parent_query(self) -> None:
|
||||
inbound = make_inbound()
|
||||
get_detail = AsyncMock(return_value=inbound)
|
||||
access = AsyncMock()
|
||||
with (
|
||||
patch.object(OperationDao, 'get_inbound_by_id', new=AsyncMock(return_value=inbound)),
|
||||
patch.object(
|
||||
AccessDao,
|
||||
'list_user_warehouse_ids',
|
||||
new=AsyncMock(return_value=[7, 3, 7]),
|
||||
) as scope,
|
||||
patch.object(OperationDao, 'get_inbound_by_id', new=get_detail),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=access),
|
||||
):
|
||||
actual = await InboundService.inbound_detail_services(self.db, 11, self.user)
|
||||
|
||||
access.assert_awaited_once_with(self.db, self.user, 7)
|
||||
scope.assert_awaited_once_with(self.db, 7)
|
||||
get_detail.assert_awaited_once_with(self.db, 11, warehouse_ids=[3, 7])
|
||||
access.assert_not_awaited()
|
||||
self.assertIsInstance(actual, InboundModel)
|
||||
|
||||
async def test_missing_detail_fails_stably_without_access_probe(self) -> None:
|
||||
access = AsyncMock()
|
||||
async def test_normal_user_detail_hides_missing_or_external_document(self) -> None:
|
||||
get_detail = AsyncMock(return_value=None)
|
||||
with (
|
||||
patch.object(OperationDao, 'get_inbound_by_id', new=AsyncMock(return_value=None)),
|
||||
patch.object(AccessService, 'assert_warehouse_access', new=access),
|
||||
self.assertRaises(ServiceException) as raised,
|
||||
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7])),
|
||||
patch.object(OperationDao, 'get_inbound_by_id', new=get_detail),
|
||||
self.assertRaises(Exception) as raised,
|
||||
):
|
||||
await InboundService.inbound_detail_services(self.db, 404, self.user)
|
||||
|
||||
self.assertIsInstance(raised.exception, PermissionException)
|
||||
self.assertEqual('无权访问该入库单', raised.exception.message)
|
||||
get_detail.assert_awaited_once_with(self.db, 404, warehouse_ids=[7])
|
||||
|
||||
async def test_admin_missing_detail_keeps_precise_not_found(self) -> None:
|
||||
scope = AsyncMock()
|
||||
get_detail = AsyncMock(return_value=None)
|
||||
with (
|
||||
patch.object(AccessDao, 'list_user_warehouse_ids', new=scope),
|
||||
patch.object(OperationDao, 'get_inbound_by_id', new=get_detail),
|
||||
self.assertRaises(ServiceException) as raised,
|
||||
):
|
||||
await InboundService.inbound_detail_services(self.db, 404, make_user(1))
|
||||
|
||||
self.assertEqual('入库单不存在', raised.exception.message)
|
||||
access.assert_not_awaited()
|
||||
scope.assert_not_awaited()
|
||||
get_detail.assert_awaited_once_with(self.db, 404, warehouse_ids=None)
|
||||
|
||||
async def test_warehouse_options_filter_disabled_and_unauthorized_records(self) -> None:
|
||||
warehouses = [
|
||||
@@ -722,6 +860,58 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
self.command = ConfirmCommandModel(idempotencyKey='confirm-key')
|
||||
self.fixed_time = datetime(2026, 7, 26, 14, 20)
|
||||
self.request_hash = InboundService._build_confirm_request_hash(11)
|
||||
self.scope_patcher = patch.object(
|
||||
AccessDao,
|
||||
'list_user_warehouse_ids',
|
||||
new=AsyncMock(return_value=[7]),
|
||||
)
|
||||
self.default_scope = self.scope_patcher.start()
|
||||
self.addCleanup(self.scope_patcher.stop)
|
||||
|
||||
async def test_confirm_scopes_initial_parent_and_hides_unmatched_document(self) -> None:
|
||||
get_detail = AsyncMock(return_value=None)
|
||||
get_by_key = AsyncMock()
|
||||
lock_order = AsyncMock()
|
||||
inventory_lock = AsyncMock()
|
||||
with (
|
||||
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7])) as scope,
|
||||
patch.object(OperationDao, 'get_inbound_by_id', new=get_detail),
|
||||
patch.object(OperationDao, 'get_inbound_by_confirm_idempotency_key', new=get_by_key),
|
||||
patch.object(OperationDao, 'get_inbound_for_update', new=lock_order),
|
||||
patch.object(InventoryDao, 'get_inventories_for_update', new=inventory_lock),
|
||||
self.assertRaises(Exception) as raised,
|
||||
):
|
||||
await InboundService.confirm_inbound_services(self.db, 404, self.command, self.user)
|
||||
|
||||
self.assertIsInstance(raised.exception, PermissionException)
|
||||
self.assertEqual('无权访问该入库单', raised.exception.message)
|
||||
scope.assert_awaited_once_with(self.db, 7)
|
||||
get_detail.assert_awaited_once_with(self.db, 404, warehouse_ids=[7])
|
||||
get_by_key.assert_not_awaited()
|
||||
lock_order.assert_not_awaited()
|
||||
inventory_lock.assert_not_awaited()
|
||||
self.db.rollback.assert_awaited_once()
|
||||
|
||||
async def test_admin_missing_confirm_keeps_precise_not_found(self) -> None:
|
||||
scope = AsyncMock()
|
||||
get_detail = AsyncMock(return_value=None)
|
||||
get_by_key = AsyncMock()
|
||||
lock_order = AsyncMock()
|
||||
with (
|
||||
patch.object(AccessDao, 'list_user_warehouse_ids', new=scope),
|
||||
patch.object(OperationDao, 'get_inbound_by_id', new=get_detail),
|
||||
patch.object(OperationDao, 'get_inbound_by_confirm_idempotency_key', new=get_by_key),
|
||||
patch.object(OperationDao, 'get_inbound_for_update', new=lock_order),
|
||||
self.assertRaises(ServiceException) as raised,
|
||||
):
|
||||
await InboundService.confirm_inbound_services(self.db, 404, self.command, make_user(1))
|
||||
|
||||
self.assertEqual('入库单不存在', raised.exception.message)
|
||||
scope.assert_not_awaited()
|
||||
get_detail.assert_awaited_once_with(self.db, 404, warehouse_ids=None)
|
||||
get_by_key.assert_not_awaited()
|
||||
lock_order.assert_not_awaited()
|
||||
self.db.rollback.assert_awaited_once()
|
||||
|
||||
async def _run_successful_confirm(self, *, commit_side_effect: Exception | None = None) -> dict[str, Any]:
|
||||
inbound = make_inbound()
|
||||
@@ -730,7 +920,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
actions: list[str] = []
|
||||
events: list[BizPalletEvent] = []
|
||||
|
||||
async def detail(*_args: Any) -> BizInboundOrder:
|
||||
async def detail(*_args: Any, **_kwargs: Any) -> BizInboundOrder:
|
||||
actions.append('detail')
|
||||
return inbound
|
||||
|
||||
@@ -740,7 +930,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
async def by_key(*_args: Any) -> None:
|
||||
actions.append('idempotency-read')
|
||||
|
||||
async def lock_order(*_args: Any) -> BizInboundOrder:
|
||||
async def lock_order(*_args: Any, **_kwargs: Any) -> BizInboundOrder:
|
||||
actions.append('order-lock')
|
||||
return inbound
|
||||
|
||||
@@ -797,10 +987,10 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(inbound_service_module, 'datetime'),
|
||||
)
|
||||
with (
|
||||
patches[0],
|
||||
patches[1],
|
||||
patches[0] as detail_read,
|
||||
patches[1] as access_probe,
|
||||
patches[2],
|
||||
patches[3],
|
||||
patches[3] as order_lock,
|
||||
patches[4] as inventory_lock,
|
||||
patches[5] as pallet_lock,
|
||||
patches[6] as ensure_transition,
|
||||
@@ -817,6 +1007,9 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
'inventories': inventories,
|
||||
'events': events,
|
||||
'actions': actions,
|
||||
'detail_read': detail_read,
|
||||
'access_probe': access_probe,
|
||||
'order_lock': order_lock,
|
||||
'inventory_lock': inventory_lock,
|
||||
'pallet_lock': pallet_lock,
|
||||
'ensure_transition': ensure_transition,
|
||||
@@ -828,10 +1021,8 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(
|
||||
[
|
||||
'detail',
|
||||
'access',
|
||||
'idempotency-read',
|
||||
'order-lock',
|
||||
'access',
|
||||
'inventory-lock',
|
||||
'pallet-lock',
|
||||
'transition-UNSTOCKED-EMPTY_IN_WAREHOUSE',
|
||||
@@ -843,6 +1034,10 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
],
|
||||
captured['actions'],
|
||||
)
|
||||
self.default_scope.assert_awaited_once_with(self.db, 7)
|
||||
captured['detail_read'].assert_awaited_once_with(self.db, 11, warehouse_ids=[7])
|
||||
captured['order_lock'].assert_awaited_once_with(self.db, 11, warehouse_ids=[7])
|
||||
captured['access_probe'].assert_not_awaited()
|
||||
captured['inventory_lock'].assert_awaited_once_with(self.db, [2, 5])
|
||||
captured['pallet_lock'].assert_awaited_once_with(self.db, [2, 5])
|
||||
captured['ensure_transition'].assert_has_calls(
|
||||
@@ -962,8 +1157,8 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
):
|
||||
result = await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user)
|
||||
|
||||
lock.assert_awaited_once_with(self.db, 11)
|
||||
self.assertEqual(3, access.await_count)
|
||||
lock.assert_awaited_once_with(self.db, 11, warehouse_ids=[7])
|
||||
access.assert_awaited_once_with(self.db, self.user, 7)
|
||||
inventory_lock.assert_not_awaited()
|
||||
self.db.commit.assert_not_awaited()
|
||||
self.db.rollback.assert_awaited_once()
|
||||
@@ -992,7 +1187,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
):
|
||||
await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user)
|
||||
|
||||
self.assertEqual(2, access.await_count)
|
||||
access.assert_awaited_once_with(self.db, self.user, 8)
|
||||
lock.assert_not_awaited()
|
||||
self.db.rollback.assert_awaited_once()
|
||||
|
||||
@@ -1035,7 +1230,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
):
|
||||
result = await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user)
|
||||
|
||||
get_detail.assert_awaited_once_with(self.db, 11)
|
||||
get_detail.assert_awaited_once_with(self.db, 11, warehouse_ids=[7])
|
||||
inventory_lock.assert_awaited_once_with(self.db, [5])
|
||||
self.assertEqual('EMPTY_IN_WAREHOUSE', inventory.flow_state)
|
||||
self.assertTrue(result.is_success)
|
||||
@@ -1063,7 +1258,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
):
|
||||
result = await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user)
|
||||
|
||||
self.assertEqual(3, access.await_count)
|
||||
access.assert_awaited_once_with(self.db, self.user, 7)
|
||||
self.assertEqual(2, key_reads.await_count)
|
||||
self.assertEqual(2, self.db.rollback.await_count)
|
||||
self.assertTrue(result.is_success)
|
||||
@@ -1098,6 +1293,68 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(2, self.db.rollback.await_count)
|
||||
|
||||
|
||||
class TestInboundLogPermission(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_real_inbound_log_wrapper_preserves_permission_code(self) -> None:
|
||||
async def receive() -> dict[str, Any]:
|
||||
return {
|
||||
'type': 'http.request',
|
||||
'body': b'{"warehouseId":404,"palletIds":[2]}',
|
||||
'more_body': False,
|
||||
}
|
||||
|
||||
request = Request(
|
||||
{
|
||||
'type': 'http',
|
||||
'method': 'POST',
|
||||
'scheme': 'http',
|
||||
'path': '/business/inbound',
|
||||
'raw_path': b'/business/inbound',
|
||||
'root_path': '',
|
||||
'query_string': b'',
|
||||
'headers': [
|
||||
(b'content-type', b'application/json'),
|
||||
(b'user-agent', b'unittest'),
|
||||
],
|
||||
'client': ('127.0.0.1', 12345),
|
||||
'server': ('testserver', 80),
|
||||
},
|
||||
receive=receive,
|
||||
)
|
||||
user = make_user()
|
||||
db = MagicMock(spec=AsyncSession)
|
||||
service = AsyncMock(
|
||||
side_effect=PermissionException(data='', message='无权访问该仓库'),
|
||||
)
|
||||
enqueue_log = AsyncMock()
|
||||
user_token = RequestContext.set_current_user(user)
|
||||
try:
|
||||
with (
|
||||
patch.object(InboundService, 'add_inbound_services', new=service),
|
||||
patch(
|
||||
'common.annotation.log_annotation.Log._get_oper_location',
|
||||
new=AsyncMock(return_value='内网IP'),
|
||||
),
|
||||
patch.object(LogQueueService, 'enqueue_operation_log', new=enqueue_log),
|
||||
patch('common.annotation.log_annotation.logger.exception') as exception_log,
|
||||
):
|
||||
response = await inbound_controller_module.add_business_inbound(
|
||||
request=request,
|
||||
add_inbound=InboundCreateModel(warehouseId=404, palletIds=[2]),
|
||||
query_db=db,
|
||||
current_user=user,
|
||||
)
|
||||
finally:
|
||||
RequestContext.reset_current_user(user_token)
|
||||
|
||||
payload = json.loads(response.body)
|
||||
self.assertEqual(403, payload['code'])
|
||||
self.assertEqual('无权访问该仓库', payload['msg'])
|
||||
self.assertFalse(payload['success'])
|
||||
service.assert_awaited_once()
|
||||
exception_log.assert_not_called()
|
||||
enqueue_log.assert_awaited_once()
|
||||
|
||||
|
||||
class TestInboundController(unittest.TestCase):
|
||||
@staticmethod
|
||||
def _routes() -> dict[tuple[str, str], APIRoute]:
|
||||
|
||||
Reference in New Issue
Block a user