Skip to content

Commit 1df2041

Browse files
jopemachineclaude
andcommitted
test(BA-6921): assert the DTO rejection every request actually gets
BaseRequestModel only maps a pydantic ValidationError onto BackendAISchemaValidationFailed inside model_validate, and every production entry point — BodyParam, QueryParam, PathParam, headers — parses that way. The raw ValidationError the direct constructor raises is therefore unreachable in service, so accepting either exception let the tests pass on a rejection no caller can observe. Reject through model_validate and assert the one error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f0c7832 commit 1df2041

2 files changed

Lines changed: 33 additions & 34 deletions

File tree

  • src/ai/backend/manager/api/adapters/app_config_fragment
  • tests/unit/common/dto/manager/v2/app_config_fragment

src/ai/backend/manager/api/adapters/app_config_fragment/adapter.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,20 @@ def _scope_owner(scope_type: AppConfigScopeType, scope_id: UUID | None) -> AppCo
6767
"""Read a request's raw ``scope_id`` as the kind of owner its ``scope_type`` calls for.
6868
6969
A request body carries ``scope_id`` as a plain UUID; only ``scope_type`` says whether it
70-
names a domain or a user. The DTO validator already rejects the mismatched combinations,
71-
so the final branch is a boundary guard rather than a reachable path.
70+
names a domain or a user. The DTO validator already rejects both contradictions below,
71+
so the raises are boundary guards rather than reachable paths.
7272
"""
73-
match scope_type, scope_id:
74-
case AppConfigScopeType.PUBLIC, None:
75-
return None
76-
case AppConfigScopeType.DOMAIN, UUID() as owner:
77-
return DomainID(owner)
78-
case AppConfigScopeType.USER, UUID() as owner:
79-
return UserID(owner)
80-
case _:
81-
raise InvalidAPIParameters(f"scope_id does not match the {scope_type.value} scope.")
73+
if scope_type is AppConfigScopeType.PUBLIC:
74+
if scope_id is not None:
75+
raise InvalidAPIParameters("The public scope names no owner.")
76+
return None
77+
if scope_id is None:
78+
raise InvalidAPIParameters(f"The {scope_type.value} scope needs an owner.")
79+
match scope_type:
80+
case AppConfigScopeType.DOMAIN:
81+
return DomainID(scope_id)
82+
case AppConfigScopeType.USER:
83+
return UserID(scope_id)
8284

8385

8486
class AppConfigFragmentAdapter(BaseAdapter):

tests/unit/common/dto/manager/v2/app_config_fragment/test_request.py

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from typing import Any
88

99
import pytest
10-
from pydantic import ValidationError
1110

1211
from ai.backend.common.data.app_config.types import AppConfigScopeType
1312
from ai.backend.common.dto.manager.v2.app_config_fragment.request import (
@@ -72,13 +71,13 @@ def test_scope_id_matching_its_scope_type_is_accepted(
7271
def test_scope_id_disagreeing_with_its_scope_type_is_rejected(
7372
self, case: _ScopeCase, config_document: dict[str, Any]
7473
) -> None:
75-
with pytest.raises((BackendAISchemaValidationFailed, ValidationError)):
76-
CreateAppConfigFragmentInput(
77-
config_name="theme",
78-
scope_type=case.scope_type,
79-
scope_id=case.scope_id,
80-
config=config_document,
81-
)
74+
with pytest.raises(BackendAISchemaValidationFailed):
75+
CreateAppConfigFragmentInput.model_validate({
76+
"config_name": "theme",
77+
"scope_type": case.scope_type,
78+
"scope_id": case.scope_id,
79+
"config": config_document,
80+
})
8281

8382
@pytest.mark.parametrize(
8483
"scope_type",
@@ -89,9 +88,7 @@ def test_scope_id_disagreeing_with_its_scope_type_is_rejected(
8988
def test_scope_id_that_is_not_a_uuid_is_rejected(
9089
self, scope_type: AppConfigScopeType, scope_id: str, config_document: dict[str, Any]
9190
) -> None:
92-
# Validated from a mapping because the field is typed UUID: a string only ever
93-
# reaches it from a request payload, never from a type-checked call.
94-
with pytest.raises((BackendAISchemaValidationFailed, ValidationError)):
91+
with pytest.raises(BackendAISchemaValidationFailed):
9592
CreateAppConfigFragmentInput.model_validate({
9693
"config_name": "theme",
9794
"scope_type": scope_type,
@@ -112,12 +109,12 @@ def test_scope_id_defaults_to_none(self, config_document: dict[str, Any]) -> Non
112109
def test_config_name_outside_its_length_bounds_is_rejected(
113110
self, config_name: str, config_document: dict[str, Any]
114111
) -> None:
115-
with pytest.raises((BackendAISchemaValidationFailed, ValidationError)):
116-
CreateAppConfigFragmentInput(
117-
config_name=config_name,
118-
scope_type=AppConfigScopeType.PUBLIC,
119-
config=config_document,
120-
)
112+
with pytest.raises(BackendAISchemaValidationFailed):
113+
CreateAppConfigFragmentInput.model_validate({
114+
"config_name": config_name,
115+
"scope_type": AppConfigScopeType.PUBLIC,
116+
"config": config_document,
117+
})
121118

122119

123120
class TestUpdateAppConfigFragmentInput:
@@ -132,7 +129,7 @@ def test_config_alone_is_a_complete_body(self, config_document: dict[str, Any])
132129
def test_config_is_required(self) -> None:
133130
# Validated from a mapping rather than the constructor: a body arriving without
134131
# ``config`` is a runtime payload, not a call the type checker would ever allow.
135-
with pytest.raises((BackendAISchemaValidationFailed, ValidationError)):
132+
with pytest.raises(BackendAISchemaValidationFailed):
136133
UpdateAppConfigFragmentInput.model_validate({})
137134

138135

@@ -148,7 +145,7 @@ def test_id_and_config_are_both_required(self, config_document: dict[str, Any])
148145
assert item.config == config_document
149146

150147
def test_omitting_id_is_rejected(self, config_document: dict[str, Any]) -> None:
151-
with pytest.raises((BackendAISchemaValidationFailed, ValidationError)):
148+
with pytest.raises(BackendAISchemaValidationFailed):
152149
AppConfigFragmentUpdateItem.model_validate({"config": config_document})
153150

154151

@@ -163,8 +160,8 @@ def test_bulk_update_accepts_items(self, config_document: dict[str, Any]) -> Non
163160
assert len(req.items) == 1
164161

165162
def test_bulk_update_rejects_an_empty_batch(self) -> None:
166-
with pytest.raises((BackendAISchemaValidationFailed, ValidationError)):
167-
BulkUpdateAppConfigFragmentInput(items=[])
163+
with pytest.raises(BackendAISchemaValidationFailed):
164+
BulkUpdateAppConfigFragmentInput.model_validate({"items": []})
168165

169166
def test_bulk_purge_accepts_ids(self) -> None:
170167
fragment_id = uuid.uuid4()
@@ -174,5 +171,5 @@ def test_bulk_purge_accepts_ids(self) -> None:
174171
assert req.ids == [fragment_id]
175172

176173
def test_bulk_purge_rejects_an_empty_batch(self) -> None:
177-
with pytest.raises((BackendAISchemaValidationFailed, ValidationError)):
178-
BulkPurgeAppConfigFragmentInput(ids=[])
174+
with pytest.raises(BackendAISchemaValidationFailed):
175+
BulkPurgeAppConfigFragmentInput.model_validate({"ids": []})

0 commit comments

Comments
 (0)