test_users.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. from uuid import uuid4
  2. import pytest
  3. from httpx import AsyncClient
  4. from sqlalchemy import select
  5. from sqlalchemy.orm import Session
  6. from app.main import app
  7. from app.models import GlobalRole, OrgRole, User, UserOrganization,Organization
  8. from app.tests.conftest import default_user_email, default_user_password, default_organization_id
  9. pytestmark = pytest.mark.asyncio
  10. class TestListUsers:
  11. async def test_requires_auth(self, client: AsyncClient):
  12. response = await client.get(app.url_path_for("list_users"))
  13. assert response.status_code == 401
  14. async def test_sees_colleagues_in_same_organization(
  15. self, client: AsyncClient, make_org_user
  16. ):
  17. viewer, headers = make_org_user(role=OrgRole.RESPO_BENEVOLE)
  18. colleague, _ = make_org_user(role=OrgRole.RESPO_COMMISSION) # same default_organization
  19. response = await client.get(app.url_path_for("list_users"), headers=headers)
  20. assert response.status_code == 200
  21. emails = [u["email"] for u in response.json()]
  22. assert viewer.email in emails
  23. assert colleague.email in emails
  24. async def test_excludes_users_from_other_organization(
  25. self, client: AsyncClient, make_org_user, session: Session
  26. ):
  27. _, headers = make_org_user(role=OrgRole.RESPO_BENEVOLE) # member of default_organization
  28. other_org = Organization(id=uuid4(), name="Other Org")
  29. session.add(other_org)
  30. session.commit()
  31. outsider = User(
  32. id=uuid4(), email="outsider@test.com", hashed_password="x", name="Outsider"
  33. )
  34. session.add(outsider)
  35. session.commit()
  36. session.add(
  37. UserOrganization(user_id=outsider.id, organization_id=other_org.id, role=OrgRole.ORG_ADMIN)
  38. )
  39. session.commit()
  40. response = await client.get(app.url_path_for("list_users"), headers=headers)
  41. assert response.status_code == 200
  42. emails = [u["email"] for u in response.json()]
  43. assert outsider.email not in emails
  44. async def test_user_with_no_organization_sees_only_self(
  45. self, client: AsyncClient, make_org_user
  46. ):
  47. user, headers = make_org_user(role=None)
  48. response = await client.get(app.url_path_for("list_users"), headers=headers)
  49. assert response.status_code == 200
  50. emails = [u["email"] for u in response.json()]
  51. assert emails == [user.email]
  52. async def test_super_admin_sees_users_across_all_organizations(
  53. self, client: AsyncClient, make_org_user, session: Session
  54. ):
  55. member, _ = make_org_user(role=OrgRole.ORG_ADMIN) # in default_organization
  56. other_org = Organization(id=uuid4(), name="Other Org")
  57. session.add(other_org)
  58. session.commit()
  59. outsider = User(
  60. id=uuid4(), email="outsider2@test.com", hashed_password="x", name="Outsider2"
  61. )
  62. session.add(outsider)
  63. session.commit()
  64. session.add(
  65. UserOrganization(user_id=outsider.id, organization_id=other_org.id, role=OrgRole.RESPO_BENEVOLE)
  66. )
  67. session.commit()
  68. _, admin_headers = make_org_user(role=None, global_role=GlobalRole.SUPER_ADMIN)
  69. response = await client.get(app.url_path_for("list_users"), headers=admin_headers)
  70. assert response.status_code == 200
  71. emails = [u["email"] for u in response.json()]
  72. assert member.email in emails
  73. assert outsider.email in emails
  74. class TestReadCurrentUser:
  75. async def test_requires_auth(self, client: AsyncClient):
  76. response = await client.get(app.url_path_for("read_current_user"))
  77. assert response.status_code == 401
  78. async def test_returns_caller(self, client: AsyncClient, default_user_headers: dict):
  79. response = await client.get(app.url_path_for("read_current_user"), headers=default_user_headers)
  80. assert response.status_code == 200
  81. assert response.json()["email"] == default_user_email
  82. async def test_includes_org_memberships(
  83. self, client: AsyncClient, make_org_user
  84. ):
  85. _, headers = make_org_user(role=OrgRole.RESPO_BENEVOLE)
  86. response = await client.get(app.url_path_for("read_current_user"), headers=headers)
  87. assert response.status_code == 200
  88. data = response.json()
  89. assert len(data["organizations"]) == 1
  90. assert data["organizations"][0]["role"] == OrgRole.RESPO_BENEVOLE.value
  91. class TestUpdateCurrentUserProfile:
  92. async def test_requires_auth(self, client: AsyncClient):
  93. response = await client.patch(app.url_path_for("update_current_user_profile"), json={"name": "New Name"})
  94. assert response.status_code == 401
  95. async def test_updates_name(self, client: AsyncClient, default_user_headers: dict, session: Session):
  96. response = await client.patch(
  97. app.url_path_for("update_current_user_profile"),
  98. headers=default_user_headers,
  99. json={"name": "Geralt of Rivia"},
  100. )
  101. assert response.status_code == 200
  102. assert response.json()["name"] == "Geralt of Rivia"
  103. async def test_updates_phone_number(self, client: AsyncClient, default_user_headers: dict):
  104. response = await client.patch(
  105. app.url_path_for("update_current_user_profile"),
  106. headers=default_user_headers,
  107. json={"phone_number": "0601020304"},
  108. )
  109. assert response.status_code == 200
  110. assert response.json()["phone_number"] == "0601020304"
  111. async def test_partial_update_does_not_clear_other_field(
  112. self, client: AsyncClient, default_user_headers: dict
  113. ):
  114. await client.patch(
  115. app.url_path_for("update_current_user_profile"),
  116. headers=default_user_headers,
  117. json={"name": "Geralt", "phone_number": "0601020304"},
  118. )
  119. response = await client.patch(
  120. app.url_path_for("update_current_user_profile"),
  121. headers=default_user_headers,
  122. json={"name": "Geralt Updated"},
  123. )
  124. assert response.status_code == 200
  125. assert response.json()["name"] == "Geralt Updated"
  126. assert response.json()["phone_number"] == "0601020304"
  127. class TestDeleteCurrentUser:
  128. async def test_requires_auth(self, client: AsyncClient):
  129. response = await client.delete(app.url_path_for("delete_current_user"))
  130. assert response.status_code == 401
  131. async def test_deletes_caller(self, client: AsyncClient, make_org_user, session: Session):
  132. user, headers = make_org_user(role=OrgRole.RESPO_BENEVOLE)
  133. response = await client.delete(app.url_path_for("delete_current_user"), headers=headers)
  134. assert response.status_code == 204
  135. assert session.get(User, user.id) is None
  136. async def test_cascades_org_membership(self, client: AsyncClient, make_org_user, session: Session):
  137. """Deleting a user should also remove their UserOrganization rows (FK cascade)."""
  138. from app.models import UserOrganization
  139. user, headers = make_org_user(role=OrgRole.RESPO_BENEVOLE)
  140. assert session.get(UserOrganization, (user.id, default_organization_id)) is not None
  141. await client.delete(app.url_path_for("delete_current_user"), headers=headers)
  142. result = session.execute(select(UserOrganization).where(UserOrganization.user_id == user.id))
  143. assert result.scalars().first() is None
  144. class TestResetPassword:
  145. async def test_requires_auth(self, client: AsyncClient):
  146. response = await client.post(
  147. app.url_path_for("reset_current_user_password"), json={"password": "new-password"}
  148. )
  149. assert response.status_code == 401
  150. async def test_updates_password(self, client: AsyncClient, default_user_headers: dict, session: Session):
  151. response = await client.post(
  152. app.url_path_for("reset_current_user_password"),
  153. headers=default_user_headers,
  154. json={"password": "new-password"},
  155. )
  156. assert response.status_code == 200
  157. from app.core.security import verify_password
  158. result = session.execute(select(User).where(User.email == default_user_email))
  159. user = result.scalars().first()
  160. assert verify_password("new-password", user.hashed_password)
  161. assert not verify_password(default_user_password, user.hashed_password)
  162. class TestRegisterUser:
  163. async def test_creates_user(self, client: AsyncClient, session: Session):
  164. response = await client.post(
  165. app.url_path_for("register_new_user"),
  166. json={"email": "new-recruit@test.com", "password": "hunter2", "name": "New Recruit"},
  167. )
  168. assert response.status_code == 200
  169. data = response.json()
  170. assert data["email"] == "new-recruit@test.com"
  171. assert data["name"] == "New Recruit"
  172. result = session.execute(select(User).where(User.email == "new-recruit@test.com"))
  173. user = result.scalars().first()
  174. assert user is not None
  175. assert user.global_role == GlobalRole.USER
  176. assert user.organizations == []
  177. async def test_no_auth_required(self, client: AsyncClient):
  178. """Registration is public -- no headers needed."""
  179. response = await client.post(
  180. app.url_path_for("register_new_user"),
  181. json={"email": "another@test.com", "password": "hunter2", "name": "Another"},
  182. )
  183. assert response.status_code == 200
  184. async def test_duplicate_email_rejected(self, client: AsyncClient, default_user):
  185. response = await client.post(
  186. app.url_path_for("register_new_user"),
  187. json={"email": default_user_email, "password": "hunter2", "name": "Impostor"},
  188. )
  189. assert response.status_code == 400
  190. async def test_missing_name_validation_error(self, client: AsyncClient):
  191. response = await client.post(
  192. app.url_path_for("register_new_user"),
  193. json={"email": "no-name@test.com", "password": "hunter2"},
  194. )
  195. assert response.status_code == 422