| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 |
- 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, Organization, OrgRole, User, UserOrganization
- from app.tests.conftest import default_organization_id, default_user_email, default_user_password
- 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.post(
- 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.post(
- 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.post(
- 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.post(
- app.url_path_for("update_current_user_profile"),
- headers=default_user_headers,
- json={"name": "Geralt", "phone_number": "0601020304"},
- )
- response = await client.post(
- 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
|