test_templates.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. from uuid import uuid4
  2. import pytest
  3. from httpx import AsyncClient
  4. from sqlalchemy.orm import Session
  5. from app.main import app
  6. from app.models import Commission, CommissionMember, OrgRole, Project, Slot, SlotTemplate
  7. from app.tests.conftest import (
  8. default_project_id,
  9. default_slot_id,
  10. default_tag_id,
  11. default_template_id,
  12. )
  13. from app.tests.shared_access import SharedProjectAccessTests
  14. pytestmark = pytest.mark.asyncio
  15. WRITE_FORBIDDEN_ROLES = [OrgRole.RESPO_COMMISSION, None] # respo_commission with NO commission set
  16. @pytest.fixture
  17. def commission_and_member(session: Session, default_public_project: Project, make_org_user):
  18. """A commission in default_public_project, with a fresh respo_commission
  19. user as its member. Returns (commission, member_user, member_headers)."""
  20. commission = Commission(project_id=default_public_project.id, name="Bar")
  21. session.add(commission)
  22. session.commit()
  23. user, headers = make_org_user(role=OrgRole.RESPO_COMMISSION)
  24. session.add(CommissionMember(commission_id=commission.id, user_id=user.id))
  25. session.commit()
  26. return commission, user, headers
  27. @pytest.fixture
  28. def other_commission(session: Session, default_public_project: Project):
  29. """A second commission the respo_commission fixture user is NOT part of."""
  30. commission = Commission(project_id=default_public_project.id, name="Scene")
  31. session.add(commission)
  32. session.commit()
  33. return commission
  34. route_kwargs = {"project_id": default_project_id}
  35. route_kwargs_2 = {**route_kwargs, "template_id": default_template_id}
  36. TEMPLATE_ROUTES = [
  37. ("GET", "list_project_templates", route_kwargs, None),
  38. ("POST", "create_template", route_kwargs, {"title": "Test"}),
  39. ("POST", "update_template", route_kwargs_2, {"title": "Updated"}),
  40. ("DELETE", "delete_template", route_kwargs_2, None),
  41. ]
  42. class TestSlotCrossCuttingAccess(SharedProjectAccessTests):
  43. @pytest.fixture(params=TEMPLATE_ROUTES, ids=lambda x: f"{x[0]}-{x[1]}")
  44. def resolved_route(self, request, default_project):
  45. return request.param
  46. class TestCreateTemplate:
  47. async def test_validation_error(
  48. self,
  49. client: AsyncClient,
  50. default_public_project: Project,
  51. default_user_headers: dict,
  52. default_user_org_membership,
  53. ):
  54. response = await client.post(
  55. app.url_path_for("create_template", project_id=default_public_project.id),
  56. json={},
  57. headers=default_user_headers,
  58. )
  59. assert response.status_code == 422
  60. @pytest.mark.parametrize(
  61. "payload",
  62. [
  63. {"title": "1st template"},
  64. {"title": "1𝖘ҭ ṥ٥ι𝙪𝓉ìóη", "place": "echo"},
  65. {"title": "1st template", "responsible_override": "echo"},
  66. {"title": "1st template", "description": "&é'(-è_çecho"},
  67. {"title": "1st template", "place": "3", "description": "&é'(-è_çecho"},
  68. {"title": "1st template", "comment": "55"},
  69. ],
  70. )
  71. async def test_org_admin_creates(
  72. self,
  73. client: AsyncClient,
  74. default_public_project: Project,
  75. default_user_headers: dict,
  76. default_user_org_membership,
  77. session: Session,
  78. payload: dict,
  79. ):
  80. response = await client.post(
  81. app.url_path_for("create_template", project_id=default_public_project.id),
  82. json=payload,
  83. headers=default_user_headers,
  84. )
  85. assert response.status_code == 200
  86. template = session.get(SlotTemplate, response.json()["id"])
  87. assert template is not None
  88. for k in ["title", "description", "place"]:
  89. val = getattr(template, k)
  90. assert val == payload[k] if k in payload else val == ""
  91. async def test_respo_benevole_creates_without_commission(
  92. self,
  93. client: AsyncClient,
  94. default_public_project: Project,
  95. make_org_user,
  96. ):
  97. _, headers = make_org_user(role=OrgRole.RESPO_BENEVOLE)
  98. response = await client.post(
  99. app.url_path_for("create_template", project_id=default_public_project.id),
  100. json={"title": "Benevole template"},
  101. headers=headers,
  102. )
  103. assert response.status_code == 200
  104. async def test_respo_commission_creates_for_own_commission(
  105. self,
  106. client: AsyncClient,
  107. default_public_project: Project,
  108. commission_and_member,
  109. ):
  110. commission, _, headers = commission_and_member
  111. response = await client.post(
  112. app.url_path_for("create_template", project_id=default_public_project.id),
  113. json={"title": "Bar setup", "commission_id": commission.id},
  114. headers=headers,
  115. )
  116. assert response.status_code == 200
  117. assert response.json()["commission_id"] == commission.id
  118. async def test_respo_commission_rejected_for_other_commission(
  119. self,
  120. client: AsyncClient,
  121. default_public_project: Project,
  122. commission_and_member,
  123. other_commission,
  124. ):
  125. _, _, headers = commission_and_member
  126. response = await client.post(
  127. app.url_path_for("create_template", project_id=default_public_project.id),
  128. json={"title": "Scene setup", "commission_id": other_commission.id},
  129. headers=headers,
  130. )
  131. assert response.status_code == 403
  132. async def test_respo_ok(
  133. self,
  134. client: AsyncClient,
  135. default_public_project: Project,
  136. make_org_user,
  137. ):
  138. _, headers = make_org_user(role=OrgRole.RESPO_COMMISSION)
  139. response = await client.post(
  140. app.url_path_for("create_template", project_id=default_public_project.id),
  141. json={"title": "No commission"},
  142. headers=headers,
  143. )
  144. assert response.status_code == 200
  145. class TestUpdateTemplate:
  146. async def test_invalid_template_id_format(
  147. self,
  148. client: AsyncClient,
  149. default_public_project: Project,
  150. default_user_headers: dict,
  151. default_user_org_membership,
  152. ):
  153. response = await client.post(
  154. app.url_path_for(
  155. "update_template", project_id=default_public_project.id, template_id="12"
  156. ),
  157. json={"title": "1st template"},
  158. headers=default_user_headers,
  159. )
  160. assert response.status_code == 422
  161. async def test_template_not_found(
  162. self,
  163. client: AsyncClient,
  164. default_public_project: Project,
  165. default_user_headers: dict,
  166. default_user_org_membership,
  167. ):
  168. response = await client.post(
  169. app.url_path_for(
  170. "update_template", project_id=default_public_project.id, template_id=uuid4()
  171. ),
  172. json={"title": "1st template"},
  173. headers=default_user_headers,
  174. )
  175. assert response.status_code == 404
  176. @pytest.mark.parametrize(
  177. "code,payload",
  178. [
  179. (422, {"title": [1.001, 2]}),
  180. (422, {"responsible_override": {"t": None}}),
  181. (422, {"place": [1, 2]}),
  182. (422, {"tags": ["1", "2"]}),
  183. (400, {"tags": [default_slot_id]}),
  184. (422, {"comment": 1235}),
  185. ],
  186. )
  187. async def test_payload_validation(
  188. self,
  189. client: AsyncClient,
  190. default_public_project: Project,
  191. default_user_headers: dict,
  192. default_user_org_membership,
  193. payload: dict,
  194. code: int,
  195. ):
  196. response = await client.post(
  197. app.url_path_for(
  198. "update_template",
  199. project_id=default_public_project.id,
  200. template_id=default_template_id,
  201. ),
  202. json=payload,
  203. headers=default_user_headers,
  204. )
  205. assert response.status_code == code
  206. @pytest.mark.parametrize(
  207. "payload",
  208. [
  209. {"title": "1st template"},
  210. {"title": "1st template", "place": "echo"},
  211. {"title": "1st template", "responsible_override": "echo"},
  212. {"title": "1st template", "description": "&é'(-è_çecho"},
  213. {"title": "1st template", "place": "Ḽơᶉëᶆ ȋṕšᶙṁ", "description": "&é'(-è_çecho"},
  214. {"title": "1st template", "place": "3", "description": "&é'(-è_çecho"},
  215. {"title": "1st template", "comment": "&é'(-è_çecho"},
  216. ],
  217. )
  218. async def test_org_admin_updates(
  219. self,
  220. client: AsyncClient,
  221. default_public_project: Project,
  222. default_user_headers: dict,
  223. default_user_org_membership,
  224. session: Session,
  225. payload: dict,
  226. ):
  227. response = await client.post(
  228. app.url_path_for(
  229. "update_template",
  230. project_id=default_public_project.id,
  231. template_id=default_template_id,
  232. ),
  233. json=payload,
  234. headers=default_user_headers,
  235. )
  236. assert response.status_code == 200
  237. template = session.get(SlotTemplate, default_template_id)
  238. assert template is not None
  239. for k in ["title", "description", "place", "comment"]:
  240. val = getattr(template, k)
  241. assert val == payload[k] if k in payload else val == ""
  242. async def test_update_tags(
  243. self,
  244. client: AsyncClient,
  245. default_public_project: Project,
  246. default_user_headers: dict,
  247. default_user_org_membership,
  248. session: Session,
  249. ):
  250. url = app.url_path_for(
  251. "update_template", project_id=default_public_project.id, template_id=default_template_id
  252. )
  253. response = await client.post(
  254. url, json={"tags": [default_tag_id]}, headers=default_user_headers
  255. )
  256. assert response.status_code == 200
  257. template = session.get(SlotTemplate, default_template_id)
  258. assert len(template.tags) == 1
  259. assert template.tags[0].id == default_tag_id
  260. response = await client.post(url, json={"tags": []}, headers=default_user_headers)
  261. assert response.status_code == 200
  262. session.refresh(template)
  263. assert len(template.tags) == 0
  264. async def test_respo_commission_updates_own_template(
  265. self,
  266. client: AsyncClient,
  267. default_public_project: Project,
  268. commission_and_member,
  269. session: Session,
  270. ):
  271. commission, _, headers = commission_and_member
  272. template = SlotTemplate(
  273. project_id=default_public_project.id, title="Bar", commission_id=commission.id
  274. )
  275. session.add(template)
  276. session.commit()
  277. response = await client.post(
  278. app.url_path_for(
  279. "update_template", project_id=default_public_project.id, template_id=template.id
  280. ),
  281. json={"title": "Bar renamed"},
  282. headers=headers,
  283. )
  284. assert response.status_code == 200
  285. session.refresh(template)
  286. assert template.title == "Bar renamed"
  287. async def test_respo_commission_rejected_for_other_commission_template(
  288. self,
  289. client: AsyncClient,
  290. default_public_project: Project,
  291. commission_and_member,
  292. other_commission,
  293. session: Session,
  294. ):
  295. _, _, headers = commission_and_member
  296. template = SlotTemplate(
  297. project_id=default_public_project.id, title="Scene", commission_id=other_commission.id
  298. )
  299. session.add(template)
  300. session.commit()
  301. response = await client.post(
  302. app.url_path_for(
  303. "update_template", project_id=default_public_project.id, template_id=template.id
  304. ),
  305. json={"title": "Hijacked"},
  306. headers=headers,
  307. )
  308. assert response.status_code == 403
  309. async def test_respo_commission_reassign_to_other_commission_rejected(
  310. self,
  311. client: AsyncClient,
  312. default_public_project: Project,
  313. commission_and_member,
  314. other_commission,
  315. session: Session,
  316. ):
  317. commission, _, headers = commission_and_member
  318. template = SlotTemplate(
  319. project_id=default_public_project.id, title="Bar", commission_id=commission.id
  320. )
  321. session.add(template)
  322. session.commit()
  323. response = await client.post(
  324. app.url_path_for(
  325. "update_template", project_id=default_public_project.id, template_id=template.id
  326. ),
  327. json={"commission_id": other_commission.id},
  328. headers=headers,
  329. )
  330. assert response.status_code == 403
  331. class TestDeleteTemplate:
  332. async def test_invalid_ids(
  333. self,
  334. client: AsyncClient,
  335. default_public_project: Project,
  336. default_user_headers: dict,
  337. default_user_org_membership,
  338. ):
  339. response = await client.delete(
  340. app.url_path_for(
  341. "delete_template",
  342. project_id=default_public_project.id,
  343. template_id="default_tag_id",
  344. ),
  345. headers=default_user_headers,
  346. )
  347. assert response.status_code == 422
  348. response = await client.delete(
  349. app.url_path_for("delete_template", project_id="ded", template_id=default_template_id),
  350. headers=default_user_headers,
  351. )
  352. assert response.status_code == 422
  353. async def test_org_admin_deletes(
  354. self,
  355. client: AsyncClient,
  356. default_public_project: Project,
  357. default_user_headers: dict,
  358. default_user_org_membership,
  359. session: Session,
  360. ):
  361. response = await client.delete(
  362. app.url_path_for(
  363. "delete_template",
  364. project_id=default_public_project.id,
  365. template_id=default_template_id,
  366. ),
  367. headers=default_user_headers,
  368. )
  369. assert response.status_code == 200
  370. assert session.get(SlotTemplate, default_template_id) is None
  371. async def test_delete_does_not_delete_slot(
  372. self,
  373. client: AsyncClient,
  374. default_public_project: Project,
  375. default_user_headers: dict,
  376. default_user_org_membership,
  377. session: Session,
  378. ):
  379. slot = session.get(Slot, default_slot_id)
  380. slot.template_id = default_template_id
  381. session.commit()
  382. response = await client.delete(
  383. app.url_path_for(
  384. "delete_template",
  385. project_id=default_public_project.id,
  386. template_id=default_template_id,
  387. ),
  388. headers=default_user_headers,
  389. )
  390. assert response.status_code == 200
  391. assert session.get(Slot, default_slot_id) is not None
  392. async def test_respo_commission_deletes_own_template(
  393. self,
  394. client: AsyncClient,
  395. default_public_project: Project,
  396. commission_and_member,
  397. session: Session,
  398. ):
  399. commission, _, headers = commission_and_member
  400. template = SlotTemplate(
  401. project_id=default_public_project.id, title="Bar", commission_id=commission.id
  402. )
  403. session.add(template)
  404. session.commit()
  405. template_id = template.id
  406. session.expunge(template)
  407. response = await client.delete(
  408. app.url_path_for(
  409. "delete_template", project_id=default_public_project.id, template_id=template_id
  410. ),
  411. headers=headers,
  412. )
  413. assert response.status_code == 200
  414. assert session.get(SlotTemplate, template_id) is None
  415. async def test_respo_commission_rejected_for_other_commission(
  416. self,
  417. client: AsyncClient,
  418. default_public_project: Project,
  419. commission_and_member,
  420. other_commission,
  421. session: Session,
  422. ):
  423. _, _, headers = commission_and_member
  424. template = SlotTemplate(
  425. project_id=default_public_project.id, title="Scene", commission_id=other_commission.id
  426. )
  427. session.add(template)
  428. session.commit()
  429. response = await client.delete(
  430. app.url_path_for(
  431. "delete_template", project_id=default_public_project.id, template_id=template.id
  432. ),
  433. headers=headers,
  434. )
  435. assert response.status_code == 403
  436. assert session.get(SlotTemplate, template.id) is not None