Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

173 lines
5.7 KiB
Python

"""Extra coverage for notifications/service.py — _send_with_retry, URL validation, service detection."""
from unittest.mock import MagicMock, patch
import pytest
from local_deep_research.notifications.service import (
NotificationService,
SendError,
ServiceError,
)
MODULE = "local_deep_research.notifications.service"
def _make_service():
"""Create a NotificationService with mocked apprise."""
with patch(f"{MODULE}.apprise") as mock_apprise_module:
mock_apprise_module.Apprise.return_value = MagicMock()
svc = NotificationService.__new__(NotificationService)
svc.apprise = mock_apprise_module.Apprise.return_value
svc.allow_private_ips = False
# Tests in this file exercise inner logic; open the gate so
# send() doesn't bail at the operator-level check.
svc.outbound_allowed = True
svc.SERVICE_PATTERNS = NotificationService.SERVICE_PATTERNS
return svc
# ===========================================================================
# _send_with_retry — direct invocation
# ===========================================================================
class TestSendWithRetry:
def test_success_returns_true(self):
svc = _make_service()
mock_apprise = MagicMock()
mock_apprise.notify.return_value = True
# Call _send_with_retry directly (bypasses Tenacity decorator in test)
result = svc._send_with_retry("Title", "Body", mock_apprise)
assert result is True
mock_apprise.notify.assert_called_once()
def test_failure_raises_send_error(self):
svc = _make_service()
mock_apprise = MagicMock()
mock_apprise.notify.return_value = False
with pytest.raises(SendError, match="Failed to send"):
svc._send_with_retry("Title", "Body", mock_apprise)
def test_with_tag_and_attach(self):
svc = _make_service()
mock_apprise = MagicMock()
mock_apprise.notify.return_value = True
result = svc._send_with_retry(
"Title", "Body", mock_apprise, tag="urgent", attach=["/tmp/f.txt"]
)
assert result is True
call_kwargs = mock_apprise.notify.call_args.kwargs
assert call_kwargs["tag"] == "urgent"
assert call_kwargs["attach"] == ["/tmp/f.txt"]
# ===========================================================================
# send — URL validation failure
# ===========================================================================
class TestSendUrlValidation:
def test_invalid_service_url_raises(self):
svc = _make_service()
mock_validator = MagicMock()
mock_validator.validate_multiple_urls.return_value = (
False,
"SSRF blocked",
)
with (
patch(f"{MODULE}.NotificationURLValidator", mock_validator),
pytest.raises((ServiceError, SendError)),
):
svc.send("Title", "Body", service_urls="file:///etc/passwd")
def test_apprise_add_fails_returns_false(self):
svc = _make_service()
mock_validator = MagicMock()
mock_validator.validate_multiple_urls.return_value = (True, None)
mock_apprise_mod = MagicMock()
temp_instance = MagicMock()
temp_instance.add.return_value = False
mock_apprise_mod.Apprise.return_value = temp_instance
with (
patch(f"{MODULE}.NotificationURLValidator", mock_validator),
patch(f"{MODULE}.apprise", mock_apprise_mod),
):
result = svc.send("Title", "Body", service_urls="discord://w/t")
assert result is False
def test_no_services_configured_returns_false(self):
svc = _make_service()
svc.apprise.__len__ = MagicMock(return_value=0)
result = svc.send("Title", "Body")
assert result is False
# ===========================================================================
# _validate_url
# ===========================================================================
class TestValidateUrlStatic:
def test_empty_raises(self):
with pytest.raises(ServiceError, match="non-empty"):
NotificationService._validate_url("")
def test_none_raises(self):
with pytest.raises(ServiceError, match="non-empty"):
NotificationService._validate_url(None)
def test_int_raises(self):
with pytest.raises(ServiceError, match="non-empty"):
NotificationService._validate_url(123)
def test_no_scheme_raises(self):
with pytest.raises(ServiceError, match="Invalid URL"):
NotificationService._validate_url("no-scheme-here")
def test_valid_discord_url(self):
NotificationService._validate_url("discord://webhook_id/token")
def test_valid_mailto_url(self):
NotificationService._validate_url("mailto://user:pass@smtp.com")
# ===========================================================================
# get_service_type
# ===========================================================================
class TestGetServiceType:
def _get_type(self, url):
svc = _make_service()
return svc.get_service_type(url)
def test_discord(self):
assert self._get_type("discord://id/token") == "discord"
def test_slack(self):
assert self._get_type("slack://token_a/token_b/token_c") == "slack"
def test_email(self):
assert self._get_type("mailto://user:pass@smtp.com") == "email"
def test_telegram(self):
assert self._get_type("tgram://bot_token/chat_id") == "telegram"
def test_unknown(self):
assert self._get_type("custom://something") == "unknown"
def test_case_insensitive(self):
# Patterns use re.IGNORECASE
assert self._get_type("Discord://ID/TOKEN") == "discord"