test_template.py 19 KB

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