from uuid import uuid4 import pytest from httpx import AsyncClient from sqlalchemy import select from sqlalchemy.orm import Session from app.main import app from app.models import GlobalRole, OrgRole, User, UserOrganization,Organization from app.tests.conftest import default_user_email, default_user_password, default_organization_id pytestmark = pytest.mark.asyncio class TestListUsers: async def test_requires_auth(self, client: AsyncClient): response = await client.get(app.url_path_for("list_users")) assert response.status_code == 401 async def test_sees_colleagues_in_same_organization( self, client: AsyncClient, make_org_user ): viewer, headers = make_org_user(role=OrgRole.RESPO_BENEVOLE) colleague, _ = make_org_user(role=OrgRole.RESPO_COMMISSION) # same default_organization response = await client.get(app.url_path_for("list_users"), headers=headers) assert response.status_code == 200 emails = [u["email"] for u in response.json()] assert viewer.email in emails assert colleague.email in emails async def test_excludes_users_from_other_organization( self, client: AsyncClient, make_org_user, session: Session ): _, headers = make_org_user(role=OrgRole.RESPO_BENEVOLE) # member of default_organization other_org = Organization(id=uuid4(), name="Other Org") session.add(other_org) session.commit() outsider = User( id=uuid4(), email="outsider@test.com", hashed_password="x", name="Outsider" ) session.add(outsider) session.commit() session.add( UserOrganization(user_id=outsider.id, organization_id=other_org.id, role=OrgRole.ORG_ADMIN) ) session.commit() response = await client.get(app.url_path_for("list_users"), headers=headers) assert response.status_code == 200 emails = [u["email"] for u in response.json()] assert outsider.email not in emails async def test_user_with_no_organization_sees_only_self( self, client: AsyncClient, make_org_user ): user, headers = make_org_user(role=None) response = await client.get(app.url_path_for("list_users"), headers=headers) assert response.status_code == 200 emails = [u["email"] for u in response.json()] assert emails == [user.email] async def test_super_admin_sees_users_across_all_organizations( self, client: AsyncClient, make_org_user, session: Session ): member, _ = make_org_user(role=OrgRole.ORG_ADMIN) # in default_organization other_org = Organization(id=uuid4(), name="Other Org") session.add(other_org) session.commit() outsider = User( id=uuid4(), email="outsider2@test.com", hashed_password="x", name="Outsider2" ) session.add(outsider) session.commit() session.add( UserOrganization(user_id=outsider.id, organization_id=other_org.id, role=OrgRole.RESPO_BENEVOLE) ) session.commit() _, admin_headers = make_org_user(role=None, global_role=GlobalRole.SUPER_ADMIN) response = await client.get(app.url_path_for("list_users"), headers=admin_headers) assert response.status_code == 200 emails = [u["email"] for u in response.json()] assert member.email in emails assert outsider.email in emails class TestReadCurrentUser: async def test_requires_auth(self, client: AsyncClient): response = await client.get(app.url_path_for("read_current_user")) assert response.status_code == 401 async def test_returns_caller(self, client: AsyncClient, default_user_headers: dict): response = await client.get(app.url_path_for("read_current_user"), headers=default_user_headers) assert response.status_code == 200 assert response.json()["email"] == default_user_email async def test_includes_org_memberships( self, client: AsyncClient, make_org_user ): _, headers = make_org_user(role=OrgRole.RESPO_BENEVOLE) response = await client.get(app.url_path_for("read_current_user"), headers=headers) assert response.status_code == 200 data = response.json() assert len(data["organizations"]) == 1 assert data["organizations"][0]["role"] == OrgRole.RESPO_BENEVOLE.value class TestUpdateCurrentUserProfile: async def test_requires_auth(self, client: AsyncClient): response = await client.patch(app.url_path_for("update_current_user_profile"), json={"name": "New Name"}) assert response.status_code == 401 async def test_updates_name(self, client: AsyncClient, default_user_headers: dict, session: Session): response = await client.patch( app.url_path_for("update_current_user_profile"), headers=default_user_headers, json={"name": "Geralt of Rivia"}, ) assert response.status_code == 200 assert response.json()["name"] == "Geralt of Rivia" async def test_updates_phone_number(self, client: AsyncClient, default_user_headers: dict): response = await client.patch( app.url_path_for("update_current_user_profile"), headers=default_user_headers, json={"phone_number": "0601020304"}, ) assert response.status_code == 200 assert response.json()["phone_number"] == "0601020304" async def test_partial_update_does_not_clear_other_field( self, client: AsyncClient, default_user_headers: dict ): await client.patch( app.url_path_for("update_current_user_profile"), headers=default_user_headers, json={"name": "Geralt", "phone_number": "0601020304"}, ) response = await client.patch( app.url_path_for("update_current_user_profile"), headers=default_user_headers, json={"name": "Geralt Updated"}, ) assert response.status_code == 200 assert response.json()["name"] == "Geralt Updated" assert response.json()["phone_number"] == "0601020304" class TestDeleteCurrentUser: async def test_requires_auth(self, client: AsyncClient): response = await client.delete(app.url_path_for("delete_current_user")) assert response.status_code == 401 async def test_deletes_caller(self, client: AsyncClient, make_org_user, session: Session): user, headers = make_org_user(role=OrgRole.RESPO_BENEVOLE) response = await client.delete(app.url_path_for("delete_current_user"), headers=headers) assert response.status_code == 204 assert session.get(User, user.id) is None async def test_cascades_org_membership(self, client: AsyncClient, make_org_user, session: Session): """Deleting a user should also remove their UserOrganization rows (FK cascade).""" from app.models import UserOrganization user, headers = make_org_user(role=OrgRole.RESPO_BENEVOLE) assert session.get(UserOrganization, (user.id, default_organization_id)) is not None await client.delete(app.url_path_for("delete_current_user"), headers=headers) result = session.execute(select(UserOrganization).where(UserOrganization.user_id == user.id)) assert result.scalars().first() is None class TestResetPassword: async def test_requires_auth(self, client: AsyncClient): response = await client.post( app.url_path_for("reset_current_user_password"), json={"password": "new-password"} ) assert response.status_code == 401 async def test_updates_password(self, client: AsyncClient, default_user_headers: dict, session: Session): response = await client.post( app.url_path_for("reset_current_user_password"), headers=default_user_headers, json={"password": "new-password"}, ) assert response.status_code == 200 from app.core.security import verify_password result = session.execute(select(User).where(User.email == default_user_email)) user = result.scalars().first() assert verify_password("new-password", user.hashed_password) assert not verify_password(default_user_password, user.hashed_password) class TestRegisterUser: async def test_creates_user(self, client: AsyncClient, session: Session): response = await client.post( app.url_path_for("register_new_user"), json={"email": "new-recruit@test.com", "password": "hunter2", "name": "New Recruit"}, ) assert response.status_code == 200 data = response.json() assert data["email"] == "new-recruit@test.com" assert data["name"] == "New Recruit" result = session.execute(select(User).where(User.email == "new-recruit@test.com")) user = result.scalars().first() assert user is not None assert user.global_role == GlobalRole.USER assert user.organizations == [] async def test_no_auth_required(self, client: AsyncClient): """Registration is public -- no headers needed.""" response = await client.post( app.url_path_for("register_new_user"), json={"email": "another@test.com", "password": "hunter2", "name": "Another"}, ) assert response.status_code == 200 async def test_duplicate_email_rejected(self, client: AsyncClient, default_user): response = await client.post( app.url_path_for("register_new_user"), json={"email": default_user_email, "password": "hunter2", "name": "Impostor"}, ) assert response.status_code == 400 async def test_missing_name_validation_error(self, client: AsyncClient): response = await client.post( app.url_path_for("register_new_user"), json={"email": "no-name@test.com", "password": "hunter2"}, ) assert response.status_code == 422