更新登录页及优化相关功能
This commit is contained in:
@@ -21,10 +21,14 @@ CONTROLLER_NAMES = (
|
||||
'inbound',
|
||||
'filling',
|
||||
'shipment',
|
||||
'shipment_receipt',
|
||||
'usage',
|
||||
'usage_fifo',
|
||||
'return',
|
||||
'report',
|
||||
'test_batch',
|
||||
'dashboard',
|
||||
'tunnel',
|
||||
)
|
||||
TARGET_PREFIXES = (
|
||||
'/master',
|
||||
@@ -43,6 +47,7 @@ EXPECTED_PAGE_PERMISSIONS = frozenset(
|
||||
'master:line:list',
|
||||
'master:warehouse:list',
|
||||
'master:pallet:list',
|
||||
'master:tunnel:list',
|
||||
'business:inbound:list',
|
||||
'business:filling:list',
|
||||
'business:shipment:list',
|
||||
@@ -80,6 +85,10 @@ EXPECTED_BUTTON_PERMISSIONS = frozenset(
|
||||
'master:pallet:add',
|
||||
'master:pallet:edit',
|
||||
'master:pallet:print',
|
||||
'master:tunnel:query',
|
||||
'master:tunnel:add',
|
||||
'master:tunnel:edit',
|
||||
'master:tunnel:remove',
|
||||
'business:inbound:query',
|
||||
'business:inbound:add',
|
||||
'business:inbound:edit',
|
||||
@@ -90,6 +99,7 @@ EXPECTED_BUTTON_PERMISSIONS = frozenset(
|
||||
'business:shipment:add',
|
||||
'business:shipment:edit',
|
||||
'business:shipment:confirm',
|
||||
'business:shipment:receive',
|
||||
'business:usage:query',
|
||||
'business:usage:add',
|
||||
'business:return:query',
|
||||
@@ -108,7 +118,10 @@ EXPECTED_PERMISSIONS = EXPECTED_PAGE_PERMISSIONS | EXPECTED_BUTTON_PERMISSIONS
|
||||
EXPECTED_WEB_PERMISSIONS = frozenset(
|
||||
permission
|
||||
for permission in EXPECTED_PERMISSIONS
|
||||
if not permission.startswith('system:testbatch:') or permission == 'system:testbatch:adopt'
|
||||
if (
|
||||
(not permission.startswith('system:testbatch:') or permission == 'system:testbatch:adopt')
|
||||
and permission != 'master:tunnel:remove'
|
||||
)
|
||||
)
|
||||
VUE_PAGE_PATHS = (
|
||||
'src/views/master/strain/index.vue',
|
||||
@@ -117,6 +130,7 @@ VUE_PAGE_PATHS = (
|
||||
'src/views/master/line/index.vue',
|
||||
'src/views/master/warehouse/index.vue',
|
||||
'src/views/master/pallet/index.vue',
|
||||
'src/views/master/tunnel/index.vue',
|
||||
'src/views/business/inbound/index.vue',
|
||||
'src/views/business/filling/index.vue',
|
||||
'src/views/business/shipment/index.vue',
|
||||
@@ -130,7 +144,10 @@ VUE_PAGE_PATHS = (
|
||||
'src/views/report/palletTrace/index.vue',
|
||||
)
|
||||
PERMISSION_LITERAL_PATTERN = re.compile(r'(?:master|business|report|system):[a-z]+:[a-z]+')
|
||||
UNGUARDED_AUTHENTICATED_ROUTES = {('/system/test-batch/options', 'GET')}
|
||||
UNGUARDED_AUTHENTICATED_ROUTES = {
|
||||
('/system/test-batch/options', 'GET'),
|
||||
('/business/dashboard/scan-exceptions', 'GET'),
|
||||
}
|
||||
REMOVED_DELETE_SYMBOLS = (
|
||||
'delStrain',
|
||||
'delCustomer',
|
||||
@@ -170,6 +187,16 @@ def _build_expected_route_permissions() -> dict[tuple[str, str], str]:
|
||||
}
|
||||
)
|
||||
|
||||
expected.update(
|
||||
{
|
||||
('/master/tunnel/list', 'GET'): 'master:tunnel:list',
|
||||
('/master/tunnel/{tunnel_id}', 'GET'): 'master:tunnel:query',
|
||||
('/master/tunnel', 'POST'): 'master:tunnel:add',
|
||||
('/master/tunnel', 'PUT'): 'master:tunnel:edit',
|
||||
('/master/tunnel/{tunnel_ids}', 'DELETE'): 'master:tunnel:remove',
|
||||
}
|
||||
)
|
||||
|
||||
expected.update(
|
||||
{
|
||||
('/master/site/{site_id}/users', 'GET'): 'master:site:assign',
|
||||
@@ -195,12 +222,20 @@ def _build_expected_route_permissions() -> dict[tuple[str, str], str]:
|
||||
('/business/shipment/site-options', 'GET'): 'business:shipment:query',
|
||||
('/business/shipment/scan/{qr_token}', 'GET'): 'business:shipment:query',
|
||||
('/business/shipment/{shipment_id}', 'GET'): 'business:shipment:query',
|
||||
('/business/shipment/{shipment_id}/labels', 'GET'): 'business:shipment:query',
|
||||
('/business/shipment-receipt/list', 'GET'): 'business:shipment:receive',
|
||||
('/business/shipment-receipt/{shipment_id}', 'GET'): 'business:shipment:receive',
|
||||
('/business/shipment-receipt/{shipment_id}/scan/{identifier}', 'GET'): 'business:shipment:receive',
|
||||
('/business/shipment-receipt/{shipment_id}/confirm', 'PUT'): 'business:shipment:receive',
|
||||
('/business/usage-fifo/recommendation', 'GET'): 'business:usage:query',
|
||||
('/business/shipment', 'POST'): 'business:shipment:add',
|
||||
('/business/shipment', 'PUT'): 'business:shipment:edit',
|
||||
('/business/shipment/{shipment_ids}', 'DELETE'): 'business:shipment:list',
|
||||
('/business/shipment/{shipment_id}/confirm', 'PUT'): 'business:shipment:confirm',
|
||||
('/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',
|
||||
@@ -249,6 +284,7 @@ STATIC_READS_BEFORE_DETAIL = {
|
||||
),
|
||||
'/business/usage/{usage_id}': (
|
||||
'/business/usage/line-options',
|
||||
'/business/usage/tunnel-options',
|
||||
'/business/usage/scan/{qr_token}',
|
||||
),
|
||||
'/business/return/{return_id}': (
|
||||
@@ -264,6 +300,7 @@ EXACT_QUERY_PARAMETERS = {
|
||||
('/business/inbound/scan/{qr_token}', 'GET'): {'warehouseId'},
|
||||
('/business/shipment/scan/{qr_token}', 'GET'): {'sourceWarehouseId', 'targetSiteId'},
|
||||
('/business/usage/line-options', 'GET'): {'siteId'},
|
||||
('/business/usage/tunnel-options', 'GET'): {'siteId'},
|
||||
('/business/return/scan/{qr_token}', 'GET'): {'mode', 'returnId'},
|
||||
}
|
||||
REQUIRED_QUERY_PARAMETERS = {
|
||||
@@ -286,7 +323,7 @@ def _auth_dependencies(route: APIRoute) -> list[CheckUserInterfaceAuth]:
|
||||
|
||||
def _assert_controller_permission_literals(test_case: unittest.TestCase, literals: list[str]) -> None:
|
||||
expected = Counter(EXPECTED_ROUTE_PERMISSIONS.values())
|
||||
test_case.assertEqual(85, expected.total())
|
||||
test_case.assertEqual(98, expected.total())
|
||||
test_case.assertEqual(expected, Counter(literals))
|
||||
test_case.assertEqual(EXPECTED_PERMISSIONS, frozenset(expected))
|
||||
|
||||
@@ -409,7 +446,7 @@ class ApiPermissionContractTest(unittest.TestCase):
|
||||
cls.app = create_app()
|
||||
cls.schema = cls.app.openapi()
|
||||
cls.target_routes = [
|
||||
route for route in cls.app.routes if isinstance(route, APIRoute) and _is_target_path(route.path)
|
||||
route for route in cls.app.routes if isinstance(route, APIRoute) and _is_target_path(route.path_format)
|
||||
]
|
||||
|
||||
def test_target_path_selector_uses_namespace_boundaries(self) -> None:
|
||||
@@ -438,11 +475,11 @@ class ApiPermissionContractTest(unittest.TestCase):
|
||||
}
|
||||
|
||||
self.assertEqual(set(EXPECTED_ROUTE_PERMISSIONS) | UNGUARDED_AUTHENTICATED_ROUTES, actual_operations)
|
||||
self.assertEqual(86, len(actual_operations))
|
||||
self.assertEqual(100, len(actual_operations))
|
||||
|
||||
openapi_ai_paths = sorted(path for path in self.schema['paths'] if path == '/ai' or path.startswith('/ai/'))
|
||||
app_ai_paths = sorted(
|
||||
route.path
|
||||
route.path_format
|
||||
for route in self.app.routes
|
||||
if getattr(route, 'path', None) == '/ai' or getattr(route, 'path', '').startswith('/ai/')
|
||||
)
|
||||
@@ -454,8 +491,8 @@ class ApiPermissionContractTest(unittest.TestCase):
|
||||
operation_keys: list[tuple[str, str]] = []
|
||||
for route in self.target_routes:
|
||||
methods = route.methods & ROUTE_HTTP_METHODS
|
||||
self.assertEqual(1, len(methods), route.path)
|
||||
key = (route.path, next(iter(methods)))
|
||||
self.assertEqual(1, len(methods), route.path_format)
|
||||
key = (route.path_format, next(iter(methods)))
|
||||
operation_keys.append(key)
|
||||
|
||||
auth_dependencies = _auth_dependencies(route)
|
||||
@@ -491,19 +528,33 @@ class ApiPermissionContractTest(unittest.TestCase):
|
||||
literals.append(node.args[0].value)
|
||||
|
||||
self.assertEqual([], malformed_calls)
|
||||
self.assertEqual(18, len(EXPECTED_PAGE_PERMISSIONS))
|
||||
self.assertEqual(43, len(EXPECTED_BUTTON_PERMISSIONS))
|
||||
self.assertEqual(19, len(EXPECTED_PAGE_PERMISSIONS))
|
||||
self.assertEqual(48, len(EXPECTED_BUTTON_PERMISSIONS))
|
||||
self.assertFalse(EXPECTED_PAGE_PERMISSIONS & EXPECTED_BUTTON_PERMISSIONS)
|
||||
self.assertEqual(61, len(EXPECTED_PERMISSIONS))
|
||||
self.assertEqual(67, len(EXPECTED_PERMISSIONS))
|
||||
_assert_controller_permission_literals(self, literals)
|
||||
self.assertEqual(
|
||||
{'add', 'adopt', 'assign', 'close', 'confirm', 'edit', 'list', 'print', 'purge', 'query', 'request'},
|
||||
{
|
||||
'add',
|
||||
'adopt',
|
||||
'assign',
|
||||
'close',
|
||||
'confirm',
|
||||
'edit',
|
||||
'list',
|
||||
'print',
|
||||
'purge',
|
||||
'query',
|
||||
'remove',
|
||||
'request',
|
||||
'receive',
|
||||
},
|
||||
{permission.rsplit(':', maxsplit=1)[1] for permission in literals},
|
||||
)
|
||||
|
||||
def test_controller_ast_permission_contract_rejects_duplicate_call(self) -> None:
|
||||
literals = list(EXPECTED_ROUTE_PERMISSIONS.values())
|
||||
self.assertEqual(85, len(literals))
|
||||
self.assertEqual(98, len(literals))
|
||||
literals.append(literals[0])
|
||||
|
||||
with self.assertRaises(AssertionError):
|
||||
@@ -521,8 +572,8 @@ class ApiPermissionContractTest(unittest.TestCase):
|
||||
|
||||
def test_vue_pages_have_exact_guarded_permission_contract(self) -> None:
|
||||
actual_permissions = set()
|
||||
self.assertEqual(17, len(VUE_PAGE_PATHS))
|
||||
self.assertEqual(17, len(set(VUE_PAGE_PATHS)))
|
||||
self.assertEqual(18, len(VUE_PAGE_PATHS))
|
||||
self.assertEqual(18, len(set(VUE_PAGE_PATHS)))
|
||||
for relative_path in VUE_PAGE_PATHS:
|
||||
source = (FRONTEND_ROOT / relative_path).read_text(encoding='utf-8')
|
||||
guarded = _extract_guarded_vue_permissions(source)
|
||||
@@ -663,7 +714,7 @@ class ApiPermissionContractTest(unittest.TestCase):
|
||||
|
||||
def test_static_scan_and_option_routes_precede_dynamic_details(self) -> None:
|
||||
route_order = {
|
||||
route.path: index
|
||||
route.path_format: index
|
||||
for index, route in enumerate(self.app.routes)
|
||||
if isinstance(route, APIRoute) and 'GET' in route.methods
|
||||
}
|
||||
@@ -705,9 +756,9 @@ class ApiPermissionContractTest(unittest.TestCase):
|
||||
actual_write_routes: dict[tuple[str, str], APIRoute] = {}
|
||||
for route in self.target_routes:
|
||||
for method in route.methods & WRITE_METHODS:
|
||||
actual_write_routes[(route.path, method)] = route
|
||||
actual_write_routes[(route.path_format, method)] = route
|
||||
|
||||
self.assertEqual(41, len(expected_write_permissions))
|
||||
self.assertEqual(46, len(expected_write_permissions))
|
||||
self.assertEqual(set(expected_write_permissions), set(actual_write_routes))
|
||||
for key, expected_permission in expected_write_permissions.items():
|
||||
with self.subTest(operation=key):
|
||||
|
||||
Reference in New Issue
Block a user