fix(users): roles/is_admin null -> 422, admin non si toglie da solo (#36 punto 4)

roles e is_admin sono NOT NULL nel DB ma Optional nello schema: {"roles":
null} o {"is_admin": null} passava la validazione Pydantic e falliva al
flush con un 500 non gestito. Ora 422 esplicito. Aggiunta anche la
protezione sull'ultimo admin: un admin non puo' togliersi is_admin da solo
(409) - unico caso raggiungibile via API dato che solo un admin puo'
chiamare l'endpoint, quindi rimuovere il flag a qualcun altro lascia sempre
il chiamante; il controllo sul conteggio resta comunque come difesa in
profondita'.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 22:45:43 +02:00
parent 8864b2021b
commit 3e99b64fc4
2 changed files with 92 additions and 1 deletions
+55
View File
@@ -107,6 +107,61 @@ class TestUpdateUser:
assert resp.json()["display_name"] == "Updated Maker"
class TestUpdateUserValidation:
"""gitea#36 point 4: roles/is_admin cannot be null, admin self/last-admin guard."""
async def test_roles_null_is_422(
self, client: AsyncClient, admin_user: User, maker_user: User,
):
"""roles is Optional in the schema but NOT NULL in the DB: an explicit
null used to reach setattr() and blow up as an unhandled 500."""
resp = await client.put(
f"/api/users/{maker_user.id}",
headers=auth_headers(admin_user),
json={"roles": None},
)
assert resp.status_code == 422
async def test_is_admin_null_is_422(
self, client: AsyncClient, admin_user: User, maker_user: User,
):
resp = await client.put(
f"/api/users/{maker_user.id}",
headers=auth_headers(admin_user),
json={"is_admin": None},
)
assert resp.status_code == 422
async def test_admin_cannot_remove_own_admin_flag(
self, client: AsyncClient, admin_user: User,
):
"""admin_user is the only admin: this is also the only reachable
"last admin" case, since only an admin can call this endpoint at
all — removing someone else's flag always leaves the caller."""
resp = await client.put(
f"/api/users/{admin_user.id}",
headers=auth_headers(admin_user),
json={"is_admin": False},
)
assert resp.status_code == 409
async def test_admin_can_demote_another_admin_when_not_last(
self, client: AsyncClient, admin_user: User, db_session: AsyncSession,
):
"""Demoting someone else is fine as long as an admin remains (the
caller): the guard must not be broader than the two stated cases."""
second_admin = await _create_user(
db_session, username="second_admin", roles=[], is_admin=True,
)
resp = await client.put(
f"/api/users/{second_admin.id}",
headers=auth_headers(admin_user),
json={"is_admin": False},
)
assert resp.status_code == 200
assert resp.json()["is_admin"] is False
class TestDeleteUser:
"""DELETE /api/users/{user_id} tests."""