test_users.py 9.9 KB

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