Skip to content

Commit be4980e

Browse files
committed
fix(review): address PR feedback on account deletion
- database/users: add study, task_study, run_study, dataset_tag checks - database/users: wrap user & group deletion in an explicit transaction - routers/users: reuse direct query existence check without unused imports - tests/users_test: add coverage for 409 conflict when resources exist - tests/users_test: add db-level side-effect assertions for successful deletion
1 parent 9662c1f commit be4980e

3 files changed

Lines changed: 94 additions & 17 deletions

File tree

src/database/users.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,47 @@ def get_user_resource_count(*, user_id: int, expdb: Connection) -> int:
8888
text("SELECT COUNT(*) FROM run WHERE uploader = :user_id"),
8989
parameters={"user_id": user_id},
9090
).scalar() or 0
91-
return int(dataset_count) + int(flow_count) + int(run_count)
9291

93-
94-
def delete_user(*, user_id: int, connection: Connection) -> None:
95-
"""Remove the user and their group memberships from the user database."""
96-
connection.execute(
97-
text("DELETE FROM users_groups WHERE user_id = :user_id"),
92+
study_count = expdb.execute(
93+
text("SELECT COUNT(*) FROM study WHERE creator = :user_id"),
9894
parameters={"user_id": user_id},
99-
)
100-
connection.execute(
101-
text("DELETE FROM users WHERE id = :user_id"),
95+
).scalar() or 0
96+
task_study_count = expdb.execute(
97+
text("SELECT COUNT(*) FROM task_study WHERE uploader = :user_id"),
98+
parameters={"user_id": user_id},
99+
).scalar() or 0
100+
run_study_count = expdb.execute(
101+
text("SELECT COUNT(*) FROM run_study WHERE uploader = :user_id"),
102102
parameters={"user_id": user_id},
103+
).scalar() or 0
104+
dataset_tag_count = expdb.execute(
105+
text("SELECT COUNT(*) FROM dataset_tag WHERE uploader = :user_id"),
106+
parameters={"user_id": user_id},
107+
).scalar() or 0
108+
109+
return int(
110+
dataset_count
111+
+ flow_count
112+
+ run_count
113+
+ study_count
114+
+ task_study_count
115+
+ run_study_count
116+
+ dataset_tag_count,
103117
)
118+
119+
120+
def delete_user(*, user_id: int, connection: Connection) -> None:
121+
"""Remove the user and their group memberships from the user database."""
122+
with connection.begin_nested() as transaction:
123+
try:
124+
connection.execute(
125+
text("DELETE FROM users_groups WHERE user_id = :user_id"),
126+
parameters={"user_id": user_id},
127+
)
128+
connection.execute(
129+
text("DELETE FROM users WHERE id = :user_id"),
130+
parameters={"user_id": user_id},
131+
)
132+
except Exception:
133+
transaction.rollback()
134+
raise

src/routers/openml/users.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,12 @@ def delete_account(
4242
detail={"code": str(int(UserError.NO_ACCESS)), "message": "No access granted"},
4343
)
4444

45-
# Verify the target user exists
46-
from database.users import get_user_id_for # noqa: PLC0415
47-
48-
# Check user exists by querying for them directly
49-
from sqlalchemy import text
45+
from sqlalchemy import text # noqa: PLC0415
5046

5147
existing = user_db.execute(
52-
text("SELECT id FROM users WHERE id = :user_id"),
53-
parameters={"user_id": user_id},
54-
).one_or_none()
48+
text("SELECT 1 FROM users WHERE id = :id LIMIT 1"),
49+
parameters={"id": user_id},
50+
).scalar()
5551

5652
if existing is None:
5753
raise HTTPException(

tests/routers/openml/users_test.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,28 @@ def test_delete_user_self(py_api: TestClient, user_test: Connection) -> None:
1919
)
2020
(new_id,) = user_test.execute(text("SELECT LAST_INSERT_ID()")).one()
2121

22+
# Add a users_groups entry to verify it gets deleted
23+
user_test.execute(
24+
text("INSERT INTO users_groups (user_id, group_id) VALUES (:id, 2)"),
25+
parameters={"id": new_id},
26+
)
27+
2228
response = py_api.delete(f"/users/{new_id}?api_key=aaaabbbbccccddddaaaabbbbccccdddd")
2329
assert response.status_code == HTTPStatus.OK
2430
assert response.json() == {"user_id": new_id, "deleted": True}
2531

32+
# Verify DB side-effects: user and groups should be gone
33+
user_count = user_test.execute(
34+
text("SELECT COUNT(*) FROM users WHERE id = :id"),
35+
parameters={"id": new_id},
36+
).scalar()
37+
group_count = user_test.execute(
38+
text("SELECT COUNT(*) FROM users_groups WHERE user_id = :id"),
39+
parameters={"id": new_id},
40+
).scalar()
41+
assert user_count == 0
42+
assert group_count == 0
43+
2644

2745
@pytest.mark.mut
2846
def test_delete_user_as_admin(py_api: TestClient, user_test: Connection) -> None:
@@ -35,10 +53,23 @@ def test_delete_user_as_admin(py_api: TestClient, user_test: Connection) -> None
3553
)
3654
(new_id,) = user_test.execute(text("SELECT LAST_INSERT_ID()")).one()
3755

56+
# Add a users_groups entry
57+
user_test.execute(
58+
text("INSERT INTO users_groups (user_id, group_id) VALUES (:id, 2)"),
59+
parameters={"id": new_id},
60+
)
61+
3862
response = py_api.delete(f"/users/{new_id}?api_key={ApiKey.ADMIN}")
3963
assert response.status_code == HTTPStatus.OK
4064
assert response.json() == {"user_id": new_id, "deleted": True}
4165

66+
# Verify DB side-effects
67+
user_count = user_test.execute(
68+
text("SELECT COUNT(*) FROM users WHERE id = :id"),
69+
parameters={"id": new_id},
70+
).scalar()
71+
assert user_count == 0
72+
4273

4374
def test_delete_user_no_auth(py_api: TestClient) -> None:
4475
"""No API key → 401."""
@@ -58,3 +89,22 @@ def test_delete_user_not_found(py_api: TestClient) -> None:
5889
response = py_api.delete(f"/users/99999999?api_key={ApiKey.ADMIN}")
5990
assert response.status_code == HTTPStatus.NOT_FOUND
6091
assert response.json()["detail"]["code"] == "120"
92+
93+
94+
def test_delete_user_has_resources(py_api: TestClient, user_test: Connection) -> None:
95+
"""A user with resources (datasets, flows, runs) gets a 409 Conflict."""
96+
# User 16 owns dataset 130 per tests/users.py definition
97+
target_id = 16
98+
response = py_api.delete(f"/users/{target_id}?api_key={ApiKey.DATASET_130_OWNER}")
99+
100+
assert response.status_code == HTTPStatus.CONFLICT
101+
assert response.json()["detail"]["code"] == "122"
102+
assert "resource(s)" in response.json()["detail"]["message"]
103+
104+
# Verify user record was NOT deleted
105+
user_count = user_test.execute(
106+
text("SELECT COUNT(*) FROM users WHERE id = :id"),
107+
parameters={"id": target_id},
108+
).scalar()
109+
assert user_count == 1
110+

0 commit comments

Comments
 (0)