test_templates.py 16 KB

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