| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117 |
- """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
|