test_daily_digest_delivery.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. """The daily digest is not lost (FO-C4).
  2. The queued events were deleted whatever the send's outcome, so a failed send
  3. lost the day's digest. A digest time inside the provider's quiet hours was
  4. "skipped", logged as sent, and its events deleted too. It now goes out at its
  5. time even in quiet hours -- that time was chosen for it, as plate-not-empty
  6. alerts also ignore them -- and a failed send keeps the events for the next.
  7. """
  8. import json
  9. from unittest.mock import AsyncMock, patch
  10. import pytest
  11. from sqlalchemy import select
  12. from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
  13. from backend.app.models.notification import NotificationDigestQueue, NotificationProvider
  14. from backend.app.services.notification_service import notification_service
  15. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  16. async def _provider_with_queue(db_session) -> int:
  17. provider = NotificationProvider(
  18. name="phone",
  19. provider_type="ntfy",
  20. enabled=True,
  21. config=json.dumps({"server": "https://ntfy.example", "topic": "t"}),
  22. daily_digest_enabled=True,
  23. daily_digest_time="07:00",
  24. )
  25. db_session.add(provider)
  26. await db_session.flush()
  27. for title in ("Print Completed", "Print Failed"):
  28. db_session.add(
  29. NotificationDigestQueue(provider_id=provider.id, event_type="print_complete", title=title, message=title)
  30. )
  31. await db_session.commit()
  32. return provider.id
  33. async def _send_digest(
  34. test_engine, provider_id: int, send_result: tuple[bool, str], *, quiet_now: bool = False
  35. ) -> AsyncMock:
  36. maker = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
  37. send = AsyncMock(return_value=send_result)
  38. with (
  39. patch("backend.app.core.database.async_session", maker),
  40. patch.object(notification_service, "_send_ntfy", send),
  41. patch.object(notification_service, "_is_in_quiet_hours", return_value=quiet_now),
  42. ):
  43. await notification_service.send_digest(provider_id)
  44. return send
  45. async def _queued(db_session, provider_id: int) -> int:
  46. db_session.expire_all()
  47. rows = await db_session.scalars(
  48. select(NotificationDigestQueue).where(NotificationDigestQueue.provider_id == provider_id)
  49. )
  50. return len(rows.all())
  51. async def test_a_failed_send_keeps_the_events_for_the_next_digest(test_engine, db_session):
  52. provider_id = await _provider_with_queue(db_session)
  53. send = await _send_digest(test_engine, provider_id, (False, "connection refused"))
  54. send.assert_awaited_once()
  55. assert await _queued(db_session, provider_id) == 2
  56. async def test_a_digest_inside_quiet_hours_is_still_sent(test_engine, db_session):
  57. provider_id = await _provider_with_queue(db_session)
  58. send = await _send_digest(test_engine, provider_id, (True, ""), quiet_now=True)
  59. send.assert_awaited_once()
  60. assert await _queued(db_session, provider_id) == 0
  61. async def test_a_sent_digest_clears_its_events(test_engine, db_session):
  62. provider_id = await _provider_with_queue(db_session)
  63. await _send_digest(test_engine, provider_id, (True, ""))
  64. assert await _queued(db_session, provider_id) == 0
  65. async def test_events_kept_after_a_failure_are_retried_once_not_forever(test_engine, db_session):
  66. """A digest that always fails -- revoked credentials, or a busy day too
  67. long for Pushover's 1024 characters -- must not grow its queue without end:
  68. kept events get the next digest, and are dropped after that."""
  69. from datetime import datetime, timedelta
  70. provider_id = await _provider_with_queue(db_session)
  71. for hours_ago, title in ((50, "Three days ago"), (26, "Yesterday")):
  72. db_session.add(
  73. NotificationDigestQueue(
  74. provider_id=provider_id,
  75. event_type="print_complete",
  76. title=title,
  77. message=title,
  78. created_at=datetime.utcnow() - timedelta(hours=hours_ago),
  79. )
  80. )
  81. await db_session.commit()
  82. send = await _send_digest(test_engine, provider_id, (False, "message too long"))
  83. body = send.await_args.args[2]
  84. assert "Yesterday" in body
  85. assert "Three days ago" not in body
  86. # Yesterday's line says which day it was from.
  87. yesterday = (datetime.utcnow() - timedelta(hours=26)).strftime("%m-%d")
  88. assert f"{yesterday} " in body
  89. assert await _queued(db_session, provider_id) == 3