修复:完善报表查询校验

This commit is contained in:
焦龙言
2026-07-27 02:04:45 +08:00
parent dfd720359a
commit 6b97bb9f49
3 changed files with 124 additions and 10 deletions
@@ -8,7 +8,7 @@ from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
from fastapi.routing import APIRoute
from pydantic import BaseModel
from pydantic import BaseModel, ValidationError
from sqlalchemy import Select
from sqlalchemy.dialects import mysql, postgresql
from sqlalchemy.ext.asyncio import AsyncSession
@@ -44,6 +44,15 @@ ReportDao = getattr(report_dao_module, 'ReportDao', None)
ReportService = getattr(report_service_module, 'ReportService', None)
report_controller = getattr(report_controller_module, 'report_controller', None)
REPORT_QUERY_MODELS = (
PalletInventoryReportQueryModel,
FillingReportQueryModel,
ShipmentReportQueryModel,
ConsumptionReportQueryModel,
ReturnReportQueryModel,
PalletTraceReportQueryModel,
)
def make_user(user_id: int = 7) -> CurrentUserModel:
return CurrentUserModel(
@@ -255,6 +264,58 @@ class TestReportPublicContract(unittest.TestCase):
self.assertTrue(forbidden_calls.isdisjoint(called_attributes))
class TestReportQueryValidation(unittest.TestCase):
def test_dates_require_strict_real_calendar_values(self) -> None:
for model_class in REPORT_QUERY_MODELS:
with self.subTest(model=model_class.__name__, case='begin-only'):
model = model_class.model_validate({'beginTime': '2026-07-01'})
self.assertEqual('2026-07-01', model.begin_time)
self.assertIsNone(model.end_time)
with self.subTest(model=model_class.__name__, case='end-only'):
model = model_class.model_validate({'endTime': '2026-07-31'})
self.assertIsNone(model.begin_time)
self.assertEqual('2026-07-31', model.end_time)
for field_name in ('beginTime', 'endTime'):
for invalid_date in ('not-a-date', '2026-02-30', '2026-7-01', '2026-07-1'):
with (
self.subTest(
model=model_class.__name__,
field=field_name,
value=invalid_date,
),
self.assertRaises(ValidationError),
):
model_class.model_validate({field_name: invalid_date})
def test_date_range_allows_none_and_equal_but_rejects_inverted_or_overflowing_end(self) -> None:
for model_class in REPORT_QUERY_MODELS:
with self.subTest(model=model_class.__name__, case='none'):
model = model_class.model_validate({'beginTime': None, 'endTime': None})
self.assertIsNone(model.begin_time)
self.assertIsNone(model.end_time)
with self.subTest(model=model_class.__name__, case='equal'):
model = model_class.model_validate({'beginTime': '2026-07-02', 'endTime': '2026-07-02'})
self.assertEqual(model.begin_time, model.end_time)
with self.subTest(model=model_class.__name__, case='max-begin'):
model = model_class.model_validate({'beginTime': '9999-12-31'})
self.assertEqual('9999-12-31', model.begin_time)
with self.subTest(model=model_class.__name__, case='inverted'), self.assertRaises(ValidationError):
model_class.model_validate({'beginTime': '2026-07-03', 'endTime': '2026-07-02'})
with self.subTest(model=model_class.__name__, case='max-end'), self.assertRaises(ValidationError):
model_class.model_validate({'endTime': '9999-12-31'})
def test_page_size_is_capped_at_100(self) -> None:
for model_class in REPORT_QUERY_MODELS:
with self.subTest(model=model_class.__name__, page_size=100):
self.assertEqual(100, model_class.model_validate({'pageSize': 100}).page_size)
for invalid_size in (101, 10**9):
with (
self.subTest(model=model_class.__name__, page_size=invalid_size),
self.assertRaises(ValidationError),
):
model_class.model_validate({'pageSize': invalid_size})
@unittest.skipUnless(ReportDao is not None, 'ReportDao 尚未实现')
class TestReportDaoQueries(unittest.IsolatedAsyncioTestCase):
def setUp(self) -> None:
@@ -417,6 +478,19 @@ class TestReportDaoQueries(unittest.IsolatedAsyncioTestCase):
sql,
)
async def test_shipment_query_requires_confirmed_fact_in_addition_to_optional_status(self) -> None:
statement = await self._capture_statement(
ReportDao.get_shipment_report_list,
ShipmentReportQueryModel(status=DocumentStatus.DRAFT),
warehouse_ids=None,
site_ids=None,
)
for sql in self._assert_compiles_for_supported_databases(statement):
self.assertIn("biz_shipment_order.status = 'draft'", sql)
self.assertIn("biz_shipment_order.status = 'confirmed'", sql)
self.assertEqual(2, sql.count('biz_shipment_order.status ='))
async def test_consumption_query_uses_usage_filling_snapshots_all_filters_and_site_scope(self) -> None:
query = ConsumptionReportQueryModel(
customerId=3,
@@ -917,9 +991,14 @@ class TestReportControllerContract(unittest.TestCase):
}
for path, parameters in expected_parameters.items():
with self.subTest(path=path):
actual = {parameter['name'] for parameter in schema['paths'][path]['get']['parameters']}
operation_parameters = schema['paths'][path]['get']['parameters']
actual = {parameter['name'] for parameter in operation_parameters}
self.assertEqual(parameters, actual)
self.assertNotIn('mode', actual)
page_size_parameter = next(
parameter for parameter in operation_parameters if parameter['name'] == 'pageSize'
)
self.assertEqual(100, page_size_parameter['schema'].get('maximum'))
def test_routes_have_exact_permissions_response_models_current_user_and_no_log(self) -> None:
expected = {