"""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.""" response = await admin_client.post("/api/v1/users/", json={ "email": "newuser@test.com", "password": "NewUser123!", "full_name": "New User", "role": "verkaeufer", "language": "de", }) 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.""" response = await verkaeufer_client.post("/api/v1/users/", json={ "email": "forbidden@test.com", "password": "Forbidden123!", "full_name": "Forbidden", "role": "admin", "language": "de", }) 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.""" response = await admin_client.post("/api/v1/users/", json={ "email": "admin@test.com", "password": "SomePassword123!", "full_name": "Duplicate", "role": "verkaeufer", "language": "de", }) 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.""" response = await admin_client.post("/api/v1/users/", json={ "email": "shortpw@test.com", "password": "short", "full_name": "Short PW", "role": "verkaeufer", "language": "de", }) assert response.status_code == 422 @pytest.mark.asyncio async def test_update_user_as_admin(admin_client: AsyncClient, admin_user: User, verkaeufer_user: User): """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 async def test_delete_user_soft_deactivate(admin_client: AsyncClient, verkaeufer_user: User): """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 async def test_delete_user_as_non_admin(verkaeufer_client: AsyncClient, admin_user: User): """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 async def test_user_response_excludes_password_hash(admin_client: AsyncClient, admin_user: User): """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