test_project.py 13 KB

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