test_template.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  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_validation_error(
  36. self,
  37. client: AsyncClient,
  38. default_public_project: Project,
  39. default_user_headers: dict,
  40. default_user_org_membership,
  41. ):
  42. response = await client.post(
  43. app.url_path_for("create_template", project_id=default_public_project.id),
  44. json={},
  45. headers=default_user_headers,
  46. )
  47. assert response.status_code == 422
  48. async def test_project_not_found(
  49. self,
  50. client: AsyncClient,
  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=uuid4()),
  56. json={"title": "1st template"},
  57. headers=default_user_headers,
  58. )
  59. assert response.status_code == 404
  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. @pytest.mark.parametrize("role", (None, OrgRole.RESPO_COMMISSION))
  133. async def test_respo_ok(
  134. self,
  135. client: AsyncClient,
  136. default_public_project: Project,
  137. make_org_user,
  138. role,
  139. ):
  140. _, headers = make_org_user(role=role)
  141. response = await client.post(
  142. app.url_path_for("create_template", project_id=default_public_project.id),
  143. json={"title": "No commission"},
  144. headers=headers,
  145. )
  146. assert response.status_code == 200
  147. class TestUpdateTemplate:
  148. async def test_requires_auth(self, client: AsyncClient, default_public_project: Project):
  149. response = await client.post(
  150. app.url_path_for(
  151. "update_template",
  152. project_id=default_public_project.id,
  153. template_id=default_template_id,
  154. ),
  155. json={"title": "x"},
  156. )
  157. assert response.status_code == 401
  158. async def test_invalid_template_id_format(
  159. self,
  160. client: AsyncClient,
  161. default_public_project: Project,
  162. default_user_headers: dict,
  163. default_user_org_membership,
  164. ):
  165. response = await client.post(
  166. app.url_path_for(
  167. "update_template", project_id=default_public_project.id, template_id="12"
  168. ),
  169. json={"title": "1st template"},
  170. headers=default_user_headers,
  171. )
  172. assert response.status_code == 422
  173. async def test_template_not_found(
  174. self,
  175. client: AsyncClient,
  176. default_public_project: Project,
  177. default_user_headers: dict,
  178. default_user_org_membership,
  179. ):
  180. response = await client.post(
  181. app.url_path_for(
  182. "update_template", project_id=default_public_project.id, template_id=uuid4()
  183. ),
  184. json={"title": "1st template"},
  185. headers=default_user_headers,
  186. )
  187. assert response.status_code == 404
  188. async def test_project_not_found(
  189. self,
  190. client: AsyncClient,
  191. default_user_headers: dict,
  192. default_user_org_membership,
  193. ):
  194. response = await client.post(
  195. app.url_path_for(
  196. "update_template", project_id=uuid4(), template_id=default_template_id
  197. ),
  198. json={"title": "1st template"},
  199. headers=default_user_headers,
  200. )
  201. assert response.status_code == 404
  202. @pytest.mark.parametrize(
  203. "code,payload",
  204. [
  205. (422, {"title": [1.001, 2]}),
  206. (422, {"responsible_override": {"t": None}}),
  207. (422, {"place": [1, 2]}),
  208. (422, {"tags": ["1", "2"]}),
  209. (400, {"tags": [default_slot_id]}),
  210. (422, {"comment": 1235}),
  211. ],
  212. )
  213. async def test_payload_validation(
  214. self,
  215. client: AsyncClient,
  216. default_public_project: Project,
  217. default_user_headers: dict,
  218. default_user_org_membership,
  219. payload: dict,
  220. code: int,
  221. ):
  222. response = await client.post(
  223. app.url_path_for(
  224. "update_template",
  225. project_id=default_public_project.id,
  226. template_id=default_template_id,
  227. ),
  228. json=payload,
  229. headers=default_user_headers,
  230. )
  231. assert response.status_code == code
  232. @pytest.mark.parametrize(
  233. "payload",
  234. [
  235. {"title": "1st template"},
  236. {"title": "1st template", "place": "echo"},
  237. {"title": "1st template", "responsible_override": "echo"},
  238. {"title": "1st template", "description": "&é'(-è_çecho"},
  239. {"title": "1st template", "place": "Ḽơᶉëᶆ ȋṕšᶙṁ", "description": "&é'(-è_çecho"},
  240. {"title": "1st template", "place": "3", "description": "&é'(-è_çecho"},
  241. {"title": "1st template", "comment": "&é'(-è_çecho"},
  242. ],
  243. )
  244. async def test_org_admin_updates(
  245. self,
  246. client: AsyncClient,
  247. default_public_project: Project,
  248. default_user_headers: dict,
  249. default_user_org_membership,
  250. session: Session,
  251. payload: dict,
  252. ):
  253. response = await client.post(
  254. app.url_path_for(
  255. "update_template",
  256. project_id=default_public_project.id,
  257. template_id=default_template_id,
  258. ),
  259. json=payload,
  260. headers=default_user_headers,
  261. )
  262. assert response.status_code == 200
  263. template = session.get(SlotTemplate, default_template_id)
  264. assert template is not None
  265. for k in ["title", "description", "place", "comment"]:
  266. val = getattr(template, k)
  267. assert val == payload[k] if k in payload else val == ""
  268. async def test_update_tags(
  269. self,
  270. client: AsyncClient,
  271. default_public_project: Project,
  272. default_user_headers: dict,
  273. default_user_org_membership,
  274. session: Session,
  275. ):
  276. url = app.url_path_for(
  277. "update_template", project_id=default_public_project.id, template_id=default_template_id
  278. )
  279. response = await client.post(
  280. url, json={"tags": [default_tag_id]}, headers=default_user_headers
  281. )
  282. assert response.status_code == 200
  283. template = session.get(SlotTemplate, default_template_id)
  284. assert len(template.tags) == 1
  285. assert template.tags[0].id == default_tag_id
  286. response = await client.post(url, json={"tags": []}, headers=default_user_headers)
  287. assert response.status_code == 200
  288. session.refresh(template)
  289. assert len(template.tags) == 0
  290. async def test_respo_commission_updates_own_template(
  291. self,
  292. client: AsyncClient,
  293. default_public_project: Project,
  294. commission_and_member,
  295. session: Session,
  296. ):
  297. commission, _, headers = commission_and_member
  298. template = SlotTemplate(
  299. project_id=default_public_project.id, title="Bar", commission_id=commission.id
  300. )
  301. session.add(template)
  302. session.commit()
  303. response = await client.post(
  304. app.url_path_for(
  305. "update_template", project_id=default_public_project.id, template_id=template.id
  306. ),
  307. json={"title": "Bar renamed"},
  308. headers=headers,
  309. )
  310. assert response.status_code == 200
  311. session.refresh(template)
  312. assert template.title == "Bar renamed"
  313. async def test_respo_commission_rejected_for_other_commission_template(
  314. self,
  315. client: AsyncClient,
  316. default_public_project: Project,
  317. commission_and_member,
  318. other_commission,
  319. session: Session,
  320. ):
  321. _, _, headers = commission_and_member
  322. template = SlotTemplate(
  323. project_id=default_public_project.id, title="Scene", commission_id=other_commission.id
  324. )
  325. session.add(template)
  326. session.commit()
  327. response = await client.post(
  328. app.url_path_for(
  329. "update_template", project_id=default_public_project.id, template_id=template.id
  330. ),
  331. json={"title": "Hijacked"},
  332. headers=headers,
  333. )
  334. assert response.status_code == 403
  335. async def test_respo_commission_reassign_to_other_commission_rejected(
  336. self,
  337. client: AsyncClient,
  338. default_public_project: Project,
  339. commission_and_member,
  340. other_commission,
  341. session: Session,
  342. ):
  343. commission, _, headers = commission_and_member
  344. template = SlotTemplate(
  345. project_id=default_public_project.id, title="Bar", commission_id=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={"commission_id": other_commission.id},
  354. headers=headers,
  355. )
  356. assert response.status_code == 403
  357. class TestDeleteTemplate:
  358. async def test_requires_auth(self, client: AsyncClient, default_public_project: Project):
  359. response = await client.delete(
  360. app.url_path_for(
  361. "delete_template",
  362. project_id=default_public_project.id,
  363. template_id=default_template_id,
  364. )
  365. )
  366. assert response.status_code == 401
  367. async def test_invalid_ids(
  368. self,
  369. client: AsyncClient,
  370. default_public_project: Project,
  371. default_user_headers: dict,
  372. default_user_org_membership,
  373. ):
  374. response = await client.delete(
  375. app.url_path_for(
  376. "delete_template",
  377. project_id=default_public_project.id,
  378. template_id="default_tag_id",
  379. ),
  380. headers=default_user_headers,
  381. )
  382. assert response.status_code == 422
  383. response = await client.delete(
  384. app.url_path_for("delete_template", project_id="ded", template_id=default_template_id),
  385. headers=default_user_headers,
  386. )
  387. assert response.status_code == 422
  388. async def test_org_admin_deletes(
  389. self,
  390. client: AsyncClient,
  391. default_public_project: Project,
  392. default_user_headers: dict,
  393. default_user_org_membership,
  394. session: Session,
  395. ):
  396. response = await client.delete(
  397. app.url_path_for(
  398. "delete_template",
  399. project_id=default_public_project.id,
  400. template_id=default_template_id,
  401. ),
  402. headers=default_user_headers,
  403. )
  404. assert response.status_code == 200
  405. assert session.get(SlotTemplate, default_template_id) is None
  406. async def test_delete_does_not_delete_slot(
  407. self,
  408. client: AsyncClient,
  409. default_public_project: Project,
  410. default_user_headers: dict,
  411. default_user_org_membership,
  412. session: Session,
  413. ):
  414. slot = session.get(Slot, default_slot_id)
  415. slot.template_id = default_template_id
  416. session.commit()
  417. response = await client.delete(
  418. app.url_path_for(
  419. "delete_template",
  420. project_id=default_public_project.id,
  421. template_id=default_template_id,
  422. ),
  423. headers=default_user_headers,
  424. )
  425. assert response.status_code == 200
  426. assert session.get(Slot, default_slot_id) is not None
  427. async def test_respo_commission_deletes_own_template(
  428. self,
  429. client: AsyncClient,
  430. default_public_project: Project,
  431. commission_and_member,
  432. session: Session,
  433. ):
  434. commission, _, headers = commission_and_member
  435. template = SlotTemplate(
  436. project_id=default_public_project.id, title="Bar", commission_id=commission.id
  437. )
  438. session.add(template)
  439. session.commit()
  440. template_id = template.id
  441. session.expunge(template)
  442. response = await client.delete(
  443. app.url_path_for(
  444. "delete_template", project_id=default_public_project.id, template_id=template_id
  445. ),
  446. headers=headers,
  447. )
  448. assert response.status_code == 200
  449. assert session.get(SlotTemplate, template_id) is None
  450. async def test_respo_commission_rejected_for_other_commission(
  451. self,
  452. client: AsyncClient,
  453. default_public_project: Project,
  454. commission_and_member,
  455. other_commission,
  456. session: Session,
  457. ):
  458. _, _, headers = commission_and_member
  459. template = SlotTemplate(
  460. project_id=default_public_project.id, title="Scene", commission_id=other_commission.id
  461. )
  462. session.add(template)
  463. session.commit()
  464. response = await client.delete(
  465. app.url_path_for(
  466. "delete_template", project_id=default_public_project.id, template_id=template.id
  467. ),
  468. headers=headers,
  469. )
  470. assert response.status_code == 403
  471. assert session.get(SlotTemplate, template.id) is not None