修复:完善报表查询校验
This commit is contained in:
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user