修复:返回使用后的余量与状态

This commit is contained in:
焦龙言
2026-07-27 06:27:29 +08:00
parent d0fa20afe8
commit e5006c6e43
4 changed files with 184 additions and 18 deletions
@@ -22,7 +22,6 @@ from common.vo import (
DataResponseModel,
PageModel,
PageResponseModel,
ResponseBaseModel,
)
from exceptions.exception import PermissionException, ServiceException
from module_admin.entity.vo.user_vo import CurrentUserModel, UserInfoModel
@@ -46,6 +45,7 @@ from module_business.entity.vo.operation_vo import (
UsageCreateModel,
UsageModel,
UsagePageQueryModel,
UsageSuccessResponseModel,
)
from module_business.service.access_service import AccessService
@@ -267,6 +267,37 @@ class TestUsagePublicContract(unittest.TestCase):
with self.subTest(invalid=invalid), self.assertRaises(ValidationError):
make_page(used_packages=invalid)
def test_usage_success_response_model_has_exact_camel_case_contract(self) -> None:
operation_vo_module = importlib.import_module('module_business.entity.vo.operation_vo')
response_model = getattr(operation_vo_module, 'UsageSuccessResponseModel', None)
self.assertIsNotNone(response_model, 'UsageSuccessResponseModel 尚未实现')
result = response_model(
usageId=61,
remainingPackages=5,
flowState=PalletFlowState.AT_SITE_PARTIAL,
)
self.assertEqual(
{
'usageId': 61,
'remainingPackages': 5,
'flowState': PalletFlowState.AT_SITE_PARTIAL,
},
result.model_dump(by_alias=True),
)
self.assertEqual(
{'usage_id', 'remaining_packages', 'flow_state'},
set(response_model.model_fields),
)
for invalid_kwargs in (
{'usageId': 0, 'remainingPackages': 5, 'flowState': PalletFlowState.AT_SITE_PARTIAL},
{'usageId': 61, 'remainingPackages': -1, 'flowState': PalletFlowState.AT_SITE_PARTIAL},
{'usageId': 61, 'remainingPackages': True, 'flowState': PalletFlowState.AT_SITE_PARTIAL},
{'usageId': 61, 'remainingPackages': 1.5, 'flowState': PalletFlowState.AT_SITE_PARTIAL},
):
with self.subTest(invalid_kwargs=invalid_kwargs), self.assertRaises(ValidationError):
response_model(**invalid_kwargs)
@unittest.skipUnless(UsageService is not None, 'UsageService 尚未实现')
class TestUsageReadServices(unittest.IsolatedAsyncioTestCase):
@@ -539,7 +570,24 @@ class TestUsageControllerContract(unittest.TestCase):
DataResponseModel[UsageModel],
routes[('/business/usage/{usage_id}', 'GET')].response_model,
)
self.assertEqual(ResponseBaseModel, routes[('/business/usage', 'POST')].response_model)
self.assertEqual(
DataResponseModel[UsageSuccessResponseModel],
routes[('/business/usage', 'POST')].response_model,
)
post_response_schema = schema['paths']['/business/usage']['post']['responses']['200']['content'][
'application/json'
]['schema']
response_schema = schema['components']['schemas'][post_response_schema['$ref'].rsplit('/', 1)[-1]]
self.assertIn('data', response_schema['properties'])
self.assertIn('data', response_schema['required'])
data_schema = schema['components']['schemas'][
response_schema['properties']['data']['$ref'].rsplit('/', 1)[-1]
]
self.assertEqual(
{'usageId', 'remainingPackages', 'flowState'},
set(data_schema['properties']),
)
def test_static_routes_precede_dynamic_detail(self) -> None:
paths = [route.path for route in usage_controller.routes]
@@ -592,6 +640,45 @@ class TestUsageControllerContract(unittest.TestCase):
self.assertNotIn(forbidden, source)
@unittest.skipUnless(usage_controller is not None, 'usage_controller 尚未实现')
class TestUsageControllerResponse(unittest.IsolatedAsyncioTestCase):
async def test_post_returns_service_result_in_data(self) -> None:
db = MagicMock(spec=AsyncSession)
user = make_user()
page = make_page()
service = AsyncMock(
return_value=CrudResponseModel(
is_success=True,
message='使用成功',
result=UsageSuccessResponseModel(
usageId=61,
remainingPackages=5,
flowState=PalletFlowState.AT_SITE_PARTIAL,
),
)
)
with patch.object(UsageService, 'record_usage_services', new=service):
response = await usage_controller_module.add_business_usage.__wrapped__(
request=MagicMock(),
add_usage=page,
query_db=db,
current_user=user,
)
service.assert_awaited_once_with(db, page, user)
payload = json.loads(response.body)
self.assertEqual('使用成功', payload['msg'])
self.assertIn('data', payload)
self.assertEqual(
{
'usageId': 61,
'remainingPackages': 5,
'flowState': PalletFlowState.AT_SITE_PARTIAL.value,
},
payload['data'],
)
@unittest.skipUnless(UsageService is not None, 'UsageService 尚未实现')
class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
def setUp(self) -> None:
@@ -861,7 +948,15 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
self.db.commit.assert_awaited_once_with()
self.db.rollback.assert_awaited_once_with()
self.assertTrue(result.is_success)
self.assertEqual(61, result.result)
self.assertIsInstance(result.result, UsageSuccessResponseModel)
self.assertEqual(
{
'usageId': 61,
'remainingPackages': 5,
'flowState': PalletFlowState.AT_SITE_PARTIAL,
},
result.result.model_dump(by_alias=True),
)
async def test_zero_remaining_closes_filling_but_keeps_line_until_return(self) -> None:
inventory = make_inventory(
@@ -884,6 +979,15 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
)
self.assertTrue(result.is_success)
self.assertIsInstance(result.result, UsageSuccessResponseModel)
self.assertEqual(
{
'usageId': 61,
'remainingPackages': 0,
'flowState': PalletFlowState.AT_SITE_EMPTY,
},
result.result.model_dump(by_alias=True),
)
self.assertEqual(PalletFlowState.AT_SITE_EMPTY.value, inventory.flow_state)
self.assertEqual(0, inventory.remaining_packages)
self.assertIsNone(inventory.active_filling_id)
@@ -1222,7 +1326,15 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
add_usage.assert_not_awaited()
self.db.commit.assert_not_awaited()
self.db.rollback.assert_awaited_once()
self.assertEqual(61, result.result)
self.assertIsInstance(result.result, UsageSuccessResponseModel)
self.assertEqual(
{
'usageId': 61,
'remainingPackages': 5,
'flowState': PalletFlowState.AT_SITE_PARTIAL,
},
result.result.model_dump(by_alias=True),
)
async def test_same_key_with_different_hash_is_stable_conflict(self) -> None:
existing = self._exact_usage(request_hash='different-hash')
@@ -1359,7 +1471,20 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
inventory.version,
),
)
self.assertEqual(61, result.result)
expected_state = (
PalletFlowState.AT_SITE_EMPTY
if remaining_after == 0
else PalletFlowState.AT_SITE_PARTIAL
)
self.assertIsInstance(result.result, UsageSuccessResponseModel)
self.assertEqual(
{
'usageId': 61,
'remainingPackages': remaining_after,
'flowState': expected_state,
},
result.result.model_dump(by_alias=True),
)
async def test_concurrent_retry_uses_fresh_scope_granted_during_inventory_wait(self) -> None:
inventory = make_inventory(
@@ -1408,7 +1533,15 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
mocks['add_event'].assert_not_awaited()
self.db.commit.assert_not_awaited()
self.assertEqual(2, self.db.rollback.await_count)
self.assertEqual(61, result.result)
self.assertIsInstance(result.result, UsageSuccessResponseModel)
self.assertEqual(
{
'usageId': 61,
'remainingPackages': 5,
'flowState': PalletFlowState.AT_SITE_PARTIAL,
},
result.result.model_dump(by_alias=True),
)
async def test_preflight_grant_revoked_before_fresh_scope_stays_hidden(self) -> None:
recovered = self._exact_usage()
@@ -1599,7 +1732,15 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
with stack:
result = await UsageService.record_usage_services(self.db, self.page, self.user)
self.assertEqual(61, result.result)
self.assertIsInstance(result.result, UsageSuccessResponseModel)
self.assertEqual(
{
'usageId': 61,
'remainingPackages': 5,
'flowState': PalletFlowState.AT_SITE_PARTIAL,
},
result.result.model_dump(by_alias=True),
)
self.assertEqual(3, scope.await_count)
self.assertEqual(3, key_read.await_count)
self.assertEqual(