test_projects.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  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, OrgRole, Project, Slot, Volunteer
  8. from app.tests.conftest import default_organization_id, default_project_id, default_project_name
  9. from app.tests.shared_access import SharedProjectAccessTests
  10. pytestmark = pytest.mark.asyncio
  11. ALL_ORG_ROLES = [OrgRole.ORG_ADMIN, OrgRole.RESPO_BENEVOLE, OrgRole.RESPO_COMMISSION]
  12. PROJECT_ROUTES = [
  13. ("GET", "list_project", {}, None),
  14. (
  15. "POST",
  16. "create_project",
  17. {},
  18. {
  19. "name": "Test",
  20. "organization_id": default_organization_id,
  21. "is_public": False,
  22. },
  23. ),
  24. ("POST", "update_project", {"project_id": default_project_id}, {"title": "Updated"}),
  25. ("POST", "create_sms_batch", {"project_id": default_project_id}, {"template": "Hello"}),
  26. ("DELETE", "delete_project", {"project_id": default_project_id}, None),
  27. ]
  28. class TestProjectCrossCutting(SharedProjectAccessTests):
  29. @pytest.fixture(params=PROJECT_ROUTES, ids=lambda x: f"{x[0]}-{x[1]}")
  30. def resolved_route(self, request, default_project):
  31. return request.param
  32. class TestListProject:
  33. @pytest.mark.parametrize("role", ALL_ORG_ROLES)
  34. async def test_org_member_sees_their_project(
  35. self, client: AsyncClient, default_project: Project, make_org_user, role
  36. ):
  37. _, headers = make_org_user(role=role)
  38. response = await client.get(app.url_path_for("list_project"), headers=headers)
  39. assert response.status_code == 200
  40. data = response.json()
  41. assert len(data) == 1
  42. assert data[0]["id"] == default_project_id
  43. async def test_super_admin_sees_all_orgs(
  44. self, client: AsyncClient, default_project: Project, make_org_user
  45. ):
  46. _, headers = make_org_user(role=None, global_role=GlobalRole.SUPER_ADMIN)
  47. response = await client.get(app.url_path_for("list_project"), headers=headers)
  48. assert response.status_code == 200
  49. assert len(response.json()) == 1
  50. async def test_excludes_other_orgs_project(
  51. self,
  52. client: AsyncClient,
  53. default_project: Project,
  54. make_org_user,
  55. session: Session,
  56. other_org_user,
  57. ):
  58. _, headers = other_org_user
  59. response = await client.get(app.url_path_for("list_project"), headers=headers)
  60. assert response.status_code == 200
  61. assert response.json() == []
  62. class TestGetProject:
  63. @pytest.mark.parametrize(
  64. "role, expected_status",
  65. [
  66. (OrgRole.ORG_ADMIN, 200),
  67. (OrgRole.RESPO_BENEVOLE, 200),
  68. (OrgRole.RESPO_COMMISSION, 200),
  69. (None, 403),
  70. ],
  71. )
  72. async def test_role_access(
  73. self, client: AsyncClient, default_project: Project, make_org_user, role, expected_status
  74. ):
  75. _, headers = make_org_user(role=role)
  76. response = await client.get(
  77. app.url_path_for("get_project", project_id=default_project_id), headers=headers
  78. )
  79. assert response.status_code == expected_status
  80. if expected_status == 200:
  81. assert response.json()["id"] == default_project_id
  82. @pytest.mark.parametrize("project_id,status", ((default_project_id, 200), (uuid.uuid4(), 404)))
  83. async def test_super_admin_bypasses_membership(
  84. self, client: AsyncClient, default_project: Project, make_org_user, project_id, status
  85. ):
  86. _, headers = make_org_user(role=None, global_role=GlobalRole.SUPER_ADMIN)
  87. response = await client.get(
  88. app.url_path_for("get_project", project_id=project_id), headers=headers
  89. )
  90. assert response.status_code == status
  91. class TestGetPublicProject:
  92. async def test_missing_or_private_returns_404(
  93. self, client: AsyncClient, default_project: Project
  94. ):
  95. response = await client.get(
  96. app.url_path_for("get_public_project", project_id=default_project_id)
  97. )
  98. assert response.status_code == 404
  99. async def test_public_project_no_auth_needed(
  100. self, client: AsyncClient, default_public_project: Project
  101. ):
  102. response = await client.get(
  103. app.url_path_for("get_public_project", project_id=default_project_id)
  104. )
  105. assert response.status_code == 200
  106. assert response.json()["name"] == default_project_name
  107. class TestListPublicProject:
  108. async def test_excludes_private(self, client: AsyncClient, default_project: Project):
  109. response = await client.get(app.url_path_for("list_public_project"))
  110. assert response.status_code == 200
  111. assert response.json() == []
  112. async def test_includes_public(self, client: AsyncClient, default_public_project: Project):
  113. response = await client.get(app.url_path_for("list_public_project"))
  114. assert response.status_code == 200
  115. assert len(response.json()) == 1
  116. class TestCreateProject:
  117. @pytest.mark.parametrize(
  118. "role, expected_status",
  119. [
  120. (OrgRole.ORG_ADMIN, 200),
  121. (OrgRole.RESPO_BENEVOLE, 403),
  122. (OrgRole.RESPO_COMMISSION, 403),
  123. (None, 403),
  124. ],
  125. )
  126. async def test_role_access(self, client: AsyncClient, make_org_user, role, expected_status):
  127. _, headers = make_org_user(role=role)
  128. response = await client.post(
  129. app.url_path_for("create_project"),
  130. headers=headers,
  131. json={"name": f"Project-{role}", "organization_id": default_organization_id},
  132. )
  133. assert response.status_code == expected_status
  134. async def test_super_admin_can_create_anywhere(self, client: AsyncClient, make_org_user):
  135. _, headers = make_org_user(role=None, global_role=GlobalRole.SUPER_ADMIN)
  136. response = await client.post(
  137. app.url_path_for("create_project"),
  138. headers=headers,
  139. json={"name": "Super Created", "organization_id": default_organization_id},
  140. )
  141. assert response.status_code == 200
  142. async def test_public_flag(self, client: AsyncClient, make_org_user, session: Session):
  143. _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
  144. response = await client.post(
  145. app.url_path_for("create_project"),
  146. headers=headers,
  147. json={"name": "Public", "is_public": True, "organization_id": default_organization_id},
  148. )
  149. print(response.json())
  150. assert response.status_code == 200
  151. project = session.execute(select(Project).where(Project.name == "Public")).scalars().first()
  152. assert project.is_public
  153. async def test_validation_error(self, client: AsyncClient, make_org_user, session: Session):
  154. _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
  155. response = await client.post(
  156. app.url_path_for("create_project"), headers=headers, json={"coucou": "Coucou"}
  157. )
  158. assert response.status_code == 422
  159. assert session.execute(select(Project)).scalars().first() is None
  160. async def test_existing_name(
  161. self, client: AsyncClient, make_org_user, default_project: Project, session: Session
  162. ):
  163. _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
  164. response = await client.post(
  165. app.url_path_for("create_project"),
  166. headers=headers,
  167. json={"name": default_project_name, "organization_id": default_organization_id},
  168. )
  169. assert response.status_code == 400
  170. class TestUpdateProject:
  171. @pytest.mark.parametrize(
  172. "role, expected_status",
  173. [
  174. (OrgRole.ORG_ADMIN, 200),
  175. (OrgRole.RESPO_BENEVOLE, 403),
  176. (OrgRole.RESPO_COMMISSION, 403),
  177. (None, 403),
  178. ],
  179. )
  180. async def test_role_access(
  181. self,
  182. client: AsyncClient,
  183. default_public_project: Project,
  184. make_org_user,
  185. session: Session,
  186. role,
  187. expected_status,
  188. ):
  189. _, headers = make_org_user(role=role)
  190. response = await client.post(
  191. app.url_path_for("update_project", project_id=default_project_id),
  192. headers=headers,
  193. json={"name": "Coucou"},
  194. )
  195. print(response.json())
  196. assert response.status_code == expected_status
  197. if expected_status == 200:
  198. project = session.get(Project, default_project_id)
  199. assert project.name == "Coucou"
  200. async def test_validation_error(
  201. self, client: AsyncClient, default_public_project: Project, make_org_user
  202. ):
  203. _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
  204. response = await client.post(
  205. app.url_path_for("update_project", project_id=default_project_id),
  206. headers=headers,
  207. json={"is_public": False},
  208. )
  209. assert response.status_code == 422
  210. async def test_updates_timestamps(
  211. self, client: AsyncClient, default_public_project: Project, make_org_user, session: Session
  212. ):
  213. _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
  214. project = session.get(Project, default_project_id)
  215. created_at = project.created_at
  216. response = await client.post(
  217. app.url_path_for("update_project", project_id=default_project_id),
  218. headers=headers,
  219. json={"name": "Coucou 2", "is_public": False},
  220. )
  221. assert response.status_code == 200
  222. session.refresh(project)
  223. assert project.name == "Coucou 2"
  224. assert project.created_at == created_at
  225. assert project.created_at < project.updated_at
  226. assert not project.is_public
  227. class TestDeleteProject:
  228. @pytest.mark.parametrize(
  229. "role, expected_status",
  230. [
  231. (OrgRole.ORG_ADMIN, 200),
  232. (OrgRole.RESPO_BENEVOLE, 403),
  233. (OrgRole.RESPO_COMMISSION, 403),
  234. (None, 403),
  235. ],
  236. )
  237. async def test_role_access(
  238. self,
  239. client: AsyncClient,
  240. default_public_project: Project,
  241. make_org_user,
  242. session: Session,
  243. role,
  244. expected_status,
  245. ):
  246. _, headers = make_org_user(role=role)
  247. response = await client.delete(
  248. app.url_path_for("delete_project", project_id=default_project_id), headers=headers
  249. )
  250. assert response.status_code == expected_status
  251. project = session.get(Project, default_project_id)
  252. if expected_status == 200:
  253. assert project is None
  254. else:
  255. assert project is not None
  256. async def test_cascades_to_volunteers_and_slots(
  257. self, client: AsyncClient, default_public_project: Project, make_org_user, session: Session
  258. ):
  259. _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
  260. response = await client.delete(
  261. app.url_path_for("delete_project", project_id=default_project_id), headers=headers
  262. )
  263. assert response.status_code == 200
  264. assert (
  265. session.execute(select(Volunteer).where(Volunteer.project_id == default_project_id))
  266. .scalars()
  267. .first()
  268. is None
  269. )
  270. assert (
  271. session.execute(select(Slot).where(Slot.project_id == default_project_id))
  272. .scalars()
  273. .first()
  274. is None
  275. )