test_sms_batch.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. from datetime import datetime, timedelta
  2. from httpx import AsyncClient
  3. import pytest
  4. from sqlalchemy import select
  5. from sqlalchemy.orm import Session
  6. from app.main import app
  7. from app.models import (
  8. Commission,
  9. CommissionMember,
  10. Project,
  11. Slot,
  12. SlotTemplate,
  13. Sms,
  14. User,
  15. Volunteer,
  16. OrgRole,
  17. )
  18. from app.tests.conftest import default_sms_id, default_volunteer_id
  19. def get_arthur(session) -> Volunteer:
  20. result = session.execute(select(Volunteer).where(Volunteer.id == default_volunteer_id))
  21. arthur = result.scalars().first()
  22. assert arthur is not None
  23. return arthur
  24. @pytest.fixture()
  25. def user_headers(make_org_user) -> dict:
  26. _,headers = make_org_user(role=OrgRole.RESPO_BENEVOLE)
  27. return headers
  28. @pytest.fixture()
  29. async def test_create_sms_batch_bad_input(
  30. client: AsyncClient,
  31. user_headers: dict,
  32. session: Session,
  33. default_public_project: Project,
  34. ):
  35. response = await client.post(
  36. app.url_path_for("create_sms_batch", project_id=default_public_project.id),
  37. json={"is_public": False},
  38. )
  39. assert response.status_code == 401
  40. async def test_basic(
  41. client: AsyncClient,
  42. user_headers: dict,
  43. session: Session,
  44. default_public_project: Project,
  45. ):
  46. slot = Slot(
  47. project_id=default_public_project.id,
  48. title="replanter excalibure",
  49. starting_time=datetime.now() + timedelta(minutes=30),
  50. ending_time=datetime.now() + timedelta(minutes=60),
  51. )
  52. slot.volunteers.append(get_arthur(session))
  53. session.add(slot)
  54. session.commit()
  55. response = await client.post(
  56. app.url_path_for("create_sms_batch", project_id=default_public_project.id),
  57. headers=user_headers,
  58. json={"template": "Bonjour {prenom}!\n{titre}"},
  59. )
  60. assert response.status_code == 200
  61. result = session.execute(
  62. select(Sms).where(
  63. (Sms.project_id == default_public_project.id) & (Sms.id != default_sms_id)
  64. )
  65. )
  66. sms = result.scalars().first()
  67. assert sms is not None
  68. assert sms.content == "Bonjour Arthur!\nreplanter excalibure"
  69. async def test_multiple_volunteer(
  70. client: AsyncClient,
  71. user_headers: dict,
  72. session: Session,
  73. default_public_project: Project,
  74. ):
  75. slot = Slot(
  76. project_id=default_public_project.id,
  77. title="replanter excalibure",
  78. starting_time=datetime.now() + timedelta(minutes=30),
  79. ending_time=datetime.now() + timedelta(minutes=60),
  80. )
  81. slot.volunteers.append(get_arthur(session))
  82. slot.volunteers.append(
  83. Volunteer(
  84. project_id=default_public_project.id,
  85. name="benevole 2",
  86. surname="echo",
  87. phone_number="t",
  88. email="",
  89. automatic_sms=True,
  90. )
  91. )
  92. session.add(slot)
  93. session.commit()
  94. response = await client.post(
  95. app.url_path_for("create_sms_batch", project_id=default_public_project.id),
  96. headers=user_headers,
  97. json={"template": "Bonjour {prenom}!\n{titre}"},
  98. )
  99. assert response.status_code == 200
  100. result = session.execute(
  101. select(Sms).where(
  102. (Sms.project_id == default_public_project.id) & (Sms.id != default_sms_id)
  103. )
  104. )
  105. count_sms = sum(1 for _ in result.fetchall())
  106. assert count_sms == 2
  107. @pytest.mark.parametrize("automatic_sms,expected", [(True, True), (False, False)])
  108. async def test_volunteer_settings(
  109. client: AsyncClient,
  110. user_headers: dict,
  111. session: Session,
  112. default_public_project: Project,
  113. automatic_sms: bool,
  114. expected: bool,
  115. ):
  116. slot = Slot(
  117. project_id=default_public_project.id,
  118. title="replanter excalibure",
  119. starting_time=datetime.now() + timedelta(minutes=30),
  120. ending_time=datetime.now() + timedelta(minutes=60),
  121. )
  122. arthur = get_arthur(session)
  123. arthur.automatic_sms = automatic_sms
  124. slot.volunteers.append(arthur)
  125. session.add(slot)
  126. session.commit()
  127. response = await client.post(
  128. app.url_path_for("create_sms_batch", project_id=default_public_project.id),
  129. headers=user_headers,
  130. json={"template": "Bonjour {prenom}!\n{titre}"},
  131. )
  132. assert response.status_code == 200
  133. result = session.execute(
  134. select(Sms).where(
  135. (Sms.project_id == default_public_project.id) & (Sms.id != default_sms_id)
  136. )
  137. )
  138. sms = result.scalars().first()
  139. assert (sms is not None) == expected
  140. async def test_only_future_slot(
  141. client: AsyncClient,
  142. user_headers: dict,
  143. session: Session,
  144. default_public_project: Project,
  145. ):
  146. slot = Slot(
  147. project_id=default_public_project.id,
  148. title="replanter excalibure",
  149. starting_time=datetime.now() + timedelta(minutes=-30),
  150. ending_time=datetime.now() + timedelta(minutes=60),
  151. )
  152. slot.volunteers.append(get_arthur(session))
  153. session.add(slot)
  154. session.commit()
  155. response = await client.post(
  156. app.url_path_for("create_sms_batch", project_id=default_public_project.id),
  157. headers=user_headers,
  158. json={"template": "Bonjour {prenom}!\n{titre}"},
  159. )
  160. assert response.status_code == 200
  161. result = session.execute(
  162. select(Sms).where(
  163. (Sms.project_id == default_public_project.id) & (Sms.id != default_sms_id)
  164. )
  165. )
  166. sms = result.scalars().first()
  167. assert sms is None
  168. @pytest.mark.parametrize("delta_t,expected", [(32, False), (28, True)])
  169. async def test_time_delta(
  170. client: AsyncClient,
  171. user_headers: dict,
  172. session: Session,
  173. default_public_project: Project,
  174. delta_t: int,
  175. expected,
  176. ):
  177. slot = Slot(
  178. project_id=default_public_project.id,
  179. title="replanter excalibure",
  180. starting_time=datetime.now() + timedelta(minutes=30),
  181. ending_time=datetime.now() + timedelta(minutes=60),
  182. )
  183. slot.volunteers.append(get_arthur(session))
  184. session.add(slot)
  185. session.commit()
  186. response = await client.post(
  187. app.url_path_for("create_sms_batch", project_id=default_public_project.id),
  188. headers=user_headers,
  189. json={"template": "Bonjour {prenom}!\n{titre}", "delta_t": delta_t},
  190. )
  191. assert response.status_code == 200
  192. result = session.execute(
  193. select(Sms).where(
  194. (Sms.project_id == default_public_project.id) & (Sms.id != default_sms_id)
  195. )
  196. )
  197. sms = result.scalars().first()
  198. assert (sms is not None) == expected
  199. starting_time = datetime.now() + timedelta(minutes=30)
  200. @pytest.mark.parametrize(
  201. "template,expected",
  202. [
  203. ("no escape", "no escape"),
  204. ("{titre}", "replanter excalibure"),
  205. ("{prenom}", "Arthur"),
  206. ("{prenom} {nom}", "Arthur Pandragon"),
  207. ("{debut}", starting_time.strftime("%Hh%M")),
  208. (
  209. "{titre} {description}, {respo}",
  210. "replanter excalibure {description}, {respo}",
  211. ),
  212. ],
  213. )
  214. async def test_content_no_template(
  215. client: AsyncClient,
  216. user_headers: dict,
  217. session: Session,
  218. default_public_project: Project,
  219. template: str,
  220. expected: str,
  221. ):
  222. """No SlotTemplate attached at all -- {respo}/{description} placeholders
  223. are left untouched, since the replace only happens when slot.template
  224. is not None. Unaffected by the commission/override change."""
  225. slot = Slot(
  226. project_id=default_public_project.id,
  227. title="replanter excalibure",
  228. starting_time=starting_time,
  229. ending_time=datetime.now() + timedelta(minutes=60),
  230. )
  231. slot.volunteers.append(get_arthur(session))
  232. session.add(slot)
  233. session.commit()
  234. response = await client.post(
  235. app.url_path_for("create_sms_batch", project_id=default_public_project.id),
  236. headers=user_headers,
  237. json={"template": template},
  238. )
  239. assert response.status_code == 200
  240. result = session.execute(
  241. select(Sms).where(
  242. (Sms.project_id == default_public_project.id) & (Sms.id != default_sms_id)
  243. )
  244. )
  245. sms = result.scalars().first()
  246. assert sms is not None
  247. assert sms.content == expected
  248. @pytest.mark.parametrize(
  249. "template,expected",
  250. [
  251. ("no escape", "no escape"),
  252. ("{titre}", "replanter excalibure"),
  253. ("{prenom}", "Arthur"),
  254. ("{prenom} {nom}", "Arthur Pandragon"),
  255. ("{debut}", starting_time.strftime("%Hh%M")),
  256. ],
  257. )
  258. async def test_content_with_template_non_respo_placeholders(
  259. client: AsyncClient,
  260. user_headers: dict,
  261. session: Session,
  262. default_public_project: Project,
  263. template: str,
  264. expected: str,
  265. ):
  266. """Placeholders unrelated to {respo} behave the same regardless of how
  267. the contact is resolved -- kept separate from the {respo}-specific cases
  268. below since those need commission/override setup."""
  269. slot_template = SlotTemplate(
  270. project_id=default_public_project.id,
  271. title="replanter excalibure",
  272. description="rendre l'épee à son caillou",
  273. place="gros caillou",
  274. )
  275. slot = Slot(
  276. project_id=default_public_project.id,
  277. title="replanter excalibure",
  278. starting_time=starting_time,
  279. ending_time=datetime.now() + timedelta(minutes=60),
  280. )
  281. slot.template = slot_template
  282. slot.volunteers.append(get_arthur(session))
  283. session.add(slot_template)
  284. session.add(slot)
  285. session.commit()
  286. response = await client.post(
  287. app.url_path_for("create_sms_batch", project_id=default_public_project.id),
  288. headers=user_headers,
  289. json={"template": template},
  290. )
  291. assert response.status_code == 200
  292. result = session.execute(
  293. select(Sms).where(
  294. (Sms.project_id == default_public_project.id) & (Sms.id != default_sms_id)
  295. )
  296. )
  297. sms = result.scalars().first()
  298. assert sms is not None
  299. assert sms.content == expected
  300. async def test_respo_placeholder_uses_override_when_set(
  301. client: AsyncClient,
  302. user_headers: dict,
  303. session: Session,
  304. default_public_project: Project,
  305. ):
  306. """responsible_override wins even if a commission is also attached."""
  307. commission = Commission(project_id=default_public_project.id, name="Bar")
  308. session.add(commission)
  309. session.commit()
  310. slot_template = SlotTemplate(
  311. project_id=default_public_project.id,
  312. title="replanter excalibure",
  313. description="rendre l'épee à son caillou",
  314. place="gros caillou",
  315. commission_id=commission.id,
  316. responsible_override="dieu",
  317. )
  318. slot = Slot(
  319. project_id=default_public_project.id,
  320. title="replanter excalibure",
  321. starting_time=starting_time,
  322. ending_time=datetime.now() + timedelta(minutes=60),
  323. )
  324. slot.template = slot_template
  325. slot.volunteers.append(get_arthur(session))
  326. session.add(slot_template)
  327. session.add(slot)
  328. session.commit()
  329. response = await client.post(
  330. app.url_path_for("create_sms_batch", project_id=default_public_project.id),
  331. headers=user_headers,
  332. json={"template": "{titre} {description}, {respo}"},
  333. )
  334. assert response.status_code == 200
  335. result = session.execute(
  336. select(Sms).where(
  337. (Sms.project_id == default_public_project.id) & (Sms.id != default_sms_id)
  338. )
  339. )
  340. sms = result.scalars().first()
  341. assert sms is not None
  342. assert sms.content == "replanter excalibure rendre l'épee à son caillou, dieu"
  343. async def test_respo_placeholder_derives_from_commission_members(
  344. client: AsyncClient,
  345. user_headers: dict,
  346. session: Session,
  347. default_public_project: Project,
  348. ):
  349. """No override set -- {respo} falls back to the commission's members'
  350. name : phone_number, comma-joined."""
  351. commission = Commission(project_id=default_public_project.id, name="Bar")
  352. session.add(commission)
  353. session.commit()
  354. member1 = User(
  355. id="c1a1a1a1-0000-0000-0000-000000000001",
  356. email="merlin@test.com",
  357. hashed_password="x",
  358. name="Merlin",
  359. phone_number="0601020304",
  360. )
  361. member2 = User(
  362. id="c1a1a1a1-0000-0000-0000-000000000002",
  363. email="morgane@test.com",
  364. hashed_password="x",
  365. name="Morgane",
  366. phone_number="0605060708",
  367. )
  368. session.add_all([member1, member2])
  369. session.commit()
  370. session.add_all(
  371. [
  372. CommissionMember(commission_id=commission.id, user_id=member1.id),
  373. CommissionMember(commission_id=commission.id, user_id=member2.id),
  374. ]
  375. )
  376. session.commit()
  377. slot_template = SlotTemplate(
  378. project_id=default_public_project.id,
  379. title="replanter excalibure",
  380. description="rendre l'épee à son caillou",
  381. place="gros caillou",
  382. commission_id=commission.id,
  383. # responsible_override intentionally left unset
  384. )
  385. slot = Slot(
  386. project_id=default_public_project.id,
  387. title="replanter excalibure",
  388. starting_time=starting_time,
  389. ending_time=datetime.now() + timedelta(minutes=60),
  390. )
  391. slot.template = slot_template
  392. slot.volunteers.append(get_arthur(session))
  393. session.add(slot_template)
  394. session.add(slot)
  395. session.commit()
  396. response = await client.post(
  397. app.url_path_for("create_sms_batch", project_id=default_public_project.id),
  398. headers=user_headers,
  399. json={"template": "{titre} {description}, {respo}"},
  400. )
  401. assert response.status_code == 200
  402. result = session.execute(
  403. select(Sms).where(
  404. (Sms.project_id == default_public_project.id) & (Sms.id != default_sms_id)
  405. )
  406. )
  407. sms = result.scalars().first()
  408. assert sms is not None
  409. assert sms.content == (
  410. "replanter excalibure rendre l'épee à son caillou, "
  411. "Merlin : 0601020304, Morgane : 0605060708"
  412. )
  413. async def test_respo_placeholder_empty_when_no_commission_or_override(
  414. client: AsyncClient,
  415. user_headers: dict,
  416. session: Session,
  417. default_public_project: Project,
  418. ):
  419. """Template attached but no commission and no override -- {respo}
  420. resolves to an empty string rather than leaving the placeholder literal
  421. (that untouched-placeholder behavior is only for slot.template is None,
  422. covered in test_content_no_template)."""
  423. slot_template = SlotTemplate(
  424. project_id=default_public_project.id,
  425. title="replanter excalibure",
  426. description="rendre l'épee à son caillou",
  427. place="gros caillou",
  428. )
  429. slot = Slot(
  430. project_id=default_public_project.id,
  431. title="replanter excalibure",
  432. starting_time=starting_time,
  433. ending_time=datetime.now() + timedelta(minutes=60),
  434. )
  435. slot.template = slot_template
  436. slot.volunteers.append(get_arthur(session))
  437. session.add(slot_template)
  438. session.add(slot)
  439. session.commit()
  440. response = await client.post(
  441. app.url_path_for("create_sms_batch", project_id=default_public_project.id),
  442. headers=user_headers,
  443. json={"template": "{titre}, {respo}"},
  444. )
  445. assert response.status_code == 200
  446. result = session.execute(
  447. select(Sms).where(
  448. (Sms.project_id == default_public_project.id) & (Sms.id != default_sms_id)
  449. )
  450. )
  451. sms = result.scalars().first()
  452. assert sms is not None
  453. assert sms.content == "replanter excalibure, "