Skip to content

Commit 3de71a7

Browse files
fregataaclaude
andcommitted
test(BA-5737): rewrite tests — SDK component test, repo unit tests
- Remove test_vfolder_in_project_service.py (service unit test). - Rename test_vfolder_project_mutations.py → test_vfolder_mutation.py and rewrite to use V2ClientRegistry SDK calls via HTTP instead of direct adapter invocation. - Add TestVFolderRepositoryTrashAndUpdate to test_vfolder_repository.py: trash_vfolder (status → DELETE_PENDING + not found) and update_vfolder_attribute (name rename + not found). - Fix changelog Vfolder → VFolder casing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b813bbf commit 3de71a7

4 files changed

Lines changed: 252 additions & 224 deletions

File tree

changes/11139.feature.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Add project-scoped `createProjectVfolderV2` GraphQL mutation and enforce RBAC on `deleteVfolderV2`, `purgeVfolderV2`, and new `updateVfolderV2` across GraphQL, REST v2, SDK, and CLI.
1+
Add project-scoped `createProjectVFolder` GraphQL mutation and enforce RBAC on `deleteVFolder`, `purgeVFolder`, and new `updateVFolder` across GraphQL, REST v2, SDK, and CLI.

tests/component/vfolder_v2/test_vfolder_project_mutations.py renamed to tests/component/vfolder_v2/test_vfolder_mutation.py

