test_notification_scope.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. """Notification providers and logs keep to the caller's printers."""
  2. from __future__ import annotations
  3. from datetime import datetime, timedelta
  4. import pytest
  5. from httpx import AsyncClient
  6. from sqlalchemy import select
  7. from backend.app.models.notification import NotificationLog
  8. from backend.tests.integration.test_printer_scope_1727 import _admin_token, _auth, _group, _user
  9. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  10. PERMS = [
  11. "printers:read",
  12. "notifications:read",
  13. "notifications:create",
  14. "notifications:update",
  15. "notifications:delete",
  16. ]
  17. def _provider(name: str, printer_id: int | None) -> dict:
  18. return {
  19. "name": name,
  20. "provider_type": "callmebot",
  21. "enabled": True,
  22. "config": {"phone_number": "+1234567890", "api_key": "test-api-key"},
  23. "printer_id": printer_id,
  24. }
  25. async def _setup(client: AsyncClient, printer_factory):
  26. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  27. admin = await _admin_token(client)
  28. perms = await _group(client, admin, "perms", permissions=PERMS)
  29. team = await _group(client, admin, "team", printer_ids=[a.id])
  30. member = _auth((await _user(client, admin, "member", [perms, team]))[0])
  31. ids = {}
  32. for name, printer in (("ours", a.id), ("theirs", b.id), ("everyone", None)):
  33. created = await client.post("/api/v1/notifications/", headers=_auth(admin), json=_provider(name, printer))
  34. assert created.status_code == 200, created.text
  35. ids[name] = created.json()["id"]
  36. return a, b, member, ids
  37. async def test_a_member_sees_and_manages_only_providers_of_their_printers(async_client: AsyncClient, printer_factory):
  38. a, b, member, ids = await _setup(async_client, printer_factory)
  39. listed = await async_client.get("/api/v1/notifications/", headers=member)
  40. assert [p["name"] for p in listed.json()] == ["ours"]
  41. for name in ("theirs", "everyone"):
  42. pid = ids[name]
  43. assert (await async_client.get(f"/api/v1/notifications/{pid}", headers=member)).status_code == 404
  44. patched = await async_client.patch(f"/api/v1/notifications/{pid}", headers=member, json={"name": "mine now"})
  45. assert patched.status_code == 404
  46. assert (await async_client.post(f"/api/v1/notifications/{pid}/test", headers=member)).status_code == 404
  47. assert (await async_client.delete(f"/api/v1/notifications/{pid}", headers=member)).status_code == 404
  48. assert (await async_client.get(f"/api/v1/notifications/{ids['ours']}", headers=member)).status_code == 200
  49. async def test_a_member_binds_providers_to_their_printers_only(async_client: AsyncClient, printer_factory):
  50. a, b, member, ids = await _setup(async_client, printer_factory)
  51. to_other = await async_client.post("/api/v1/notifications/", headers=member, json=_provider("x", b.id))
  52. to_all = await async_client.post("/api/v1/notifications/", headers=member, json=_provider("y", None))
  53. to_own = await async_client.post("/api/v1/notifications/", headers=member, json=_provider("z", a.id))
  54. retarget_other = await async_client.patch(
  55. f"/api/v1/notifications/{ids['ours']}", headers=member, json={"printer_id": b.id}
  56. )
  57. retarget_all = await async_client.patch(
  58. f"/api/v1/notifications/{ids['ours']}", headers=member, json={"printer_id": None}
  59. )
  60. assert (to_other.status_code, to_all.status_code, to_own.status_code) == (404, 400, 200)
  61. assert (retarget_other.status_code, retarget_all.status_code) == (404, 400)
  62. async def test_logs_and_their_counts_keep_to_the_team_printers(async_client: AsyncClient, db_session, printer_factory):
  63. a, b, member, ids = await _setup(async_client, printer_factory)
  64. a_id, b_id = a.id, b.id
  65. old = datetime.utcnow() - timedelta(days=40)
  66. for printer, when in ((a_id, None), (b_id, None), (None, None), (a_id, old), (b_id, old), (None, old)):
  67. row = NotificationLog(
  68. provider_id=ids["ours"], event_type="print_start", title="t", message="m", printer_id=printer
  69. )
  70. if when is not None:
  71. row.created_at = when
  72. db_session.add(row)
  73. await db_session.commit()
  74. listed = await async_client.get("/api/v1/notifications/logs", headers=member)
  75. stats = await async_client.get("/api/v1/notifications/logs/stats", headers=member)
  76. cleared = await async_client.delete("/api/v1/notifications/logs", headers=member, params={"older_than_days": 30})
  77. assert sorted((row["printer_id"] or 0) for row in listed.json()) == [0, a_id]
  78. assert stats.json()["total"] == 2
  79. assert cleared.json()["deleted"] == 1
  80. db_session.expire_all()
  81. left = sorted((pid or 0) for (pid,) in (await db_session.execute(select(NotificationLog.printer_id))).all())
  82. assert left == sorted([a_id, b_id, 0, b_id, 0])
  83. async def test_testing_a_stored_provider_answers_a_hidden_one_like_a_missing_one(
  84. async_client: AsyncClient, printer_factory
  85. ):
  86. a, b, member, ids = await _setup(async_client, printer_factory)
  87. body = {"provider_type": "callmebot", "config": {"phone_number": "+1234567890", "api_key": "********"}}
  88. hidden = await async_client.post(
  89. "/api/v1/notifications/test-config", headers=member, json={**body, "provider_id": ids["theirs"]}
  90. )
  91. missing = await async_client.post(
  92. "/api/v1/notifications/test-config", headers=member, json={**body, "provider_id": 999_999}
  93. )
  94. assert (
  95. (hidden.status_code, hidden.json())
  96. == (missing.status_code, missing.json())
  97. == (
  98. 404,
  99. {"detail": "Notification provider not found"},
  100. )
  101. )
  102. async def test_app_messages_reach_only_the_team_printers_channels(async_client: AsyncClient, printer_factory):
  103. from unittest.mock import AsyncMock, patch
  104. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  105. admin = await _admin_token(async_client)
  106. perms = await _group(async_client, admin, "perms", permissions=PERMS)
  107. team = await _group(async_client, admin, "team", printer_ids=[a.id])
  108. member = _auth((await _user(async_client, admin, "member", [perms, team]))[0])
  109. for name, printer in (("ours", a.id), ("theirs", b.id), ("everyone", None)):
  110. created = await async_client.post(
  111. "/api/v1/notifications/", headers=_auth(admin), json={**_provider(name, printer), "on_app_message": True}
  112. )
  113. assert created.status_code == 200, created.text
  114. channels = await async_client.get("/api/v1/notifications/app-message/channels", headers=member)
  115. with patch(
  116. "backend.app.services.notification_service.NotificationService._send_to_providers", new=AsyncMock()
  117. ) as send:
  118. sent = await async_client.post(
  119. "/api/v1/notifications/app-message", headers=member, json={"title": "t", "message": "m"}
  120. )
  121. assert [c["name"] for c in channels.json()] == ["ours"]
  122. assert sent.json()["channels"] == 1
  123. assert [p.name for p in send.await_args.args[0]] == ["ours"]
  124. async def test_the_log_does_not_name_providers_the_caller_cannot_see(
  125. async_client: AsyncClient, db_session, printer_factory
  126. ):
  127. a, b, member, ids = await _setup(async_client, printer_factory)
  128. a_id = a.id
  129. for provider in ("ours", "everyone"):
  130. db_session.add(
  131. NotificationLog(
  132. provider_id=ids[provider],
  133. event_type="print_start",
  134. title="t",
  135. message="m",
  136. printer_id=a_id,
  137. success=False,
  138. error_message=f"failed for {provider}",
  139. )
  140. )
  141. await db_session.commit()
  142. listed = await async_client.get("/api/v1/notifications/logs", headers=member)
  143. stats = await async_client.get("/api/v1/notifications/logs/stats", headers=member)
  144. rows = sorted(listed.json(), key=lambda row: str(row["provider_name"]))
  145. assert [(row["provider_name"], row["error_message"]) for row in rows] == [
  146. (None, None),
  147. ("ours", "failed for ours"),
  148. ]
  149. assert stats.json()["by_provider"] == {"ours": 1, "Other": 1}
  150. async def test_log_entries_without_a_printer_stay_with_their_provider(
  151. async_client: AsyncClient, db_session, printer_factory
  152. ):
  153. a, b, member, ids = await _setup(async_client, printer_factory)
  154. for provider in ("ours", "everyone"):
  155. db_session.add(
  156. NotificationLog(
  157. provider_id=ids[provider], event_type="app:key", title=provider, message="m", printer_id=None
  158. )
  159. )
  160. await db_session.commit()
  161. listed = await async_client.get("/api/v1/notifications/logs", headers=member)
  162. stats = await async_client.get("/api/v1/notifications/logs/stats", headers=member)
  163. assert [row["title"] for row in listed.json()] == ["ours"]
  164. assert stats.json()["total"] == 1
  165. async def test_a_log_entry_whose_provider_is_gone_shows_no_error(
  166. async_client: AsyncClient, db_session, printer_factory
  167. ):
  168. if db_session.bind.dialect.name != "sqlite":
  169. pytest.skip("only SQLite can keep such an entry")
  170. a, b, member, ids = await _setup(async_client, printer_factory)
  171. db_session.add(
  172. NotificationLog(
  173. provider_id=999_999,
  174. event_type="print_start",
  175. title="t",
  176. message="m",
  177. printer_id=a.id,
  178. success=False,
  179. error_message="failed",
  180. )
  181. )
  182. await db_session.commit()
  183. listed = await async_client.get("/api/v1/notifications/logs", headers=member)
  184. assert [(row["provider_name"], row["error_message"]) for row in listed.json()] == [(None, None)]