test_project.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  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.main import app
  7. from app.models import GlobalRole, Organization, OrgRole, Project, Slot, Volunteer
  8. from app.tests.conftest import default_organization_id, default_project_id, default_project_name
  9. pytestmark = pytest.mark.asyncio
  10. ALL_ORG_ROLES = [OrgRole.ORG_ADMIN, OrgRole.RESPO_BENEVOLE, OrgRole.RESPO_COMMISSION]
  11. class TestListProject:
  12. async def test_requires_auth(self, client: AsyncClient):
  13. response = await client.get(app.url_path_for("list_project"))
  14. assert response.status_code == 401
  15. @pytest.mark.parametrize("role", ALL_ORG_ROLES)
  16. async def test_org_member_sees_their_project(
  17. self, client: AsyncClient, default_project: Project, make_org_user, role
  18. ):
  19. _, headers = make_org_user(role=role)
  20. response = await client.get(app.url_path_for("list_project"), headers=headers)
  21. assert response.status_code == 200
  22. data = response.json()
  23. assert len(data) == 1
  24. assert data[0]["id"] == default_project_id
  25. async def test_super_admin_sees_all_orgs(
  26. self, client: AsyncClient, default_project: Project, make_org_user
  27. ):
  28. _, headers = make_org_user(role=None, global_role=GlobalRole.SUPER_ADMIN)
  29. response = await client.get(app.url_path_for("list_project"), headers=headers)
  30. assert response.status_code == 200
  31. assert len(response.json()) == 1
  32. async def test_excludes_other_orgs_project(
  33. self,
  34. client: AsyncClient,
  35. default_project: Project,
  36. make_org_user,
  37. session: Session,
  38. other_org_user,
  39. ):
  40. _, headers = other_org_user
  41. response = await client.get(app.url_path_for("list_project"), headers=headers)
  42. assert response.status_code == 200
  43. assert response.json() == []
  44. class TestGetProject:
  45. async def test_requires_auth(self, client: AsyncClient, default_project: Project):
  46. response = await client.get(app.url_path_for("get_project", project_id=default_project_id))
  47. print(response.text)
  48. assert response.status_code == 401
  49. @pytest.mark.parametrize(
  50. "role, expected_status",
  51. [
  52. (OrgRole.ORG_ADMIN, 200),
  53. (OrgRole.RESPO_BENEVOLE, 200),
  54. (OrgRole.RESPO_COMMISSION, 200),
  55. (None, 403),
  56. ],
  57. )
  58. async def test_role_access(
  59. self, client: AsyncClient, default_project: Project, make_org_user, role, expected_status
  60. ):
  61. _, headers = make_org_user(role=role)
  62. response = await client.get(
  63. app.url_path_for("get_project", project_id=default_project_id), headers=headers
  64. )
  65. assert response.status_code == expected_status
  66. if expected_status == 200:
  67. assert response.json()["id"] == default_project_id
  68. @pytest.mark.parametrize("project_id,status", ((default_project_id, 200), (uuid.uuid4(), 404)))
  69. async def test_super_admin_bypasses_membership(
  70. self, client: AsyncClient, default_project: Project, make_org_user, project_id, status
  71. ):
  72. _, headers = make_org_user(role=None, global_role=GlobalRole.SUPER_ADMIN)
  73. response = await client.get(
  74. app.url_path_for("get_project", project_id=project_id), headers=headers
  75. )
  76. assert response.status_code == status
  77. async def test_forbidden_other_org(
  78. self, client: AsyncClient, default_project: Project, other_org_user
  79. ):
  80. _, headers = other_org_user
  81. response = await client.delete(
  82. app.url_path_for("get_project", project_id=default_project.id),
  83. headers=headers,
  84. )
  85. assert response.status_code == 403
  86. class TestGetPublicProject:
  87. async def test_missing_or_private_returns_404(
  88. self, client: AsyncClient, default_project: Project
  89. ):
  90. response = await client.get(
  91. app.url_path_for("get_public_project", project_id=default_project_id)
  92. )
  93. assert response.status_code == 404
  94. async def test_public_project_no_auth_needed(
  95. self, client: AsyncClient, default_public_project: Project
  96. ):
  97. response = await client.get(
  98. app.url_path_for("get_public_project", project_id=default_project_id)
  99. )
  100. assert response.status_code == 200
  101. assert response.json()["name"] == default_project_name
  102. class TestListPublicProject:
  103. async def test_excludes_private(self, client: AsyncClient, default_project: Project):
  104. response = await client.get(app.url_path_for("list_public_project"))
  105. assert response.status_code == 200
  106. assert response.json() == []
  107. async def test_includes_public(self, client: AsyncClient, default_public_project: Project):
  108. response = await client.get(app.url_path_for("list_public_project"))
  109. assert response.status_code == 200
  110. assert len(response.json()) == 1
  111. class TestCreateProject:
  112. async def test_requires_auth(
  113. self, client: AsyncClient, default_organization: Organization, session: Session
  114. ):
  115. response = await client.post(
  116. app.url_path_for("create_project"),
  117. json={"name": "Coucou", "organization_id": default_organization_id},
  118. )
  119. assert response.status_code == 401
  120. assert session.execute(select(Project)).scalars().first() is None
  121. @pytest.mark.parametrize(
  122. "role, expected_status",
  123. [
  124. (OrgRole.ORG_ADMIN, 200),
  125. (OrgRole.RESPO_BENEVOLE, 403),
  126. (OrgRole.RESPO_COMMISSION, 403),
  127. (None, 403),
  128. ],
  129. )
  130. async def test_role_access(self, client: AsyncClient, make_org_user, role, expected_status):
  131. _, headers = make_org_user(role=role)
  132. response = await client.post(
  133. app.url_path_for("create_project"),
  134. headers=headers,
  135. json={"name": f"Project-{role}", "organization_id": default_organization_id},
  136. )
  137. assert response.status_code == expected_status
  138. async def test_super_admin_can_create_anywhere(self, client: AsyncClient, make_org_user):
  139. _, headers = make_org_user(role=None, global_role=GlobalRole.SUPER_ADMIN)
  140. response = await client.post(
  141. app.url_path_for("create_project"),
  142. headers=headers,
  143. json={"name": "Super Created", "organization_id": default_organization_id},
  144. )
  145. assert response.status_code == 200
  146. async def test_public_flag(self, client: AsyncClient, make_org_user, session: Session):
  147. _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
  148. response = await client.post(
  149. app.url_path_for("create_project"),
  150. headers=headers,
  151. json={"name": "Public", "is_public": True, "organization_id": default_organization_id},
  152. )
  153. print(response.json())
  154. assert response.status_code == 200
  155. project = session.execute(select(Project).where(Project.name == "Public")).scalars().first()
  156. assert project.is_public
  157. async def test_validation_error(self, client: AsyncClient, make_org_user, session: Session):
  158. _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
  159. response = await client.post(
  160. app.url_path_for("create_project"), headers=headers, json={"coucou": "Coucou"}
  161. )
  162. assert response.status_code == 422
  163. assert session.execute(select(Project)).scalars().first() is None
  164. async def test_existing_name(
  165. self, client: AsyncClient, make_org_user, default_project: Project, session: Session
  166. ):
  167. _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
  168. response = await client.post(
  169. app.url_path_for("create_project"),
  170. headers=headers,
  171. json={"name": default_project_name, "organization_id": default_organization_id},
  172. )
  173. assert response.status_code == 400
  174. async def test_forbidden_other_org(
  175. self, client: AsyncClient, default_project: Project, other_org_user
  176. ):
  177. _, headers = other_org_user
  178. response = await client.post(
  179. app.url_path_for("create_project"),
  180. json={"name": "Coucou", "organization_id": default_organization_id},
  181. headers=headers,
  182. )
  183. assert response.status_code == 403
  184. class TestUpdateProject:
  185. @pytest.mark.parametrize(
  186. "role, expected_status",
  187. [
  188. (OrgRole.ORG_ADMIN, 200),
  189. (OrgRole.RESPO_BENEVOLE, 403),
  190. (OrgRole.RESPO_COMMISSION, 403),
  191. (None, 403),
  192. ],
  193. )
  194. async def test_role_access(
  195. self,
  196. client: AsyncClient,
  197. default_public_project: Project,
  198. make_org_user,
  199. session: Session,
  200. role,
  201. expected_status,
  202. ):
  203. _, headers = make_org_user(role=role)
  204. response = await client.post(
  205. app.url_path_for("update_project", project_id=default_project_id),
  206. headers=headers,
  207. json={"name": "Coucou"},
  208. )
  209. print(response.json())
  210. assert response.status_code == expected_status
  211. if expected_status == 200:
  212. project = session.get(Project, default_project_id)
  213. assert project.name == "Coucou"
  214. async def test_requires_auth(
  215. self, client: AsyncClient, default_project: Project, session: Session
  216. ):
  217. response = await client.post(
  218. app.url_path_for("update_project", project_id=default_project_id),
  219. json={"name": "Coucou 2"},
  220. )
  221. assert response.status_code == 401
  222. assert session.get(Project, default_project_id).name == default_project_name
  223. async def test_forbidden_other_org(
  224. self, client: AsyncClient, default_project: Project, other_org_user
  225. ):
  226. _, headers = other_org_user
  227. response = await client.post(
  228. app.url_path_for("update_project", project_id=default_project_id),
  229. json={"name": "Coucou 2"},
  230. headers=headers,
  231. )
  232. assert response.status_code == 403
  233. async def test_validation_error(
  234. self, client: AsyncClient, default_public_project: Project, make_org_user
  235. ):
  236. _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
  237. response = await client.post(
  238. app.url_path_for("update_project", project_id=default_project_id),
  239. headers=headers,
  240. json={"is_public": False},
  241. )
  242. assert response.status_code == 422
  243. async def test_updates_timestamps(
  244. self, client: AsyncClient, default_public_project: Project, make_org_user, session: Session
  245. ):
  246. _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
  247. project = session.get(Project, default_project_id)
  248. created_at = project.created_at
  249. response = await client.post(
  250. app.url_path_for("update_project", project_id=default_project_id),
  251. headers=headers,
  252. json={"name": "Coucou 2", "is_public": False},
  253. )
  254. assert response.status_code == 200
  255. session.refresh(project)
  256. assert project.name == "Coucou 2"
  257. assert project.created_at == created_at
  258. assert project.created_at < project.updated_at
  259. assert not project.is_public
  260. class TestDeleteProject:
  261. @pytest.mark.parametrize(
  262. "role, expected_status",
  263. [
  264. (OrgRole.ORG_ADMIN, 200),
  265. (OrgRole.RESPO_BENEVOLE, 403),
  266. (OrgRole.RESPO_COMMISSION, 403),
  267. (None, 403),
  268. ],
  269. )
  270. async def test_role_access(
  271. self,
  272. client: AsyncClient,
  273. default_public_project: Project,
  274. make_org_user,
  275. session: Session,
  276. role,
  277. expected_status,
  278. ):
  279. _, headers = make_org_user(role=role)
  280. response = await client.delete(
  281. app.url_path_for("delete_project", project_id=default_project_id), headers=headers
  282. )
  283. assert response.status_code == expected_status
  284. project = session.get(Project, default_project_id)
  285. if expected_status == 200:
  286. assert project is None
  287. else:
  288. assert project is not None
  289. async def test_requires_auth(self, client: AsyncClient, default_public_project: Project):
  290. response = await client.delete(
  291. app.url_path_for("delete_project", project_id=default_project_id)
  292. )
  293. assert response.status_code == 401
  294. async def test_forbidden_other_org(
  295. self, client: AsyncClient, default_project: Project, other_org_user
  296. ):
  297. _, headers = other_org_user
  298. response = await client.post(
  299. app.url_path_for("delete_project", project_id=default_project_id),
  300. headers=headers,
  301. )
  302. assert response.status_code == 403
  303. async def test_cascades_to_volunteers_and_slots(
  304. self, client: AsyncClient, default_public_project: Project, make_org_user, session: Session
  305. ):
  306. _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
  307. response = await client.delete(
  308. app.url_path_for("delete_project", project_id=default_project_id), headers=headers
  309. )
  310. assert response.status_code == 200
  311. assert (
  312. session.execute(select(Volunteer).where(Volunteer.project_id == default_project_id))
  313. .scalars()
  314. .first()
  315. is None
  316. )
  317. assert (
  318. session.execute(select(Slot).where(Slot.project_id == default_project_id))
  319. .scalars()
  320. .first()
  321. is None
  322. )
  323. async def test_nonexistent_project_returns_404(self, client: AsyncClient, make_org_user):
  324. """require_org_role looks the project up before the handler runs, so a
  325. missing project now 404s -- this differs from the pre-refactor behavior
  326. where DELETE on a nonexistent id silently returned 200."""
  327. _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
  328. response = await client.delete(
  329. app.url_path_for("delete_project", project_id=uuid.uuid4()), headers=headers
  330. )
  331. assert response.status_code == 404