2026-07-14 11:51:32 +02:00
|
|
|
"""Tests for users endpoints: list, create, update, delete (admin only)."""
|
|
|
|
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from httpx import AsyncClient
|
|
|
|
|
|
|
|
|
|
from app.models.user import User
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_list_users_as_admin(admin_client: AsyncClient, admin_user: User):
|
|
|
|
|
"""GET /api/v1/users as admin returns 200 + paginated list."""
|
|
|
|
|
response = await admin_client.get("/api/v1/users/")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert "items" in data
|
|
|
|
|
assert "total" in data
|
|
|
|
|
assert "page" in data
|
|
|
|
|
assert "page_size" in data
|
|
|
|
|
assert data["total"] >= 1
|
|
|
|
|
assert data["page"] == 1
|
|
|
|
|
assert data["page_size"] == 20
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_list_users_as_non_admin(verkaeufer_client: AsyncClient):
|
|
|
|
|
"""GET /api/v1/users as verkaeufer returns 403."""
|
|
|
|
|
response = await verkaeufer_client.get("/api/v1/users/")
|
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert "error" in data["detail"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_list_users_without_auth(client: AsyncClient):
|
|
|
|
|
"""GET /api/v1/users without token returns 401."""
|
|
|
|
|
response = await client.get("/api/v1/users/")
|
|
|
|
|
assert response.status_code == 401
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_list_users_pagination(admin_client: AsyncClient, admin_user: User):
|
|
|
|
|
"""GET /api/v1/users?page=1&page_size=5 returns correct pagination."""
|
|
|
|
|
response = await admin_client.get("/api/v1/users/?page=1&page_size=5")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["page"] == 1
|
|
|
|
|
assert data["page_size"] == 5
|
|
|
|
|
assert len(data["items"]) <= 5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_create_user_as_admin(admin_client: AsyncClient):
|
|
|
|
|
"""POST /api/v1/users as admin creates a new user, returns 201."""
|
2026-07-17 21:28:58 +02:00
|
|
|
response = await admin_client.post(
|
|
|
|
|
"/api/v1/users/",
|
|
|
|
|
json={
|
|
|
|
|
"email": "newuser@test.com",
|
|
|
|
|
"password": "NewUser123!",
|
|
|
|
|
"full_name": "New User",
|
|
|
|
|
"role": "verkaeufer",
|
|
|
|
|
"language": "de",
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-07-14 11:51:32 +02:00
|
|
|
assert response.status_code == 201
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["email"] == "newuser@test.com"
|
|
|
|
|
assert data["full_name"] == "New User"
|
|
|
|
|
assert data["role"] == "verkaeufer"
|
|
|
|
|
assert data["is_active"] is True
|
|
|
|
|
assert "password_hash" not in data
|
|
|
|
|
assert "password" not in data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_create_user_as_non_admin(verkaeufer_client: AsyncClient):
|
|
|
|
|
"""POST /api/v1/users as verkaeufer returns 403."""
|
2026-07-17 21:28:58 +02:00
|
|
|
response = await verkaeufer_client.post(
|
|
|
|
|
"/api/v1/users/",
|
|
|
|
|
json={
|
|
|
|
|
"email": "forbidden@test.com",
|
|
|
|
|
"password": "Forbidden123!",
|
|
|
|
|
"full_name": "Forbidden",
|
|
|
|
|
"role": "admin",
|
|
|
|
|
"language": "de",
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-07-14 11:51:32 +02:00
|
|
|
assert response.status_code == 403
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_create_user_duplicate_email(admin_client: AsyncClient, admin_user: User):
|
|
|
|
|
"""POST /api/v1/users with existing email returns 409."""
|
2026-07-17 21:28:58 +02:00
|
|
|
response = await admin_client.post(
|
|
|
|
|
"/api/v1/users/",
|
|
|
|
|
json={
|
|
|
|
|
"email": "admin@test.com",
|
|
|
|
|
"password": "SomePassword123!",
|
|
|
|
|
"full_name": "Duplicate",
|
|
|
|
|
"role": "verkaeufer",
|
|
|
|
|
"language": "de",
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-07-14 11:51:32 +02:00
|
|
|
assert response.status_code == 409
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_create_user_short_password(admin_client: AsyncClient):
|
|
|
|
|
"""POST /api/v1/users with password < 8 chars returns 422."""
|
2026-07-17 21:28:58 +02:00
|
|
|
response = await admin_client.post(
|
|
|
|
|
"/api/v1/users/",
|
|
|
|
|
json={
|
|
|
|
|
"email": "shortpw@test.com",
|
|
|
|
|
"password": "short",
|
|
|
|
|
"full_name": "Short PW",
|
|
|
|
|
"role": "verkaeufer",
|
|
|
|
|
"language": "de",
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-07-14 11:51:32 +02:00
|
|
|
assert response.status_code == 422
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-07-17 21:28:58 +02:00
|
|
|
async def test_update_user_as_admin(
|
|
|
|
|
admin_client: AsyncClient, admin_user: User, verkaeufer_user: User
|
|
|
|
|
):
|
2026-07-14 11:51:32 +02:00
|
|
|
"""PUT /api/v1/users/:id as admin updates user fields, returns 200."""
|
|
|
|
|
response = await admin_client.put(
|
|
|
|
|
f"/api/v1/users/{verkaeufer_user.id}",
|
|
|
|
|
json={"full_name": "Updated Name", "language": "de"},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["full_name"] == "Updated Name"
|
|
|
|
|
assert data["language"] == "de"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_update_user_nonexistent(admin_client: AsyncClient):
|
|
|
|
|
"""PUT /api/v1/users/:id with unknown id returns 404."""
|
|
|
|
|
fake_id = uuid.uuid4()
|
|
|
|
|
response = await admin_client.put(
|
|
|
|
|
f"/api/v1/users/{fake_id}",
|
|
|
|
|
json={"full_name": "Nobody"},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-07-17 21:28:58 +02:00
|
|
|
async def test_delete_user_soft_deactivate(
|
|
|
|
|
admin_client: AsyncClient, verkaeufer_user: User
|
|
|
|
|
):
|
2026-07-14 11:51:32 +02:00
|
|
|
"""DELETE /api/v1/users/:id soft-deletes (is_active=false), returns 200."""
|
|
|
|
|
response = await admin_client.delete(f"/api/v1/users/{verkaeufer_user.id}")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["is_active"] is False
|
|
|
|
|
assert data["id"] == str(verkaeufer_user.id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_delete_user_nonexistent(admin_client: AsyncClient):
|
|
|
|
|
"""DELETE /api/v1/users/:id with unknown id returns 404."""
|
|
|
|
|
fake_id = uuid.uuid4()
|
|
|
|
|
response = await admin_client.delete(f"/api/v1/users/{fake_id}")
|
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-07-17 21:28:58 +02:00
|
|
|
async def test_delete_user_as_non_admin(
|
|
|
|
|
verkaeufer_client: AsyncClient, admin_user: User
|
|
|
|
|
):
|
2026-07-14 11:51:32 +02:00
|
|
|
"""DELETE /api/v1/users/:id as verkaeufer returns 403."""
|
|
|
|
|
response = await verkaeufer_client.delete(f"/api/v1/users/{admin_user.id}")
|
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-07-17 21:28:58 +02:00
|
|
|
async def test_user_response_excludes_password_hash(
|
|
|
|
|
admin_client: AsyncClient, admin_user: User
|
|
|
|
|
):
|
2026-07-14 11:51:32 +02:00
|
|
|
"""User response never includes password_hash or password fields."""
|
|
|
|
|
response = await admin_client.get("/api/v1/users/")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
for item in data["items"]:
|
|
|
|
|
assert "password_hash" not in item
|
|
|
|
|
assert "password" not in item
|