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
140 lines
5.8 KiB
Python
140 lines
5.8 KiB
Python
"""
|
|
Tests for NotificationService edge cases: test_service() error paths
|
|
and get_service_type() pattern coverage.
|
|
"""
|
|
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
from local_deep_research.notifications.service import NotificationService
|
|
|
|
|
|
class TestTestServiceErrorPaths:
|
|
"""Tests for test_service() error branches."""
|
|
|
|
@patch("local_deep_research.notifications.service.apprise.Apprise")
|
|
@patch(
|
|
"local_deep_research.notifications.service.NotificationURLValidator.validate_service_url",
|
|
return_value=(
|
|
False,
|
|
"Blocked private/internal IP address: 169.254.169.254",
|
|
),
|
|
)
|
|
def test_ssrf_validation_fails(self, mock_validator, mock_apprise_class):
|
|
# The classic SSRF target — AWS IMDS at 169.254.169.254 — is the
|
|
# canonical fixture for this branch. The hint is intentionally
|
|
# NOT asserted here: this test pins only that the validator's
|
|
# reason reaches the user. Hint suppression for always-blocked
|
|
# categories (metadata, 6to4, Teredo, etc.) is pinned end-to-end
|
|
# by test_test_service_ip_rejection_matrix in
|
|
# test_notification_coverage.py.
|
|
service = NotificationService(outbound_allowed=True)
|
|
result = service.test_service("http://169.254.169.254/metadata")
|
|
assert result["success"] is False
|
|
# The validator's reason is surfaced verbatim — it echoes only
|
|
# the user-supplied hostname, so it is not a server-side leak.
|
|
assert "private/internal IP" in result["error"]
|
|
assert "169.254.169.254" in result["error"]
|
|
|
|
@patch("local_deep_research.notifications.service.apprise.Apprise")
|
|
@patch(
|
|
"local_deep_research.notifications.service.NotificationURLValidator.validate_service_url",
|
|
return_value=(True, None),
|
|
)
|
|
def test_apprise_add_fails(self, mock_validator, mock_apprise_class):
|
|
mock_instance = MagicMock()
|
|
mock_instance.add.return_value = False
|
|
mock_apprise_class.return_value = mock_instance
|
|
|
|
service = NotificationService(outbound_allowed=True)
|
|
result = service.test_service("discord://webhook/token")
|
|
assert result["success"] is False
|
|
assert "Failed to add" in result["error"]
|
|
|
|
@patch("local_deep_research.notifications.service.apprise.Apprise")
|
|
@patch(
|
|
"local_deep_research.notifications.service.NotificationURLValidator.validate_service_url",
|
|
return_value=(True, None),
|
|
)
|
|
def test_apprise_notify_fails(self, mock_validator, mock_apprise_class):
|
|
mock_instance = MagicMock()
|
|
mock_instance.add.return_value = True
|
|
mock_instance.notify.return_value = False
|
|
mock_apprise_class.return_value = mock_instance
|
|
|
|
service = NotificationService(outbound_allowed=True)
|
|
result = service.test_service("discord://webhook/token")
|
|
assert result["success"] is False
|
|
assert "Failed to send" in result["error"]
|
|
|
|
@patch("local_deep_research.notifications.service.apprise.Apprise")
|
|
@patch(
|
|
"local_deep_research.notifications.service.NotificationURLValidator.validate_service_url",
|
|
return_value=(True, None),
|
|
)
|
|
def test_success_path(self, mock_validator, mock_apprise_class):
|
|
mock_instance = MagicMock()
|
|
mock_instance.add.return_value = True
|
|
mock_instance.notify.return_value = True
|
|
mock_apprise_class.return_value = mock_instance
|
|
|
|
service = NotificationService(outbound_allowed=True)
|
|
result = service.test_service("discord://webhook/token")
|
|
assert result["success"] is True
|
|
assert "successfully" in result["message"]
|
|
|
|
@patch("local_deep_research.notifications.service.apprise.Apprise")
|
|
@patch(
|
|
"local_deep_research.notifications.service.NotificationURLValidator.validate_service_url",
|
|
return_value=(False, "Blocked unsafe protocol: javascript"),
|
|
)
|
|
def test_validation_reason_surfaced_without_internals(
|
|
self, mock_validator, mock_apprise_class
|
|
):
|
|
service = NotificationService(outbound_allowed=True)
|
|
result = service.test_service("javascript://evil.com")
|
|
assert result["success"] is False
|
|
# The validator's reason IS surfaced (it echoes only the
|
|
# user-supplied scheme). What must NEVER appear is server-internal
|
|
# detail: stack traces, exception class names, DNS resolver
|
|
# internals. The validator's error_msg never contains these, but
|
|
# pin the contract so a future regression that smuggles them in
|
|
# fails this test.
|
|
assert "Blocked unsafe protocol" in result["error"]
|
|
for forbidden in (
|
|
"Traceback",
|
|
"socket.gaierror",
|
|
"Exception",
|
|
"internal DNS",
|
|
"raise ",
|
|
):
|
|
assert forbidden not in result["error"]
|
|
|
|
|
|
class TestGetServiceTypePatterns:
|
|
"""Tests for get_service_type() across all SERVICE_PATTERNS."""
|
|
|
|
def test_email_detection(self):
|
|
service = NotificationService(outbound_allowed=True)
|
|
assert service.get_service_type("mailto://user@example.com") == "email"
|
|
|
|
def test_slack_detection(self):
|
|
service = NotificationService(outbound_allowed=True)
|
|
assert (
|
|
service.get_service_type("slack://token_a/token_b/token_c")
|
|
== "slack"
|
|
)
|
|
|
|
def test_telegram_detection(self):
|
|
service = NotificationService(outbound_allowed=True)
|
|
assert (
|
|
service.get_service_type("tgram://bottoken/chat_id") == "telegram"
|
|
)
|
|
|
|
def test_smtp_detection(self):
|
|
service = NotificationService(outbound_allowed=True)
|
|
assert service.get_service_type("smtp://user:pass@mail.com") == "smtp"
|
|
|
|
def test_smtps_detection(self):
|
|
service = NotificationService(outbound_allowed=True)
|
|
assert service.get_service_type("smtps://user:pass@mail.com") == "smtp"
|