This commit is contained in:
parent
c481c7c88f
commit
a22601a854
4 changed files with 234 additions and 4 deletions
|
|
@ -2,7 +2,9 @@ from __future__ import annotations
|
|||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import cast
|
||||
|
||||
from reader import Entry
|
||||
from reader import Feed
|
||||
|
|
@ -12,6 +14,7 @@ from reader import make_reader
|
|||
from discord_rss_bot.filter.blacklist import entry_should_be_skipped
|
||||
from discord_rss_bot.filter.blacklist import feed_has_blacklist_tags
|
||||
from discord_rss_bot.filter.evaluator import evaluate_entry_filters
|
||||
from discord_rss_bot.filter.evaluator import get_entry_fields
|
||||
from discord_rss_bot.filter.evaluator import get_filter_values_from_reader
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -67,6 +70,23 @@ def check_if_has_tag(reader: Reader, feed: Feed, blacklist_name: str) -> None:
|
|||
assert feed_has_blacklist_tags(reader=reader, feed=feed) is False, asset_msg
|
||||
|
||||
|
||||
def test_get_entry_fields_uses_authors_str() -> None:
|
||||
entry = cast(
|
||||
"Entry",
|
||||
SimpleNamespace(
|
||||
title="Title",
|
||||
summary="Summary",
|
||||
content=[],
|
||||
author="Legacy Author",
|
||||
authors_str="Author One, Author Two",
|
||||
),
|
||||
)
|
||||
|
||||
fields: dict[str, str] = get_entry_fields(entry)
|
||||
|
||||
assert fields["author"] == "Author One, Author Two"
|
||||
|
||||
|
||||
def test_should_be_skipped() -> None:
|
||||
reader: Reader = get_reader()
|
||||
|
||||
|
|
|
|||
|
|
@ -188,6 +188,41 @@ def test_replace_tags_in_text_message_skips_non_string_replacement_values(
|
|||
assert rendered == "{{entry_id}}"
|
||||
|
||||
|
||||
@patch("discord_rss_bot.custom_message.get_custom_message")
|
||||
def test_replace_tags_in_text_message_uses_authors_str(mock_get_custom_message: MagicMock) -> None:
|
||||
mock_get_custom_message.return_value = "{{feed_author}} | {{entry_author}}"
|
||||
entry_ns: SimpleNamespace = make_entry("<p>Summary</p>")
|
||||
entry_ns.feed.author = "Legacy Feed Author"
|
||||
entry_ns.feed.authors_str = "Feed Author One, Feed Author Two"
|
||||
entry_ns.author = "Legacy Entry Author"
|
||||
entry_ns.authors_str = "Entry Author One, Entry Author Two"
|
||||
|
||||
rendered: str = replace_tags_in_text_message(
|
||||
typing.cast("Entry", entry_ns),
|
||||
reader=MagicMock(),
|
||||
)
|
||||
|
||||
assert rendered == "Feed Author One, Feed Author Two | Entry Author One, Entry Author Two"
|
||||
|
||||
|
||||
@patch("discord_rss_bot.custom_message.get_embed")
|
||||
def test_replace_tags_in_embed_uses_authors_str(mock_get_embed: MagicMock) -> None:
|
||||
mock_get_embed.return_value = CustomEmbed(description="{{feed_author}} | {{entry_author}}")
|
||||
entry_ns: SimpleNamespace = make_entry("<p>Summary</p>")
|
||||
entry_ns.feed.author = "Legacy Feed Author"
|
||||
entry_ns.feed.authors_str = "Feed Author One, Feed Author Two"
|
||||
entry_ns.author = "Legacy Entry Author"
|
||||
entry_ns.authors_str = "Entry Author One, Entry Author Two"
|
||||
|
||||
embed: CustomEmbed = replace_tags_in_embed(
|
||||
entry_ns.feed,
|
||||
typing.cast("Entry", entry_ns),
|
||||
reader=MagicMock(),
|
||||
)
|
||||
|
||||
assert embed.description == "Feed Author One, Feed Author Two | Entry Author One, Entry Author Two"
|
||||
|
||||
|
||||
def test_get_first_image_prefers_content_image_over_summary_image() -> None:
|
||||
summary = '<p><img src="https://example.com/from-summary.jpg" /></p>'
|
||||
content = '<p><img src="https://example.com/from-content.jpg" /></p>'
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from pathlib import Path
|
|||
from typing import LiteralString
|
||||
from typing import cast
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import call
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -1400,6 +1401,64 @@ def test_send_webhook_message_uploads_files_as_multipart(mock_request: MagicMock
|
|||
assert "json" not in mock_request.call_args.kwargs
|
||||
|
||||
|
||||
@patch("discord_rss_bot.feeds.time.sleep")
|
||||
@patch("discord_rss_bot.feeds.httpx2.request")
|
||||
def test_request_discord_webhook_retries_rate_limit_with_httpx2(
|
||||
mock_request: MagicMock,
|
||||
mock_sleep: MagicMock,
|
||||
) -> None:
|
||||
rate_limited_response = MagicMock(status_code=429, headers={})
|
||||
rate_limited_response.json.return_value = {"retry_after": 0.25}
|
||||
success_response = MagicMock(status_code=200)
|
||||
mock_request.side_effect = [rate_limited_response, success_response]
|
||||
payload: JsonObject = {"content": "Retry entry"}
|
||||
request_call = call(
|
||||
"POST",
|
||||
"https://discord.com/api/webhooks/123/abc",
|
||||
params={"wait": "true"},
|
||||
timeout=30.0,
|
||||
json=payload,
|
||||
)
|
||||
|
||||
result = feeds.request_discord_webhook(
|
||||
"POST",
|
||||
"https://discord.com/api/webhooks/123/abc",
|
||||
payload=payload,
|
||||
params={"wait": "true"},
|
||||
files=None,
|
||||
timeout=30.0,
|
||||
rate_limit_retry=True,
|
||||
)
|
||||
|
||||
assert result is success_response
|
||||
assert mock_request.call_args_list == [request_call, request_call]
|
||||
mock_sleep.assert_called_once_with(0.25)
|
||||
|
||||
|
||||
@patch("discord_rss_bot.feeds.httpx2.request")
|
||||
def test_edit_sent_webhook_message_patches_message_with_httpx2(mock_request: MagicMock) -> None:
|
||||
response = MagicMock(status_code=200, text='{"id": "message-3"}')
|
||||
mock_request.return_value = response
|
||||
payload: JsonObject = {"content": "Updated entry"}
|
||||
webhook = feeds.DiscordWebhook(url="https://discord.com/api/webhooks/123/abc")
|
||||
|
||||
result = feeds.edit_sent_webhook_message(
|
||||
"https://discord.com/api/webhooks/123/abc?thread_id=456",
|
||||
"message-3",
|
||||
webhook,
|
||||
payload,
|
||||
)
|
||||
|
||||
assert result is response
|
||||
mock_request.assert_called_once_with(
|
||||
"PATCH",
|
||||
"https://discord.com/api/webhooks/123/abc/messages/message-3",
|
||||
params={"thread_id": "456", "wait": "true"},
|
||||
timeout=30.0,
|
||||
json=payload,
|
||||
)
|
||||
|
||||
|
||||
@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(
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from dataclasses import dataclass
|
|||
from dataclasses import field
|
||||
from datetime import UTC
|
||||
from datetime import datetime
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import cast
|
||||
from unittest.mock import MagicMock
|
||||
|
|
@ -421,8 +422,8 @@ def test_blacklist_preview_shows_labeled_field_values_for_substring_match() -> N
|
|||
feed=self.feed,
|
||||
title="World of Warcraft",
|
||||
summary="<p>Massive MMO news update</p>",
|
||||
author="Blizzard",
|
||||
authors_str="Blizzard",
|
||||
author="Legacy Blizzard Author",
|
||||
authors_str="Blizzard Author One, Blizzard Author Two",
|
||||
link="https://example.com/wow-1",
|
||||
published=datetime(2024, 1, 1, tzinfo=UTC),
|
||||
content=[DummyContent("<p>The expansion launches soon.</p>")],
|
||||
|
|
@ -464,10 +465,90 @@ def test_blacklist_preview_shows_labeled_field_values_for_substring_match() -> N
|
|||
assert '<mark class="filter-preview__match filter-preview__match--danger">orld</mark>' in response.text
|
||||
assert "Massive MMO news update" in response.text
|
||||
assert "The expansion launches soon." in response.text
|
||||
assert "By Blizzard Author One, Blizzard Author Two |" in response.text
|
||||
assert "Legacy Blizzard Author" not in response.text
|
||||
finally:
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_author_templates_render_authors_str() -> None:
|
||||
request = SimpleNamespace(url="https://example.com/page", base_url="https://example.com/")
|
||||
feed = SimpleNamespace(
|
||||
title="Example Feed",
|
||||
url="https://example.com/feed.xml",
|
||||
author="Legacy Feed Author",
|
||||
authors_str="Feed Author One, Feed Author Two",
|
||||
added=None,
|
||||
last_exception=None,
|
||||
last_updated=None,
|
||||
link="https://example.com/feed",
|
||||
subtitle="",
|
||||
updated=None,
|
||||
updates_enabled=True,
|
||||
user_title="",
|
||||
version="atom10",
|
||||
)
|
||||
entry = SimpleNamespace(
|
||||
id="entry-1",
|
||||
title="Entry Title",
|
||||
link="https://example.com/entry-1",
|
||||
author="Legacy Entry Author",
|
||||
authors_str="Entry Author One, Entry Author Two",
|
||||
added=None,
|
||||
content=[],
|
||||
important=False,
|
||||
published=None,
|
||||
read=False,
|
||||
read_modified=None,
|
||||
summary="Summary",
|
||||
updated=None,
|
||||
)
|
||||
filter_row = SimpleNamespace(
|
||||
entry=entry,
|
||||
published_label="Never",
|
||||
status_class="success",
|
||||
status_label="Sent",
|
||||
decision=SimpleNamespace(reason="Sent", blacklist_match=None, whitelist_match=None),
|
||||
field_rows=[],
|
||||
first_image="",
|
||||
)
|
||||
preview_summary = SimpleNamespace(
|
||||
total=1,
|
||||
sent=1,
|
||||
skipped=0,
|
||||
blacklist_matches=0,
|
||||
whitelist_matches=0,
|
||||
)
|
||||
|
||||
custom_html: str = main_module.templates.get_template("custom.html").render(
|
||||
request=request,
|
||||
feed=feed,
|
||||
entry=entry,
|
||||
custom_message="",
|
||||
)
|
||||
embed_html: str = main_module.templates.get_template("embed.html").render(
|
||||
request=request,
|
||||
feed=feed,
|
||||
entry=entry,
|
||||
)
|
||||
filter_preview_html: str = main_module.templates.get_template("_filter_preview.html").render(
|
||||
feed=feed,
|
||||
preview_limit=50,
|
||||
preview_summary=preview_summary,
|
||||
preview_helper_text="",
|
||||
preview_rows=[filter_row],
|
||||
)
|
||||
|
||||
for html in (custom_html, embed_html):
|
||||
assert "Feed Author One, Feed Author Two" in html
|
||||
assert "Entry Author One, Entry Author Two" in html
|
||||
assert "Legacy Feed Author" not in html
|
||||
assert "Legacy Entry Author" not in html
|
||||
|
||||
assert "By Entry Author One, Entry Author Two |" in filter_preview_html
|
||||
assert "Legacy Entry Author" not in filter_preview_html
|
||||
|
||||
|
||||
def test_settings_page_shows_screenshot_layout_setting() -> None:
|
||||
response: Response = client.get(url="/settings")
|
||||
assert response.status_code == 200, f"/settings failed: {response.text}"
|
||||
|
|
@ -1464,8 +1545,8 @@ def test_create_html_marks_entries_from_another_feed(monkeypatch: pytest.MonkeyP
|
|||
original_feed_url: str | None = None
|
||||
link: str = "https://example.com/post"
|
||||
title: str = "Example title"
|
||||
author: str = "Author"
|
||||
authors_str: str = "Author"
|
||||
author: str = "Legacy Author"
|
||||
authors_str: str = "Author One, Author Two"
|
||||
summary: str = "Summary"
|
||||
content: list[DummyContent] = field(default_factory=lambda: [DummyContent("Content")])
|
||||
published: None = None
|
||||
|
|
@ -1504,6 +1585,41 @@ def test_create_html_marks_entries_from_another_feed(monkeypatch: pytest.MonkeyP
|
|||
|
||||
assert "From another feed: https://example.com/feed-b.xml" in html
|
||||
assert "From another feed: https://example.com/feed-a.xml" not in html
|
||||
assert "By Author One, Author Two @" in html
|
||||
assert "By Legacy Author @" not in html
|
||||
|
||||
|
||||
@patch("discord_rss_bot.main.httpx2.get")
|
||||
def test_get_data_from_hook_url_fetches_metadata_with_httpx2(mock_get: MagicMock) -> None:
|
||||
hook_url = "https://discord.com/api/webhooks/123/token"
|
||||
response = MagicMock(is_success=True)
|
||||
response.text = (
|
||||
'{"type": 1, "id": "123", "name": "Discord Hook", "avatar": "avatar", '
|
||||
'"channel_id": "456", "guild_id": "789", "token": "token"}'
|
||||
)
|
||||
mock_get.return_value = response
|
||||
main_module.get_data_from_hook_url.cache_clear()
|
||||
|
||||
hook_info = main_module.get_data_from_hook_url("Saved Hook", f" {hook_url} ")
|
||||
|
||||
mock_get.assert_called_once_with(hook_url, timeout=10.0)
|
||||
assert hook_info.custom_name == "Saved Hook"
|
||||
assert hook_info.name == "Discord Hook"
|
||||
assert hook_info.channel_id == "456"
|
||||
main_module.get_data_from_hook_url.cache_clear()
|
||||
|
||||
|
||||
@patch("discord_rss_bot.main.httpx2.get")
|
||||
def test_resolve_final_feed_url_follows_redirects_with_httpx2(mock_get: MagicMock) -> None:
|
||||
response = MagicMock(is_success=True)
|
||||
response.url = "https://example.com/final.xml"
|
||||
mock_get.return_value = response
|
||||
|
||||
resolved_url, error = main_module.resolve_final_feed_url(" https://example.com/original.xml ")
|
||||
|
||||
mock_get.assert_called_once_with("https://example.com/original.xml", follow_redirects=True, timeout=10.0)
|
||||
assert resolved_url == "https://example.com/final.xml"
|
||||
assert error is None
|
||||
|
||||
|
||||
def test_webhook_entries_webhook_not_found() -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue