|
|
@@ -0,0 +1,117 @@
|
|
|
+"""The daily digest is not lost (FO-C4).
|
|
|
+
|
|
|
+The queued events were deleted whatever the send's outcome, so a failed send
|
|
|
+lost the day's digest. A digest time inside the provider's quiet hours was
|
|
|
+"skipped", logged as sent, and its events deleted too. It now goes out at its
|
|
|
+time even in quiet hours -- that time was chosen for it, as plate-not-empty
|
|
|
+alerts also ignore them -- and a failed send keeps the events for the next.
|
|
|
+"""
|
|
|
+
|
|
|
+import json
|
|
|
+from unittest.mock import AsyncMock, patch
|
|
|
+
|
|
|
+import pytest
|
|
|
+from sqlalchemy import select
|
|
|
+from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
|
+
|
|
|
+from backend.app.models.notification import NotificationDigestQueue, NotificationProvider
|
|
|
+from backend.app.services.notification_service import notification_service
|
|
|
+
|
|
|
+pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
|
|
|
+
|
|
|
+
|
|
|
+async def _provider_with_queue(db_session) -> int:
|
|
|
+ provider = NotificationProvider(
|
|
|
+ name="phone",
|
|
|
+ provider_type="ntfy",
|
|
|
+ enabled=True,
|
|
|
+ config=json.dumps({"server": "https://ntfy.example", "topic": "t"}),
|
|
|
+ daily_digest_enabled=True,
|
|
|
+ daily_digest_time="07:00",
|
|
|
+ )
|
|
|
+ db_session.add(provider)
|
|
|
+ await db_session.flush()
|
|
|
+ for title in ("Print Completed", "Print Failed"):
|
|
|
+ db_session.add(
|
|
|
+ NotificationDigestQueue(provider_id=provider.id, event_type="print_complete", title=title, message=title)
|
|
|
+ )
|
|
|
+ await db_session.commit()
|
|
|
+ return provider.id
|
|
|
+
|
|
|
+
|
|
|
+async def _send_digest(
|
|
|
+ test_engine, provider_id: int, send_result: tuple[bool, str], *, quiet_now: bool = False
|
|
|
+) -> AsyncMock:
|
|
|
+ maker = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
|
|
|
+ send = AsyncMock(return_value=send_result)
|
|
|
+ with (
|
|
|
+ patch("backend.app.core.database.async_session", maker),
|
|
|
+ patch.object(notification_service, "_send_ntfy", send),
|
|
|
+ patch.object(notification_service, "_is_in_quiet_hours", return_value=quiet_now),
|
|
|
+ ):
|
|
|
+ await notification_service.send_digest(provider_id)
|
|
|
+ return send
|
|
|
+
|
|
|
+
|
|
|
+async def _queued(db_session, provider_id: int) -> int:
|
|
|
+ db_session.expire_all()
|
|
|
+ rows = await db_session.scalars(
|
|
|
+ select(NotificationDigestQueue).where(NotificationDigestQueue.provider_id == provider_id)
|
|
|
+ )
|
|
|
+ return len(rows.all())
|
|
|
+
|
|
|
+
|
|
|
+async def test_a_failed_send_keeps_the_events_for_the_next_digest(test_engine, db_session):
|
|
|
+ provider_id = await _provider_with_queue(db_session)
|
|
|
+
|
|
|
+ send = await _send_digest(test_engine, provider_id, (False, "connection refused"))
|
|
|
+
|
|
|
+ send.assert_awaited_once()
|
|
|
+ assert await _queued(db_session, provider_id) == 2
|
|
|
+
|
|
|
+
|
|
|
+async def test_a_digest_inside_quiet_hours_is_still_sent(test_engine, db_session):
|
|
|
+ provider_id = await _provider_with_queue(db_session)
|
|
|
+
|
|
|
+ send = await _send_digest(test_engine, provider_id, (True, ""), quiet_now=True)
|
|
|
+
|
|
|
+ send.assert_awaited_once()
|
|
|
+ assert await _queued(db_session, provider_id) == 0
|
|
|
+
|
|
|
+
|
|
|
+async def test_a_sent_digest_clears_its_events(test_engine, db_session):
|
|
|
+ provider_id = await _provider_with_queue(db_session)
|
|
|
+
|
|
|
+ await _send_digest(test_engine, provider_id, (True, ""))
|
|
|
+
|
|
|
+ assert await _queued(db_session, provider_id) == 0
|
|
|
+
|
|
|
+
|
|
|
+async def test_events_kept_after_a_failure_are_retried_once_not_forever(test_engine, db_session):
|
|
|
+ """A digest that always fails -- revoked credentials, or a busy day too
|
|
|
+ long for Pushover's 1024 characters -- must not grow its queue without end:
|
|
|
+ kept events get the next digest, and are dropped after that."""
|
|
|
+ from datetime import datetime, timedelta
|
|
|
+
|
|
|
+ provider_id = await _provider_with_queue(db_session)
|
|
|
+ for hours_ago, title in ((50, "Three days ago"), (26, "Yesterday")):
|
|
|
+ db_session.add(
|
|
|
+ NotificationDigestQueue(
|
|
|
+ provider_id=provider_id,
|
|
|
+ event_type="print_complete",
|
|
|
+ title=title,
|
|
|
+ message=title,
|
|
|
+ created_at=datetime.utcnow() - timedelta(hours=hours_ago),
|
|
|
+ )
|
|
|
+ )
|
|
|
+ await db_session.commit()
|
|
|
+
|
|
|
+ send = await _send_digest(test_engine, provider_id, (False, "message too long"))
|
|
|
+
|
|
|
+ body = send.await_args.args[2]
|
|
|
+ assert "Yesterday" in body
|
|
|
+ assert "Three days ago" not in body
|
|
|
+ # Yesterday's line says which day it was from.
|
|
|
+ yesterday = (datetime.utcnow() - timedelta(hours=26)).strftime("%m-%d")
|
|
|
+ assert f"{yesterday} " in body
|
|
|
+ assert await _queued(db_session, provider_id) == 3
|