|
|
@@ -0,0 +1,496 @@
|
|
|
+import importlib
|
|
|
+import io
|
|
|
+import unittest
|
|
|
+
|
|
|
+from app import GatewayApp
|
|
|
+from public_gateway import PublicGatewayApp
|
|
|
+from services.output_presenter import OutputPresenter
|
|
|
+
|
|
|
+
|
|
|
+PROGRESS_COLUMNS = [
|
|
|
+ ('bl_number', '提单号'), ('container_code', '柜号'),
|
|
|
+ ('container_type', '柜型'), ('warehouse_name', '仓库名称'),
|
|
|
+ ('providers_name', '拖车行'), ('trailer_status', '提柜状态'),
|
|
|
+ ('latest_remark', '最新操作备注'), ('is_direct_send', '直送'),
|
|
|
+ ('cabinet_type', '整拼类型'), ('vessel_name', '船名航次'),
|
|
|
+ ('route', '航线'), ('clearance_check', '清关查验'),
|
|
|
+ ('etd', 'ETD'), ('atd', 'ATD'), ('eta', 'ETA'), ('ata', 'ATA'),
|
|
|
+ ('loading_port', '起运港'), ('destination_port', '目的港'),
|
|
|
+ ('wharf_name', '到港码头'), ('appt_time', 'APPT时间'),
|
|
|
+ ('delivery_end_time', '卡车实际派送时间'),
|
|
|
+ ('est_pickup_time', '预计提柜时间'), ('pickup_time', '实际提柜时间'),
|
|
|
+ ('inbound_date', '到仓时间'), ('container_return_time', '还柜时间'),
|
|
|
+ ('pickup_prescription', '提柜时效'), ('return_prescription', '还柜时效'),
|
|
|
+ ('wharf_wait_time', '码头等待时间'), ('amazon_wait_time', '亚马逊等待时间'),
|
|
|
+ ('bill_status', '应付费用状态'),
|
|
|
+]
|
|
|
+DO_COLUMNS = [
|
|
|
+ ('trailer_number', '拖车单号'), ('outbound_number', '出库单号'),
|
|
|
+ ('container_code', '柜号'), ('container_type', '柜型'),
|
|
|
+ ('bl_number', '提单号'), ('is_direct_send', '是否直送'),
|
|
|
+ ('original_do_file', '原DO单'), ('new_do_file', '新DO单'),
|
|
|
+ ('email_subject', '邮件主题'), ('email_from', '邮件发送人'),
|
|
|
+ ('received_at', '邮件接收时间'),
|
|
|
+]
|
|
|
+FILTER_TYPES = [
|
|
|
+ '海外提柜类型', '拖车行', '海外仓', '直送柜', '应付费用', '整拼类型', '运输方式',
|
|
|
+]
|
|
|
+STAGES = [
|
|
|
+ '待安排拖车', '待提柜', '待还柜(海)', '已还柜(海)', '已提货(空)', 'DO单制作',
|
|
|
+]
|
|
|
+
|
|
|
+
|
|
|
+class RecordingApiClient:
|
|
|
+ def __init__(self):
|
|
|
+ self.calls = []
|
|
|
+
|
|
|
+ def list_enabled_tools(self, request_id=''):
|
|
|
+ return {
|
|
|
+ 'code': 'MCP_0000',
|
|
|
+ 'data': {
|
|
|
+ 'tool_codes': [
|
|
|
+ 'query_destination_trailer_list',
|
|
|
+ 'list_destination_trailer_filter_options',
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ def call_tool(self, tool_code, route_path, payload, request_id):
|
|
|
+ self.calls.append((tool_code, route_path, payload, request_id))
|
|
|
+ return {'code': 'MCP_0000', 'data': {}, 'meta': {}}
|
|
|
+
|
|
|
+
|
|
|
+class DestinationTrailerToolContractTest(unittest.TestCase):
|
|
|
+ def tool_class(self, module_name, class_name):
|
|
|
+ return getattr(importlib.import_module('tools.' + module_name), class_name)
|
|
|
+
|
|
|
+ def test_query_schema_requires_stage_and_forbids_identity(self):
|
|
|
+ cls = self.tool_class(
|
|
|
+ 'query_destination_trailer_list', 'QueryDestinationTrailerListTool'
|
|
|
+ )
|
|
|
+ metadata = cls().metadata()
|
|
|
+ schema = metadata['input_schema']
|
|
|
+ self.assertEqual('query_destination_trailer_list', metadata['name'])
|
|
|
+ self.assertEqual('/mcp/tools/queryDestinationTrailerList', cls.route_path)
|
|
|
+ self.assertFalse(schema['additionalProperties'])
|
|
|
+ self.assertEqual(['trailer_stage'], schema['required'])
|
|
|
+ self.assertEqual(STAGES, schema['properties']['trailer_stage']['enum'])
|
|
|
+ self.assertNotIn('default', schema['properties']['trailer_stage'])
|
|
|
+ for forbidden in ('company_id', 'admin_id', 'is_super', 'outbound_id'):
|
|
|
+ self.assertNotIn(forbidden, schema['properties'])
|
|
|
+ self.assertIn('使用场景:', metadata['description'])
|
|
|
+ self.assertIn('禁止使用:', metadata['description'])
|
|
|
+ self.assertIn('号码类型不明确时必须先询问用户', metadata['description'])
|
|
|
+ self.assertIn('不得根据号码格式猜测', metadata['description'])
|
|
|
+ self.assertIn('不得跨字段或跨工具试查', metadata['description'])
|
|
|
+ self.assertIn('query_outbound_list', metadata['description'])
|
|
|
+ self.assertIn('提单号、柜号、日期和其他筛选均为可选', metadata['description'])
|
|
|
+ self.assertNotIn('必须再有', metadata['description'])
|
|
|
+
|
|
|
+ def test_query_forwards_numbers_dates_and_rejects_invalid(self):
|
|
|
+ cls = self.tool_class(
|
|
|
+ 'query_destination_trailer_list', 'QueryDestinationTrailerListTool'
|
|
|
+ )
|
|
|
+ client = RecordingApiClient()
|
|
|
+ cls(client).call(
|
|
|
+ trailer_stage='待提柜',
|
|
|
+ bl_numbers=[' BL-1 ', 'BL-1'],
|
|
|
+ container_codes=['CONT-1'],
|
|
|
+ providers_id=8,
|
|
|
+ warehouse_id=3,
|
|
|
+ is_direct_send=0,
|
|
|
+ bill_status=10,
|
|
|
+ cabinet_type=1,
|
|
|
+ shipping_method=2,
|
|
|
+ eta_start='2026-07-01',
|
|
|
+ eta_end='2026-07-31',
|
|
|
+ page=2,
|
|
|
+ limit=30,
|
|
|
+ request_id='rq_dt',
|
|
|
+ )
|
|
|
+ self.assertEqual(
|
|
|
+ {
|
|
|
+ 'trailer_stage': '待提柜',
|
|
|
+ 'bl_numbers': ['BL-1'],
|
|
|
+ 'container_codes': ['CONT-1'],
|
|
|
+ 'providers_id': 8,
|
|
|
+ 'warehouse_id': 3,
|
|
|
+ 'is_direct_send': 0,
|
|
|
+ 'bill_status': 10,
|
|
|
+ 'cabinet_type': 1,
|
|
|
+ 'shipping_method': 2,
|
|
|
+ 'eta_start': '2026-07-01',
|
|
|
+ 'eta_end': '2026-07-31',
|
|
|
+ 'page': 2,
|
|
|
+ 'limit': 30,
|
|
|
+ },
|
|
|
+ client.calls[-1][2],
|
|
|
+ )
|
|
|
+ self.assertEqual(
|
|
|
+ '/mcp/tools/queryDestinationTrailerList',
|
|
|
+ client.calls[-1][1],
|
|
|
+ )
|
|
|
+ cls(client).call(trailer_stage='待提柜')
|
|
|
+ self.assertEqual(
|
|
|
+ {
|
|
|
+ 'trailer_stage': '待提柜',
|
|
|
+ 'page': 1,
|
|
|
+ 'limit': 20,
|
|
|
+ },
|
|
|
+ client.calls[-1][2],
|
|
|
+ )
|
|
|
+ cls(client).call(trailer_stage='待提柜', providers_id=8)
|
|
|
+ with self.assertRaises(RuntimeError):
|
|
|
+ cls().call(trailer_stage='待提柜')
|
|
|
+ invalid = (
|
|
|
+ {'trailer_stage': '全部', 'bl_numbers': ['BL-1']},
|
|
|
+ {'trailer_stage': '待提柜', 'eta_start': '2026-07-01'},
|
|
|
+ {
|
|
|
+ 'trailer_stage': '待提柜',
|
|
|
+ 'eta_start': '2026-07-01',
|
|
|
+ 'eta_end': '2026-08-01',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'trailer_stage': 'DO单制作',
|
|
|
+ 'bl_numbers': ['BL-1'],
|
|
|
+ 'providers_id': 8,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 'trailer_stage': 'DO单制作',
|
|
|
+ 'eta_start': '2026-07-01',
|
|
|
+ 'eta_end': '2026-07-02',
|
|
|
+ },
|
|
|
+ )
|
|
|
+ for arguments in invalid:
|
|
|
+ with self.subTest(arguments=arguments):
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ cls(client).call(**arguments)
|
|
|
+
|
|
|
+ def test_filter_schema_and_forwarding(self):
|
|
|
+ cls = self.tool_class(
|
|
|
+ 'list_destination_trailer_filter_options',
|
|
|
+ 'ListDestinationTrailerFilterOptionsTool',
|
|
|
+ )
|
|
|
+ metadata = cls().metadata()
|
|
|
+ schema = metadata['input_schema']
|
|
|
+ self.assertEqual(['filter_type'], schema['required'])
|
|
|
+ self.assertEqual(FILTER_TYPES, schema['properties']['filter_type']['enum'])
|
|
|
+ self.assertIn('使用场景:', metadata['description'])
|
|
|
+ self.assertIn('禁止使用:', metadata['description'])
|
|
|
+ client = RecordingApiClient()
|
|
|
+ cls(client).call('拖车行', keyword='美西', page=2, limit=10)
|
|
|
+ self.assertEqual(
|
|
|
+ {
|
|
|
+ 'filter_type': '拖车行',
|
|
|
+ 'keyword': '美西',
|
|
|
+ 'page': 2,
|
|
|
+ 'limit': 10,
|
|
|
+ },
|
|
|
+ client.calls[-1][2],
|
|
|
+ )
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ cls(client).call('排舱阶段')
|
|
|
+ with self.assertRaises(RuntimeError):
|
|
|
+ cls().call('海外提柜类型')
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ cls(client).call('海外提柜类型', keyword='x' * 101)
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ cls(client).call('海外提柜类型', page=0)
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ cls(client).call('海外提柜类型', limit=True)
|
|
|
+
|
|
|
+ def test_query_covers_remaining_validation_branches(self):
|
|
|
+ cls = self.tool_class(
|
|
|
+ 'query_destination_trailer_list', 'QueryDestinationTrailerListTool'
|
|
|
+ )
|
|
|
+ client = RecordingApiClient()
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ cls(client).call(
|
|
|
+ trailer_stage='待提柜',
|
|
|
+ bl_numbers=['X'] * 201,
|
|
|
+ )
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ cls(client).call(
|
|
|
+ trailer_stage='待提柜',
|
|
|
+ eta_start='2026-07-02',
|
|
|
+ eta_end='2026-07-01',
|
|
|
+ )
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ cls(client).call(
|
|
|
+ trailer_stage='待提柜',
|
|
|
+ bl_numbers=['BL-1'],
|
|
|
+ bill_status=99,
|
|
|
+ )
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ cls(client).call(trailer_stage='待提柜', bl_numbers='BL-1')
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ cls(client).call(trailer_stage='待提柜', bl_numbers=[1])
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ cls(client).call(trailer_stage='待提柜', bl_numbers=[' '])
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ cls(client).call(
|
|
|
+ trailer_stage='待提柜',
|
|
|
+ eta_start=20260701,
|
|
|
+ eta_end='2026-07-02',
|
|
|
+ )
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ cls(client).call(
|
|
|
+ trailer_stage='待提柜',
|
|
|
+ eta_start='2026-99-01',
|
|
|
+ eta_end='2026-07-02',
|
|
|
+ )
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ cls(client).call(
|
|
|
+ trailer_stage='待提柜',
|
|
|
+ eta_start='2026-7-01',
|
|
|
+ eta_end='2026-07-02',
|
|
|
+ )
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ cls(client).call(
|
|
|
+ trailer_stage='待提柜',
|
|
|
+ bl_numbers=['BL-1'],
|
|
|
+ providers_id=True,
|
|
|
+ )
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ cls(client).call(
|
|
|
+ trailer_stage='待提柜',
|
|
|
+ bl_numbers=['BL-1'],
|
|
|
+ warehouse_id=0,
|
|
|
+ )
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ cls(client).call(
|
|
|
+ trailer_stage='待提柜',
|
|
|
+ bl_numbers=['BL-1'],
|
|
|
+ page=0,
|
|
|
+ )
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ cls(client).call(
|
|
|
+ trailer_stage='待提柜',
|
|
|
+ bl_numbers=['BL-1'],
|
|
|
+ limit=True,
|
|
|
+ )
|
|
|
+ cls(client).call(
|
|
|
+ trailer_stage='待安排拖车',
|
|
|
+ pickup_time_start='2026-07-01',
|
|
|
+ pickup_time_end='2026-07-02',
|
|
|
+ providers_id=8,
|
|
|
+ bill_status=20,
|
|
|
+ )
|
|
|
+ cls(client).call(trailer_stage='DO单制作', bl_numbers=['BL-1'])
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ cls(client).call(
|
|
|
+ trailer_stage='待提柜',
|
|
|
+ eta_start='20260701',
|
|
|
+ eta_end='2026-07-02',
|
|
|
+ )
|
|
|
+
|
|
|
+ def test_cli_forwards_optional_trailer_filters(self):
|
|
|
+ client = RecordingApiClient()
|
|
|
+ app = GatewayApp(api_client=client)
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ app.run_cli([
|
|
|
+ 'call', '--tool', 'query_destination_trailer_list',
|
|
|
+ ], stdout=io.StringIO())
|
|
|
+ app.run_cli([
|
|
|
+ 'call', '--tool', 'query_destination_trailer_list',
|
|
|
+ '--trailer-stage', '待还柜(海)',
|
|
|
+ '--container-codes', 'C1',
|
|
|
+ '--providers-id', '8',
|
|
|
+ '--warehouse-id', '3',
|
|
|
+ '--is-direct-send', '1',
|
|
|
+ '--bill-status', '30',
|
|
|
+ '--cabinet-type', '2',
|
|
|
+ '--shipping-method', '2',
|
|
|
+ '--eta-start', '2026-07-01',
|
|
|
+ '--eta-end', '2026-07-02',
|
|
|
+ '--pickup-time-start', '2026-07-03',
|
|
|
+ '--pickup-time-end', '2026-07-04',
|
|
|
+ '--container-return-time-start', '2026-07-05',
|
|
|
+ '--container-return-time-end', '2026-07-06',
|
|
|
+ ], stdout=io.StringIO())
|
|
|
+ self.assertEqual(
|
|
|
+ {
|
|
|
+ 'trailer_stage': '待还柜(海)',
|
|
|
+ 'container_codes': ['C1'],
|
|
|
+ 'providers_id': 8,
|
|
|
+ 'warehouse_id': 3,
|
|
|
+ 'is_direct_send': 1,
|
|
|
+ 'bill_status': 30,
|
|
|
+ 'cabinet_type': 2,
|
|
|
+ 'shipping_method': 2,
|
|
|
+ 'eta_start': '2026-07-01',
|
|
|
+ 'eta_end': '2026-07-02',
|
|
|
+ 'pickup_time_start': '2026-07-03',
|
|
|
+ 'pickup_time_end': '2026-07-04',
|
|
|
+ 'container_return_time_start': '2026-07-05',
|
|
|
+ 'container_return_time_end': '2026-07-06',
|
|
|
+ 'page': 1,
|
|
|
+ 'limit': 20,
|
|
|
+ },
|
|
|
+ client.calls[-1][2],
|
|
|
+ )
|
|
|
+
|
|
|
+ def test_presenter_fails_closed_on_unknown_shape(self):
|
|
|
+ presenter = OutputPresenter()
|
|
|
+ meta = {
|
|
|
+ 'page': 1, 'limit': 20, 'has_more': False, 'request_id': 'rq_x',
|
|
|
+ }
|
|
|
+ self.assertTrue(presenter.present('query_destination_trailer_list', {
|
|
|
+ 'code': 'MCP_0000',
|
|
|
+ 'data': {'trailer_stage': '待提柜', 'columns': [], 'records': [], 'extra': 1},
|
|
|
+ 'meta': meta,
|
|
|
+ })['is_error'])
|
|
|
+ self.assertTrue(presenter.present('query_destination_trailer_list', {
|
|
|
+ 'code': 'MCP_0000',
|
|
|
+ 'data': {
|
|
|
+ 'trailer_stage': '未知',
|
|
|
+ 'columns': [],
|
|
|
+ 'records': [],
|
|
|
+ },
|
|
|
+ 'meta': meta,
|
|
|
+ })['is_error'])
|
|
|
+ self.assertTrue(presenter.present('query_destination_trailer_list', {
|
|
|
+ 'code': 'MCP_0000',
|
|
|
+ 'data': {
|
|
|
+ 'trailer_stage': '待提柜',
|
|
|
+ 'columns': [{'key': 'bl_number', 'name': '提单号'}],
|
|
|
+ 'records': [],
|
|
|
+ },
|
|
|
+ 'meta': meta,
|
|
|
+ })['is_error'])
|
|
|
+ columns = [{'key': key, 'name': name} for key, name in PROGRESS_COLUMNS]
|
|
|
+ columns[0] = {'key': 'bl_number', 'name': '错'}
|
|
|
+ self.assertTrue(presenter.present('query_destination_trailer_list', {
|
|
|
+ 'code': 'MCP_0000',
|
|
|
+ 'data': {
|
|
|
+ 'trailer_stage': '待提柜',
|
|
|
+ 'columns': columns,
|
|
|
+ 'records': [],
|
|
|
+ },
|
|
|
+ 'meta': meta,
|
|
|
+ })['is_error'])
|
|
|
+ record = {key: 'v' for key, _ in PROGRESS_COLUMNS}
|
|
|
+ record['bl_number'] = 1
|
|
|
+ self.assertTrue(presenter.present('query_destination_trailer_list', {
|
|
|
+ 'code': 'MCP_0000',
|
|
|
+ 'data': {
|
|
|
+ 'trailer_stage': '待提柜',
|
|
|
+ 'columns': [
|
|
|
+ {'key': key, 'name': name} for key, name in PROGRESS_COLUMNS
|
|
|
+ ],
|
|
|
+ 'records': [record],
|
|
|
+ },
|
|
|
+ 'meta': meta,
|
|
|
+ })['is_error'])
|
|
|
+
|
|
|
+ def test_local_public_registry_cli_and_safe_counts(self):
|
|
|
+ local = GatewayApp().registered_tool_names()
|
|
|
+ public = PublicGatewayApp(None, None).registered_tool_names()
|
|
|
+ self.assertEqual(local, public)
|
|
|
+ self.assertEqual(28, len(local))
|
|
|
+ self.assertEqual(27, len(OutputPresenter.SAFE_TOOLS))
|
|
|
+ self.assertIn('query_destination_trailer_list', local)
|
|
|
+ self.assertIn('list_destination_trailer_filter_options', local)
|
|
|
+
|
|
|
+ client = RecordingApiClient()
|
|
|
+ app = GatewayApp(api_client=client)
|
|
|
+ app.run_cli([
|
|
|
+ 'call', '--tool', 'query_destination_trailer_list',
|
|
|
+ '--trailer-stage', '待提柜', '--bl-numbers', ' BL-1,BL-2 ',
|
|
|
+ '--page', '2', '--limit', '30',
|
|
|
+ ], stdout=io.StringIO())
|
|
|
+ self.assertEqual(
|
|
|
+ {
|
|
|
+ 'trailer_stage': '待提柜',
|
|
|
+ 'bl_numbers': ['BL-1', 'BL-2'],
|
|
|
+ 'page': 2,
|
|
|
+ 'limit': 30,
|
|
|
+ },
|
|
|
+ client.calls[-1][2],
|
|
|
+ )
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ app.run_cli([
|
|
|
+ 'call', '--tool', 'list_destination_trailer_filter_options',
|
|
|
+ ], stdout=io.StringIO())
|
|
|
+ app.run_cli([
|
|
|
+ 'call', '--tool', 'list_destination_trailer_filter_options',
|
|
|
+ '--filter-type', '海外提柜类型',
|
|
|
+ ], stdout=io.StringIO())
|
|
|
+ self.assertEqual(
|
|
|
+ 'list_destination_trailer_filter_options',
|
|
|
+ client.calls[-1][0],
|
|
|
+ )
|
|
|
+
|
|
|
+ def test_presenter_locks_thirty_and_eleven_columns(self):
|
|
|
+ presenter = OutputPresenter()
|
|
|
+ progress_record = {key: 'v' for key, _ in PROGRESS_COLUMNS}
|
|
|
+ progress_record['trailer_status'] = '待提柜'
|
|
|
+ result = presenter.present('query_destination_trailer_list', {
|
|
|
+ 'code': 'MCP_0000',
|
|
|
+ 'data': {
|
|
|
+ 'trailer_stage': '待提柜',
|
|
|
+ 'columns': [
|
|
|
+ {'key': key, 'name': name} for key, name in PROGRESS_COLUMNS
|
|
|
+ ],
|
|
|
+ 'records': [progress_record],
|
|
|
+ },
|
|
|
+ 'meta': {
|
|
|
+ 'page': 1, 'limit': 20, 'has_more': False, 'request_id': 'rq_p',
|
|
|
+ },
|
|
|
+ })
|
|
|
+ self.assertFalse(result['is_error'])
|
|
|
+ self.assertEqual(30, len(result['structured_content']['headers']))
|
|
|
+ self.assertEqual('待提柜', result['structured_content']['trailer_stage'])
|
|
|
+
|
|
|
+ do_record = {key: 'd' for key, _ in DO_COLUMNS}
|
|
|
+ do_record['is_direct_send'] = '否'
|
|
|
+ do_record['original_do_file'] = ''
|
|
|
+ do_record['new_do_file'] = 'https://files.example/do.pdf'
|
|
|
+ do_ok = presenter.present('query_destination_trailer_list', {
|
|
|
+ 'code': 'MCP_0000',
|
|
|
+ 'data': {
|
|
|
+ 'trailer_stage': 'DO单制作',
|
|
|
+ 'columns': [
|
|
|
+ {'key': key, 'name': name} for key, name in DO_COLUMNS
|
|
|
+ ],
|
|
|
+ 'records': [do_record],
|
|
|
+ },
|
|
|
+ 'meta': {
|
|
|
+ 'page': 1, 'limit': 20, 'has_more': False, 'request_id': 'rq_d',
|
|
|
+ },
|
|
|
+ })
|
|
|
+ self.assertFalse(do_ok['is_error'])
|
|
|
+ self.assertEqual(11, len(do_ok['structured_content']['headers']))
|
|
|
+
|
|
|
+ bad_url = dict(do_record)
|
|
|
+ bad_url['new_do_file'] = 'javascript:alert(1)'
|
|
|
+ bad = presenter.present('query_destination_trailer_list', {
|
|
|
+ 'code': 'MCP_0000',
|
|
|
+ 'data': {
|
|
|
+ 'trailer_stage': 'DO单制作',
|
|
|
+ 'columns': [
|
|
|
+ {'key': key, 'name': name} for key, name in DO_COLUMNS
|
|
|
+ ],
|
|
|
+ 'records': [bad_url],
|
|
|
+ },
|
|
|
+ 'meta': {
|
|
|
+ 'page': 1, 'limit': 20, 'has_more': False, 'request_id': 'rq_b',
|
|
|
+ },
|
|
|
+ })
|
|
|
+ self.assertTrue(bad['is_error'])
|
|
|
+ extra = dict(progress_record)
|
|
|
+ extra['outbound_number'] = 'OB1'
|
|
|
+ extra_result = presenter.present('query_destination_trailer_list', {
|
|
|
+ 'code': 'MCP_0000',
|
|
|
+ 'data': {
|
|
|
+ 'trailer_stage': '待提柜',
|
|
|
+ 'columns': [
|
|
|
+ {'key': key, 'name': name} for key, name in PROGRESS_COLUMNS
|
|
|
+ ],
|
|
|
+ 'records': [extra],
|
|
|
+ },
|
|
|
+ 'meta': {
|
|
|
+ 'page': 1, 'limit': 20, 'has_more': False, 'request_id': 'rq_e',
|
|
|
+ },
|
|
|
+ })
|
|
|
+ self.assertTrue(extra_result['is_error'])
|