Skip to content

Commit 0b615b9

Browse files
author
igennova
committed
Refine user deletion tests and logging
1 parent 96f3122 commit 0b615b9

2 files changed

Lines changed: 36 additions & 41 deletions

File tree

src/routers/openml/users.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""User account HTTP endpoints."""
22

3+
from http import HTTPStatus
34
from typing import Annotated
45

56
from fastapi import APIRouter, Depends, Path, Response
@@ -23,11 +24,13 @@
2324
@router.delete(
2425
"/{user_id}",
2526
responses={
26-
204: {"description": "User account deleted."},
27-
401: {"description": "Authentication failed or missing."},
28-
403: {"description": "Not allowed to delete this account."},
29-
404: {"description": "User id not found."},
30-
409: {"description": "User still has datasets, flows, runs, or studies."},
27+
HTTPStatus.NO_CONTENT: {"description": "User account deleted."},
28+
HTTPStatus.UNAUTHORIZED: {"description": "Authentication failed or missing."},
29+
HTTPStatus.FORBIDDEN: {"description": "Not allowed to delete this account."},
30+
HTTPStatus.NOT_FOUND: {"description": "User id not found."},
31+
HTTPStatus.CONFLICT: {
32+
"description": "User still has datasets, flows, runs, or studies.",
33+
},
3134
},
3235
)
3336
async def delete_user_account(
@@ -42,6 +45,8 @@ async def delete_user_account(
4245
datasets, tasks, or tags). Users may only delete their own account.
4346
Administrators may delete any account that satisfies the no-resources rule.
4447
"""
48+
# How to handle users that do have associated resources is an ongoing discussion,
49+
# see also: https://github.com/openml/server-api/issues/194
4550
if current_user.user_id != user_id and not await current_user.is_admin():
4651
msg = "You may only delete your own user account."
4752
raise ForbiddenError(msg)
@@ -63,4 +68,4 @@ async def delete_user_account(
6368
raise AccountHasResourcesError(_ACCOUNT_HAS_RESOURCES_MSG) from exc
6469

6570
logger.info("User account {user_id} was removed.", user_id=user_id)
66-
return Response(status_code=204)
71+
return Response(status_code=HTTPStatus.NO_CONTENT)

tests/routers/openml/users_delete_test.py

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
"""Tests for DELETE /users/{user_id} (Phase 1: no resources, self or admin)."""
1+
"""Tests for DELETE /users/{user_id}."""
22

33
import uuid
4-
from collections.abc import AsyncGenerator
54
from http import HTTPStatus
65
from typing import NamedTuple
76

@@ -15,7 +14,7 @@
1514
from core.errors import AccountHasResourcesError, ForbiddenError, UserNotFoundError
1615
from database.users import UserGroup
1716
from routers.openml.users import delete_user_account
18-
from tests.users import ADMIN_USER, SOME_USER, ApiKey
17+
from tests.users import ADMIN_USER, OWNER_USER, SOME_USER, ApiKey
1918

2019

2120
async def test_delete_user_missing_auth(py_api: httpx.AsyncClient) -> None:
@@ -32,7 +31,7 @@ class DisposableUser(NamedTuple):
3231

3332

3433
@pytest.fixture
35-
async def disposable_user(user_test: AsyncConnection) -> AsyncGenerator[DisposableUser]:
34+
async def disposable_user(user_test: AsyncConnection) -> DisposableUser:
3635
api_key = uuid.uuid4().hex
3736
suffix = uuid.uuid4().hex[:10]
3837
username = f"tmp_user_{suffix}"
@@ -58,23 +57,20 @@ async def disposable_user(user_test: AsyncConnection) -> AsyncGenerator[Disposab
5857
text("INSERT INTO users_groups (user_id, group_id) VALUES (:uid, :gid)"),
5958
parameters={"uid": new_id, "gid": UserGroup.READ_WRITE.value},
6059
)
61-
yield DisposableUser(user_id=new_id, api_key=api_key)
62-
await user_test.execute(
63-
text("DELETE FROM users_groups WHERE user_id = :uid"),
64-
parameters={"uid": new_id},
65-
)
66-
await user_test.execute(
67-
text("DELETE FROM users WHERE id = :uid"),
68-
parameters={"uid": new_id},
69-
)
60+
return DisposableUser(user_id=new_id, api_key=api_key)
61+
# No explicit teardown: the ``user_test`` fixture rolls back at the end
62+
# of the test, which removes the rows inserted above.
7063

7164

7265
@pytest.mark.mut
7366
async def test_delete_user_api_success_self_delete(
7467
py_api: httpx.AsyncClient,
7568
user_test: AsyncConnection,
7669
disposable_user: DisposableUser,
70+
mocker: pytest_mock.MockerFixture,
7771
) -> None:
72+
log_info = mocker.patch("routers.openml.users.logger.info")
73+
7874
response = await py_api.delete(
7975
f"/users/{disposable_user.user_id}",
8076
params={"api_key": disposable_user.api_key},
@@ -88,6 +84,11 @@ async def test_delete_user_api_success_self_delete(
8884
)
8985
assert exists.one_or_none() is None
9086

87+
log_info.assert_any_call(
88+
"User account {user_id} was removed.",
89+
user_id=disposable_user.user_id,
90+
)
91+
9192

9293
@pytest.mark.mut
9394
async def test_delete_user_api_success_admin_deletes_disposable_user(
@@ -143,43 +144,32 @@ async def test_delete_user_direct_forbidden(
143144
assert exc_info.value.status_code == HTTPStatus.FORBIDDEN
144145
assert exc_info.value.uri == ForbiddenError.uri
145146

147+
admin_row = await user_test.execute(
148+
text("SELECT 1 FROM users WHERE id = :id LIMIT 1"),
149+
parameters={"id": ADMIN_USER.user_id},
150+
)
151+
assert admin_row.one_or_none() is not None
152+
146153

147154
async def test_delete_user_direct_conflict_has_resources(
148155
user_test: AsyncConnection,
149156
expdb_test: AsyncConnection,
150157
) -> None:
151158
with pytest.raises(AccountHasResourcesError, match="Cannot delete this account") as exc_info:
152159
await delete_user_account(
153-
user_id=16,
160+
user_id=OWNER_USER.user_id,
154161
current_user=ADMIN_USER,
155162
expdb=expdb_test,
156163
userdb=user_test,
157164
)
158165
assert exc_info.value.status_code == HTTPStatus.CONFLICT
159166
assert exc_info.value.uri == AccountHasResourcesError.uri
160167

161-
162-
@pytest.mark.mut
163-
async def test_delete_user_direct_success_logs_info(
164-
user_test: AsyncConnection,
165-
expdb_test: AsyncConnection,
166-
disposable_user: DisposableUser,
167-
mocker: pytest_mock.MockerFixture,
168-
) -> None:
169-
log_info = mocker.patch("routers.openml.users.logger.info")
170-
171-
response = await delete_user_account(
172-
user_id=disposable_user.user_id,
173-
current_user=ADMIN_USER,
174-
expdb=expdb_test,
175-
userdb=user_test,
176-
)
177-
178-
assert response.status_code == HTTPStatus.NO_CONTENT
179-
log_info.assert_called_once_with(
180-
"User account {user_id} was removed.",
181-
user_id=disposable_user.user_id,
168+
owner_row = await user_test.execute(
169+
text("SELECT 1 FROM users WHERE id = :id LIMIT 1"),
170+
parameters={"id": OWNER_USER.user_id},
182171
)
172+
assert owner_row.one_or_none() is not None
183173

184174

185175
@pytest.mark.mut

0 commit comments

Comments
 (0)