test_slot.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730
  1. from datetime import UTC, datetime, timedelta
  2. from uuid import uuid4
  3. import pytest
  4. from httpx import AsyncClient
  5. from sqlalchemy import select
  6. from sqlalchemy.orm import Session
  7. from app.main import app
  8. from app.models import (
  9. Commission,
  10. CommissionMember,
  11. OrgRole,
  12. Project,
  13. Slot,
  14. SlotTemplate,
  15. Volunteer,
  16. )
  17. from app.tests.conftest import (
  18. default_project_id,
  19. default_slot_id,
  20. default_template_id,
  21. default_volunteer_id,
  22. )
  23. pytestmark = pytest.mark.asyncio
  24. @pytest.fixture
  25. def commission_and_member(session: Session, default_public_project: Project, make_org_user):
  26. commission = Commission(project_id=default_public_project.id, name="Bar")
  27. session.add(commission)
  28. session.commit()
  29. user, headers = make_org_user(role=OrgRole.RESPO_COMMISSION)
  30. session.add(CommissionMember(commission_id=commission.id, user_id=user.id))
  31. session.commit()
  32. return commission, user, headers
  33. @pytest.fixture
  34. def other_commission(session: Session, default_public_project: Project):
  35. commission = Commission(project_id=default_public_project.id, name="Scene")
  36. session.add(commission)
  37. session.commit()
  38. return commission
  39. @pytest.fixture
  40. def commission_template(
  41. session: Session, default_public_project: Project, commission_and_member
  42. ) -> SlotTemplate:
  43. commission, _, _ = commission_and_member
  44. template = SlotTemplate(
  45. project_id=default_public_project.id, title="Bar template", commission_id=commission.id
  46. )
  47. session.add(template)
  48. session.commit()
  49. return template
  50. @pytest.fixture
  51. def other_commission_template(
  52. session: Session, default_public_project: Project, other_commission
  53. ) -> SlotTemplate:
  54. template = SlotTemplate(
  55. project_id=default_public_project.id,
  56. title="Scene template",
  57. commission_id=other_commission.id,
  58. )
  59. session.add(template)
  60. session.commit()
  61. return template
  62. class TestListProjectSlots:
  63. async def test_requires_auth(self, client: AsyncClient, default_public_project: Project):
  64. response = await client.get(
  65. app.url_path_for("list_project_slots", project_id=default_project_id)
  66. )
  67. assert response.status_code == 401
  68. async def test_project_not_found(
  69. self, client: AsyncClient, default_user_headers: dict, default_user_org_membership
  70. ):
  71. response = await client.get(
  72. app.url_path_for("list_project_slots", project_id=uuid4()), headers=default_user_headers
  73. )
  74. assert response.status_code == 404
  75. async def test_invalid_project_id(
  76. self, client: AsyncClient, default_user_headers: dict, default_user_org_membership
  77. ):
  78. response = await client.get(
  79. app.url_path_for("list_project_slots", project_id="pas un uuid valid"),
  80. headers=default_user_headers,
  81. )
  82. assert response.status_code == 422
  83. async def test_returns_slots(
  84. self,
  85. client: AsyncClient,
  86. default_public_project: Project,
  87. default_user_headers: dict,
  88. default_user_org_membership,
  89. ):
  90. response = await client.get(
  91. app.url_path_for("list_project_slots", project_id=default_project_id),
  92. headers=default_user_headers,
  93. )
  94. assert response.status_code == 200
  95. data = response.json()
  96. assert len(data) == 1
  97. assert data[0]["title"] == "être roi"
  98. assert data[0]["id"] == default_slot_id
  99. assert "created_at" in data[0]
  100. async def test_respo_commission_sees_all_project_slots(
  101. self,
  102. client: AsyncClient,
  103. default_public_project: Project,
  104. commission_and_member,
  105. ):
  106. """respo_commission has read access to the whole plan, not just
  107. their own commission's slots."""
  108. _, _, headers = commission_and_member
  109. response = await client.get(
  110. app.url_path_for("list_project_slots", project_id=default_project_id), headers=headers
  111. )
  112. assert response.status_code == 200
  113. assert len(response.json()) == 1
  114. async def test_cannot_look_other_org_slot(
  115. self,
  116. client: AsyncClient,
  117. default_public_project: Project,
  118. session: Session,
  119. commission_and_member,
  120. other_org_user,
  121. ):
  122. """An admin of a project belonging to a DIFFERENT organization cannot access the project."""
  123. _, headers = other_org_user
  124. response = await client.get(
  125. app.url_path_for("list_project_slots", project_id=default_public_project.id),
  126. headers=headers,
  127. )
  128. assert response.status_code == 403
  129. class TestCreateSlot:
  130. async def test_requires_auth(self, client: AsyncClient):
  131. response = await client.post(app.url_path_for("create_slot", project_id=default_project_id))
  132. assert response.status_code == 401
  133. async def test_project_not_found(
  134. self, client: AsyncClient, default_user_headers: dict, default_user_org_membership
  135. ):
  136. starting_time = datetime(1900, 1, 1)
  137. payload = {
  138. "title": "être mort",
  139. "starting_time": starting_time.isoformat(),
  140. "ending_time": (starting_time + timedelta(minutes=60)).isoformat(),
  141. }
  142. response = await client.post(
  143. app.url_path_for("create_slot", project_id=uuid4()),
  144. json=payload,
  145. headers=default_user_headers,
  146. )
  147. assert response.status_code == 404
  148. async def test_org_admin_creates(
  149. self,
  150. client: AsyncClient,
  151. default_public_project: Project,
  152. default_user_headers: dict,
  153. default_user_org_membership,
  154. session: Session,
  155. ):
  156. starting_time = datetime(1900, 1, 1)
  157. payload = {
  158. "title": "être mort",
  159. "starting_time": starting_time.isoformat(),
  160. "ending_time": (starting_time + timedelta(minutes=60)).isoformat(),
  161. }
  162. response = await client.post(
  163. app.url_path_for("create_slot", project_id=default_project_id),
  164. json=payload,
  165. headers=default_user_headers,
  166. )
  167. assert response.status_code == 200
  168. assert response.json()["id"] != default_slot_id
  169. assert response.json()["title"] == "être mort"
  170. assert response.json()["required_volunteers"] == 0
  171. result = session.execute(select(Slot).where(Slot.project_id == default_project_id))
  172. slots = result.scalars().all()
  173. assert len(slots) > 1
  174. slot = [s for s in slots if s.id != default_slot_id][0]
  175. assert slot.title == "être mort"
  176. assert abs(slot.starting_time - starting_time.replace(tzinfo=UTC)) < timedelta(minutes=30)
  177. del payload["title"]
  178. response = await client.post(
  179. app.url_path_for("create_slot", project_id=default_project_id),
  180. json=payload,
  181. headers=default_user_headers,
  182. )
  183. assert response.status_code == 422
  184. async def test_min_volunteer(
  185. self,
  186. client: AsyncClient,
  187. default_public_project: Project,
  188. default_user_headers: dict,
  189. default_user_org_membership,
  190. session: Session,
  191. ):
  192. starting_time = datetime(1900, 1, 1)
  193. payload = {
  194. "title": "être mort",
  195. "starting_time": starting_time.isoformat(),
  196. "ending_time": (starting_time + timedelta(minutes=60)).isoformat(),
  197. "required_volunteers": 2,
  198. }
  199. response = await client.post(
  200. app.url_path_for("create_slot", project_id=default_project_id),
  201. json=payload,
  202. headers=default_user_headers,
  203. )
  204. assert response.status_code == 200
  205. slots = (
  206. session.execute(select(Slot).where(Slot.project_id == default_project_id))
  207. .scalars()
  208. .all()
  209. )
  210. slot = [s for s in slots if s.id != default_slot_id][0]
  211. assert slot.required_volunteers == 2
  212. async def test_respo_benevole_creates_without_template(
  213. self,
  214. client: AsyncClient,
  215. default_public_project: Project,
  216. make_org_user,
  217. ):
  218. _, headers = make_org_user(role=OrgRole.RESPO_BENEVOLE)
  219. starting_time = datetime(1900, 1, 1)
  220. response = await client.post(
  221. app.url_path_for("create_slot", project_id=default_project_id),
  222. json={
  223. "title": "Benevole slot",
  224. "starting_time": starting_time.isoformat(),
  225. "ending_time": (starting_time + timedelta(minutes=60)).isoformat(),
  226. },
  227. headers=headers,
  228. )
  229. assert response.status_code == 200
  230. async def test_respo_commission_creates_with_own_template(
  231. self,
  232. client: AsyncClient,
  233. default_public_project: Project,
  234. commission_and_member,
  235. commission_template,
  236. ):
  237. _, _, headers = commission_and_member
  238. starting_time = datetime(1900, 1, 1)
  239. response = await client.post(
  240. app.url_path_for("create_slot", project_id=default_project_id),
  241. json={
  242. "title": "Bar shift",
  243. "starting_time": starting_time.isoformat(),
  244. "ending_time": (starting_time + timedelta(minutes=60)).isoformat(),
  245. "template_id": commission_template.id,
  246. },
  247. headers=headers,
  248. )
  249. assert response.status_code == 200
  250. assert response.json()["template_id"] == commission_template.id
  251. async def test_respo_commission_rejected_with_other_commission_template(
  252. self,
  253. client: AsyncClient,
  254. default_public_project: Project,
  255. commission_and_member,
  256. other_commission_template,
  257. ):
  258. _, _, headers = commission_and_member
  259. starting_time = datetime(1900, 1, 1)
  260. response = await client.post(
  261. app.url_path_for("create_slot", project_id=default_project_id),
  262. json={
  263. "title": "Scene shift",
  264. "starting_time": starting_time.isoformat(),
  265. "ending_time": (starting_time + timedelta(minutes=60)).isoformat(),
  266. "template_id": other_commission_template.id,
  267. },
  268. headers=headers,
  269. )
  270. assert response.status_code == 403
  271. async def test_cannot_create_other_org_slot(
  272. self,
  273. client: AsyncClient,
  274. default_public_project: Project,
  275. session: Session,
  276. commission_and_member,
  277. other_org_user,
  278. ):
  279. """An admin of a project belonging to a DIFFERENT organization cannot access the project."""
  280. _, headers = other_org_user
  281. starting_time = datetime(1900, 1, 1)
  282. response = await client.post(
  283. app.url_path_for("create_slot", project_id=default_project_id),
  284. json={
  285. "title": "Scene shift",
  286. "starting_time": starting_time.isoformat(),
  287. "ending_time": (starting_time + timedelta(minutes=60)).isoformat(),
  288. },
  289. headers=headers,
  290. )
  291. assert response.status_code == 403
  292. class TestUpdateSlot:
  293. async def test_requires_auth(self, client: AsyncClient, default_public_project: Project):
  294. response = await client.post(
  295. app.url_path_for("update_slot", project_id=default_project_id, slot_id=default_slot_id)
  296. )
  297. assert response.status_code == 401
  298. async def test_project_not_found(
  299. self,
  300. client: AsyncClient,
  301. default_public_project: Project,
  302. default_user_headers: dict,
  303. default_user_org_membership,
  304. ):
  305. response = await client.post(
  306. app.url_path_for("update_slot", project_id=uuid4(), slot_id=default_slot_id),
  307. json={"title": "x"},
  308. headers=default_user_headers,
  309. )
  310. assert response.status_code == 404
  311. async def test_slot_not_found(
  312. self,
  313. client: AsyncClient,
  314. default_public_project: Project,
  315. default_user_headers: dict,
  316. default_user_org_membership,
  317. ):
  318. response = await client.post(
  319. app.url_path_for("update_slot", project_id=default_project_id, slot_id=uuid4()),
  320. json={"title": "x"},
  321. headers=default_user_headers,
  322. )
  323. assert response.status_code == 404
  324. async def test_org_admin_updates_each_field(
  325. self,
  326. client: AsyncClient,
  327. default_public_project: Project,
  328. default_user_headers: dict,
  329. default_user_org_membership,
  330. ):
  331. starting_time = datetime(2000, 1, 1, tzinfo=UTC)
  332. payload = {
  333. "title": "être mort 2 fois",
  334. "starting_time": starting_time.isoformat(),
  335. "ending_time": (starting_time + timedelta(minutes=60)).isoformat(),
  336. "required_volunteers": 2,
  337. }
  338. for k, v in payload.items():
  339. response = await client.post(
  340. app.url_path_for(
  341. "update_slot", project_id=default_project_id, slot_id=default_slot_id
  342. ),
  343. json={k: v},
  344. headers=default_user_headers,
  345. )
  346. assert response.status_code == 200
  347. assert response.json()["id"] == default_slot_id
  348. if "time" in k:
  349. assert datetime.fromisoformat(response.json()[k]) == datetime.fromisoformat(v)
  350. else:
  351. assert response.json()[k] == v
  352. async def test_remove_template(
  353. self,
  354. client: AsyncClient,
  355. default_public_project: Project,
  356. default_user_headers: dict,
  357. default_user_org_membership,
  358. session: Session,
  359. ):
  360. response = await client.post(
  361. app.url_path_for("update_slot", project_id=default_project_id, slot_id=default_slot_id),
  362. json={"template_id": default_template_id},
  363. headers=default_user_headers,
  364. )
  365. assert response.status_code == 200
  366. slot = session.execute(
  367. select(Slot).where(Slot.project_id == default_project_id)
  368. ).scalar_one()
  369. assert slot.template_id == default_template_id
  370. response = await client.post(
  371. app.url_path_for("update_slot", project_id=default_project_id, slot_id=default_slot_id),
  372. json={"template_id": ""},
  373. headers=default_user_headers,
  374. )
  375. assert response.status_code == 200
  376. session.refresh(slot)
  377. assert slot.template_id is None
  378. async def test_bad_template_id(
  379. self,
  380. client: AsyncClient,
  381. default_public_project: Project,
  382. default_user_headers: dict,
  383. default_user_org_membership,
  384. ):
  385. path = app.url_path_for(
  386. "update_slot", project_id=default_project_id, slot_id=default_slot_id
  387. )
  388. response = await client.post(
  389. path, json={"template_id": "invalid uuid"}, headers=default_user_headers
  390. )
  391. assert response.status_code == 422
  392. response = await client.post(
  393. path, json={"template_id": str(uuid4())}, headers=default_user_headers
  394. )
  395. assert response.status_code == 400
  396. async def test_assign_template(
  397. self,
  398. client: AsyncClient,
  399. default_public_project: Project,
  400. default_user_headers: dict,
  401. default_user_org_membership,
  402. session: Session,
  403. ):
  404. path = app.url_path_for(
  405. "update_slot", project_id=default_project_id, slot_id=default_slot_id
  406. )
  407. template = SlotTemplate(project_id=default_project_id, title="template 0")
  408. session.add(template)
  409. session.commit()
  410. response = await client.post(
  411. path, json={"template_id": template.id}, headers=default_user_headers
  412. )
  413. assert response.status_code == 200
  414. assert response.json()["template_id"] == template.id
  415. session.refresh(template)
  416. assert len(template.slots) == 1
  417. response = await client.post(path, json={"template_id": None}, headers=default_user_headers)
  418. assert response.status_code == 200
  419. assert response.json()["template_id"] is None
  420. async def test_update_volunteers(
  421. self,
  422. client: AsyncClient,
  423. default_public_project: Project,
  424. default_user_headers: dict,
  425. default_user_org_membership,
  426. session: Session,
  427. ):
  428. path = app.url_path_for(
  429. "update_slot", project_id=default_project_id, slot_id=default_slot_id
  430. )
  431. response = await client.post(path, json={"volunteers": []}, headers=default_user_headers)
  432. assert response.status_code == 200
  433. volunteer = (
  434. session.execute(select(Volunteer).where(Volunteer.id == default_volunteer_id))
  435. .scalars()
  436. .first()
  437. )
  438. assert volunteer.slots_id == []
  439. response = await client.post(
  440. path, json={"volunteers": [default_volunteer_id]}, headers=default_user_headers
  441. )
  442. assert response.status_code == 200
  443. session.refresh(volunteer)
  444. assert volunteer.slots_id == [default_slot_id]
  445. response = await client.post(
  446. path, json={"volunteers": [str(uuid4())]}, headers=default_user_headers
  447. )
  448. assert response.status_code == 400
  449. response = await client.post(
  450. path, json={"volunteers": ["not uuid str"]}, headers=default_user_headers
  451. )
  452. assert response.status_code == 422
  453. async def test_respo_commission_updates_own_slot(
  454. self,
  455. client: AsyncClient,
  456. default_public_project: Project,
  457. commission_and_member,
  458. commission_template,
  459. session: Session,
  460. ):
  461. _, _, headers = commission_and_member
  462. slot = Slot(
  463. project_id=default_project_id,
  464. title="Bar shift",
  465. starting_time=datetime.now(),
  466. ending_time=datetime.now() + timedelta(hours=1),
  467. template_id=commission_template.id,
  468. )
  469. session.add(slot)
  470. session.commit()
  471. response = await client.post(
  472. app.url_path_for("update_slot", project_id=default_project_id, slot_id=slot.id),
  473. json={"title": "Bar shift renamed"},
  474. headers=headers,
  475. )
  476. assert response.status_code == 200
  477. session.refresh(slot)
  478. assert slot.title == "Bar shift renamed"
  479. async def test_respo_commission_rejected_for_other_commission_slot(
  480. self,
  481. client: AsyncClient,
  482. default_public_project: Project,
  483. commission_and_member,
  484. other_commission_template,
  485. session: Session,
  486. ):
  487. _, _, headers = commission_and_member
  488. slot = Slot(
  489. project_id=default_project_id,
  490. title="Scene shift",
  491. starting_time=datetime.now(),
  492. ending_time=datetime.now() + timedelta(hours=1),
  493. template_id=other_commission_template.id,
  494. )
  495. session.add(slot)
  496. session.commit()
  497. response = await client.post(
  498. app.url_path_for("update_slot", project_id=default_project_id, slot_id=slot.id),
  499. json={"title": "Hijacked"},
  500. headers=headers,
  501. )
  502. assert response.status_code == 403
  503. async def test_respo_commission_reassign_to_other_commission_template_rejected(
  504. self,
  505. client: AsyncClient,
  506. default_public_project: Project,
  507. commission_and_member,
  508. commission_template,
  509. other_commission_template,
  510. session: Session,
  511. ):
  512. _, _, headers = commission_and_member
  513. slot = Slot(
  514. project_id=default_project_id,
  515. title="Bar shift",
  516. starting_time=datetime.now(),
  517. ending_time=datetime.now() + timedelta(hours=1),
  518. template_id=commission_template.id,
  519. )
  520. session.add(slot)
  521. session.commit()
  522. response = await client.post(
  523. app.url_path_for("update_slot", project_id=default_project_id, slot_id=slot.id),
  524. json={"template_id": other_commission_template.id},
  525. headers=headers,
  526. )
  527. assert response.status_code == 403
  528. async def test_cannot_update_other_org_slot(
  529. self,
  530. client: AsyncClient,
  531. default_project: Project,
  532. session: Session,
  533. commission_and_member,
  534. other_org_user,
  535. ):
  536. """An admin of a project belonging to a DIFFERENT organization cannot access the project."""
  537. _, headers = other_org_user
  538. slot = Slot(
  539. project_id=default_project.id,
  540. title="Bar shift",
  541. starting_time=datetime.now(),
  542. ending_time=datetime.now() + timedelta(hours=1),
  543. )
  544. session.add(slot)
  545. session.commit()
  546. response = await client.post(
  547. app.url_path_for("update_slot", project_id=default_project_id, slot_id=slot.id),
  548. json={"title": "Bar shift renamed"},
  549. headers=headers,
  550. )
  551. assert response.status_code == 403
  552. class TestDeleteSlot:
  553. async def test_requires_auth(self, client: AsyncClient, default_public_project: Project):
  554. response = await client.delete(
  555. app.url_path_for("delete_slot", project_id=default_project_id, slot_id=default_slot_id)
  556. )
  557. assert response.status_code == 401
  558. async def test_forbidden_other_org(
  559. self, client: AsyncClient, default_public_project: Project, other_org_user
  560. ):
  561. _, headers = other_org_user
  562. response = await client.delete(
  563. app.url_path_for(
  564. "delete_slot",
  565. project_id=default_project_id,
  566. slot_id=default_slot_id,
  567. ),
  568. headers=headers,
  569. )
  570. assert response.status_code == 403
  571. async def test_invalid_slot_id(
  572. self,
  573. client: AsyncClient,
  574. default_public_project: Project,
  575. default_user_headers: dict,
  576. default_user_org_membership,
  577. ):
  578. response = await client.delete(
  579. app.url_path_for("delete_slot", project_id=default_project_id, slot_id="not uidstr"),
  580. headers=default_user_headers,
  581. )
  582. assert response.status_code == 422
  583. async def test_org_admin_deletes(
  584. self,
  585. client: AsyncClient,
  586. default_public_project: Project,
  587. default_user_headers: dict,
  588. default_user_org_membership,
  589. session: Session,
  590. ):
  591. response = await client.delete(
  592. app.url_path_for("delete_slot", project_id=default_project_id, slot_id=default_slot_id),
  593. headers=default_user_headers,
  594. )
  595. assert response.status_code == 200
  596. assert session.get(Slot, default_slot_id) is None
  597. volunteer = (
  598. session.execute(select(Volunteer).where(Volunteer.id == default_volunteer_id))
  599. .scalars()
  600. .first()
  601. )
  602. assert default_slot_id not in volunteer.slots_id
  603. async def test_nonexistent_slot_returns_404(
  604. self,
  605. client: AsyncClient,
  606. default_public_project: Project,
  607. default_user_headers: dict,
  608. default_user_org_membership,
  609. ):
  610. """require_commission_scope resolves the slot before the handler
  611. runs, so a missing slot_id now 404s -- differs from the old bulk
  612. DELETE behavior, which silently returned 200 either way."""
  613. response = await client.delete(
  614. app.url_path_for("delete_slot", project_id=default_project_id, slot_id=uuid4()),
  615. headers=default_user_headers,
  616. )
  617. assert response.status_code == 404
  618. async def test_respo_commission_deletes_own_slot(
  619. self,
  620. client: AsyncClient,
  621. default_public_project: Project,
  622. commission_and_member,
  623. commission_template,
  624. session: Session,
  625. ):
  626. _, _, headers = commission_and_member
  627. slot = Slot(
  628. project_id=default_project_id,
  629. title="Bar shift",
  630. starting_time=datetime.now(),
  631. ending_time=datetime.now() + timedelta(hours=1),
  632. template_id=commission_template.id,
  633. )
  634. session.add(slot)
  635. session.commit()
  636. slot_id = slot.id
  637. session.expunge(slot)
  638. response = await client.delete(
  639. app.url_path_for("delete_slot", project_id=default_project_id, slot_id=slot_id),
  640. headers=headers,
  641. )
  642. assert response.status_code == 200
  643. assert session.get(Slot, slot_id) is None
  644. async def test_respo_commission_rejected_for_other_commission_slot(
  645. self,
  646. client: AsyncClient,
  647. default_public_project: Project,
  648. commission_and_member,
  649. other_commission_template,
  650. session: Session,
  651. ):
  652. _, _, headers = commission_and_member
  653. slot = Slot(
  654. project_id=default_project_id,
  655. title="Scene shift",
  656. starting_time=datetime.now(),
  657. ending_time=datetime.now() + timedelta(hours=1),
  658. template_id=other_commission_template.id,
  659. )
  660. session.add(slot)
  661. session.commit()
  662. response = await client.delete(
  663. app.url_path_for("delete_slot", project_id=default_project_id, slot_id=slot.id),
  664. headers=headers,
  665. )
  666. assert response.status_code == 403
  667. assert session.get(Slot, slot.id) is not None