Edit sent Discord webhooks if entry values updates
All checks were successful
Test and build Docker image / docker (push) Successful in 1m48s
All checks were successful
Test and build Docker image / docker (push) Successful in 1m48s
This commit is contained in:
parent
d85bc16904
commit
36d55566fc
13 changed files with 1313 additions and 141 deletions
|
|
@ -8,14 +8,27 @@ import warnings
|
|||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Any
|
||||
from typing import Protocol
|
||||
from typing import cast
|
||||
|
||||
from bs4 import MarkupResemblesLocatorWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class CachedReaderFactory(Protocol):
|
||||
"""Reader factory with lru_cache controls used by tests."""
|
||||
|
||||
def __call__(self) -> None:
|
||||
"""Create or return the cached reader."""
|
||||
|
||||
def cache_clear(self) -> None:
|
||||
"""Clear the cached reader."""
|
||||
|
||||
|
||||
def pytest_addoption(parser: pytest.Parser) -> None:
|
||||
"""Register custom command-line options for optional integration tests."""
|
||||
parser.addoption(
|
||||
|
|
@ -44,21 +57,23 @@ def pytest_sessionstart(session: pytest.Session) -> None:
|
|||
|
||||
# If modules were imported before this hook (unlikely), force them to use
|
||||
# the worker-specific location.
|
||||
settings_module: Any = sys.modules.get("discord_rss_bot.settings")
|
||||
settings_module: ModuleType | None = sys.modules.get("discord_rss_bot.settings")
|
||||
if settings_module is not None:
|
||||
settings_module.data_dir = str(worker_data_dir)
|
||||
get_reader: Any = getattr(settings_module, "get_reader", None)
|
||||
if get_reader is not None and hasattr(get_reader, "cache_clear"):
|
||||
get_reader_attr = getattr(settings_module, "get_reader", None)
|
||||
if get_reader_attr is not None and hasattr(get_reader_attr, "cache_clear"):
|
||||
get_reader = cast("CachedReaderFactory", get_reader_attr)
|
||||
get_reader.cache_clear()
|
||||
|
||||
main_module: Any = sys.modules.get("discord_rss_bot.main")
|
||||
main_module: ModuleType | None = sys.modules.get("discord_rss_bot.main")
|
||||
if main_module is not None and settings_module is not None:
|
||||
with suppress(Exception):
|
||||
current_reader = getattr(main_module, "reader", None)
|
||||
if current_reader is not None:
|
||||
current_reader.close()
|
||||
get_reader: Any = getattr(settings_module, "get_reader", None)
|
||||
if callable(get_reader):
|
||||
get_reader_attr = getattr(settings_module, "get_reader", None)
|
||||
if callable(get_reader_attr):
|
||||
get_reader = cast("CachedReaderFactory", get_reader_attr)
|
||||
get_reader()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ from __future__ import annotations
|
|||
import asyncio
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import UTC
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import LiteralString
|
||||
from unittest.mock import MagicMock
|
||||
|
|
@ -17,6 +19,7 @@ from reader import StorageError
|
|||
from reader import make_reader
|
||||
|
||||
from discord_rss_bot import feeds
|
||||
from discord_rss_bot.feeds import JsonObject
|
||||
from discord_rss_bot.feeds import capture_full_page_screenshot
|
||||
from discord_rss_bot.feeds import create_feed
|
||||
from discord_rss_bot.feeds import create_screenshot_webhook
|
||||
|
|
@ -354,6 +357,19 @@ def test_create_feed_inherits_global_text_delivery_mode() -> None:
|
|||
reader.set_tag.assert_any_call("https://example.com/feed.xml", "should_send_embed", False)
|
||||
|
||||
|
||||
def test_create_feed_enables_sent_webhook_tracking_by_default() -> None:
|
||||
reader = MagicMock()
|
||||
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||
"webhooks": [{"name": "Main", "url": "https://discord.com/api/webhooks/123/abc"}],
|
||||
"screenshot_layout": "desktop",
|
||||
"delivery_mode": "embed",
|
||||
}.get(key, default)
|
||||
|
||||
create_feed(reader, "https://example.com/feed.xml", "Main")
|
||||
|
||||
reader.set_tag.assert_any_call("https://example.com/feed.xml", feeds.SAVE_SENT_WEBHOOKS_TAG, True)
|
||||
|
||||
|
||||
def test_create_feed_falls_back_to_embed_when_global_delivery_mode_is_invalid() -> None:
|
||||
reader = MagicMock()
|
||||
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||
|
|
@ -882,3 +898,191 @@ def test_execute_webhook_logs_info_on_success(mock_logger: MagicMock) -> None:
|
|||
execute_webhook(webhook, entry, reader)
|
||||
|
||||
mock_logger.info.assert_called_once_with("Sent entry to Discord: %s", "entry-9")
|
||||
|
||||
|
||||
def test_execute_webhook_records_sent_webhook_message() -> None:
|
||||
webhook_url = "https://discord.com/api/webhooks/123/abc"
|
||||
state: dict[str, feeds.JsonValue] = {}
|
||||
|
||||
def get_tag(_resource: str | tuple[()], key: str, default: feeds.JsonValue = None) -> feeds.JsonValue:
|
||||
if key == feeds.SENT_WEBHOOKS_TAG:
|
||||
return state.get(feeds.SENT_WEBHOOKS_TAG, default)
|
||||
if key == feeds.SAVE_SENT_WEBHOOKS_TAG:
|
||||
return True
|
||||
if key == "webhook":
|
||||
return webhook_url
|
||||
if key == "delivery_mode":
|
||||
return "text"
|
||||
return default
|
||||
|
||||
def set_tag(_resource: str | tuple[()], key: str, value: feeds.JsonValue) -> None:
|
||||
state[key] = value
|
||||
|
||||
reader = MagicMock()
|
||||
reader.get_tag.side_effect = get_tag
|
||||
reader.set_tag.side_effect = set_tag
|
||||
|
||||
entry = MagicMock()
|
||||
entry.id = "entry-1"
|
||||
entry.title = "Entry title"
|
||||
entry.link = "https://example.com/entry-1"
|
||||
entry.updated = datetime(2026, 5, 8, tzinfo=UTC)
|
||||
entry.feed_url = "https://example.com/feed.xml"
|
||||
entry.feed.url = "https://example.com/feed.xml"
|
||||
entry.feed.title = "Example feed"
|
||||
entry.feed.updates_enabled = True
|
||||
|
||||
webhook = MagicMock()
|
||||
webhook.json = {"content": "Entry title", "embeds": [], "attachments": []}
|
||||
response = MagicMock()
|
||||
response.status_code = 200
|
||||
response.text = '{"id": "message-1"}'
|
||||
response.json.return_value = {"id": "message-1"}
|
||||
webhook.execute.return_value = response
|
||||
|
||||
execute_webhook(webhook, entry, reader)
|
||||
|
||||
records = state[feeds.SENT_WEBHOOKS_TAG]
|
||||
assert isinstance(records, list)
|
||||
assert len(records) == 1
|
||||
assert isinstance(records[0], dict)
|
||||
assert records[0]["feed_url"] == "https://example.com/feed.xml"
|
||||
assert records[0]["entry_id"] == "entry-1"
|
||||
assert records[0]["webhook_url"] == webhook_url
|
||||
assert records[0]["message_id"] == "message-1"
|
||||
assert records[0]["last_status_code"] == 200
|
||||
assert records[0]["discord_response"] == {"id": "message-1"}
|
||||
assert records[0]["response_text"] == '{"id": "message-1"}'
|
||||
|
||||
assert isinstance(records[0]["payload"], dict)
|
||||
assert records[0]["payload"]["content"] == "Entry title"
|
||||
|
||||
|
||||
def test_execute_webhook_does_not_record_when_feed_tracking_disabled() -> None:
|
||||
webhook_url = "https://discord.com/api/webhooks/123/abc"
|
||||
reader = MagicMock()
|
||||
reader.get_tag.side_effect = lambda _resource, key, default=None: {
|
||||
feeds.SAVE_SENT_WEBHOOKS_TAG: False,
|
||||
"webhook": webhook_url,
|
||||
}.get(key, default)
|
||||
|
||||
entry = MagicMock()
|
||||
entry.id = "entry-2"
|
||||
entry.feed_url = "https://example.com/feed.xml"
|
||||
entry.feed.url = "https://example.com/feed.xml"
|
||||
entry.feed.updates_enabled = True
|
||||
|
||||
webhook = MagicMock()
|
||||
webhook.json = {"content": "Entry title", "embeds": [], "attachments": []}
|
||||
response = MagicMock()
|
||||
response.status_code = 200
|
||||
response.text = '{"id": "message-2"}'
|
||||
response.json.return_value = {"id": "message-2"}
|
||||
webhook.execute.return_value = response
|
||||
|
||||
execute_webhook(webhook, entry, reader)
|
||||
|
||||
reader.set_tag.assert_not_called()
|
||||
|
||||
|
||||
@patch("discord_rss_bot.feeds.edit_sent_webhook_message")
|
||||
@patch("discord_rss_bot.feeds.create_webhook_for_entry")
|
||||
def test_update_sent_webhooks_for_modified_entries_edits_changed_payload(
|
||||
mock_create_webhook_for_entry: MagicMock,
|
||||
mock_edit_sent_webhook_message: MagicMock,
|
||||
) -> None:
|
||||
webhook_url = "https://discord.com/api/webhooks/123/abc"
|
||||
old_payload: JsonObject = {"content": "Old title", "embeds": [], "attachments": []}
|
||||
state: dict[str, feeds.JsonValue] = {
|
||||
feeds.SENT_WEBHOOKS_TAG: [
|
||||
{
|
||||
"feed_url": "https://example.com/feed.xml",
|
||||
"entry_id": "entry-3",
|
||||
"webhook_url": webhook_url,
|
||||
"message_id": "message-3",
|
||||
"payload": old_payload,
|
||||
"payload_hash": feeds.hash_webhook_payload(old_payload),
|
||||
"update_count": 0,
|
||||
}, # pyright: ignore[reportAssignmentType, reportArgumentType]
|
||||
],
|
||||
}
|
||||
|
||||
def get_tag(_resource: str | tuple[()], key: str, default: feeds.JsonValue = None) -> feeds.JsonValue:
|
||||
if key == feeds.SENT_WEBHOOKS_TAG:
|
||||
return state[feeds.SENT_WEBHOOKS_TAG]
|
||||
if key == feeds.SAVE_SENT_WEBHOOKS_TAG:
|
||||
return True
|
||||
return default
|
||||
|
||||
def set_tag(_resource: str | tuple[()], key: str, value: feeds.JsonValue) -> None:
|
||||
state[key] = value
|
||||
|
||||
entry = MagicMock()
|
||||
entry.id = "entry-3"
|
||||
entry.title = "New title"
|
||||
entry.link = "https://example.com/entry-3"
|
||||
entry.updated = datetime(2026, 5, 8, tzinfo=UTC)
|
||||
entry.feed.url = "https://example.com/feed.xml"
|
||||
entry.feed.title = "Example feed"
|
||||
|
||||
reader = MagicMock()
|
||||
reader.get_tag.side_effect = get_tag
|
||||
reader.set_tag.side_effect = set_tag
|
||||
reader.get_entry.return_value = entry
|
||||
|
||||
webhook = MagicMock()
|
||||
webhook.json = {"content": "New title", "embeds": [], "attachments": []}
|
||||
mock_create_webhook_for_entry.return_value = (webhook, "text")
|
||||
|
||||
response = MagicMock()
|
||||
response.status_code = 200
|
||||
response.text = '{"id": "message-3"}'
|
||||
response.json.return_value = {"id": "message-3"}
|
||||
mock_edit_sent_webhook_message.return_value = response
|
||||
|
||||
updated_count = feeds.update_sent_webhooks_for_modified_entries(
|
||||
reader,
|
||||
[("https://example.com/feed.xml", "entry-3")],
|
||||
)
|
||||
|
||||
assert updated_count == 1
|
||||
mock_edit_sent_webhook_message.assert_called_once()
|
||||
records = state[feeds.SENT_WEBHOOKS_TAG]
|
||||
assert isinstance(records, list)
|
||||
assert isinstance(records[0], dict)
|
||||
assert isinstance(records[0]["payload"], dict)
|
||||
assert records[0]["payload"]["content"] == "New title"
|
||||
assert records[0]["discord_response"] == {"id": "message-3"}
|
||||
assert records[0]["response_text"] == '{"id": "message-3"}'
|
||||
assert records[0]["update_count"] == 1
|
||||
assert not records[0]["last_error"]
|
||||
|
||||
|
||||
def test_update_feeds_and_collect_modified_entries_only_returns_modified_entries() -> None:
|
||||
class StubReader:
|
||||
def __init__(self) -> None:
|
||||
self.after_entry_update_hooks = []
|
||||
|
||||
def update_feeds(self, *, scheduled: bool, workers: int) -> None:
|
||||
assert scheduled is True
|
||||
assert workers == 1
|
||||
new_entry = MagicMock()
|
||||
new_entry.feed_url = "https://example.com/feed.xml"
|
||||
new_entry.id = "new"
|
||||
modified_entry = MagicMock()
|
||||
modified_entry.feed_url = "https://example.com/feed.xml"
|
||||
modified_entry.id = "modified"
|
||||
for hook in list(self.after_entry_update_hooks):
|
||||
hook(self, new_entry, feeds.EntryUpdateStatus.NEW)
|
||||
hook(self, modified_entry, feeds.EntryUpdateStatus.MODIFIED)
|
||||
|
||||
reader = StubReader()
|
||||
|
||||
modified_entries: list[tuple[str, str]] = feeds.update_feeds_and_collect_modified_entries(
|
||||
reader, # pyright: ignore[reportArgumentType]
|
||||
scheduled=True,
|
||||
workers=1,
|
||||
)
|
||||
|
||||
assert modified_entries == [("https://example.com/feed.xml", "modified")]
|
||||
assert reader.after_entry_update_hooks == []
|
||||
|
|
|
|||
|
|
@ -6,13 +6,15 @@ import shutil
|
|||
import subprocess # noqa: S404
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from discord_rss_bot.git_backup import JsonObject
|
||||
from discord_rss_bot.git_backup import JsonValue
|
||||
from discord_rss_bot.git_backup import commit_state_change
|
||||
from discord_rss_bot.git_backup import export_state
|
||||
from discord_rss_bot.git_backup import get_backup_path
|
||||
|
|
@ -172,7 +174,7 @@ def test_export_state_creates_state_json(tmp_path: Path) -> None:
|
|||
feed_or_key: tuple | str,
|
||||
tag: str | None = None,
|
||||
default: str | None = None,
|
||||
) -> list[Any] | str | None:
|
||||
) -> list[JsonValue] | str | None:
|
||||
if feed_or_key == () and tag is None:
|
||||
# Called for global webhooks list
|
||||
return []
|
||||
|
|
@ -191,7 +193,7 @@ def test_export_state_creates_state_json(tmp_path: Path) -> None:
|
|||
state_file: Path = backup_path / "state.json"
|
||||
assert state_file.exists(), "state.json should be created by export_state"
|
||||
|
||||
data: dict[str, Any] = json.loads(state_file.read_text(encoding="utf-8"))
|
||||
data = cast("JsonObject", json.loads(state_file.read_text(encoding="utf-8")))
|
||||
assert "feeds" in data
|
||||
assert "webhooks" in data
|
||||
assert data["feeds"][0]["url"] == "https://example.com/feed.rss"
|
||||
|
|
@ -209,7 +211,7 @@ def test_export_state_omits_empty_tags(tmp_path: Path) -> None:
|
|||
feed_or_key: tuple | str,
|
||||
tag: str | None = None,
|
||||
default: str | None = None,
|
||||
) -> list[Any] | str | None:
|
||||
) -> list[JsonValue] | str | None:
|
||||
if feed_or_key == ():
|
||||
return []
|
||||
|
||||
|
|
@ -222,7 +224,7 @@ def test_export_state_omits_empty_tags(tmp_path: Path) -> None:
|
|||
backup_path.mkdir()
|
||||
export_state(mock_reader, backup_path)
|
||||
|
||||
data: dict[str, Any] = json.loads((backup_path / "state.json").read_text())
|
||||
data = cast("JsonObject", json.loads((backup_path / "state.json").read_text()))
|
||||
|
||||
# Only "url" key should be present (no empty-value tags)
|
||||
assert list(data["feeds"][0].keys()) == ["url"]
|
||||
|
|
@ -570,7 +572,7 @@ def test_embed_backup_end_to_end(monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|||
# Verify state.json contains embed data
|
||||
state_file: Path = backup_path / "state.json"
|
||||
assert state_file.exists(), "state.json should exist in backup repo"
|
||||
state_data: dict[str, Any] = json.loads(state_file.read_text(encoding="utf-8"))
|
||||
state_data = cast("JsonObject", json.loads(state_file.read_text(encoding="utf-8")))
|
||||
|
||||
# Find our test feed in the state
|
||||
test_feed_data = next((feed for feed in state_data["feeds"] if feed["url"] == test_feed_url), None)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from unittest.mock import patch
|
|||
from fastapi.testclient import TestClient
|
||||
|
||||
import discord_rss_bot.main as main_module
|
||||
from discord_rss_bot import feeds
|
||||
from discord_rss_bot.main import app
|
||||
from discord_rss_bot.main import create_html_for_feed
|
||||
from discord_rss_bot.main import get_reader_dependency
|
||||
|
|
@ -31,6 +32,8 @@ client: TestClient = TestClient(app)
|
|||
webhook_name: str = "Hello, I am a webhook!"
|
||||
webhook_url: str = "https://discord.com/api/webhooks/1234567890/abcdefghijklmnopqrstuvwxyz"
|
||||
feed_url: str = "https://lovinator.space/rss_test.xml"
|
||||
type TestTagValue = str | bool | int | list[dict[str, str]] | feeds.JsonValue | None
|
||||
type TestKwargValue = str | int | None
|
||||
|
||||
|
||||
def encoded_feed_url(url: str) -> str:
|
||||
|
|
@ -348,14 +351,14 @@ def test_blacklist_preview_uses_50_entry_limit() -> None:
|
|||
def get_feed(self, _feed_url: str) -> DummyFeed:
|
||||
return self.feed
|
||||
|
||||
def get_entries(self, **kwargs: object) -> list[Entry]:
|
||||
def get_entries(self, **kwargs: TestKwargValue) -> list[Entry]:
|
||||
limit = kwargs.get("limit")
|
||||
self.recorded_limit = limit if isinstance(limit, int) else None
|
||||
if isinstance(limit, int):
|
||||
return self.entries[:limit]
|
||||
return self.entries
|
||||
|
||||
def get_tag(self, _resource: object, _key: str, default: object = None) -> object:
|
||||
def get_tag(self, _resource: str | DummyFeed, _key: str, default: TestTagValue = None) -> TestTagValue:
|
||||
return default
|
||||
|
||||
stub_reader = StubReader()
|
||||
|
|
@ -420,10 +423,10 @@ def test_blacklist_preview_shows_labeled_field_values_for_substring_match() -> N
|
|||
def get_feed(self, _feed_url: str) -> DummyFeed:
|
||||
return self.feed
|
||||
|
||||
def get_entries(self, **_kwargs: object) -> list[Entry]:
|
||||
def get_entries(self, **_kwargs: TestKwargValue) -> list[Entry]:
|
||||
return self.entries
|
||||
|
||||
def get_tag(self, _resource: object, _key: str, default: object = None) -> object:
|
||||
def get_tag(self, _resource: str | DummyFeed, _key: str, default: TestTagValue = None) -> TestTagValue:
|
||||
return default
|
||||
|
||||
stub_reader = StubReader()
|
||||
|
|
@ -528,6 +531,103 @@ def test_c3kay_feed_delivery_mode_toggle_routes_update_stored_tags() -> None:
|
|||
assert reader.get_tag(c3kay_feed_url, "should_send_embed") is True
|
||||
|
||||
|
||||
def test_set_feed_save_sent_webhooks_route_updates_stored_tag() -> None:
|
||||
@dataclass(slots=True)
|
||||
class DummyFeed:
|
||||
url: str
|
||||
title: str
|
||||
|
||||
class StubReader:
|
||||
def __init__(self) -> None:
|
||||
self.feed = DummyFeed(url="https://example.com/feed.xml", title="Example")
|
||||
self.tags: dict[tuple[str, str], bool] = {}
|
||||
|
||||
def get_feed(self, feed_url: str) -> DummyFeed:
|
||||
assert feed_url == self.feed.url
|
||||
return self.feed
|
||||
|
||||
def set_tag(self, resource: str, key: str, value: bool) -> None: # noqa: FBT001
|
||||
self.tags[resource, key] = value
|
||||
|
||||
stub_reader = StubReader()
|
||||
app.dependency_overrides[get_reader_dependency] = lambda: stub_reader
|
||||
|
||||
try:
|
||||
with patch("discord_rss_bot.main.commit_state_change"):
|
||||
response: Response = client.post(
|
||||
url="/set_feed_save_sent_webhooks",
|
||||
data={"feed_url": stub_reader.feed.url, "enabled": "false"},
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
assert response.status_code == 303, f"/set_feed_save_sent_webhooks failed: {response.text}"
|
||||
assert stub_reader.tags[stub_reader.feed.url, feeds.SAVE_SENT_WEBHOOKS_TAG] is False
|
||||
finally:
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_sent_webhooks_view_shows_saved_records() -> None:
|
||||
@dataclass(slots=True)
|
||||
class DummyFeed:
|
||||
url: str
|
||||
title: str
|
||||
|
||||
sent_webhook_url = "https://discord.com/api/webhooks/123/abc"
|
||||
sent_feed_url = "https://example.com/feed.xml"
|
||||
|
||||
class StubReader:
|
||||
def __init__(self) -> None:
|
||||
self.feed = DummyFeed(url=sent_feed_url, title="Example feed")
|
||||
|
||||
def get_tag(
|
||||
self,
|
||||
resource: str | tuple[()],
|
||||
key: str,
|
||||
default: feeds.JsonValue = None,
|
||||
) -> feeds.JsonValue:
|
||||
if resource == () and key == feeds.SENT_WEBHOOKS_TAG:
|
||||
return [
|
||||
{
|
||||
"feed_url": sent_feed_url,
|
||||
"feed_title": "Example feed",
|
||||
"entry_id": "entry-1",
|
||||
"entry_title": "Fixed typo",
|
||||
"entry_link": "https://example.com/entry-1",
|
||||
"webhook_url": sent_webhook_url,
|
||||
"message_id": "message-1",
|
||||
"delivery_mode": "text",
|
||||
"payload": {"content": "Fixed typo", "embeds": [], "attachments": []},
|
||||
"discord_response": {"id": "message-1", "channel_id": "channel-1"},
|
||||
"response_text": '{"id": "message-1", "channel_id": "channel-1"}',
|
||||
"last_updated_at": "2026-05-08T12:00:00+00:00",
|
||||
"last_status_code": 200,
|
||||
"update_count": 1,
|
||||
},
|
||||
]
|
||||
if resource == () and key == "webhooks":
|
||||
return [{"name": "Main", "url": sent_webhook_url}]
|
||||
return default
|
||||
|
||||
def get_feeds(self) -> list[DummyFeed]:
|
||||
return [self.feed]
|
||||
|
||||
app.dependency_overrides[get_reader_dependency] = StubReader
|
||||
|
||||
try:
|
||||
response: Response = client.get(url="/sent_webhooks")
|
||||
|
||||
assert response.status_code == 200, f"/sent_webhooks failed: {response.text}"
|
||||
assert "Fixed typo" in response.text
|
||||
assert "message-1" in response.text
|
||||
assert "channel-1" in response.text
|
||||
assert sent_webhook_url not in response.text
|
||||
assert "HTTP 200" in response.text
|
||||
assert "Example feed" in response.text
|
||||
assert "Main" in response.text
|
||||
finally:
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_set_global_screenshot_layout() -> None:
|
||||
response: Response = client.post(url="/set_global_screenshot_layout", data={"screenshot_layout": "mobile"})
|
||||
assert response.status_code == 200, f"Failed to set global screenshot layout: {response.text}"
|
||||
|
|
@ -964,6 +1064,35 @@ def test_update_feed_not_found() -> None:
|
|||
assert "Feed not found" in response.text
|
||||
|
||||
|
||||
def test_update_feed_updates_saved_webhooks_for_modified_entries() -> None:
|
||||
class StubReader:
|
||||
pass
|
||||
|
||||
stub_reader = StubReader()
|
||||
modified_entries = [("https://example.com/feed.xml", "entry-1")]
|
||||
app.dependency_overrides[get_reader_dependency] = lambda: stub_reader
|
||||
|
||||
try:
|
||||
with (
|
||||
patch(
|
||||
"discord_rss_bot.main.update_feed_and_collect_modified_entries",
|
||||
return_value=modified_entries,
|
||||
) as mock_update_feed,
|
||||
patch("discord_rss_bot.main.update_sent_webhooks_for_modified_entries") as mock_update_webhooks,
|
||||
):
|
||||
response: Response = client.get(
|
||||
url="/update",
|
||||
params={"feed_url": "https://example.com/feed.xml"},
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
assert response.status_code == 303, f"Expected redirect after update, got: {response.text}"
|
||||
mock_update_feed.assert_called_once_with(stub_reader, "https://example.com/feed.xml")
|
||||
mock_update_webhooks.assert_called_once_with(stub_reader, modified_entries)
|
||||
finally:
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_post_entry_send_to_discord() -> None:
|
||||
"""Test that /post_entry sends an entry to Discord and redirects to the feed page.
|
||||
|
||||
|
|
@ -1046,7 +1175,7 @@ def test_post_entry_uses_feed_url_to_disambiguate_duplicate_ids() -> None:
|
|||
|
||||
selected_feed_urls: list[str] = []
|
||||
|
||||
def fake_send_entry_to_discord(entry: Entry, reader: object) -> None:
|
||||
def fake_send_entry_to_discord(entry: Entry, reader: Reader) -> None:
|
||||
selected_feed_urls.append(entry.feed.url)
|
||||
|
||||
app.dependency_overrides[get_reader_dependency] = StubReader
|
||||
|
|
@ -1547,7 +1676,12 @@ def test_webhook_entries_sort_newest_and_non_null_published_first() -> None:
|
|||
]
|
||||
|
||||
class StubReader:
|
||||
def get_tag(self, resource: object, key: str, default: object = None) -> object:
|
||||
def get_tag(
|
||||
self,
|
||||
resource: str | tuple[()] | DummyFeed,
|
||||
key: str,
|
||||
default: TestTagValue = None,
|
||||
) -> TestTagValue:
|
||||
if resource == () and key == "webhooks":
|
||||
return [{"name": webhook_name, "url": webhook_url}]
|
||||
if key == "webhook" and isinstance(resource, str):
|
||||
|
|
@ -1557,12 +1691,12 @@ def test_webhook_entries_sort_newest_and_non_null_published_first() -> None:
|
|||
def get_feeds(self) -> list[DummyFeed]:
|
||||
return [dummy_feed]
|
||||
|
||||
def get_entries(self, **_kwargs: object) -> list[Entry]:
|
||||
def get_entries(self, **_kwargs: TestKwargValue) -> list[Entry]:
|
||||
return unsorted_entries
|
||||
|
||||
observed_order: list[str] = []
|
||||
|
||||
def capture_entries(*, reader: object, entries: list[Entry], current_feed_url: str = "") -> str:
|
||||
def capture_entries(*, reader: Reader, entries: list[Entry], current_feed_url: str = "") -> str:
|
||||
del reader, current_feed_url
|
||||
observed_order.extend(entry.id for entry in entries)
|
||||
return ""
|
||||
|
|
@ -1761,7 +1895,12 @@ def test_webhook_entries_mass_update_preview_shows_old_and_new_urls() -> None:
|
|||
DummyFeed(url="https://unchanged.example.com/rss/c.xml", title="C"),
|
||||
]
|
||||
|
||||
def get_tag(self, resource: object, key: str, default: object = None) -> object:
|
||||
def get_tag(
|
||||
self,
|
||||
resource: str | tuple[()] | DummyFeed,
|
||||
key: str,
|
||||
default: TestTagValue = None,
|
||||
) -> TestTagValue:
|
||||
if resource == () and key == "webhooks":
|
||||
return [{"name": webhook_name, "url": webhook_url}]
|
||||
if key == "webhook" and isinstance(resource, str):
|
||||
|
|
@ -1774,7 +1913,7 @@ def test_webhook_entries_mass_update_preview_shows_old_and_new_urls() -> None:
|
|||
def get_feeds(self) -> list[DummyFeed]:
|
||||
return self._feeds
|
||||
|
||||
def get_entries(self, **_kwargs: object) -> list[Entry]:
|
||||
def get_entries(self, **_kwargs: TestKwargValue) -> list[Entry]:
|
||||
return []
|
||||
|
||||
app.dependency_overrides[get_reader_dependency] = StubReader
|
||||
|
|
@ -1827,7 +1966,12 @@ def test_bulk_change_feed_urls_updates_matching_feeds() -> None:
|
|||
self.change_calls: list[tuple[str, str]] = []
|
||||
self.updated_feeds: list[str] = []
|
||||
|
||||
def get_tag(self, resource: object, key: str, default: object = None) -> object:
|
||||
def get_tag(
|
||||
self,
|
||||
resource: str | tuple[()] | DummyFeed,
|
||||
key: str,
|
||||
default: TestTagValue = None,
|
||||
) -> TestTagValue:
|
||||
if resource == () and key == "webhooks":
|
||||
return [{"name": webhook_name, "url": webhook_url}]
|
||||
if key == "webhook" and isinstance(resource, str):
|
||||
|
|
@ -1843,7 +1987,7 @@ def test_bulk_change_feed_urls_updates_matching_feeds() -> None:
|
|||
def update_feed(self, feed_url: str) -> None:
|
||||
self.updated_feeds.append(feed_url)
|
||||
|
||||
def get_entries(self, **_kwargs: object) -> list[Entry]:
|
||||
def get_entries(self, **_kwargs: TestKwargValue) -> list[Entry]:
|
||||
return []
|
||||
|
||||
def set_entry_read(self, _entry: Entry, _value: bool) -> None: # noqa: FBT001
|
||||
|
|
@ -1899,7 +2043,7 @@ def test_webhook_entries_mass_update_preview_fragment_endpoint() -> None:
|
|||
DummyFeed(url="https://old.example.com/rss/b.xml", title="B"),
|
||||
]
|
||||
|
||||
def get_tag(self, resource: object, key: str, default: object = None) -> object:
|
||||
def get_tag(self, resource: str | DummyFeed, key: str, default: TestTagValue = None) -> TestTagValue:
|
||||
if key == "webhook" and isinstance(resource, str):
|
||||
return webhook_url
|
||||
return default
|
||||
|
|
@ -1947,7 +2091,12 @@ def test_bulk_change_feed_urls_force_update_overwrites_conflict() -> None: # no
|
|||
self.delete_calls: list[str] = []
|
||||
self.change_calls: list[tuple[str, str]] = []
|
||||
|
||||
def get_tag(self, resource: object, key: str, default: object = None) -> object:
|
||||
def get_tag(
|
||||
self,
|
||||
resource: str | tuple[()] | DummyFeed,
|
||||
key: str,
|
||||
default: TestTagValue = None,
|
||||
) -> TestTagValue:
|
||||
if resource == () and key == "webhooks":
|
||||
return [{"name": webhook_name, "url": webhook_url}]
|
||||
if key == "webhook" and isinstance(resource, str):
|
||||
|
|
@ -1966,7 +2115,7 @@ def test_bulk_change_feed_urls_force_update_overwrites_conflict() -> None: # no
|
|||
def update_feed(self, _feed_url: str) -> None:
|
||||
return
|
||||
|
||||
def get_entries(self, **_kwargs: object) -> list[Entry]:
|
||||
def get_entries(self, **_kwargs: TestKwargValue) -> list[Entry]:
|
||||
return []
|
||||
|
||||
def set_entry_read(self, _entry: Entry, _value: bool) -> None: # noqa: FBT001
|
||||
|
|
@ -2019,7 +2168,12 @@ def test_bulk_change_feed_urls_force_update_ignores_resolution_error() -> None:
|
|||
]
|
||||
self.change_calls: list[tuple[str, str]] = []
|
||||
|
||||
def get_tag(self, resource: object, key: str, default: object = None) -> object:
|
||||
def get_tag(
|
||||
self,
|
||||
resource: str | tuple[()] | DummyFeed,
|
||||
key: str,
|
||||
default: TestTagValue = None,
|
||||
) -> TestTagValue:
|
||||
if resource == () and key == "webhooks":
|
||||
return [{"name": webhook_name, "url": webhook_url}]
|
||||
if key == "webhook" and isinstance(resource, str):
|
||||
|
|
@ -2035,7 +2189,7 @@ def test_bulk_change_feed_urls_force_update_ignores_resolution_error() -> None:
|
|||
def update_feed(self, _feed_url: str) -> None:
|
||||
return
|
||||
|
||||
def get_entries(self, **_kwargs: object) -> list[Entry]:
|
||||
def get_entries(self, **_kwargs: TestKwargValue) -> list[Entry]:
|
||||
return []
|
||||
|
||||
def set_entry_read(self, _entry: Entry, _value: bool) -> None: # noqa: FBT001
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue