更新登录页及优化相关功能
This commit is contained in:
@@ -32,7 +32,7 @@ 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 BizCustomerSite, BizPallet, BizProductionLine
|
||||
from module_business.entity.do.master_do import BizCustomerSite, BizPallet, BizProductionLine, BizTunnel
|
||||
from module_business.entity.do.operation_do import (
|
||||
BizFillingRecord,
|
||||
BizPalletEvent,
|
||||
@@ -43,6 +43,7 @@ from module_business.entity.vo.master_vo import LinePageQueryModel
|
||||
from module_business.entity.vo.operation_vo import (
|
||||
BusinessOptionModel,
|
||||
PalletScanResponseModel,
|
||||
TunnelEntrySuccessResponseModel,
|
||||
UsageCreateModel,
|
||||
UsageModel,
|
||||
UsagePageQueryModel,
|
||||
@@ -82,12 +83,14 @@ def integrity_error() -> IntegrityError:
|
||||
def make_page(
|
||||
*,
|
||||
qr_token: str = 'qr-2',
|
||||
tunnel_id: int = 9,
|
||||
line_id: int = 4,
|
||||
used_packages: int = 3,
|
||||
idempotency_key: str = 'usage-key',
|
||||
) -> UsageCreateModel:
|
||||
return UsageCreateModel(
|
||||
qrToken=qr_token,
|
||||
tunnelId=tunnel_id,
|
||||
lineId=line_id,
|
||||
usedPackages=used_packages,
|
||||
idempotencyKey=idempotency_key,
|
||||
@@ -98,7 +101,7 @@ def make_pallet(
|
||||
pallet_id: int = 2,
|
||||
*,
|
||||
qr_token: str = 'qr-2',
|
||||
capacity_packages: int = 20,
|
||||
capacity_packages: int = 100,
|
||||
asset_status: str = '0',
|
||||
test_batch_id: int | None = None,
|
||||
) -> BizPallet:
|
||||
@@ -116,6 +119,8 @@ def make_inventory(
|
||||
pallet_id: int = 2,
|
||||
*,
|
||||
site_id: int | None = 9,
|
||||
tunnel_id: int | None = 9,
|
||||
trace_no: str | None = 'TRACE-9',
|
||||
current_line_id: int | None = None,
|
||||
active_filling_id: int | None = 31,
|
||||
remaining_packages: int = 8,
|
||||
@@ -128,6 +133,8 @@ def make_inventory(
|
||||
active_filling_id=active_filling_id,
|
||||
warehouse_id=None,
|
||||
site_id=site_id,
|
||||
tunnel_id=tunnel_id,
|
||||
trace_no=trace_no,
|
||||
current_line_id=current_line_id,
|
||||
remaining_packages=remaining_packages,
|
||||
flow_state=flow_state,
|
||||
@@ -155,7 +162,7 @@ def make_filling(
|
||||
strain_name_snapshot='香菇 11',
|
||||
batch_no='B-20260726',
|
||||
package_spec='20kg',
|
||||
initial_packages=20,
|
||||
initial_packages=100,
|
||||
active_slot=active_slot,
|
||||
closed_at=None,
|
||||
idempotency_key='fill-key',
|
||||
@@ -182,6 +189,25 @@ def make_site(
|
||||
)
|
||||
|
||||
|
||||
def make_tunnel(
|
||||
tunnel_id: int = 9,
|
||||
*,
|
||||
site_id: int = 9,
|
||||
status: str = '0',
|
||||
current_trace_no: str | None = 'TRACE-9',
|
||||
test_batch_id: int | None = None,
|
||||
) -> BizTunnel:
|
||||
return BizTunnel(
|
||||
tunnel_id=tunnel_id,
|
||||
site_id=site_id,
|
||||
tunnel_code=f'T-{tunnel_id}',
|
||||
tunnel_name=f'隧道 {tunnel_id}',
|
||||
current_trace_no=current_trace_no,
|
||||
status=status,
|
||||
test_batch_id=test_batch_id,
|
||||
)
|
||||
|
||||
|
||||
def make_line(
|
||||
line_id: int = 4,
|
||||
*,
|
||||
@@ -214,6 +240,9 @@ def make_usage(
|
||||
pallet_id=2,
|
||||
active_filling_id=31,
|
||||
site_id=site_id,
|
||||
tunnel_id=9,
|
||||
trace_no='TRACE-9',
|
||||
batch_no_snapshot='B-20260726',
|
||||
line_id=line_id,
|
||||
used_packages=3,
|
||||
remaining_packages_after=5,
|
||||
@@ -246,7 +275,9 @@ class TestUsagePublicContract(unittest.TestCase):
|
||||
'get_usage_list_services',
|
||||
'usage_detail_services',
|
||||
'get_line_options_services',
|
||||
'get_tunnel_options_services',
|
||||
'scan_usage_pallet_services',
|
||||
'enter_tunnel_services',
|
||||
'record_usage_services',
|
||||
):
|
||||
with self.subTest(method=method_name):
|
||||
@@ -257,7 +288,9 @@ class TestUsagePublicContract(unittest.TestCase):
|
||||
'get_usage_list_services',
|
||||
'usage_detail_services',
|
||||
'get_line_options_services',
|
||||
'get_tunnel_options_services',
|
||||
'scan_usage_pallet_services',
|
||||
'enter_tunnel_services',
|
||||
'record_usage_services',
|
||||
}
|
||||
self.assertEqual(expected, {name for name in expected if hasattr(UsageService, name)})
|
||||
@@ -276,7 +309,7 @@ class TestUsagePublicContract(unittest.TestCase):
|
||||
page = make_page(qr_token=' qr-2 ', idempotency_key='key-a')
|
||||
expected = hashlib.sha256(
|
||||
json.dumps(
|
||||
{'line_id': 4, 'qr_token': 'qr-2', 'used_packages': 3},
|
||||
{'line_id': 4, 'qr_token': 'qr-2', 'tunnel_id': 9, 'used_packages': 3},
|
||||
sort_keys=True,
|
||||
separators=(',', ':'),
|
||||
).encode()
|
||||
@@ -511,6 +544,7 @@ class TestUsageReadServices(unittest.IsolatedAsyncioTestCase):
|
||||
with (
|
||||
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=pallet)),
|
||||
patch.object(MasterDao, 'get_pallet_inventory', new=AsyncMock(return_value=inventory)),
|
||||
patch.object(MasterDao, 'get_tunnel', new=AsyncMock(return_value=make_tunnel())),
|
||||
patch.object(AccessService, 'assert_site_access', new=access),
|
||||
patch.object(OperationDao, 'get_filling_by_id', new=get_filling),
|
||||
patch.object(InventoryDao, 'get_inventory_for_update', new=inventory_lock),
|
||||
@@ -601,18 +635,20 @@ class TestUsageControllerContract(unittest.TestCase):
|
||||
@staticmethod
|
||||
def _routes() -> dict[tuple[str, str], APIRoute]:
|
||||
return {
|
||||
(route.path, next(iter(route.methods))): route
|
||||
(route.path_format, next(iter(route.methods))): route
|
||||
for route in usage_controller.routes
|
||||
if isinstance(route, APIRoute)
|
||||
}
|
||||
|
||||
def test_openapi_has_exact_five_operations_aliases_and_response_models(self) -> None:
|
||||
def test_openapi_has_exact_seven_operations_aliases_and_response_models(self) -> None:
|
||||
app = FastAPI()
|
||||
app.include_router(usage_controller)
|
||||
schema = app.openapi()
|
||||
expected = {
|
||||
('/business/usage/list', 'get'),
|
||||
('/business/usage/line-options', 'get'),
|
||||
('/business/usage/tunnel-options', 'get'),
|
||||
('/business/usage/enter-tunnel', 'post'),
|
||||
('/business/usage/scan/{qr_token}', 'get'),
|
||||
('/business/usage/{usage_id}', 'get'),
|
||||
('/business/usage', 'post'),
|
||||
@@ -622,6 +658,13 @@ class TestUsageControllerContract(unittest.TestCase):
|
||||
{'siteId'},
|
||||
{parameter['name'] for parameter in schema['paths']['/business/usage/line-options']['get']['parameters']},
|
||||
)
|
||||
self.assertEqual(
|
||||
{'siteId'},
|
||||
{
|
||||
parameter['name']
|
||||
for parameter in schema['paths']['/business/usage/tunnel-options']['get']['parameters']
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
{'qr_token'},
|
||||
{
|
||||
@@ -636,6 +679,14 @@ class TestUsageControllerContract(unittest.TestCase):
|
||||
DataResponseModel[list[BusinessOptionModel]],
|
||||
routes[('/business/usage/line-options', 'GET')].response_model,
|
||||
)
|
||||
self.assertEqual(
|
||||
DataResponseModel[list[BusinessOptionModel]],
|
||||
routes[('/business/usage/tunnel-options', 'GET')].response_model,
|
||||
)
|
||||
self.assertEqual(
|
||||
DataResponseModel[TunnelEntrySuccessResponseModel],
|
||||
routes[('/business/usage/enter-tunnel', 'POST')].response_model,
|
||||
)
|
||||
self.assertEqual(
|
||||
DataResponseModel[PalletScanResponseModel],
|
||||
routes[('/business/usage/scan/{qr_token}', 'GET')].response_model,
|
||||
@@ -662,15 +713,19 @@ class TestUsageControllerContract(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_static_routes_precede_dynamic_detail(self) -> None:
|
||||
paths = [route.path for route in usage_controller.routes]
|
||||
paths = [route.path_format for route in usage_controller.routes]
|
||||
detail_index = paths.index('/business/usage/{usage_id}')
|
||||
self.assertLess(paths.index('/business/usage/line-options'), detail_index)
|
||||
self.assertLess(paths.index('/business/usage/tunnel-options'), detail_index)
|
||||
self.assertLess(paths.index('/business/usage/enter-tunnel'), detail_index)
|
||||
self.assertLess(paths.index('/business/usage/scan/{qr_token}'), detail_index)
|
||||
|
||||
def test_routes_have_exact_three_permissions_current_user_and_only_post_log(self) -> None:
|
||||
expected = {
|
||||
('/business/usage/list', 'GET'): 'business:usage:list',
|
||||
('/business/usage/line-options', 'GET'): 'business:usage:query',
|
||||
('/business/usage/tunnel-options', 'GET'): 'business:usage:query',
|
||||
('/business/usage/enter-tunnel', 'POST'): 'business:usage:add',
|
||||
('/business/usage/scan/{qr_token}', 'GET'): 'business:usage:query',
|
||||
('/business/usage/{usage_id}', 'GET'): 'business:usage:query',
|
||||
('/business/usage', 'POST'): 'business:usage:add',
|
||||
@@ -691,7 +746,12 @@ class TestUsageControllerContract(unittest.TestCase):
|
||||
),
|
||||
key,
|
||||
)
|
||||
self.assertEqual(key == ('/business/usage', 'POST'), hasattr(route.endpoint, '__wrapped__'))
|
||||
expected_logged = {
|
||||
('/business/usage/enter-tunnel', 'POST'),
|
||||
('/business/usage/scan/{qr_token}', 'GET'),
|
||||
('/business/usage', 'POST'),
|
||||
}
|
||||
self.assertEqual(key in expected_logged, hasattr(route.endpoint, '__wrapped__'))
|
||||
actual[key] = permissions[0].perm
|
||||
|
||||
self.assertEqual(expected, actual)
|
||||
@@ -763,6 +823,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
|
||||
self.inventory = make_inventory()
|
||||
self.filling = make_filling()
|
||||
self.site = make_site()
|
||||
self.tunnel = make_tunnel()
|
||||
self.line = make_line()
|
||||
self.fixed_time = datetime(2026, 7, 26, 11, 30)
|
||||
|
||||
@@ -808,6 +869,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
|
||||
pallet_lock = AsyncMock(return_value=[selected_pallet])
|
||||
filling_lock = AsyncMock(return_value=selected_filling)
|
||||
site_lock = AsyncMock(return_value=selected_site)
|
||||
tunnel_read = AsyncMock(return_value=self.tunnel)
|
||||
preflight_line_read = AsyncMock(return_value=selected_line)
|
||||
line_lock = AsyncMock(return_value=selected_line)
|
||||
|
||||
@@ -830,6 +892,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
|
||||
stack.enter_context(patch.object(InventoryDao, 'get_pallets_by_ids', new=pallet_lock))
|
||||
stack.enter_context(patch.object(OperationDao, 'get_active_filling_for_update', new=filling_lock))
|
||||
stack.enter_context(patch.object(MasterDao, 'get_site', new=site_lock))
|
||||
stack.enter_context(patch.object(MasterDao, 'get_tunnel', new=tunnel_read))
|
||||
stack.enter_context(patch.object(MasterDao, 'get_line', new=AsyncMock(side_effect=read_line)))
|
||||
stack.enter_context(patch.object(OperationDao, 'add_usage_record', new=usage_write))
|
||||
stack.enter_context(patch.object(OperationDao, 'update_filling_record', new=filling_write))
|
||||
@@ -845,6 +908,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
|
||||
'pallet_lock': pallet_lock,
|
||||
'filling_lock': filling_lock,
|
||||
'site_lock': site_lock,
|
||||
'tunnel_read': tunnel_read,
|
||||
'preflight_line_read': preflight_line_read,
|
||||
'line_lock': line_lock,
|
||||
'add_usage': usage_write,
|
||||
@@ -955,6 +1019,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(side_effect=lock_pallets)),
|
||||
patch.object(OperationDao, 'get_active_filling_for_update', new=AsyncMock(side_effect=lock_filling)),
|
||||
patch.object(MasterDao, 'get_site', new=AsyncMock(side_effect=lock_site)),
|
||||
patch.object(MasterDao, 'get_tunnel', new=AsyncMock(return_value=self.tunnel)),
|
||||
patch.object(MasterDao, 'get_line', new=AsyncMock(side_effect=lock_line)),
|
||||
patch.object(OperationDao, 'add_usage_record', new=AsyncMock(side_effect=add_usage)),
|
||||
patch.object(OperationDao, 'update_filling_record', new=update_filling),
|
||||
@@ -1158,6 +1223,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
|
||||
make_inventory(remaining_packages=8),
|
||||
UsageCreateModel.model_construct(
|
||||
qr_token='qr-2',
|
||||
tunnel_id=9,
|
||||
line_id=4,
|
||||
used_packages=True,
|
||||
idempotency_key='usage-key',
|
||||
@@ -1268,6 +1334,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_record_hides_missing_and_non_site_qr_but_admin_gets_precise_error(self) -> None:
|
||||
with (
|
||||
patch.object(AccessDao, 'list_user_site_ids', new=AsyncMock(return_value=[9])),
|
||||
patch.object(MasterDao, 'get_tunnel', new=AsyncMock(return_value=self.tunnel)),
|
||||
patch.object(MasterDao, 'get_line', new=AsyncMock(return_value=make_line())),
|
||||
patch.object(OperationDao, 'get_usage_by_idempotency_key', new=AsyncMock(return_value=None)),
|
||||
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=None)),
|
||||
@@ -1279,6 +1346,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
self.db.reset_mock()
|
||||
with (
|
||||
patch.object(MasterDao, 'get_tunnel', new=AsyncMock(return_value=self.tunnel)),
|
||||
patch.object(MasterDao, 'get_line', new=AsyncMock(return_value=make_line())),
|
||||
patch.object(OperationDao, 'get_usage_by_idempotency_key', new=AsyncMock(return_value=None)),
|
||||
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=None)),
|
||||
@@ -1308,6 +1376,14 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
|
||||
self.core.db = self.db
|
||||
self.core.user = self.user
|
||||
self.core.page = self.page
|
||||
self.tunnel = self.core.tunnel
|
||||
self._tunnel_patcher = patch.object(
|
||||
MasterDao,
|
||||
'get_tunnel',
|
||||
new=AsyncMock(return_value=self.tunnel),
|
||||
)
|
||||
self._tunnel_patcher.start()
|
||||
self.addCleanup(self._tunnel_patcher.stop)
|
||||
|
||||
def _exact_usage(self, *, request_hash: str | None = None) -> BizUsageRecord:
|
||||
return make_usage(
|
||||
|
||||
Reference in New Issue
Block a user