Add more tests
Some checks failed
Test and build Docker image / docker (push) Failing after 1s

This commit is contained in:
Joakim Hellsén 2026-05-31 01:18:28 +02:00
commit a22601a854
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
4 changed files with 234 additions and 4 deletions

View file

@ -2,7 +2,9 @@ from __future__ import annotations
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from types import SimpleNamespace
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import cast
from reader import Entry from reader import Entry
from reader import Feed 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 entry_should_be_skipped
from discord_rss_bot.filter.blacklist import feed_has_blacklist_tags 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 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 from discord_rss_bot.filter.evaluator import get_filter_values_from_reader
if TYPE_CHECKING: 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 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: def test_should_be_skipped() -> None:
reader: Reader = get_reader() reader: Reader = get_reader()

View file

@ -188,6 +188,41 @@ def test_replace_tags_in_text_message_skips_non_string_replacement_values(
assert rendered == "{{entry_id}}" 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: def test_get_first_image_prefers_content_image_over_summary_image() -> None:
summary = '<p><img src="https://example.com/from-summary.jpg" /></p>' summary = '<p><img src="https://example.com/from-summary.jpg" /></p>'
content = '<p><img src="https://example.com/from-content.jpg" /></p>' content = '<p><img src="https://example.com/from-content.jpg" /></p>'

View file

@ -9,6 +9,7 @@ from pathlib import Path
from typing import LiteralString from typing import LiteralString
from typing import cast from typing import cast
from unittest.mock import MagicMock from unittest.mock import MagicMock
from unittest.mock import call
from unittest.mock import patch from unittest.mock import patch
import pytest 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 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.edit_sent_webhook_message")
@patch("discord_rss_bot.feeds.create_webhook_for_entry") @patch("discord_rss_bot.feeds.create_webhook_for_entry")
def test_update_sent_webhooks_for_modified_entries_edits_changed_payload( def test_update_sent_webhooks_for_modified_entries_edits_changed_payload(

View file

@ -7,6 +7,7 @@ from dataclasses import dataclass
from dataclasses import field from dataclasses import field
from datetime import UTC from datetime import UTC
from datetime import datetime from datetime import datetime
from types import SimpleNamespace
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import cast from typing import cast
from unittest.mock import MagicMock from unittest.mock import MagicMock
@ -421,8 +422,8 @@ def test_blacklist_preview_shows_labeled_field_values_for_substring_match() -> N
feed=self.feed, feed=self.feed,
title="World of Warcraft", title="World of Warcraft",
summary="<p>Massive MMO news update</p>", summary="<p>Massive MMO news update</p>",
author="Blizzard", author="Legacy Blizzard Author",
authors_str="Blizzard", authors_str="Blizzard Author One, Blizzard Author Two",
link="https://example.com/wow-1", link="https://example.com/wow-1",
published=datetime(2024, 1, 1, tzinfo=UTC), published=datetime(2024, 1, 1, tzinfo=UTC),
content=[DummyContent("<p>The expansion launches soon.</p>")], 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 '<mark class="filter-preview__match filter-preview__match--danger">orld</mark>' in response.text
assert "Massive MMO news update" in response.text assert "Massive MMO news update" in response.text
assert "The expansion launches soon." 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: finally:
app.dependency_overrides = {} 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: def test_settings_page_shows_screenshot_layout_setting() -> None:
response: Response = client.get(url="/settings") response: Response = client.get(url="/settings")
assert response.status_code == 200, f"/settings failed: {response.text}" 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 original_feed_url: str | None = None
link: str = "https://example.com/post" link: str = "https://example.com/post"
title: str = "Example title" title: str = "Example title"
author: str = "Author" author: str = "Legacy Author"
authors_str: str = "Author" authors_str: str = "Author One, Author Two"
summary: str = "Summary" summary: str = "Summary"
content: list[DummyContent] = field(default_factory=lambda: [DummyContent("Content")]) content: list[DummyContent] = field(default_factory=lambda: [DummyContent("Content")])
published: None = None 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-b.xml" in html
assert "From another feed: https://example.com/feed-a.xml" not 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: def test_webhook_entries_webhook_not_found() -> None: