mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-09-24 21:50:25 +01:00
## Backport bug fixes from `NemesisHubris/litfinder` Forwards a curated set of bug fixes from [NemesisHubris/litfinder](https://github.com/NemesisHubris/litfinder) — a community fork of this project — that address open issues here. All commits preserve original authorship via `git cherry-pick`; this PR is a backport rather than original work. Each fix has been reviewed locally, lint/format-cleaned to match this repo's existing ruff config, and verified with the test suite. Rebrand strings, license switches, and features have been deliberately excluded. ### Upstream issues addressed - **#999** — Mirror URLs with query params no longer break search requests (strip query string/fragment in `normalize_http_url`) - **#956** — Apprise notifications now respect the configured proxy (proxy env vars injected before dispatch) - **#1025** — rTorrent: separate `RTORRENT_AUDIOBOOK_LABEL` setting, falls back to book label if unset - **#1010** — Stop button in Activity no longer makes the panel disappear (snapshot refresh on cancel) - **#1021** — Anna's Archive slow-download countdown now caps retries instead of looping forever - **#1040** — Empty destination directory cleaned up when write probe fails - **PR #1031** — Language detection from Anna's Archive distant path when listing metadata is missing ### Additional fixes (no open issue but clear bugs) - **fix: Python 2 `except` syntax across 27 files** — `except X, Y:` is a SyntaxError in Python 3 and prevents affected modules from importing at runtime. Mechanical sweep to `except (X, Y):`. - **fix(abb): info hash validation with magnet fallback** — adds SHA-1/SHA-256 hex validation on extracted info hashes; falls back to scanning the full page for a magnet link (e.g. posted in comments) when the table value is malformed. Also extends the exact-phrase fallback to manual queries and defaults the ABB listing language to `en` when missing, preventing valid results from being hidden by the language filter. Includes a small test-fixture fix (`test(abb): use valid hex info hashes in scraper test fixtures`) since the existing fixtures used non-hex placeholders that the new validation correctly rejects. - **fix: Anna's Archive title parser** — handles nested edition spans and filters `lgli` catalog descriptor entries (e.g. "Book/Online Audio") that were polluting search results. ### Deliberately not included - LitFinder rebranding (UI strings, Apprise app ID, logo). The `fix: three upstream bugs` commit (#999/#956/#1025) was cherry-picked with Apprise app-id, description, and logo-URL strings reverted from "LitFinder" back to "Shelfmark"; noted in the commit body. - Features from the LitFinder fork (multi-variant title search, multi-book flat-folder grouping, fuzzy text matching, "Leave in Place" output handler, admin display name, custom-source plugin system). These are larger behavior changes that each warrant their own focused review — happy to send any of them separately if of interest. - LitFinder-specific test environment and CI infrastructure. ### Verification - Backend: **1879 passed**, 96 skipped (1 preexisting failure on `seleniumbase`-dependent test in local venv; runs fine in the standard Docker image with the `browser` extra) - Lint, format, dead-code: all clean against this repo's existing ruff/vulture config - One follow-up cleanup commit (`style: ruff lint and format fixes for ported commits`) brings the cherry-picked code into compliance with this repo's ruff settings — no behavior changes there ### Etiquette / credit Per-commit authorship preserved by cherry-pick. The only edits to the original commits are: - `fix: three upstream bugs` — Apprise rebrand strings reverted to "Shelfmark" (noted in commit body, original author retained as `Co-Authored-By` via cherry-pick) - One follow-up `style:` commit for ruff config alignment Big thanks to [@NemesisHubris](https://github.com/NemesisHubris) for the original work in LitFinder; this PR exists to make sure these fixes reach Shelfmark's wider user base. Happy to revise scope, split into smaller PRs, or split off the Py2 cleanup separately if that's preferable. --------- Co-authored-by: NemesisHubris <155838970+NemesisHubris@users.noreply.github.com> Co-authored-by: CaliBrain <calibrain@l4n.xyz>
669 lines
22 KiB
Python
669 lines
22 KiB
Python
"""Tests for core notification rendering and dispatch helpers."""
|
|
|
|
import logging
|
|
|
|
from shelfmark.core import notifications as notifications_module
|
|
|
|
|
|
class _FakeExecutor:
|
|
def __init__(self):
|
|
self.calls = []
|
|
|
|
def submit(self, fn, *args, **kwargs):
|
|
self.calls.append((fn, args, kwargs))
|
|
return object()
|
|
|
|
|
|
class _FakeNotifyType:
|
|
INFO = "INFO"
|
|
SUCCESS = "SUCCESS"
|
|
WARNING = "WARNING"
|
|
FAILURE = "FAILURE"
|
|
|
|
|
|
class _FakePlugin:
|
|
"""Fake Apprise plugin returned by instantiate()."""
|
|
|
|
def __init__(self, raw_url: str):
|
|
self._url = raw_url
|
|
self.app_id = "FakePlugin"
|
|
|
|
def url(self, privacy=False):
|
|
return self._url
|
|
|
|
|
|
class _FakeAppriseClient:
|
|
def __init__(self):
|
|
self.add_calls = []
|
|
self.instantiate_calls: list[dict[str, object | None]] = []
|
|
self.notify_calls = []
|
|
self.notify_result = True
|
|
self.notify_results_by_url: dict[str, bool] = {}
|
|
self.reject_urls: set[str] = set()
|
|
self.instantiate_exceptions_by_url: dict[str, Exception] = {}
|
|
self.notify_exceptions_by_url: dict[str, Exception] = {}
|
|
self.notify_warning_messages: list[str] = []
|
|
self.notify_info_messages: list[str] = []
|
|
self._active_url: str | None = None
|
|
|
|
def add(self, plugin):
|
|
url = getattr(plugin, "_url", str(plugin))
|
|
self.add_calls.append(url)
|
|
self._active_url = url
|
|
return True
|
|
|
|
def notify(self, **kwargs):
|
|
self.notify_calls.append(kwargs)
|
|
for message in self.notify_info_messages:
|
|
logging.getLogger("apprise.plugins.pushover").info(message)
|
|
for message in self.notify_warning_messages:
|
|
logging.getLogger("apprise.plugins.pushover").warning(message)
|
|
if self._active_url and self._active_url in self.notify_exceptions_by_url:
|
|
raise self.notify_exceptions_by_url[self._active_url]
|
|
if self._active_url and self._active_url in self.notify_results_by_url:
|
|
return self.notify_results_by_url[self._active_url]
|
|
return self.notify_result
|
|
|
|
|
|
class _FakeAppriseClass:
|
|
"""Fake for apprise.Apprise that acts as both constructor and has instantiate()."""
|
|
|
|
def __init__(self, module):
|
|
self._module = module
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
self._module.apprise_kwargs = kwargs
|
|
asset = kwargs.get("asset")
|
|
self._module.asset_kwargs = getattr(asset, "kwargs", None)
|
|
self._module.client.asset = asset
|
|
return self._module.client
|
|
|
|
def instantiate(self, url, asset=None, tag=None, suppress_exceptions=True):
|
|
_ = (tag, suppress_exceptions)
|
|
client = self._module.client
|
|
client.instantiate_calls.append(
|
|
{
|
|
"url": url,
|
|
"asset_kwargs": getattr(asset, "kwargs", None),
|
|
}
|
|
)
|
|
if url in client.instantiate_exceptions_by_url:
|
|
raise client.instantiate_exceptions_by_url[url]
|
|
if url in client.reject_urls:
|
|
return None
|
|
return _FakePlugin(url)
|
|
|
|
|
|
class _FakeAppriseModule:
|
|
NotifyType = _FakeNotifyType
|
|
asset_kwargs: dict[str, str] | None = None
|
|
|
|
def __init__(self):
|
|
self.client = _FakeAppriseClient()
|
|
self.apprise_kwargs = {}
|
|
self.Apprise = _FakeAppriseClass(self)
|
|
|
|
class AppriseAsset:
|
|
def __init__(self, **kwargs):
|
|
self.kwargs = kwargs
|
|
|
|
|
|
def test_render_message_includes_admin_note_for_rejection():
|
|
context = notifications_module.NotificationContext(
|
|
event=notifications_module.NotificationEvent.REQUEST_REJECTED,
|
|
title="Example Book",
|
|
author="Example Author",
|
|
admin_note="Missing metadata",
|
|
)
|
|
|
|
title, body = notifications_module._render_message(context)
|
|
|
|
assert title == "Request Rejected"
|
|
assert "Missing metadata" in body
|
|
|
|
|
|
def test_render_message_includes_error_line_for_download_failure():
|
|
context = notifications_module.NotificationContext(
|
|
event=notifications_module.NotificationEvent.DOWNLOAD_FAILED,
|
|
title="Example Book",
|
|
author="Example Author",
|
|
error_message="Connection timeout",
|
|
)
|
|
|
|
title, body = notifications_module._render_message(context)
|
|
|
|
assert title == "Download Failed"
|
|
assert "Connection timeout" in body
|
|
|
|
|
|
def test_render_message_uses_request_approved_copy():
|
|
context = notifications_module.NotificationContext(
|
|
event=notifications_module.NotificationEvent.REQUEST_FULFILLED,
|
|
title="Example Book",
|
|
author="Example Author",
|
|
)
|
|
|
|
title, body = notifications_module._render_message(context)
|
|
|
|
assert title == "Request Approved"
|
|
assert "was approved." in body
|
|
|
|
|
|
def test_notify_admin_submits_non_blocking_when_route_matches_event(monkeypatch):
|
|
fake_executor = _FakeExecutor()
|
|
monkeypatch.setattr(notifications_module, "_executor", fake_executor)
|
|
monkeypatch.setattr(
|
|
notifications_module,
|
|
"_resolve_admin_routes",
|
|
lambda: [{"event": "request_created", "url": "discord://Webhook/Token"}],
|
|
)
|
|
|
|
context = notifications_module.NotificationContext(
|
|
event=notifications_module.NotificationEvent.REQUEST_CREATED,
|
|
title="Example Book",
|
|
author="Example Author",
|
|
username="reader",
|
|
)
|
|
|
|
notifications_module.notify_admin(
|
|
notifications_module.NotificationEvent.REQUEST_CREATED,
|
|
context,
|
|
)
|
|
|
|
assert len(fake_executor.calls) == 1
|
|
|
|
|
|
def test_notify_admin_skips_when_no_route_matches_event(monkeypatch):
|
|
fake_executor = _FakeExecutor()
|
|
monkeypatch.setattr(notifications_module, "_executor", fake_executor)
|
|
monkeypatch.setattr(
|
|
notifications_module,
|
|
"_resolve_admin_routes",
|
|
lambda: [{"event": "download_failed", "url": "discord://Webhook/Token"}],
|
|
)
|
|
|
|
context = notifications_module.NotificationContext(
|
|
event=notifications_module.NotificationEvent.REQUEST_CREATED,
|
|
title="Example Book",
|
|
author="Example Author",
|
|
)
|
|
|
|
notifications_module.notify_admin(
|
|
notifications_module.NotificationEvent.REQUEST_CREATED,
|
|
context,
|
|
)
|
|
|
|
assert fake_executor.calls == []
|
|
|
|
|
|
def test_send_admin_event_passes_expected_title_body_and_notify_type(monkeypatch):
|
|
fake_apprise = _FakeAppriseModule()
|
|
monkeypatch.setattr(notifications_module, "apprise", fake_apprise)
|
|
|
|
context = notifications_module.NotificationContext(
|
|
event=notifications_module.NotificationEvent.REQUEST_REJECTED,
|
|
title="Example Book",
|
|
author="Example Author",
|
|
admin_note="Rule blocked this source",
|
|
)
|
|
|
|
result = notifications_module._send_admin_event(
|
|
notifications_module.NotificationEvent.REQUEST_REJECTED,
|
|
context,
|
|
["discord://Webhook/Token"],
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert fake_apprise.client.notify_calls
|
|
notify_kwargs = fake_apprise.client.notify_calls[0]
|
|
assert notify_kwargs["title"] == "Request Rejected"
|
|
assert "Rule blocked this source" in notify_kwargs["body"]
|
|
assert notify_kwargs["notify_type"] == _FakeNotifyType.WARNING
|
|
|
|
|
|
def test_dispatch_to_apprise_uses_shelfmark_asset_defaults(monkeypatch):
|
|
fake_apprise = _FakeAppriseModule()
|
|
monkeypatch.setattr(notifications_module, "apprise", fake_apprise)
|
|
|
|
result = notifications_module._dispatch_to_apprise(
|
|
["ntfys://ntfy.sh/shelfmark"],
|
|
title="Test",
|
|
body="Body",
|
|
notify_type=_FakeNotifyType.INFO,
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert fake_apprise.asset_kwargs is not None
|
|
assert fake_apprise.asset_kwargs["app_id"] == "Shelfmark"
|
|
assert "logo.png" in fake_apprise.asset_kwargs["image_url_logo"]
|
|
|
|
|
|
def test_dispatch_to_apprise_passes_shelfmark_asset_to_instantiate(monkeypatch):
|
|
fake_apprise = _FakeAppriseModule()
|
|
monkeypatch.setattr(notifications_module, "apprise", fake_apprise)
|
|
|
|
result = notifications_module._dispatch_to_apprise(
|
|
["ntfys://ntfy.sh/shelfmark"],
|
|
title="Test",
|
|
body="Body",
|
|
notify_type=_FakeNotifyType.INFO,
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert fake_apprise.client.instantiate_calls
|
|
instantiate_call = fake_apprise.client.instantiate_calls[0]
|
|
asset_kwargs = instantiate_call["asset_kwargs"]
|
|
assert isinstance(asset_kwargs, dict)
|
|
assert asset_kwargs["app_id"] == "Shelfmark"
|
|
assert "logo.png" in asset_kwargs["image_url_logo"]
|
|
|
|
|
|
def test_dispatch_to_apprise_logs_captured_apprise_info_messages(monkeypatch):
|
|
fake_apprise = _FakeAppriseModule()
|
|
fake_apprise.client.notify_info_messages = ["Sent Pushover notification to ALL_DEVICES."]
|
|
monkeypatch.setattr(notifications_module, "apprise", fake_apprise)
|
|
|
|
info_messages: list[str] = []
|
|
|
|
def _fake_info(message, *args, **kwargs):
|
|
_ = kwargs
|
|
info_messages.append(message % args if args else str(message))
|
|
|
|
monkeypatch.setattr(notifications_module.logger, "info", _fake_info)
|
|
|
|
result = notifications_module._dispatch_to_apprise(
|
|
["ntfys://ntfy.sh/shelfmark"],
|
|
title="Test",
|
|
body="Body",
|
|
notify_type=_FakeNotifyType.INFO,
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert any(
|
|
"Apprise source [apprise.plugins.pushover]: Sent Pushover notification to ALL_DEVICES."
|
|
in message
|
|
for message in info_messages
|
|
)
|
|
|
|
|
|
def test_dispatch_to_apprise_notify_false_returns_generic_failure_and_logs(monkeypatch):
|
|
fake_apprise = _FakeAppriseModule()
|
|
fake_apprise.client.notify_result = False
|
|
monkeypatch.setattr(notifications_module, "apprise", fake_apprise)
|
|
|
|
warning_messages: list[str] = []
|
|
|
|
def _fake_warning(message, *args, **kwargs):
|
|
_ = kwargs
|
|
warning_messages.append(message % args if args else str(message))
|
|
|
|
monkeypatch.setattr(notifications_module.logger, "warning", _fake_warning)
|
|
|
|
result = notifications_module._dispatch_to_apprise(
|
|
["pover://user_key@app_token"],
|
|
title="Test",
|
|
body="Body",
|
|
notify_type=_FakeNotifyType.INFO,
|
|
)
|
|
|
|
assert result["success"] is False
|
|
assert result["message"] == "Notification delivery failed"
|
|
assert result["details"] == ["pover: delivery failed"]
|
|
assert any("scheme(s): pover" in message for message in warning_messages)
|
|
|
|
|
|
def test_dispatch_to_apprise_partial_success_returns_success(monkeypatch):
|
|
fake_apprise = _FakeAppriseModule()
|
|
fake_apprise.client.notify_results_by_url = {
|
|
"gotifys://gotify.example/token": False,
|
|
"ntfys://ntfy.sh/shelfmark": True,
|
|
}
|
|
monkeypatch.setattr(notifications_module, "apprise", fake_apprise)
|
|
|
|
result = notifications_module._dispatch_to_apprise(
|
|
["gotifys://gotify.example/token", "ntfys://ntfy.sh/shelfmark"],
|
|
title="Test",
|
|
body="Body",
|
|
notify_type=_FakeNotifyType.INFO,
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert result["message"] == "Notification sent to 1 URL(s) (1 URL(s) failed)"
|
|
assert result["details"] == ["gotifys: delivery failed"]
|
|
|
|
|
|
def test_dispatch_to_apprise_logs_captured_apprise_warning_messages(monkeypatch):
|
|
fake_apprise = _FakeAppriseModule()
|
|
fake_apprise.client.notify_result = False
|
|
fake_apprise.client.notify_warning_messages = [
|
|
"Failed to send Pushover notification to ALL_DEVICES: Unauthorized - Invalid Token., error=401."
|
|
]
|
|
monkeypatch.setattr(notifications_module, "apprise", fake_apprise)
|
|
|
|
warning_messages: list[str] = []
|
|
|
|
def _fake_warning(message, *args, **kwargs):
|
|
_ = kwargs
|
|
warning_messages.append(message % args if args else str(message))
|
|
|
|
monkeypatch.setattr(notifications_module.logger, "warning", _fake_warning)
|
|
|
|
result = notifications_module._dispatch_to_apprise(
|
|
["pover://user_key@app_token"],
|
|
title="Test",
|
|
body="Body",
|
|
notify_type=_FakeNotifyType.INFO,
|
|
)
|
|
|
|
assert result["success"] is False
|
|
assert any(
|
|
"pover: apprise.plugins.pushover: Failed to send Pushover notification" in detail
|
|
for detail in result.get("details", [])
|
|
)
|
|
assert any(
|
|
"Apprise source [apprise.plugins.pushover]: Failed to send Pushover notification" in msg
|
|
for msg in warning_messages
|
|
)
|
|
|
|
|
|
def test_dispatch_to_apprise_logs_add_exception_at_debug_with_trace(monkeypatch):
|
|
fake_apprise = _FakeAppriseModule()
|
|
fake_apprise.client.instantiate_exceptions_by_url = {
|
|
"ntfys://ntfy.sh/shelfmark": RuntimeError("add exploded"),
|
|
}
|
|
monkeypatch.setattr(notifications_module, "apprise", fake_apprise)
|
|
|
|
debug_calls: list[tuple[str, tuple[object, ...], dict[str, object]]] = []
|
|
|
|
def _fake_debug(message, *args, **kwargs):
|
|
debug_calls.append((str(message), args, kwargs))
|
|
|
|
monkeypatch.setattr(notifications_module.logger, "debug", _fake_debug)
|
|
|
|
result = notifications_module._dispatch_to_apprise(
|
|
["ntfys://ntfy.sh/shelfmark"],
|
|
title="Test",
|
|
body="Body",
|
|
notify_type=_FakeNotifyType.INFO,
|
|
)
|
|
|
|
assert result["success"] is False
|
|
assert result["message"] == "No valid notification URLs configured"
|
|
assert result["details"] == ["ntfys: route registration failed (RuntimeError: add exploded)"]
|
|
assert any(
|
|
"Apprise route registration raised RuntimeError" in (message % args if args else message)
|
|
and kwargs.get("exc_info")
|
|
== (
|
|
RuntimeError,
|
|
fake_apprise.client.instantiate_exceptions_by_url["ntfys://ntfy.sh/shelfmark"],
|
|
fake_apprise.client.instantiate_exceptions_by_url[
|
|
"ntfys://ntfy.sh/shelfmark"
|
|
].__traceback__,
|
|
)
|
|
for message, args, kwargs in debug_calls
|
|
)
|
|
|
|
|
|
def test_dispatch_to_apprise_logs_notify_exception_at_debug_with_trace(monkeypatch):
|
|
fake_apprise = _FakeAppriseModule()
|
|
fake_apprise.client.notify_exceptions_by_url = {
|
|
"ntfys://ntfy.sh/shelfmark": RuntimeError("notify exploded"),
|
|
}
|
|
monkeypatch.setattr(notifications_module, "apprise", fake_apprise)
|
|
|
|
debug_calls: list[tuple[str, tuple[object, ...], dict[str, object]]] = []
|
|
|
|
def _fake_debug(message, *args, **kwargs):
|
|
debug_calls.append((str(message), args, kwargs))
|
|
|
|
monkeypatch.setattr(notifications_module.logger, "debug", _fake_debug)
|
|
|
|
result = notifications_module._dispatch_to_apprise(
|
|
["ntfys://ntfy.sh/shelfmark"],
|
|
title="Test",
|
|
body="Body",
|
|
notify_type=_FakeNotifyType.INFO,
|
|
)
|
|
|
|
assert result["success"] is False
|
|
assert result["message"] == "Notification delivery failed"
|
|
assert result["details"] == ["ntfys: notify raised RuntimeError: notify exploded"]
|
|
assert any(
|
|
"Apprise notify raised RuntimeError" in (message % args if args else message)
|
|
and kwargs.get("exc_info")
|
|
== (
|
|
RuntimeError,
|
|
fake_apprise.client.notify_exceptions_by_url["ntfys://ntfy.sh/shelfmark"],
|
|
fake_apprise.client.notify_exceptions_by_url["ntfys://ntfy.sh/shelfmark"].__traceback__,
|
|
)
|
|
for message, args, kwargs in debug_calls
|
|
)
|
|
|
|
|
|
def test_resolve_admin_routes_returns_empty_when_no_routes(monkeypatch):
|
|
def _fake_get(key, default=None):
|
|
if key == "ADMIN_NOTIFICATION_ROUTES":
|
|
return []
|
|
return default
|
|
|
|
monkeypatch.setattr(notifications_module.app_config, "get", _fake_get)
|
|
|
|
routes = notifications_module._resolve_admin_routes()
|
|
|
|
assert routes == []
|
|
|
|
|
|
def test_resolve_user_routes_uses_user_overrides(monkeypatch):
|
|
def _fake_get(key, default=None, user_id=None):
|
|
if user_id != 42:
|
|
return default
|
|
values = {
|
|
"USER_NOTIFICATION_ROUTES": [
|
|
{"event": "all", "url": " ntfys://ntfy.sh/alice "},
|
|
{"event": "download_failed", "url": "ntfys://ntfy.sh/errors"},
|
|
{"event": "download_failed", "url": "ntfys://ntfy.sh/errors"},
|
|
],
|
|
}
|
|
return values.get(key, default)
|
|
|
|
monkeypatch.setattr(notifications_module.app_config, "get", _fake_get)
|
|
|
|
routes = notifications_module._resolve_user_routes(42)
|
|
|
|
assert routes == [
|
|
{"event": "all", "url": "ntfys://ntfy.sh/alice"},
|
|
{"event": "download_failed", "url": "ntfys://ntfy.sh/errors"},
|
|
]
|
|
|
|
|
|
def test_notify_user_submits_non_blocking_when_route_matches_event(monkeypatch):
|
|
fake_executor = _FakeExecutor()
|
|
monkeypatch.setattr(notifications_module, "_executor", fake_executor)
|
|
monkeypatch.setattr(
|
|
notifications_module,
|
|
"_resolve_user_routes",
|
|
lambda _user_id: [{"event": "download_failed", "url": "discord://Webhook/Token"}],
|
|
)
|
|
|
|
context = notifications_module.NotificationContext(
|
|
event=notifications_module.NotificationEvent.DOWNLOAD_FAILED,
|
|
title="Example Book",
|
|
author="Example Author",
|
|
username="reader",
|
|
)
|
|
|
|
notifications_module.notify_user(
|
|
7,
|
|
notifications_module.NotificationEvent.DOWNLOAD_FAILED,
|
|
context,
|
|
)
|
|
|
|
assert len(fake_executor.calls) == 1
|
|
|
|
|
|
def test_notify_user_skips_when_user_id_is_invalid(monkeypatch):
|
|
fake_executor = _FakeExecutor()
|
|
monkeypatch.setattr(notifications_module, "_executor", fake_executor)
|
|
|
|
context = notifications_module.NotificationContext(
|
|
event=notifications_module.NotificationEvent.DOWNLOAD_COMPLETE,
|
|
title="Example Book",
|
|
author="Example Author",
|
|
)
|
|
|
|
notifications_module.notify_user(
|
|
None,
|
|
notifications_module.NotificationEvent.DOWNLOAD_COMPLETE,
|
|
context,
|
|
)
|
|
|
|
assert fake_executor.calls == []
|
|
|
|
|
|
def test_resolve_route_urls_for_event_includes_all_and_specific_rows():
|
|
routes = [
|
|
{"event": "all", "url": "ntfys://ntfy.sh/all"},
|
|
{"event": "download_failed", "url": "ntfys://ntfy.sh/errors"},
|
|
{"event": "request_created", "url": "ntfys://ntfy.sh/requests"},
|
|
]
|
|
|
|
urls = notifications_module._resolve_route_urls_for_event(
|
|
routes,
|
|
notifications_module.NotificationEvent.DOWNLOAD_FAILED,
|
|
)
|
|
|
|
assert urls == [
|
|
"ntfys://ntfy.sh/all",
|
|
"ntfys://ntfy.sh/errors",
|
|
]
|
|
|
|
|
|
def test_resolve_route_urls_for_event_deduplicates_matching_urls():
|
|
routes = [
|
|
{"event": "all", "url": "ntfys://ntfy.sh/shared"},
|
|
{"event": "download_failed", "url": "ntfys://ntfy.sh/shared"},
|
|
{"event": "download_failed", "url": "ntfys://ntfy.sh/errors"},
|
|
]
|
|
|
|
urls = notifications_module._resolve_route_urls_for_event(
|
|
routes,
|
|
notifications_module.NotificationEvent.DOWNLOAD_FAILED,
|
|
)
|
|
|
|
assert urls == [
|
|
"ntfys://ntfy.sh/shared",
|
|
"ntfys://ntfy.sh/errors",
|
|
]
|
|
|
|
|
|
def test_resolve_admin_routes_expands_multiselect_event_rows(monkeypatch):
|
|
def _fake_get(key, default=None):
|
|
if key == "ADMIN_NOTIFICATION_ROUTES":
|
|
return [
|
|
{"event": ["request_created", "download_failed"], "url": "ntfys://ntfy.sh/multi"},
|
|
{"event": ["all", "download_complete"], "url": "ntfys://ntfy.sh/all"},
|
|
]
|
|
return default
|
|
|
|
monkeypatch.setattr(notifications_module.app_config, "get", _fake_get)
|
|
|
|
routes = notifications_module._resolve_admin_routes()
|
|
|
|
assert routes == [
|
|
{"event": "request_created", "url": "ntfys://ntfy.sh/multi"},
|
|
{"event": "download_failed", "url": "ntfys://ntfy.sh/multi"},
|
|
{"event": "all", "url": "ntfys://ntfy.sh/all"},
|
|
]
|
|
|
|
|
|
def test_resolve_user_routes_expands_multiselect_event_rows(monkeypatch):
|
|
def _fake_get(key, default=None, user_id=None):
|
|
if key != "USER_NOTIFICATION_ROUTES" or user_id != 7:
|
|
return default
|
|
return [
|
|
{
|
|
"event": ["download_complete", "request_fulfilled"],
|
|
"url": "ntfys://ntfy.sh/user-main",
|
|
},
|
|
{"event": ["all", "download_failed"], "url": "ntfys://ntfy.sh/user-all"},
|
|
]
|
|
|
|
monkeypatch.setattr(notifications_module.app_config, "get", _fake_get)
|
|
|
|
routes = notifications_module._resolve_user_routes(7)
|
|
|
|
assert routes == [
|
|
{"event": "download_complete", "url": "ntfys://ntfy.sh/user-main"},
|
|
{"event": "request_fulfilled", "url": "ntfys://ntfy.sh/user-main"},
|
|
{"event": "all", "url": "ntfys://ntfy.sh/user-all"},
|
|
]
|
|
|
|
|
|
class TestAppriseProxyEnv:
|
|
"""Regression tests for issue #956 — proxy settings ignored for notifications."""
|
|
|
|
def _patch_config(self, monkeypatch, values):
|
|
from shelfmark.core import config as config_module
|
|
|
|
def _fake_get(key, default="", **_kwargs):
|
|
return values.get(key, default)
|
|
|
|
monkeypatch.setattr(config_module.config, "get", _fake_get)
|
|
|
|
def test_http_proxy_mode_injects_proxy_env(self, monkeypatch):
|
|
self._patch_config(
|
|
monkeypatch,
|
|
{
|
|
"PROXY_MODE": "http",
|
|
"HTTP_PROXY": "http://proxy.example.com:8080",
|
|
"HTTPS_PROXY": "",
|
|
"NO_PROXY": "",
|
|
},
|
|
)
|
|
monkeypatch.delenv("HTTP_PROXY", raising=False)
|
|
monkeypatch.delenv("HTTPS_PROXY", raising=False)
|
|
|
|
result = notifications_module._apprise_proxy_env()
|
|
|
|
assert result["HTTP_PROXY"] == "http://proxy.example.com:8080"
|
|
assert result["HTTPS_PROXY"] == "http://proxy.example.com:8080"
|
|
|
|
def test_socks5_proxy_mode_injects_socks_env(self, monkeypatch):
|
|
self._patch_config(
|
|
monkeypatch,
|
|
{
|
|
"PROXY_MODE": "socks5",
|
|
"SOCKS5_PROXY": "socks5://proxy.example.com:1080",
|
|
"NO_PROXY": "",
|
|
},
|
|
)
|
|
monkeypatch.delenv("HTTP_PROXY", raising=False)
|
|
monkeypatch.delenv("HTTPS_PROXY", raising=False)
|
|
|
|
result = notifications_module._apprise_proxy_env()
|
|
|
|
assert result["HTTP_PROXY"] == "socks5://proxy.example.com:1080"
|
|
assert result["HTTPS_PROXY"] == "socks5://proxy.example.com:1080"
|
|
|
|
def test_no_proxy_mode_returns_empty_dict(self, monkeypatch):
|
|
self._patch_config(monkeypatch, {"PROXY_MODE": ""})
|
|
|
|
result = notifications_module._apprise_proxy_env()
|
|
|
|
assert result == {}
|
|
|
|
def test_does_not_override_already_set_env_vars(self, monkeypatch):
|
|
self._patch_config(
|
|
monkeypatch,
|
|
{
|
|
"PROXY_MODE": "http",
|
|
"HTTP_PROXY": "http://new-proxy.example.com:8080",
|
|
"NO_PROXY": "",
|
|
},
|
|
)
|
|
monkeypatch.setenv("HTTP_PROXY", "http://existing-proxy.example.com:3128")
|
|
|
|
result = notifications_module._apprise_proxy_env()
|
|
|
|
assert "HTTP_PROXY" not in result
|