"""Tests for users router (/api/users).""" import pytest from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession from models.user import User from tests.conftest import _create_user, auth_headers class TestListUsers: """GET /api/users tests.""" async def test_list_users_admin( self, client: AsyncClient, admin_user: User ): """Admin can list all users.""" resp = await client.get("/api/users", headers=auth_headers(admin_user)) assert resp.status_code == 200 data = resp.json() assert isinstance(data, list) # At least the admin user assert len(data) >= 1 usernames = [u["username"] for u in data] assert "admin" in usernames async def test_list_users_forbidden( self, client: AsyncClient, maker_user: User ): """Non-admin user cannot list users.""" resp = await client.get("/api/users", headers=auth_headers(maker_user)) assert resp.status_code == 403 class TestGetUser: """GET /api/users/{user_id} is not directly exposed; use get_me or list.""" async def test_get_me(self, client: AsyncClient, maker_user: User): """GET /api/auth/me returns current user profile.""" resp = await client.get( "/api/auth/me", headers=auth_headers(maker_user) ) assert resp.status_code == 200 data = resp.json() assert data["username"] == "maker" assert data["display_name"] == "Maker User" assert "Maker" in data["roles"] class TestCreateUser: """POST /api/users tests.""" async def test_create_user( self, client: AsyncClient, admin_user: User ): """Admin can create a new user.""" resp = await client.post( "/api/users", headers=auth_headers(admin_user), json={ "username": "newuser", "password": "NewPass123", "display_name": "New User", "roles": ["MeasurementTec"], "is_admin": False, }, ) assert resp.status_code == 201 data = resp.json() assert data["username"] == "newuser" assert data["display_name"] == "New User" assert "MeasurementTec" in data["roles"] assert data["active"] is True async def test_create_user_duplicate_username( self, client: AsyncClient, admin_user: User ): """Creating a user with existing username returns 409.""" resp = await client.post( "/api/users", headers=auth_headers(admin_user), json={ "username": "admin", "password": "DupPass123", "display_name": "Dup", "roles": [], }, ) assert resp.status_code == 409 class TestUpdateUser: """PUT /api/users/{user_id} tests.""" async def test_update_user( self, client: AsyncClient, admin_user: User, maker_user: User, ): """Admin can update another user.""" resp = await client.put( f"/api/users/{maker_user.id}", headers=auth_headers(admin_user), json={"display_name": "Updated Maker"}, ) assert resp.status_code == 200 assert resp.json()["display_name"] == "Updated Maker" class TestDeleteUser: """DELETE /api/users/{user_id} tests.""" async def test_delete_user( self, client: AsyncClient, admin_user: User, db_session: AsyncSession, ): """Admin can soft-delete another user.""" target = await _create_user( db_session, username="to_delete", display_name="Delete Me" ) resp = await client.delete( f"/api/users/{target.id}", headers=auth_headers(admin_user) ) assert resp.status_code == 204 async def test_delete_self_forbidden( self, client: AsyncClient, admin_user: User ): """Admin cannot deactivate themselves.""" resp = await client.delete( f"/api/users/{admin_user.id}", headers=auth_headers(admin_user) ) assert resp.status_code == 400 assert "yourself" in resp.json()["detail"] class TestProfile: """PUT /api/auth/me tests.""" async def test_update_me(self, client: AsyncClient, maker_user: User): """User can update own profile.""" resp = await client.put( "/api/auth/me", headers=auth_headers(maker_user), json={"display_name": "My New Name", "theme_pref": "dark"}, ) assert resp.status_code == 200 data = resp.json() assert data["display_name"] == "My New Name" assert data["theme_pref"] == "dark" async def test_change_password_not_in_profile( self, client: AsyncClient, maker_user: User ): """Profile update schema does not accept password field (422).""" resp = await client.put( "/api/auth/me", headers=auth_headers(maker_user), json={"password": "HackedPass1"}, ) # The field is simply ignored (exclude_unset), so no change should occur # but the request itself is valid (empty update) assert resp.status_code == 200