test_commissions.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. import uuid
  2. import pytest
  3. from httpx import AsyncClient
  4. from sqlalchemy import select
  5. from sqlalchemy.orm import Session
  6. from app.core.session import session as session_maker
  7. from app.main import app
  8. from app.models import Commission, CommissionMember, Organization, OrgRole, Project, User
  9. from app.tests.shared_access import SharedProjectAccessTests
  10. pytestmark = pytest.mark.asyncio
  11. MANAGE_ROLES = [OrgRole.ORG_ADMIN, OrgRole.RESPO_BENEVOLE]
  12. FORBIDDEN_MANAGE_ROLES = [OrgRole.RESPO_COMMISSION]
  13. READ_ROLES = [OrgRole.ORG_ADMIN, OrgRole.RESPO_BENEVOLE, OrgRole.RESPO_COMMISSION]
  14. @pytest.fixture
  15. def two_users() -> tuple[User, User]:
  16. with session_maker() as db:
  17. u1 = User(
  18. email="u1@test.com",
  19. hashed_password="hashed1",
  20. name="Arthur Pendragon",
  21. phone_number="0601010101",
  22. )
  23. u2 = User(
  24. email="u2@test.com",
  25. hashed_password="hashed2",
  26. name="Merlin Enchanteur",
  27. phone_number="0602020202",
  28. )
  29. db.add_all([u1, u2])
  30. db.commit()
  31. db.refresh(u1)
  32. db.refresh(u2)
  33. return u1, u2
  34. @pytest.fixture
  35. def default_commission(default_project: Project) -> Commission:
  36. with session_maker() as db:
  37. commission = Commission(project_id=default_project.id, name="Logistique")
  38. db.add(commission)
  39. db.commit()
  40. db.refresh(commission)
  41. return commission
  42. COMMISSION_ROUTES = [
  43. ("GET", "list_project_commissions", {"project_id": "PROJECT"}, None),
  44. ("POST", "create_commission", {"project_id": "PROJECT"}, {"name": "Test"}),
  45. ("GET", "get_commission", {"project_id": "PROJECT", "commission_id": "COMMISSION"}, None),
  46. (
  47. "PATCH",
  48. "update_commission",
  49. {"project_id": "PROJECT", "commission_id": "COMMISSION"},
  50. {"name": "Updated"},
  51. ),
  52. ("DELETE", "delete_commission", {"project_id": "PROJECT", "commission_id": "COMMISSION"}, None),
  53. (
  54. "POST",
  55. "add_members_to_commission",
  56. {"project_id": "PROJECT", "commission_id": "COMMISSION"},
  57. {"user_ids": ["USER"]},
  58. ),
  59. (
  60. "DELETE",
  61. "remove_member_from_commission",
  62. {
  63. "project_id": "PROJECT",
  64. "commission_id": "COMMISSION",
  65. "user_id": "USER",
  66. },
  67. None,
  68. ),
  69. ]
  70. class TestCommissionCrossCutting(SharedProjectAccessTests):
  71. @pytest.fixture(params=COMMISSION_ROUTES, ids=lambda x: f"{x[0]}-{x[1]}")
  72. def resolved_route(self, request, default_project, default_commission):
  73. method, endpoint, kwargs, payload = request.param
  74. resolved_kwargs = {
  75. k: (
  76. default_project.id
  77. if v == "PROJECT"
  78. else default_commission.id
  79. if v == "COMMISSION"
  80. else v
  81. )
  82. for k, v in kwargs.items()
  83. }
  84. return method, endpoint, resolved_kwargs, payload
  85. class TestListCommissions:
  86. @pytest.mark.parametrize("role", READ_ROLES)
  87. async def test_role_access(
  88. self,
  89. client: AsyncClient,
  90. default_project: Project,
  91. default_commission: Commission,
  92. make_org_user,
  93. role,
  94. ):
  95. _, headers = make_org_user(role=role)
  96. response = await client.get(
  97. app.url_path_for("list_project_commissions", project_id=default_project.id),
  98. headers=headers,
  99. )
  100. assert response.status_code == 200
  101. data = response.json()
  102. assert len(data) == 1
  103. assert data[0]["name"] == "Logistique"
  104. class TestCreateCommission:
  105. @pytest.mark.parametrize("role", MANAGE_ROLES)
  106. async def test_role_access(
  107. self, client: AsyncClient, default_project: Project, make_org_user, session: Session, role
  108. ):
  109. _, headers = make_org_user(role=role)
  110. response = await client.post(
  111. app.url_path_for("create_commission", project_id=default_project.id),
  112. headers=headers,
  113. json={"name": "Accueil"},
  114. )
  115. assert response.status_code == 200
  116. commission = (
  117. session.execute(select(Commission).where(Commission.name == "Accueil"))
  118. .scalars()
  119. .first()
  120. )
  121. assert commission is not None
  122. assert commission.project_id == str(default_project.id)
  123. @pytest.mark.parametrize("role", FORBIDDEN_MANAGE_ROLES)
  124. async def test_forbidden_manage_roles(
  125. self, client: AsyncClient, default_project: Project, make_org_user, role
  126. ):
  127. _, headers = make_org_user(role=role)
  128. response = await client.post(
  129. app.url_path_for("create_commission", project_id=default_project.id),
  130. headers=headers,
  131. json={"name": "Nope"},
  132. )
  133. assert response.status_code == 403
  134. class TestGetCommission:
  135. async def test_commission_not_found(
  136. self, client: AsyncClient, make_org_user, default_project: Project
  137. ):
  138. _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
  139. response = await client.get(
  140. app.url_path_for(
  141. "get_commission", project_id=default_project.id, commission_id=uuid.uuid4()
  142. ),
  143. headers=headers,
  144. )
  145. assert response.status_code == 404
  146. async def test_commission_from_other_project_not_found(
  147. self, client: AsyncClient, default_commission: Commission, make_org_user, session: Session
  148. ):
  149. other_org = Organization(id=str(uuid.uuid4()), name="Other Org")
  150. session.add(other_org)
  151. session.commit()
  152. other_project = Project(name="Other Project", is_public=False, organization_id=other_org.id)
  153. session.add(other_project)
  154. session.commit()
  155. _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
  156. response = await client.get(
  157. app.url_path_for(
  158. "get_commission", project_id=other_project.id, commission_id=default_commission.id
  159. ),
  160. headers=headers,
  161. )
  162. # Even if they have the org admin role, the project_id URL mismatch means 403 or 404
  163. assert response.status_code in (403, 404)
  164. class TestUpdateCommission:
  165. async def test_updates_name(
  166. self,
  167. client: AsyncClient,
  168. default_project: Project,
  169. default_commission: Commission,
  170. make_org_user,
  171. session: Session,
  172. ):
  173. _, headers = make_org_user(role=OrgRole.RESPO_BENEVOLE)
  174. response = await client.patch(
  175. app.url_path_for(
  176. "update_commission",
  177. project_id=default_project.id,
  178. commission_id=default_commission.id,
  179. ),
  180. headers=headers,
  181. json={"name": "Securite Renamed"},
  182. )
  183. assert response.status_code == 200
  184. commission = session.get(Commission, default_commission.id)
  185. assert commission.name == "Securite Renamed"
  186. class TestDeleteCommission:
  187. async def test_deletes_commission_and_members_links_but_not_users(
  188. self,
  189. client: AsyncClient,
  190. default_project: Project,
  191. default_commission: Commission,
  192. two_users,
  193. make_org_user,
  194. session: Session,
  195. ):
  196. u1, _ = two_users
  197. commission = session.get(Commission, default_commission.id)
  198. commission.members.append(CommissionMember(user_id=u1.id))
  199. session.commit()
  200. _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
  201. response = await client.delete(
  202. app.url_path_for(
  203. "delete_commission",
  204. project_id=default_project.id,
  205. commission_id=default_commission.id,
  206. ),
  207. headers=headers,
  208. )
  209. assert response.status_code == 200
  210. assert session.get(Commission, default_commission.id) is None
  211. # User should still exist
  212. assert session.get(User, u1.id) is not None
  213. # Association row should be cascaded
  214. assoc = (
  215. session.execute(
  216. select(CommissionMember).where(
  217. CommissionMember.commission_id == default_commission.id
  218. )
  219. )
  220. .scalars()
  221. .all()
  222. )
  223. assert len(assoc) == 0
  224. class TestCommissionMembership:
  225. async def test_add_users_to_commission(
  226. self,
  227. client: AsyncClient,
  228. default_project: Project,
  229. default_commission: Commission,
  230. two_users,
  231. make_org_user,
  232. ):
  233. u1, u2 = two_users
  234. _, headers = make_org_user(role=OrgRole.RESPO_BENEVOLE)
  235. response = await client.post(
  236. app.url_path_for(
  237. "add_members_to_commission",
  238. project_id=default_project.id,
  239. commission_id=default_commission.id,
  240. ),
  241. headers=headers,
  242. json={"user_ids": [str(u1.id), str(u2.id)]},
  243. )
  244. assert response.status_code == 200
  245. data = response.json()
  246. assert len(data["contacts"]) == 2
  247. names = [contact["name"] for contact in data["contacts"]]
  248. assert "Arthur Pendragon" in names
  249. assert "Merlin Enchanteur" in names
  250. async def test_add_is_idempotent(
  251. self,
  252. client: AsyncClient,
  253. default_project: Project,
  254. default_commission: Commission,
  255. two_users,
  256. make_org_user,
  257. ):
  258. u1, _ = two_users
  259. _, headers = make_org_user(role=OrgRole.RESPO_BENEVOLE)
  260. await client.post(
  261. app.url_path_for(
  262. "add_members_to_commission",
  263. project_id=default_project.id,
  264. commission_id=default_commission.id,
  265. ),
  266. headers=headers,
  267. json={"user_ids": [str(u1.id)]},
  268. )
  269. response = await client.post(
  270. app.url_path_for(
  271. "add_members_to_commission",
  272. project_id=default_project.id,
  273. commission_id=default_commission.id,
  274. ),
  275. headers=headers,
  276. json={"user_ids": [str(u1.id)]},
  277. )
  278. assert response.status_code == 200
  279. assert len(response.json()["contacts"]) == 1
  280. async def test_remove_member_from_commission(
  281. self,
  282. client: AsyncClient,
  283. default_project: Project,
  284. default_commission: Commission,
  285. two_users,
  286. make_org_user,
  287. session: Session,
  288. ):
  289. u1, u2 = two_users
  290. commission = session.get(Commission, default_commission.id)
  291. commission.members.append(CommissionMember(user_id=u1.id))
  292. commission.members.append(CommissionMember(user_id=u2.id))
  293. session.commit()
  294. _, headers = make_org_user(role=OrgRole.RESPO_BENEVOLE)
  295. response = await client.delete(
  296. app.url_path_for(
  297. "remove_member_from_commission",
  298. project_id=default_project.id,
  299. commission_id=default_commission.id,
  300. user_id=u1.id,
  301. ),
  302. headers=headers,
  303. )
  304. assert response.status_code == 200
  305. data = response.json()
  306. assert len(data["contacts"]) == 1
  307. assert data["contacts"][0]["name"] == u2.name