204000aabc
Resolve 3 merge conflicts and renumber the automation_hooks migration to follow main's migration chain (036_add_document_translation_fields). Conflicts resolved: - app/api/__init__.py: add automation_router alongside main's new routers - app/utils/settings_service.py: add automation_hooks_enabled alongside compliance_enabled - tests/conftest.py: add AutomationHook alongside AuditLog/ComplianceTemplate imports Migration renumbered: - 027_add_automation_hooks → 037_add_automation_hooks - down_revision: 026_add_scheduled_jobs → 036_add_document_translation_fields Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
636 lines
24 KiB
Python
636 lines
24 KiB
Python
"""Tests for app/utils/user_notification.py.
|
||
|
||
Covers all previously-uncovered branches:
|
||
- create_in_app_notification: exception/rollback path
|
||
- _send_email_notification: full SMTP success path, TLS disabled, no credentials
|
||
- _send_webhook_notification: success path with/without secret header
|
||
- dispatch_user_notification: preference loop (email, webhook), no target_id,
|
||
inactive target, invalid/empty JSON config, JSON decode error, outer exception
|
||
- dispatch_user_notification: push notification path (success and exception)
|
||
- notify_user_document_processed / notify_user_document_failed: happy-path smoke tests
|
||
"""
|
||
|
||
import json
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
import pytest
|
||
from sqlalchemy import create_engine
|
||
from sqlalchemy.orm import sessionmaker
|
||
from sqlalchemy.pool import StaticPool
|
||
|
||
from app.database import Base
|
||
from app.models import InAppNotification, UserNotificationPreference, UserNotificationTarget
|
||
|
||
_OWNER = "dispatch-test-user@example.com"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Shared fixture helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.fixture()
|
||
def mem_engine():
|
||
"""In-memory SQLite engine for user_notification tests."""
|
||
engine = create_engine(
|
||
"sqlite:///:memory:",
|
||
connect_args={"check_same_thread": False},
|
||
poolclass=StaticPool,
|
||
)
|
||
Base.metadata.create_all(bind=engine)
|
||
yield engine
|
||
Base.metadata.drop_all(bind=engine)
|
||
|
||
|
||
@pytest.fixture()
|
||
def Session(mem_engine): # noqa: N802
|
||
"""Session factory bound to mem_engine."""
|
||
return sessionmaker(bind=mem_engine)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# create_in_app_notification
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestCreateInAppNotification:
|
||
"""Tests for create_in_app_notification()."""
|
||
|
||
def test_returns_none_on_db_exception(self, Session):
|
||
"""create_in_app_notification should return None and rollback on error."""
|
||
from app.utils.user_notification import create_in_app_notification
|
||
|
||
# Provide a session whose commit raises to exercise the except branch
|
||
bad_session = MagicMock()
|
||
bad_session.add = MagicMock()
|
||
bad_session.commit = MagicMock(side_effect=RuntimeError("DB is down"))
|
||
bad_session.rollback = MagicMock()
|
||
bad_session.close = MagicMock()
|
||
|
||
BadSession = MagicMock(return_value=bad_session) # noqa: N806
|
||
|
||
with patch("app.utils.user_notification.SessionLocal", BadSession):
|
||
result = create_in_app_notification(
|
||
owner_id=_OWNER,
|
||
event_type="document.processed",
|
||
title="Oops",
|
||
message="Something went wrong",
|
||
)
|
||
|
||
assert result is None
|
||
bad_session.rollback.assert_called_once()
|
||
bad_session.close.assert_called_once()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# _send_email_notification
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestSendEmailNotification:
|
||
"""Tests for _send_email_notification()."""
|
||
|
||
def test_success_with_tls_and_credentials(self):
|
||
"""Email is sent with STARTTLS and login when fully configured."""
|
||
from app.utils.user_notification import _send_email_notification
|
||
|
||
config = {
|
||
"smtp_host": "smtp.example.com",
|
||
"smtp_port": "587",
|
||
"smtp_username": "user@example.com",
|
||
"smtp_password": "secret",
|
||
"smtp_use_tls": True,
|
||
"recipient_email": "dest@example.com",
|
||
}
|
||
|
||
mock_server = MagicMock()
|
||
mock_smtp_cls = MagicMock(return_value=mock_server)
|
||
mock_server.__enter__ = MagicMock(return_value=mock_server)
|
||
mock_server.__exit__ = MagicMock(return_value=False)
|
||
|
||
with patch("app.utils.user_notification.smtplib.SMTP", mock_smtp_cls):
|
||
result = _send_email_notification(config, "Subject", "Body text")
|
||
|
||
assert result is True
|
||
mock_server.starttls.assert_called_once()
|
||
mock_server.login.assert_called_once_with("user@example.com", "secret")
|
||
mock_server.send_message.assert_called_once()
|
||
|
||
def test_success_without_tls_and_without_credentials(self):
|
||
"""Email sent without STARTTLS and login when tls=False and no creds."""
|
||
from app.utils.user_notification import _send_email_notification
|
||
|
||
config = {
|
||
"smtp_host": "relay.internal",
|
||
"smtp_port": 25,
|
||
"smtp_use_tls": False,
|
||
"recipient_email": "dest@example.com",
|
||
}
|
||
|
||
mock_server = MagicMock()
|
||
mock_smtp_cls = MagicMock(return_value=mock_server)
|
||
mock_server.__enter__ = MagicMock(return_value=mock_server)
|
||
mock_server.__exit__ = MagicMock(return_value=False)
|
||
|
||
with patch("app.utils.user_notification.smtplib.SMTP", mock_smtp_cls):
|
||
result = _send_email_notification(config, "Subject", "No TLS body")
|
||
|
||
assert result is True
|
||
mock_server.starttls.assert_not_called()
|
||
mock_server.login.assert_not_called()
|
||
mock_server.send_message.assert_called_once()
|
||
|
||
def test_returns_false_on_smtp_exception(self):
|
||
"""_send_email_notification returns False when SMTP.connect raises."""
|
||
from app.utils.user_notification import _send_email_notification
|
||
|
||
config = {
|
||
"smtp_host": "smtp.example.com",
|
||
"smtp_port": 587,
|
||
"recipient_email": "dest@example.com",
|
||
}
|
||
|
||
with patch(
|
||
"app.utils.user_notification.smtplib.SMTP",
|
||
side_effect=ConnectionRefusedError("refused"),
|
||
):
|
||
result = _send_email_notification(config, "Subject", "Body")
|
||
|
||
assert result is False
|
||
|
||
def test_sender_email_defaults_to_smtp_username(self):
|
||
"""When sender_email is absent the smtp_username is used as From."""
|
||
from app.utils.user_notification import _send_email_notification
|
||
|
||
captured_msgs = []
|
||
|
||
config = {
|
||
"smtp_host": "smtp.example.com",
|
||
"smtp_port": 587,
|
||
"smtp_username": "sender@example.com",
|
||
"smtp_use_tls": False,
|
||
"recipient_email": "dest@example.com",
|
||
}
|
||
|
||
mock_server = MagicMock()
|
||
|
||
def capture_send(msg):
|
||
captured_msgs.append(msg)
|
||
|
||
mock_server.send_message = capture_send
|
||
mock_server.__enter__ = MagicMock(return_value=mock_server)
|
||
mock_server.__exit__ = MagicMock(return_value=False)
|
||
|
||
with patch("app.utils.user_notification.smtplib.SMTP", return_value=mock_server):
|
||
result = _send_email_notification(config, "Hi", "Body")
|
||
|
||
assert result is True
|
||
assert captured_msgs[0]["From"] == "sender@example.com"
|
||
|
||
def test_sender_email_defaults_to_noreply_when_no_username(self):
|
||
"""When no sender_email and no smtp_username, From falls back to noreply."""
|
||
from app.utils.user_notification import _send_email_notification
|
||
|
||
captured_msgs = []
|
||
|
||
config = {
|
||
"smtp_host": "smtp.example.com",
|
||
"smtp_port": 25,
|
||
"smtp_use_tls": False,
|
||
"recipient_email": "dest@example.com",
|
||
}
|
||
|
||
mock_server = MagicMock()
|
||
|
||
def capture_send(msg):
|
||
captured_msgs.append(msg)
|
||
|
||
mock_server.send_message = capture_send
|
||
mock_server.__enter__ = MagicMock(return_value=mock_server)
|
||
mock_server.__exit__ = MagicMock(return_value=False)
|
||
|
||
with patch("app.utils.user_notification.smtplib.SMTP", return_value=mock_server):
|
||
result = _send_email_notification(config, "Hi", "Body")
|
||
|
||
assert result is True
|
||
assert captured_msgs[0]["From"] == "noreply@docuelevate.local"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# _send_webhook_notification
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestSendWebhookNotification:
|
||
"""Tests for _send_webhook_notification()."""
|
||
|
||
def test_success_with_secret_header(self):
|
||
"""Webhook sent and X-DocuElevate-Secret header set when secret provided."""
|
||
from app.utils.user_notification import _send_webhook_notification
|
||
|
||
mock_response = MagicMock()
|
||
mock_response.status_code = 200
|
||
mock_response.raise_for_status = MagicMock()
|
||
|
||
with patch("app.utils.user_notification.httpx.post", return_value=mock_response) as mock_post:
|
||
result = _send_webhook_notification(
|
||
{"url": "https://hook.example.com/test", "secret": "mysecret"},
|
||
"document.processed",
|
||
"Title",
|
||
"Body",
|
||
)
|
||
|
||
assert result is True
|
||
_, kwargs = mock_post.call_args
|
||
assert kwargs["headers"]["X-DocuElevate-Secret"] == "mysecret"
|
||
assert kwargs["json"]["event"] == "document.processed"
|
||
|
||
def test_success_without_secret(self):
|
||
"""Webhook sent without X-DocuElevate-Secret header when no secret."""
|
||
from app.utils.user_notification import _send_webhook_notification
|
||
|
||
mock_response = MagicMock()
|
||
mock_response.status_code = 200
|
||
mock_response.raise_for_status = MagicMock()
|
||
|
||
with patch("app.utils.user_notification.httpx.post", return_value=mock_response) as mock_post:
|
||
result = _send_webhook_notification(
|
||
{"url": "https://hook.example.com/test"},
|
||
"document.failed",
|
||
"Failed",
|
||
"Error details",
|
||
)
|
||
|
||
assert result is True
|
||
_, kwargs = mock_post.call_args
|
||
assert "X-DocuElevate-Secret" not in kwargs["headers"]
|
||
|
||
def test_returns_false_on_http_error(self):
|
||
"""_send_webhook_notification returns False when httpx raises."""
|
||
from app.utils.user_notification import _send_webhook_notification
|
||
|
||
with patch(
|
||
"app.utils.user_notification.httpx.post",
|
||
side_effect=Exception("connection error"),
|
||
):
|
||
result = _send_webhook_notification(
|
||
{"url": "https://hook.example.com/test"},
|
||
"document.processed",
|
||
"T",
|
||
"M",
|
||
)
|
||
|
||
assert result is False
|
||
|
||
def test_returns_false_on_raise_for_status(self):
|
||
"""Returns False when response.raise_for_status() throws."""
|
||
import httpx as _httpx
|
||
|
||
from app.utils.user_notification import _send_webhook_notification
|
||
|
||
mock_response = MagicMock()
|
||
mock_response.raise_for_status = MagicMock(
|
||
side_effect=_httpx.HTTPStatusError(
|
||
"400",
|
||
request=MagicMock(),
|
||
response=MagicMock(),
|
||
)
|
||
)
|
||
|
||
with patch("app.utils.user_notification.httpx.post", return_value=mock_response):
|
||
result = _send_webhook_notification(
|
||
{"url": "https://hook.example.com/test"},
|
||
"document.processed",
|
||
"T",
|
||
"M",
|
||
)
|
||
|
||
assert result is False
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# dispatch_user_notification – preference loop
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestDispatchUserNotification:
|
||
"""Tests for dispatch_user_notification() preference dispatch logic."""
|
||
|
||
def _make_target(self, session, channel_type: str, config_dict: dict | None = None, is_active: bool = True):
|
||
target = UserNotificationTarget(
|
||
owner_id=_OWNER,
|
||
channel_type=channel_type,
|
||
name=f"{channel_type}-target",
|
||
config=json.dumps(config_dict) if config_dict is not None else None,
|
||
is_active=is_active,
|
||
)
|
||
session.add(target)
|
||
session.commit()
|
||
session.refresh(target)
|
||
return target
|
||
|
||
def _make_pref(self, session, channel_type: str, target_id: int | None, is_enabled: bool = True):
|
||
pref = UserNotificationPreference(
|
||
owner_id=_OWNER,
|
||
event_type="document.processed",
|
||
channel_type=channel_type,
|
||
target_id=target_id,
|
||
is_enabled=is_enabled,
|
||
)
|
||
session.add(pref)
|
||
session.commit()
|
||
return pref
|
||
|
||
def test_dispatches_email_when_pref_enabled(self, Session):
|
||
"""Email notification is sent for an active email preference."""
|
||
s = Session()
|
||
target = self._make_target(
|
||
s,
|
||
"email",
|
||
{
|
||
"smtp_host": "smtp.example.com",
|
||
"recipient_email": "u@example.com",
|
||
"smtp_use_tls": False,
|
||
},
|
||
)
|
||
self._make_pref(s, "email", target.id)
|
||
s.close()
|
||
|
||
with (
|
||
patch("app.utils.user_notification.SessionLocal", Session),
|
||
patch("app.utils.user_notification._send_email_notification", return_value=True) as mock_email,
|
||
patch("app.utils.push_notification.send_push_to_owner"),
|
||
):
|
||
from app.utils.user_notification import dispatch_user_notification
|
||
|
||
dispatch_user_notification(_OWNER, "document.processed", "Title", "Body")
|
||
|
||
mock_email.assert_called_once()
|
||
|
||
def test_dispatches_webhook_when_pref_enabled(self, Session):
|
||
"""Webhook notification is sent for an active webhook preference."""
|
||
s = Session()
|
||
target = self._make_target(s, "webhook", {"url": "https://hook.example.com"})
|
||
self._make_pref(s, "webhook", target.id)
|
||
s.close()
|
||
|
||
with (
|
||
patch("app.utils.user_notification.SessionLocal", Session),
|
||
patch("app.utils.user_notification._send_webhook_notification", return_value=True) as mock_hook,
|
||
patch("app.utils.push_notification.send_push_to_owner"),
|
||
):
|
||
from app.utils.user_notification import dispatch_user_notification
|
||
|
||
dispatch_user_notification(_OWNER, "document.processed", "Title", "Body")
|
||
|
||
mock_hook.assert_called_once()
|
||
|
||
def test_skips_pref_with_no_target_id(self, Session):
|
||
"""Preferences without a target_id are skipped (in-app only)."""
|
||
s = Session()
|
||
self._make_pref(s, "email", target_id=None)
|
||
s.close()
|
||
|
||
with (
|
||
patch("app.utils.user_notification.SessionLocal", Session),
|
||
patch("app.utils.user_notification._send_email_notification") as mock_email,
|
||
patch("app.utils.push_notification.send_push_to_owner"),
|
||
):
|
||
from app.utils.user_notification import dispatch_user_notification
|
||
|
||
dispatch_user_notification(_OWNER, "document.processed", "Title", "Body")
|
||
|
||
mock_email.assert_not_called()
|
||
|
||
def test_skips_inactive_target(self, Session):
|
||
"""Preferences pointing at an inactive target are skipped."""
|
||
s = Session()
|
||
target = self._make_target(s, "email", {"smtp_host": "x", "recipient_email": "y"}, is_active=False)
|
||
self._make_pref(s, "email", target.id)
|
||
s.close()
|
||
|
||
with (
|
||
patch("app.utils.user_notification.SessionLocal", Session),
|
||
patch("app.utils.user_notification._send_email_notification") as mock_email,
|
||
patch("app.utils.push_notification.send_push_to_owner"),
|
||
):
|
||
from app.utils.user_notification import dispatch_user_notification
|
||
|
||
dispatch_user_notification(_OWNER, "document.processed", "Title", "Body")
|
||
|
||
mock_email.assert_not_called()
|
||
|
||
def test_handles_invalid_json_config(self, Session):
|
||
"""Invalid JSON in target.config falls back to empty dict (no crash)."""
|
||
target = UserNotificationTarget(
|
||
owner_id=_OWNER,
|
||
channel_type="email",
|
||
name="bad-config-target",
|
||
config="NOT_VALID_JSON",
|
||
is_active=True,
|
||
)
|
||
s = Session()
|
||
s.add(target)
|
||
s.commit()
|
||
s.refresh(target)
|
||
self._make_pref(s, "email", target.id)
|
||
s.close()
|
||
|
||
with (
|
||
patch("app.utils.user_notification.SessionLocal", Session),
|
||
patch("app.utils.user_notification._send_email_notification", return_value=False) as mock_email,
|
||
patch("app.utils.push_notification.send_push_to_owner"),
|
||
):
|
||
from app.utils.user_notification import dispatch_user_notification
|
||
|
||
# Should not raise even though config is bad JSON
|
||
dispatch_user_notification(_OWNER, "document.processed", "Title", "Body")
|
||
|
||
# Called with empty config dict, which is missing smtp_host → returns False
|
||
mock_email.assert_called_once_with({}, "Title", "Body")
|
||
|
||
def test_handles_null_config(self, Session):
|
||
"""NULL target.config is treated as empty dict (no crash)."""
|
||
target = UserNotificationTarget(
|
||
owner_id=_OWNER,
|
||
channel_type="webhook",
|
||
name="null-config-target",
|
||
config=None,
|
||
is_active=True,
|
||
)
|
||
s = Session()
|
||
s.add(target)
|
||
s.commit()
|
||
s.refresh(target)
|
||
self._make_pref(s, "webhook", target.id)
|
||
s.close()
|
||
|
||
with (
|
||
patch("app.utils.user_notification.SessionLocal", Session),
|
||
patch("app.utils.user_notification._send_webhook_notification", return_value=False) as mock_hook,
|
||
patch("app.utils.push_notification.send_push_to_owner"),
|
||
):
|
||
from app.utils.user_notification import dispatch_user_notification
|
||
|
||
dispatch_user_notification(_OWNER, "document.processed", "Title", "Body")
|
||
|
||
mock_hook.assert_called_once_with({}, "document.processed", "Title", "Body")
|
||
|
||
def test_outer_exception_does_not_propagate(self):
|
||
"""An exception in the preference query must be caught and logged."""
|
||
bad_session = MagicMock()
|
||
bad_session.query = MagicMock(side_effect=RuntimeError("DB gone"))
|
||
bad_session.add = MagicMock()
|
||
bad_session.commit = MagicMock()
|
||
bad_session.refresh = MagicMock(return_value=MagicMock())
|
||
bad_session.close = MagicMock()
|
||
|
||
BadSession = MagicMock(return_value=bad_session) # noqa: N806
|
||
|
||
with (
|
||
patch("app.utils.user_notification.SessionLocal", BadSession),
|
||
patch("app.utils.push_notification.send_push_to_owner"),
|
||
):
|
||
from app.utils.user_notification import dispatch_user_notification
|
||
|
||
# Must not raise
|
||
dispatch_user_notification(_OWNER, "document.processed", "Title", "Body")
|
||
|
||
def test_push_notification_sent(self, Session):
|
||
"""Push notification is sent via send_push_to_owner."""
|
||
with (
|
||
patch("app.utils.user_notification.SessionLocal", Session),
|
||
patch("app.utils.push_notification.send_push_to_owner") as mock_push,
|
||
):
|
||
from app.utils.user_notification import dispatch_user_notification
|
||
|
||
dispatch_user_notification(_OWNER, "document.processed", "Push Title", "Push Body", file_id=99)
|
||
|
||
mock_push.assert_called_once_with(
|
||
owner_id=_OWNER,
|
||
title="Push Title",
|
||
body="Push Body",
|
||
data={"event_type": "document.processed", "file_id": 99},
|
||
)
|
||
|
||
def test_push_exception_does_not_propagate(self, Session):
|
||
"""An exception in send_push_to_owner must be caught and logged."""
|
||
with (
|
||
patch("app.utils.user_notification.SessionLocal", Session),
|
||
patch(
|
||
"app.utils.push_notification.send_push_to_owner",
|
||
side_effect=RuntimeError("push service down"),
|
||
),
|
||
):
|
||
from app.utils.user_notification import dispatch_user_notification
|
||
|
||
# Must not raise
|
||
dispatch_user_notification(_OWNER, "document.processed", "T", "M")
|
||
|
||
def test_unknown_channel_type_is_skipped(self, Session):
|
||
"""Preferences with an unrecognised channel_type are silently skipped.
|
||
|
||
The live query filters to ("email", "webhook"), so this branch is only
|
||
reachable via a mocked session that bypasses the filter. The test
|
||
exercises the dead else-branch in dispatch_user_notification so that
|
||
branch coverage reaches 100%.
|
||
"""
|
||
import json as _json
|
||
|
||
target = UserNotificationTarget(
|
||
owner_id=_OWNER,
|
||
channel_type="sms",
|
||
name="sms-target",
|
||
config=_json.dumps({"phone": "+1555000000"}),
|
||
is_active=True,
|
||
)
|
||
s = Session()
|
||
s.add(target)
|
||
s.commit()
|
||
s.refresh(target)
|
||
target_id = target.id
|
||
|
||
unknown_pref = MagicMock()
|
||
unknown_pref.target_id = target_id
|
||
unknown_pref.channel_type = "sms"
|
||
|
||
mock_query = MagicMock()
|
||
mock_query.filter.return_value = mock_query
|
||
mock_query.all.return_value = [unknown_pref]
|
||
|
||
# Build a real session but intercept only the query for preferences
|
||
real_session = Session()
|
||
|
||
def fake_query(model):
|
||
from app.models import UserNotificationPreference as _UNP
|
||
|
||
if model is _UNP:
|
||
return mock_query
|
||
return real_session.query(model)
|
||
|
||
real_session.query = fake_query # type: ignore[method-assign]
|
||
real_session_cls = MagicMock(return_value=real_session)
|
||
|
||
with (
|
||
patch("app.utils.user_notification.SessionLocal", real_session_cls),
|
||
patch("app.utils.user_notification._send_email_notification") as mock_email,
|
||
patch("app.utils.user_notification._send_webhook_notification") as mock_hook,
|
||
patch("app.utils.push_notification.send_push_to_owner"),
|
||
):
|
||
from app.utils.user_notification import dispatch_user_notification
|
||
|
||
dispatch_user_notification(_OWNER, "document.processed", "T", "M")
|
||
|
||
mock_email.assert_not_called()
|
||
mock_hook.assert_not_called()
|
||
real_session.close()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# notify_user_document_processed / notify_user_document_failed
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestNotifyUserDocumentHelpers:
|
||
"""Smoke tests for the convenience wrappers."""
|
||
|
||
def test_notify_processed_creates_in_app_record(self, Session):
|
||
"""notify_user_document_processed creates an InAppNotification."""
|
||
with (
|
||
patch("app.utils.user_notification.SessionLocal", Session),
|
||
patch("app.utils.push_notification.send_push_to_owner"),
|
||
):
|
||
from app.utils.user_notification import notify_user_document_processed
|
||
|
||
notify_user_document_processed(owner_id=_OWNER, filename="report.pdf", file_id=5)
|
||
|
||
s = Session()
|
||
notifs = s.query(InAppNotification).filter_by(owner_id=_OWNER).all()
|
||
s.close()
|
||
assert len(notifs) == 1
|
||
assert "report.pdf" in notifs[0].title
|
||
assert notifs[0].event_type == "document.processed"
|
||
|
||
def test_notify_failed_creates_in_app_record(self, Session):
|
||
"""notify_user_document_failed creates an InAppNotification."""
|
||
with (
|
||
patch("app.utils.user_notification.SessionLocal", Session),
|
||
patch("app.utils.push_notification.send_push_to_owner"),
|
||
):
|
||
from app.utils.user_notification import notify_user_document_failed
|
||
|
||
notify_user_document_failed(owner_id=_OWNER, filename="broken.pdf", error="Timeout")
|
||
|
||
s = Session()
|
||
notifs = s.query(InAppNotification).filter_by(owner_id=_OWNER).all()
|
||
s.close()
|
||
assert len(notifs) == 1
|
||
assert "broken.pdf" in notifs[0].title
|
||
assert "Timeout" in notifs[0].message
|
||
assert notifs[0].event_type == "document.failed"
|