Lines changed: 91 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
"""Component tests for v2 VFolder RBAC-enforced mutations.
1+
"""Component tests for v2 VFolder RBAC-enforced mutations via SDK.
22
3-
Exercises ``SingleEntityActionProcessor`` (delete/purge) and
4-
``ScopeActionProcessor`` (create) end-to-end against the real DB
5-
and real RBAC validators.
3+
Exercises create (project-scoped), delete, purge, and update mutations
4+
through the real HTTP server + V2ClientRegistry SDK.
65
"""
76

87
from __future__ import annotations
@@ -15,9 +14,13 @@
1514
from unittest.mock import MagicMock
1615

1716
import pytest
17+
import yarl
1818

19-
from ai.backend.common.contexts.user import with_user
20-
from ai.backend.common.data.user.types import UserData, UserRole
19+
from ai.backend.client.v2.auth import HMACAuth
20+
from ai.backend.client.v2.config import ClientConfig
21+
from ai.backend.client.v2.exceptions import PermissionDeniedError
22+
from ai.backend.client.v2.v2_registry import V2ClientRegistry
23+
from ai.backend.common.dto.manager.v2.vfolder.request import UpdateVFolderInput
2124
from ai.backend.common.types import QuotaScopeID, QuotaScopeType, VFolderUsageMode
2225
from ai.backend.manager.actions.validators import ActionValidators
2326
from ai.backend.manager.actions.validators.rbac import RBACValidators
@@ -26,12 +29,15 @@
2629
SingleEntityActionRBACValidator,
2730
)
2831
from ai.backend.manager.api.adapters.vfolder import VFolderAdapter
32+
from ai.backend.manager.api.rest.routing import RouteRegistry
33+
from ai.backend.manager.api.rest.types import RouteDeps
34+
from ai.backend.manager.api.rest.v2.vfolder.handler import V2VFolderHandler
35+
from ai.backend.manager.api.rest.v2.vfolder.registry import register_v2_vfolder_routes
2936
from ai.backend.manager.data.vfolder.types import (
3037
VFolderMountPermission,
3138
VFolderOperationStatus,
3239
VFolderOwnershipType,
3340
)
34-
from ai.backend.manager.errors.permission import NotEnoughPermission
3541
from ai.backend.manager.models.utils import ExtendedAsyncSAEngine
3642
from ai.backend.manager.models.vfolder import vfolders
3743
from ai.backend.manager.repositories.permission_controller.repository import (
@@ -45,7 +51,7 @@
4551

4652
if TYPE_CHECKING:
4753
from sqlalchemy.ext.asyncio.engine import AsyncEngine as SAEngine
48-
from tests.component.conftest import UserFixtureData
54+
from tests.component.conftest import ServerInfo, UserFixtureData
4955

5056

5157
@dataclass(frozen=True)
@@ -54,6 +60,9 @@ class ProjectVFolderFixtureData:
5460
project_id: uuid.UUID
5561

5662

63+
# ── Fixtures ──────────────────────────────────────────────────────
64+
65+
5766
@pytest.fixture()
5867
def rbac_permission_repo(
5968
database_engine: ExtendedAsyncSAEngine,
@@ -62,11 +71,11 @@ def rbac_permission_repo(
6271

6372

6473
@pytest.fixture()
65-
def vfolder_processors_with_real_rbac(
74+
def vfolder_processors(
6675
database_engine: ExtendedAsyncSAEngine,
6776
rbac_permission_repo: PermissionControllerRepository,
6877
) -> VFolderProcessors:
69-
"""VFolderProcessors wired with real scope + single-entity RBAC validators."""
78+
"""Override: real scope + single-entity RBAC validators."""
7079
vfolder_repository = VfolderRepository(database_engine)
7180
user_repository = UserRepository(database_engine)
7281
service = VFolderService(
@@ -91,36 +100,54 @@ def vfolder_processors_with_real_rbac(
91100

92101

93102
@pytest.fixture()
94-
def vfolder_adapter(
95-
vfolder_processors_with_real_rbac: VFolderProcessors,
96-
) -> VFolderAdapter:
103+
def server_module_registries(
104+
route_deps: RouteDeps,
105+
vfolder_processors: VFolderProcessors,
106+
) -> list[RouteRegistry]:
107+
"""Register v2 vfolder REST routes with real RBAC."""
97108
processors = MagicMock(spec=Processors)
98-
processors.vfolder = vfolder_processors_with_real_rbac
99-
return VFolderAdapter(processors)
109+
processors.vfolder = vfolder_processors
110+
adapter = VFolderAdapter(processors)
111+
handler = V2VFolderHandler(adapter=adapter)
112+
v2_reg = RouteRegistry.create("v2", route_deps.cors_options)
113+
v2_reg.add_subregistry(register_v2_vfolder_routes(handler, route_deps))
114+
return [v2_reg]
100115

101116

102117
@pytest.fixture()
103-
def admin_user_data(admin_user_fixture: UserFixtureData) -> UserData:
104-
return UserData(
105-
user_id=admin_user_fixture.user_uuid,
106-
is_authorized=True,
107-
is_admin=True,
108-
is_superadmin=True,
109-
role=UserRole.SUPERADMIN,
110-
domain_name="default",
118+
async def admin_v2_registry(
119+
server: ServerInfo,
120+
admin_user_fixture: UserFixtureData,
121+
) -> AsyncIterator[V2ClientRegistry]:
122+
registry = await V2ClientRegistry.create(
123+
ClientConfig(endpoint=yarl.URL(server.url)),
124+
HMACAuth(
125+
access_key=admin_user_fixture.keypair.access_key,
126+
secret_key=admin_user_fixture.keypair.secret_key,
127+
),
111128
)
129+
try:
130+
yield registry
131+
finally:
132+
await registry.close()
112133

113134

114135
@pytest.fixture()
115-
def regular_user_data(regular_user_fixture: UserFixtureData) -> UserData:
116-
return UserData(
117-
user_id=regular_user_fixture.user_uuid,
118-
is_authorized=True,
119-
is_admin=False,
120-
is_superadmin=False,
121-
role=UserRole.USER,
122-
domain_name="default",
136+
async def user_v2_registry(
137+
server: ServerInfo,
138+
regular_user_fixture: UserFixtureData,
139+
) -> AsyncIterator[V2ClientRegistry]:
140+
registry = await V2ClientRegistry.create(
141+
ClientConfig(endpoint=yarl.URL(server.url)),
142+
HMACAuth(
143+
access_key=regular_user_fixture.keypair.access_key,
144+
secret_key=regular_user_fixture.keypair.secret_key,
145+
),
123146
)
147+
try:
148+
yield registry
149+
finally:
150+
await registry.close()
124151

125152

126153
@pytest.fixture()
@@ -157,39 +184,51 @@ async def project_vfolder(
157184
await conn.execute(vfolders.delete().where(vfolders.c.id == vfolder_id))
158185

159186

160-
class TestDeleteVFolderV2ByIdRBAC:
161-
"""SingleEntityActionProcessor RBAC enforcement for vfolder delete."""
187+
# ── Tests ─────────────────────────────────────────────────────────
162188

163-
async def test_regular_user_without_grant_is_denied(
189+
190+
class TestDeleteVFolderRBAC:
191+
"""DELETE /v2/vfolders/{id} — SingleEntityActionProcessor RBAC."""
192+
193+
async def test_regular_user_denied(
164194
self,
165-
vfolder_adapter: VFolderAdapter,
166-
regular_user_data: UserData,
195+
user_v2_registry: V2ClientRegistry,
167196
project_vfolder: ProjectVFolderFixtureData,
168197
) -> None:
169-
with with_user(regular_user_data):
170-
with pytest.raises(NotEnoughPermission):
171-
await vfolder_adapter.delete(project_vfolder.id)
198+
with pytest.raises(PermissionDeniedError):
199+
await user_v2_registry.vfolder.delete(project_vfolder.id)
172200

173-
async def test_superadmin_bypasses_rbac(
201+
async def test_superadmin_succeeds(
174202
self,
175-
vfolder_adapter: VFolderAdapter,
176-
admin_user_data: UserData,
203+
admin_v2_registry: V2ClientRegistry,
177204
project_vfolder: ProjectVFolderFixtureData,
178205
) -> None:
179-
with with_user(admin_user_data):
180-
payload = await vfolder_adapter.delete(project_vfolder.id)
206+
payload = await admin_v2_registry.vfolder.delete(project_vfolder.id)
181207
assert payload.id == project_vfolder.id
182208

183209

184-
class TestPurgeVFolderV2ByIdRBAC:
185-
"""SingleEntityActionProcessor RBAC enforcement for vfolder purge."""
210+
class TestPurgeVFolderRBAC:
211+
"""POST /v2/vfolders/{id}/purge — SingleEntityActionProcessor RBAC."""
186212

187-
async def test_regular_user_without_grant_is_denied(
213+
async def test_regular_user_denied(
188214
self,
189-
vfolder_adapter: VFolderAdapter,
190-
regular_user_data: UserData,
215+
user_v2_registry: V2ClientRegistry,
191216
project_vfolder: ProjectVFolderFixtureData,
192217
) -> None:
193-
with with_user(regular_user_data):
194-
with pytest.raises(NotEnoughPermission):
195-
await vfolder_adapter.purge(project_vfolder.id)
218+
with pytest.raises(PermissionDeniedError):
219+
await user_v2_registry.vfolder.purge(project_vfolder.id)
220+
221+
222+
class TestUpdateVFolderRBAC:
223+
"""PATCH /v2/vfolders/{id} — SingleEntityActionProcessor RBAC."""
224+
225+
async def test_regular_user_denied(
226+
self,
227+
user_v2_registry: V2ClientRegistry,
228+
project_vfolder: ProjectVFolderFixtureData,
229+
) -> None:
230+
with pytest.raises(PermissionDeniedError):
231+
await user_v2_registry.vfolder.update(
232+
project_vfolder.id,
233+
UpdateVFolderInput(name="new-name"),
234+
)

0 commit comments

Comments
 (0)