Skip to content

Commit 3adaf5e

Browse files
jopemachineclaude
andcommitted
test(BA-6921): cover the app_config_fragment v2 request DTOs
The scope_type/scope_id agreement is the one piece of real logic in these DTOs: public must name no owner, domain and user must name one, and a blank id is as unusable as a missing one. Enumerate the full scope enum on both the accepted and the rejected side so a new scope variant cannot slip through untested. Also pin the update body split — the single-fragment body carries no id, while the bulk item does — and the min_length on both bulk batches. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 28f6a26 commit 3adaf5e

3 files changed

Lines changed: 164 additions & 0 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
python_tests(
2+
name="tests",
3+
)

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

Whitespace-only changes.
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
"""Tests for ai.backend.common.dto.manager.v2.app_config_fragment.request module."""
2+
3+
from __future__ import annotations
4+
5+
import uuid
6+
from dataclasses import dataclass
7+
from typing import Any
8+
9+
import pytest
10+
from pydantic import ValidationError
11+
12+
from ai.backend.common.data.app_config.types import AppConfigScopeType
13+
from ai.backend.common.dto.manager.v2.app_config_fragment.request import (
14+
AppConfigFragmentUpdateItem,
15+
BulkPurgeAppConfigFragmentInput,
16+
BulkUpdateAppConfigFragmentInput,
17+
CreateAppConfigFragmentInput,
18+
UpdateAppConfigFragmentInput,
19+
)
20+
from ai.backend.common.exception import BackendAISchemaValidationFailed
21+
22+
_SCOPE_ID = "11111111-1111-1111-1111-111111111111"
23+
24+
25+
@dataclass(frozen=True)
26+
class _ScopeCase:
27+
scope_type: AppConfigScopeType
28+
scope_id: str | None
29+
30+
31+
@pytest.fixture
32+
def config_document() -> dict[str, Any]:
33+
return {"theme": {"mode": "dark"}, "banner": "hello"}
34+
35+
36+
class TestCreateAppConfigFragmentInput:
37+
"""Tests for the scope_id / scope_type agreement enforced by CreateAppConfigFragmentInput."""
38+
39+
@pytest.mark.parametrize(
40+
"case",
41+
[
42+
_ScopeCase(scope_type=AppConfigScopeType.PUBLIC, scope_id=None),
43+
_ScopeCase(scope_type=AppConfigScopeType.DOMAIN, scope_id=_SCOPE_ID),
44+
_ScopeCase(scope_type=AppConfigScopeType.USER, scope_id=_SCOPE_ID),
45+
],
46+
ids=lambda case: case.scope_type.value,
47+
)
48+
def test_scope_id_matching_its_scope_type_is_accepted(
49+
self, case: _ScopeCase, config_document: dict[str, Any]
50+
) -> None:
51+
req = CreateAppConfigFragmentInput(
52+
config_name="theme",
53+
scope_type=case.scope_type,
54+
scope_id=case.scope_id,
55+
config=config_document,
56+
)
57+
58+
assert req.scope_type is case.scope_type
59+
assert req.scope_id == case.scope_id
60+
61+
@pytest.mark.parametrize(
62+
"case",
63+
[
64+
# public has no owner, so naming one is a contradiction.
65+
_ScopeCase(scope_type=AppConfigScopeType.PUBLIC, scope_id=_SCOPE_ID),
66+
# domain and user both require an owner; absent and blank are equally unusable.
67+
_ScopeCase(scope_type=AppConfigScopeType.DOMAIN, scope_id=None),
68+
_ScopeCase(scope_type=AppConfigScopeType.DOMAIN, scope_id=""),
69+
_ScopeCase(scope_type=AppConfigScopeType.USER, scope_id=None),
70+
_ScopeCase(scope_type=AppConfigScopeType.USER, scope_id=""),
71+
],
72+
ids=lambda case: f"{case.scope_type.value}-{case.scope_id!r}",
73+
)
74+
def test_scope_id_disagreeing_with_its_scope_type_is_rejected(
75+
self, case: _ScopeCase, config_document: dict[str, Any]
76+
) -> None:
77+
with pytest.raises((BackendAISchemaValidationFailed, ValidationError)):
78+
CreateAppConfigFragmentInput(
79+
config_name="theme",
80+
scope_type=case.scope_type,
81+
scope_id=case.scope_id,
82+
config=config_document,
83+
)
84+
85+
def test_scope_id_defaults_to_none(self, config_document: dict[str, Any]) -> None:
86+
req = CreateAppConfigFragmentInput(
87+
config_name="theme",
88+
scope_type=AppConfigScopeType.PUBLIC,
89+
config=config_document,
90+
)
91+
92+
assert req.scope_id is None
93+
94+
@pytest.mark.parametrize("config_name", ["", "x" * 129], ids=["empty", "too-long"])
95+
def test_config_name_outside_its_length_bounds_is_rejected(
96+
self, config_name: str, config_document: dict[str, Any]
97+
) -> None:
98+
with pytest.raises((BackendAISchemaValidationFailed, ValidationError)):
99+
CreateAppConfigFragmentInput(
100+
config_name=config_name,
101+
scope_type=AppConfigScopeType.PUBLIC,
102+
config=config_document,
103+
)
104+
105+
106+
class TestUpdateAppConfigFragmentInput:
107+
"""The single-fragment update body carries no id — the request path identifies the target."""
108+
109+
def test_config_alone_is_a_complete_body(self, config_document: dict[str, Any]) -> None:
110+
req = UpdateAppConfigFragmentInput(config=config_document)
111+
112+
assert req.config == config_document
113+
assert not hasattr(req, "id")
114+
115+
def test_config_is_required(self) -> None:
116+
# Validated from a mapping rather than the constructor: a body arriving without
117+
# ``config`` is a runtime payload, not a call the type checker would ever allow.
118+
with pytest.raises((BackendAISchemaValidationFailed, ValidationError)):
119+
UpdateAppConfigFragmentInput.model_validate({})
120+
121+
122+
class TestAppConfigFragmentUpdateItem:
123+
"""The bulk item does carry an id, since one request addresses many fragments."""
124+
125+
def test_id_and_config_are_both_required(self, config_document: dict[str, Any]) -> None:
126+
fragment_id = uuid.uuid4()
127+
128+
item = AppConfigFragmentUpdateItem(id=fragment_id, config=config_document)
129+
130+
assert item.id == fragment_id
131+
assert item.config == config_document
132+
133+
def test_omitting_id_is_rejected(self, config_document: dict[str, Any]) -> None:
134+
with pytest.raises((BackendAISchemaValidationFailed, ValidationError)):
135+
AppConfigFragmentUpdateItem.model_validate({"config": config_document})
136+
137+
138+
class TestBulkAppConfigFragmentInputs:
139+
"""Both bulk bodies reject an empty batch rather than treating it as a no-op."""
140+
141+
def test_bulk_update_accepts_items(self, config_document: dict[str, Any]) -> None:
142+
req = BulkUpdateAppConfigFragmentInput(
143+
items=[AppConfigFragmentUpdateItem(id=uuid.uuid4(), config=config_document)]
144+
)
145+
146+
assert len(req.items) == 1
147+
148+
def test_bulk_update_rejects_an_empty_batch(self) -> None:
149+
with pytest.raises((BackendAISchemaValidationFailed, ValidationError)):
150+
BulkUpdateAppConfigFragmentInput(items=[])
151+
152+
def test_bulk_purge_accepts_ids(self) -> None:
153+
fragment_id = uuid.uuid4()
154+
155+
req = BulkPurgeAppConfigFragmentInput(ids=[fragment_id])
156+
157+
assert req.ids == [fragment_id]
158+
159+
def test_bulk_purge_rejects_an_empty_batch(self) -> None:
160+
with pytest.raises((BackendAISchemaValidationFailed, ValidationError)):
161+
BulkPurgeAppConfigFragmentInput(ids=[])

0 commit comments

Comments
 (0)