Instead of embed or text mode, optionally send a full-page screenshot of the entry URL as a Discord file upload
All checks were successful
Test and build Docker image / docker (push) Successful in 1m26s
All checks were successful
Test and build Docker image / docker (push) Successful in 1m26s
This commit is contained in:
parent
c55610affa
commit
9ec0166e7f
14 changed files with 1571 additions and 241 deletions
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
|
@ -16,10 +17,16 @@ from reader import StorageError
|
|||
from reader import make_reader
|
||||
|
||||
from discord_rss_bot import feeds
|
||||
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
|
||||
from discord_rss_bot.feeds import execute_webhook
|
||||
from discord_rss_bot.feeds import extract_domain
|
||||
from discord_rss_bot.feeds import get_entry_delivery_mode
|
||||
from discord_rss_bot.feeds import get_screenshot_layout
|
||||
from discord_rss_bot.feeds import get_webhook_url
|
||||
from discord_rss_bot.feeds import is_youtube_feed
|
||||
from discord_rss_bot.feeds import screenshot_filename_for_entry
|
||||
from discord_rss_bot.feeds import send_discord_quest_notification
|
||||
from discord_rss_bot.feeds import send_entry_to_discord
|
||||
from discord_rss_bot.feeds import send_to_discord
|
||||
|
|
@ -153,6 +160,450 @@ def test_should_send_embed_check_normal_feeds(mock_logger: MagicMock) -> None:
|
|||
assert result is False, "Normal feeds should not use embeds when disabled"
|
||||
|
||||
|
||||
def test_get_entry_delivery_mode_prefers_delivery_mode_tag() -> None:
|
||||
reader = MagicMock()
|
||||
entry = MagicMock()
|
||||
entry.feed.url = "https://example.com/feed.xml"
|
||||
|
||||
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||
"delivery_mode": "screenshot",
|
||||
"should_send_embed": True,
|
||||
}.get(key, default)
|
||||
|
||||
result = get_entry_delivery_mode(reader, entry)
|
||||
|
||||
assert result == "screenshot"
|
||||
|
||||
|
||||
def test_get_entry_delivery_mode_falls_back_to_legacy_embed_flag() -> None:
|
||||
reader = MagicMock()
|
||||
entry = MagicMock()
|
||||
entry.feed.url = "https://example.com/feed.xml"
|
||||
|
||||
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||
"delivery_mode": "",
|
||||
"should_send_embed": False,
|
||||
}.get(key, default)
|
||||
|
||||
result = get_entry_delivery_mode(reader, entry)
|
||||
|
||||
assert result == "text"
|
||||
|
||||
|
||||
@patch("discord_rss_bot.feeds.execute_webhook")
|
||||
@patch("discord_rss_bot.feeds.create_text_webhook")
|
||||
@patch("discord_rss_bot.feeds.create_hoyolab_webhook")
|
||||
@patch("discord_rss_bot.feeds.fetch_hoyolab_post")
|
||||
def test_send_entry_to_discord_hoyolab_text_mode_uses_text_webhook(
|
||||
mock_fetch_hoyolab_post: MagicMock,
|
||||
mock_create_hoyolab_webhook: MagicMock,
|
||||
mock_create_text_webhook: MagicMock,
|
||||
mock_execute_webhook: MagicMock,
|
||||
) -> None:
|
||||
entry = MagicMock()
|
||||
entry.id = "entry-1"
|
||||
entry.feed.url = "https://feeds.c3kay.de/hoyolab.xml"
|
||||
entry.feed_url = "https://feeds.c3kay.de/hoyolab.xml"
|
||||
entry.link = "https://www.hoyolab.com/article/38588239"
|
||||
|
||||
reader = MagicMock()
|
||||
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||
"webhook": "https://discord.test/webhook",
|
||||
"delivery_mode": "text",
|
||||
}.get(key, default)
|
||||
|
||||
text_webhook = MagicMock()
|
||||
mock_create_text_webhook.return_value = text_webhook
|
||||
|
||||
result = send_entry_to_discord(entry, reader)
|
||||
|
||||
assert result is None
|
||||
mock_fetch_hoyolab_post.assert_not_called()
|
||||
mock_create_hoyolab_webhook.assert_not_called()
|
||||
mock_create_text_webhook.assert_called_once_with(
|
||||
"https://discord.test/webhook",
|
||||
entry,
|
||||
reader=reader,
|
||||
use_default_message_on_empty=False,
|
||||
)
|
||||
mock_execute_webhook.assert_called_once_with(text_webhook, entry, reader=reader)
|
||||
|
||||
|
||||
@patch("discord_rss_bot.feeds.execute_webhook")
|
||||
@patch("discord_rss_bot.feeds.create_screenshot_webhook")
|
||||
@patch("discord_rss_bot.feeds.create_hoyolab_webhook")
|
||||
@patch("discord_rss_bot.feeds.fetch_hoyolab_post")
|
||||
def test_send_entry_to_discord_hoyolab_screenshot_mode_uses_screenshot_webhook(
|
||||
mock_fetch_hoyolab_post: MagicMock,
|
||||
mock_create_hoyolab_webhook: MagicMock,
|
||||
mock_create_screenshot_webhook: MagicMock,
|
||||
mock_execute_webhook: MagicMock,
|
||||
) -> None:
|
||||
entry = MagicMock()
|
||||
entry.id = "entry-2"
|
||||
entry.feed.url = "https://feeds.c3kay.de/hoyolab.xml"
|
||||
entry.feed_url = "https://feeds.c3kay.de/hoyolab.xml"
|
||||
entry.link = "https://www.hoyolab.com/article/38588239"
|
||||
|
||||
reader = MagicMock()
|
||||
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||
"webhook": "https://discord.test/webhook",
|
||||
"delivery_mode": "screenshot",
|
||||
}.get(key, default)
|
||||
|
||||
screenshot_webhook = MagicMock()
|
||||
mock_create_screenshot_webhook.return_value = screenshot_webhook
|
||||
|
||||
result = send_entry_to_discord(entry, reader)
|
||||
|
||||
assert result is None
|
||||
mock_fetch_hoyolab_post.assert_not_called()
|
||||
mock_create_hoyolab_webhook.assert_not_called()
|
||||
mock_create_screenshot_webhook.assert_called_once_with(
|
||||
"https://discord.test/webhook",
|
||||
entry,
|
||||
reader=reader,
|
||||
)
|
||||
mock_execute_webhook.assert_called_once_with(screenshot_webhook, entry, reader=reader)
|
||||
|
||||
|
||||
@patch("discord_rss_bot.feeds.execute_webhook")
|
||||
@patch("discord_rss_bot.feeds.create_embed_webhook")
|
||||
@patch("discord_rss_bot.feeds.create_hoyolab_webhook")
|
||||
@patch("discord_rss_bot.feeds.fetch_hoyolab_post")
|
||||
def test_send_entry_to_discord_hoyolab_embed_mode_uses_hoyolab_webhook(
|
||||
mock_fetch_hoyolab_post: MagicMock,
|
||||
mock_create_hoyolab_webhook: MagicMock,
|
||||
mock_create_embed_webhook: MagicMock,
|
||||
mock_execute_webhook: MagicMock,
|
||||
) -> None:
|
||||
entry = MagicMock()
|
||||
entry.id = "entry-3"
|
||||
entry.feed.url = "https://feeds.c3kay.de/hoyolab.xml"
|
||||
entry.feed_url = "https://feeds.c3kay.de/hoyolab.xml"
|
||||
entry.link = "https://www.hoyolab.com/article/38588239"
|
||||
|
||||
reader = MagicMock()
|
||||
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||
"webhook": "https://discord.test/webhook",
|
||||
"delivery_mode": "embed",
|
||||
}.get(key, default)
|
||||
|
||||
mock_fetch_hoyolab_post.return_value = {"post": {"subject": "News"}}
|
||||
hoyolab_webhook = MagicMock()
|
||||
mock_create_hoyolab_webhook.return_value = hoyolab_webhook
|
||||
|
||||
result = send_entry_to_discord(entry, reader)
|
||||
|
||||
assert result is None
|
||||
mock_fetch_hoyolab_post.assert_called_once_with("38588239")
|
||||
mock_create_hoyolab_webhook.assert_called_once_with(
|
||||
"https://discord.test/webhook",
|
||||
entry,
|
||||
{"post": {"subject": "News"}},
|
||||
)
|
||||
mock_create_embed_webhook.assert_not_called()
|
||||
mock_execute_webhook.assert_called_once_with(hoyolab_webhook, entry, reader=reader)
|
||||
|
||||
|
||||
def test_get_screenshot_layout_prefers_mobile_tag() -> None:
|
||||
reader = MagicMock()
|
||||
feed = MagicMock()
|
||||
feed.url = "https://example.com/feed.xml"
|
||||
reader.get_tag.return_value = "mobile"
|
||||
|
||||
result = get_screenshot_layout(reader, feed)
|
||||
|
||||
assert result == "mobile"
|
||||
|
||||
|
||||
def test_get_screenshot_layout_defaults_to_desktop() -> None:
|
||||
reader = MagicMock()
|
||||
feed = MagicMock()
|
||||
feed.url = "https://example.com/feed.xml"
|
||||
reader.get_tag.return_value = "unknown"
|
||||
|
||||
result = get_screenshot_layout(reader, feed)
|
||||
|
||||
assert result == "desktop"
|
||||
|
||||
|
||||
def test_create_feed_inherits_global_screenshot_layout() -> 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": "mobile",
|
||||
}.get(key, default)
|
||||
|
||||
create_feed(reader, "https://example.com/feed.xml", "Main")
|
||||
|
||||
reader.set_tag.assert_any_call("https://example.com/feed.xml", "screenshot_layout", "mobile")
|
||||
|
||||
|
||||
def test_create_feed_inherits_global_text_delivery_mode() -> 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": "text",
|
||||
}.get(key, default)
|
||||
|
||||
create_feed(reader, "https://example.com/feed.xml", "Main")
|
||||
|
||||
reader.set_tag.assert_any_call("https://example.com/feed.xml", "delivery_mode", "text")
|
||||
reader.set_tag.assert_any_call("https://example.com/feed.xml", "should_send_embed", False)
|
||||
|
||||
|
||||
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
|
||||
"webhooks": [{"name": "Main", "url": "https://discord.com/api/webhooks/123/abc"}],
|
||||
"screenshot_layout": "desktop",
|
||||
"delivery_mode": "invalid",
|
||||
}.get(key, default)
|
||||
|
||||
create_feed(reader, "https://example.com/feed.xml", "Main")
|
||||
|
||||
reader.set_tag.assert_any_call("https://example.com/feed.xml", "delivery_mode", "embed")
|
||||
reader.set_tag.assert_any_call("https://example.com/feed.xml", "should_send_embed", True)
|
||||
|
||||
|
||||
@patch("discord_rss_bot.feeds.capture_full_page_screenshot")
|
||||
@patch("discord_rss_bot.feeds.DiscordWebhook")
|
||||
def test_create_screenshot_webhook_adds_image_file(
|
||||
mock_discord_webhook: MagicMock,
|
||||
mock_capture: MagicMock,
|
||||
) -> None:
|
||||
mock_capture.return_value = b"png-bytes"
|
||||
webhook = MagicMock()
|
||||
mock_discord_webhook.return_value = webhook
|
||||
|
||||
entry = MagicMock()
|
||||
entry.id = "entry-abc"
|
||||
entry.link = "https://example.com/article"
|
||||
reader = MagicMock()
|
||||
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||
"screenshot_layout": "mobile",
|
||||
}.get(key, default)
|
||||
|
||||
result = create_screenshot_webhook("https://discord.com/api/webhooks/123/abc", entry, reader)
|
||||
|
||||
assert result == webhook
|
||||
mock_discord_webhook.assert_called_once_with(
|
||||
url="https://discord.com/api/webhooks/123/abc",
|
||||
content="<https://example.com/article>",
|
||||
rate_limit_retry=True,
|
||||
)
|
||||
mock_capture.assert_called_once_with(
|
||||
"https://example.com/article",
|
||||
screenshot_layout="mobile",
|
||||
screenshot_type="png",
|
||||
)
|
||||
webhook.add_file.assert_called_once()
|
||||
|
||||
|
||||
@patch("discord_rss_bot.feeds.capture_full_page_screenshot")
|
||||
@patch("discord_rss_bot.feeds.DiscordWebhook")
|
||||
def test_create_screenshot_webhook_retries_jpeg_when_png_too_large(
|
||||
mock_discord_webhook: MagicMock,
|
||||
mock_capture: MagicMock,
|
||||
) -> None:
|
||||
oversized_png = b"x" * (8 * 1024 * 1024 + 1024)
|
||||
compressed_jpeg = b"y" * (7 * 1024 * 1024)
|
||||
mock_capture.side_effect = [oversized_png, compressed_jpeg]
|
||||
|
||||
webhook = MagicMock()
|
||||
mock_discord_webhook.return_value = webhook
|
||||
|
||||
entry = MagicMock()
|
||||
entry.id = "entry-large"
|
||||
entry.link = "https://example.com/large-article"
|
||||
reader = MagicMock()
|
||||
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||
"screenshot_layout": "desktop",
|
||||
}.get(key, default)
|
||||
|
||||
result = create_screenshot_webhook("https://discord.com/api/webhooks/123/abc", entry, reader)
|
||||
|
||||
assert result == webhook
|
||||
assert mock_capture.call_count == 2
|
||||
assert mock_capture.call_args_list[0].kwargs == {
|
||||
"screenshot_layout": "desktop",
|
||||
"screenshot_type": "png",
|
||||
}
|
||||
assert mock_capture.call_args_list[1].kwargs == {
|
||||
"screenshot_layout": "desktop",
|
||||
"screenshot_type": "jpeg",
|
||||
"jpeg_quality": 85,
|
||||
}
|
||||
webhook.add_file.assert_called_once()
|
||||
|
||||
|
||||
@patch("discord_rss_bot.feeds.create_text_webhook")
|
||||
@patch("discord_rss_bot.feeds.capture_full_page_screenshot")
|
||||
def test_create_screenshot_webhook_falls_back_when_all_formats_too_large(
|
||||
mock_capture: MagicMock,
|
||||
mock_create_text_webhook: MagicMock,
|
||||
) -> None:
|
||||
oversized_bytes = b"z" * (9 * 1024 * 1024)
|
||||
# 1 PNG attempt + 4 JPEG quality attempts
|
||||
mock_capture.side_effect = [oversized_bytes, oversized_bytes, oversized_bytes, oversized_bytes, oversized_bytes]
|
||||
fallback_webhook = MagicMock()
|
||||
mock_create_text_webhook.return_value = fallback_webhook
|
||||
|
||||
entry = MagicMock()
|
||||
entry.id = "entry-too-large"
|
||||
entry.link = "https://example.com/very-large"
|
||||
reader = MagicMock()
|
||||
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||
"screenshot_layout": "desktop",
|
||||
}.get(key, default)
|
||||
|
||||
result = create_screenshot_webhook("https://discord.com/api/webhooks/123/abc", entry, reader)
|
||||
|
||||
assert result == fallback_webhook
|
||||
assert mock_capture.call_count == 5
|
||||
mock_create_text_webhook.assert_called_once_with(
|
||||
"https://discord.com/api/webhooks/123/abc",
|
||||
entry,
|
||||
reader=reader,
|
||||
use_default_message_on_empty=True,
|
||||
)
|
||||
|
||||
|
||||
@patch("discord_rss_bot.feeds.capture_full_page_screenshot")
|
||||
@patch("discord_rss_bot.feeds.create_text_webhook")
|
||||
def test_create_screenshot_webhook_falls_back_when_entry_has_no_link(
|
||||
mock_create_text_webhook: MagicMock,
|
||||
mock_capture: MagicMock,
|
||||
) -> None:
|
||||
entry = MagicMock()
|
||||
entry.id = "entry-no-link"
|
||||
entry.link = None
|
||||
reader = MagicMock()
|
||||
fallback_webhook = MagicMock()
|
||||
mock_create_text_webhook.return_value = fallback_webhook
|
||||
|
||||
result = create_screenshot_webhook("https://discord.com/api/webhooks/123/abc", entry, reader)
|
||||
|
||||
assert result == fallback_webhook
|
||||
mock_capture.assert_not_called()
|
||||
mock_create_text_webhook.assert_called_once_with(
|
||||
"https://discord.com/api/webhooks/123/abc",
|
||||
entry,
|
||||
reader=reader,
|
||||
use_default_message_on_empty=True,
|
||||
)
|
||||
|
||||
|
||||
def test_screenshot_filename_for_entry_custom_extension() -> None:
|
||||
entry = MagicMock()
|
||||
entry.id = "hello/world?id=123"
|
||||
|
||||
filename = screenshot_filename_for_entry(entry, extension="JPG")
|
||||
|
||||
assert filename.endswith(".jpg")
|
||||
assert "/" not in filename
|
||||
assert "?" not in filename
|
||||
|
||||
|
||||
@patch("discord_rss_bot.feeds._capture_full_page_screenshot_sync", return_value=b"jpeg-bytes")
|
||||
def test_capture_full_page_screenshot_forwards_jpeg_options(mock_capture_sync: MagicMock) -> None:
|
||||
result = capture_full_page_screenshot(
|
||||
"https://example.com/article",
|
||||
screenshot_layout="mobile",
|
||||
screenshot_type="jpeg",
|
||||
jpeg_quality=55,
|
||||
)
|
||||
|
||||
assert result == b"jpeg-bytes"
|
||||
mock_capture_sync.assert_called_once_with(
|
||||
"https://example.com/article",
|
||||
screenshot_layout="mobile",
|
||||
screenshot_type="jpeg",
|
||||
jpeg_quality=55,
|
||||
)
|
||||
|
||||
|
||||
@patch("discord_rss_bot.feeds.create_text_webhook")
|
||||
@patch("discord_rss_bot.feeds.capture_full_page_screenshot")
|
||||
def test_create_screenshot_webhook_falls_back_to_text_on_failure(
|
||||
mock_capture: MagicMock,
|
||||
mock_create_text_webhook: MagicMock,
|
||||
) -> None:
|
||||
mock_capture.return_value = None
|
||||
fallback_webhook = MagicMock()
|
||||
mock_create_text_webhook.return_value = fallback_webhook
|
||||
|
||||
entry = MagicMock()
|
||||
entry.id = "entry-def"
|
||||
entry.link = "https://example.com/article"
|
||||
reader = MagicMock()
|
||||
|
||||
result = create_screenshot_webhook("https://discord.com/api/webhooks/123/abc", entry, reader)
|
||||
|
||||
assert result == fallback_webhook
|
||||
mock_create_text_webhook.assert_called_once_with(
|
||||
"https://discord.com/api/webhooks/123/abc",
|
||||
entry,
|
||||
reader=reader,
|
||||
use_default_message_on_empty=True,
|
||||
)
|
||||
|
||||
|
||||
def test_capture_full_page_screenshot_uses_thread_when_loop_running() -> None:
|
||||
"""Capture should offload sync Playwright work when called from an active event loop."""
|
||||
with patch("discord_rss_bot.feeds._capture_full_page_screenshot_sync", return_value=b"png") as mock_capture_sync:
|
||||
|
||||
async def run_capture() -> bytes | None:
|
||||
return feeds.capture_full_page_screenshot(
|
||||
"https://example.com/article",
|
||||
screenshot_layout="desktop",
|
||||
screenshot_type="png",
|
||||
)
|
||||
|
||||
result = asyncio.run(run_capture())
|
||||
|
||||
assert result == b"png"
|
||||
mock_capture_sync.assert_called_once_with(
|
||||
"https://example.com/article",
|
||||
screenshot_layout="desktop",
|
||||
screenshot_type="png",
|
||||
jpeg_quality=85,
|
||||
)
|
||||
|
||||
|
||||
@patch("discord_rss_bot.feeds.get_entry_delivery_mode")
|
||||
@patch("discord_rss_bot.feeds.create_screenshot_webhook")
|
||||
@patch("discord_rss_bot.feeds.execute_webhook")
|
||||
def test_send_entry_to_discord_uses_screenshot_mode(
|
||||
mock_execute_webhook: MagicMock,
|
||||
mock_create_screenshot_webhook: MagicMock,
|
||||
mock_get_entry_delivery_mode: MagicMock,
|
||||
) -> None:
|
||||
reader = MagicMock()
|
||||
entry = MagicMock()
|
||||
entry.feed.url = "https://example.com/feed.xml"
|
||||
entry.feed_url = "https://example.com/feed.xml"
|
||||
|
||||
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||
"webhook": "https://discord.com/api/webhooks/123/abc",
|
||||
}.get(key, default)
|
||||
|
||||
mock_get_entry_delivery_mode.return_value = "screenshot"
|
||||
screenshot_webhook = MagicMock()
|
||||
mock_create_screenshot_webhook.return_value = screenshot_webhook
|
||||
|
||||
send_entry_to_discord(entry, reader)
|
||||
|
||||
mock_create_screenshot_webhook.assert_called_once_with(
|
||||
"https://discord.com/api/webhooks/123/abc",
|
||||
entry,
|
||||
reader=reader,
|
||||
)
|
||||
mock_execute_webhook.assert_called_once_with(screenshot_webhook, entry, reader=reader)
|
||||
|
||||
|
||||
@patch("discord_rss_bot.feeds.get_reader")
|
||||
@patch("discord_rss_bot.feeds.get_custom_message")
|
||||
@patch("discord_rss_bot.feeds.replace_tags_in_text_message")
|
||||
|
|
|
|||
|
|
@ -436,6 +436,77 @@ def test_post_use_text_triggers_backup(monkeypatch: pytest.MonkeyPatch, tmp_path
|
|||
assert test_feed_url in commit_message
|
||||
|
||||
|
||||
def test_post_use_screenshot_triggers_backup(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
"""Posting to /use_screenshot should trigger a git backup."""
|
||||
backup_path: Path = tmp_path / "backup"
|
||||
monkeypatch.setenv("GIT_BACKUP_PATH", str(backup_path))
|
||||
monkeypatch.delenv("GIT_BACKUP_REMOTE", raising=False)
|
||||
|
||||
with patch("discord_rss_bot.main.commit_state_change") as mock_commit:
|
||||
response = client.post(url="/use_screenshot", data={"feed_url": test_feed_url})
|
||||
assert response.status_code == 200, f"Failed to enable screenshot mode: {response.text}"
|
||||
mock_commit.assert_called_once()
|
||||
|
||||
call_args = mock_commit.call_args
|
||||
assert call_args is not None
|
||||
commit_message: str = call_args[0][1]
|
||||
assert "Enable screenshot mode" in commit_message
|
||||
assert test_feed_url in commit_message
|
||||
|
||||
|
||||
def test_post_use_screenshot_mobile_triggers_backup(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
"""Posting to /use_screenshot_mobile should trigger a git backup."""
|
||||
backup_path: Path = tmp_path / "backup"
|
||||
monkeypatch.setenv("GIT_BACKUP_PATH", str(backup_path))
|
||||
monkeypatch.delenv("GIT_BACKUP_REMOTE", raising=False)
|
||||
|
||||
with patch("discord_rss_bot.main.commit_state_change") as mock_commit:
|
||||
response = client.post(url="/use_screenshot_mobile", data={"feed_url": test_feed_url})
|
||||
assert response.status_code == 200, f"Failed to enable screenshot mobile layout: {response.text}"
|
||||
mock_commit.assert_called_once()
|
||||
|
||||
call_args = mock_commit.call_args
|
||||
assert call_args is not None
|
||||
commit_message: str = call_args[0][1]
|
||||
assert "Enable screenshot mobile layout" in commit_message
|
||||
assert test_feed_url in commit_message
|
||||
|
||||
|
||||
def test_post_use_screenshot_desktop_triggers_backup(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
"""Posting to /use_screenshot_desktop should trigger a git backup."""
|
||||
backup_path: Path = tmp_path / "backup"
|
||||
monkeypatch.setenv("GIT_BACKUP_PATH", str(backup_path))
|
||||
monkeypatch.delenv("GIT_BACKUP_REMOTE", raising=False)
|
||||
|
||||
with patch("discord_rss_bot.main.commit_state_change") as mock_commit:
|
||||
response = client.post(url="/use_screenshot_desktop", data={"feed_url": test_feed_url})
|
||||
assert response.status_code == 200, f"Failed to enable screenshot desktop layout: {response.text}"
|
||||
mock_commit.assert_called_once()
|
||||
|
||||
call_args = mock_commit.call_args
|
||||
assert call_args is not None
|
||||
commit_message: str = call_args[0][1]
|
||||
assert "Enable screenshot desktop layout" in commit_message
|
||||
assert test_feed_url in commit_message
|
||||
|
||||
|
||||
def test_post_set_global_screenshot_layout_triggers_backup(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
"""Posting to /set_global_screenshot_layout should trigger a git backup."""
|
||||
backup_path: Path = tmp_path / "backup"
|
||||
monkeypatch.setenv("GIT_BACKUP_PATH", str(backup_path))
|
||||
monkeypatch.delenv("GIT_BACKUP_REMOTE", raising=False)
|
||||
|
||||
with patch("discord_rss_bot.main.commit_state_change") as mock_commit:
|
||||
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}"
|
||||
mock_commit.assert_called_once()
|
||||
|
||||
call_args = mock_commit.call_args
|
||||
assert call_args is not None
|
||||
commit_message: str = call_args[0][1]
|
||||
assert "Set global screenshot layout to mobile" in commit_message
|
||||
|
||||
|
||||
def test_post_custom_message_triggers_backup(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
"""Posting to /custom should trigger a git backup."""
|
||||
backup_path: Path = tmp_path / "backup"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import re
|
||||
import urllib.parse
|
||||
from dataclasses import dataclass
|
||||
|
|
@ -169,6 +170,66 @@ def test_get() -> None:
|
|||
assert response.status_code == 200, f"/whitelist failed: {response.text}"
|
||||
|
||||
|
||||
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}"
|
||||
assert "Default delivery mode for new feeds" in response.text
|
||||
assert "Default screenshot layout for new feeds" in response.text
|
||||
assert "uv run playwright install chromium" in response.text
|
||||
|
||||
|
||||
def test_set_global_delivery_mode() -> None:
|
||||
response: Response = client.post(url="/set_global_delivery_mode", data={"delivery_mode": "text"})
|
||||
assert response.status_code == 200, f"Failed to set global delivery mode: {response.text}"
|
||||
|
||||
response = client.get(url="/settings")
|
||||
assert response.status_code == 200, f"/settings failed after setting delivery mode: {response.text}"
|
||||
assert re.search(r"<option\s+value=\"text\"[^>]*\bselected\b", response.text)
|
||||
|
||||
|
||||
def test_add_page_shows_global_default_delivery_mode_hint() -> None:
|
||||
response: Response = client.post(url="/set_global_delivery_mode", data={"delivery_mode": "text"})
|
||||
assert response.status_code == 200, f"Failed to set global delivery mode: {response.text}"
|
||||
|
||||
response = client.get(url="/add")
|
||||
assert response.status_code == 200, f"/add failed: {response.text}"
|
||||
assert "New feeds currently default to" in response.text
|
||||
assert "text" in response.text
|
||||
|
||||
|
||||
def test_c3kay_feed_delivery_mode_toggle_routes_update_stored_tags() -> None:
|
||||
reader = get_reader_dependency()
|
||||
c3kay_feed_url = "https://feeds.c3kay.de/hoyolab-ui-toggle-test.xml"
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
reader.add_feed(c3kay_feed_url)
|
||||
|
||||
response: Response = client.post(url="/use_text", data={"feed_url": c3kay_feed_url})
|
||||
assert response.status_code == 200, f"Failed to set text mode: {response.text}"
|
||||
assert reader.get_tag(c3kay_feed_url, "delivery_mode") == "text"
|
||||
assert reader.get_tag(c3kay_feed_url, "should_send_embed") is False
|
||||
|
||||
response = client.post(url="/use_screenshot_mobile", data={"feed_url": c3kay_feed_url})
|
||||
assert response.status_code == 200, f"Failed to set screenshot mobile mode: {response.text}"
|
||||
assert reader.get_tag(c3kay_feed_url, "delivery_mode") == "screenshot"
|
||||
assert reader.get_tag(c3kay_feed_url, "screenshot_layout") == "mobile"
|
||||
assert reader.get_tag(c3kay_feed_url, "should_send_embed") is False
|
||||
|
||||
response = client.post(url="/use_embed", data={"feed_url": c3kay_feed_url})
|
||||
assert response.status_code == 200, f"Failed to set embed mode: {response.text}"
|
||||
assert reader.get_tag(c3kay_feed_url, "delivery_mode") == "embed"
|
||||
assert reader.get_tag(c3kay_feed_url, "should_send_embed") is True
|
||||
|
||||
|
||||
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}"
|
||||
|
||||
response = client.get(url="/settings")
|
||||
assert response.status_code == 200, f"/settings failed after setting layout: {response.text}"
|
||||
assert re.search(r"<option\s+value=\"mobile\"[^>]*\bselected\b", response.text)
|
||||
|
||||
|
||||
def test_pause_feed() -> None:
|
||||
"""Test the /pause_feed page."""
|
||||
# Ensure webhook exists for this test regardless of test order.
|
||||
|
|
|
|||
|
|
@ -61,3 +61,83 @@ def test_get_webhook_for_entry() -> None:
|
|||
|
||||
# Close the reader, so we can delete the directory.
|
||||
reader.close()
|
||||
|
||||
|
||||
def test_get_reader_sets_default_global_screenshot_layout() -> None:
|
||||
"""get_reader should initialize global screenshot layout to desktop when missing."""
|
||||
get_reader.cache_clear()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
Path.mkdir(Path(temp_dir), exist_ok=True)
|
||||
|
||||
custom_loc: pathlib.Path = pathlib.Path(temp_dir, "screenshot_default_db.sqlite")
|
||||
reader: Reader = get_reader(custom_location=custom_loc)
|
||||
|
||||
screenshot_layout = reader.get_tag((), "screenshot_layout", None)
|
||||
assert screenshot_layout == "desktop", (
|
||||
f"Expected default global screenshot layout to be 'desktop', got: {screenshot_layout}"
|
||||
)
|
||||
|
||||
reader.close()
|
||||
get_reader.cache_clear()
|
||||
|
||||
|
||||
def test_get_reader_preserves_existing_global_screenshot_layout() -> None:
|
||||
"""get_reader should not overwrite an existing global screenshot layout value."""
|
||||
get_reader.cache_clear()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
Path.mkdir(Path(temp_dir), exist_ok=True)
|
||||
|
||||
custom_loc: pathlib.Path = pathlib.Path(temp_dir, "screenshot_existing_db.sqlite")
|
||||
first_reader: Reader = get_reader(custom_location=custom_loc)
|
||||
first_reader.set_tag((), "screenshot_layout", "mobile") # pyright: ignore[reportArgumentType]
|
||||
first_reader.close()
|
||||
get_reader.cache_clear()
|
||||
|
||||
second_reader: Reader = get_reader(custom_location=custom_loc)
|
||||
screenshot_layout = second_reader.get_tag((), "screenshot_layout", None)
|
||||
assert screenshot_layout == "mobile", (
|
||||
f"Expected existing global screenshot layout to stay 'mobile', got: {screenshot_layout}"
|
||||
)
|
||||
|
||||
second_reader.close()
|
||||
get_reader.cache_clear()
|
||||
|
||||
|
||||
def test_get_reader_sets_default_global_delivery_mode() -> None:
|
||||
"""get_reader should initialize global delivery mode to embed when missing."""
|
||||
get_reader.cache_clear()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
Path.mkdir(Path(temp_dir), exist_ok=True)
|
||||
|
||||
custom_loc: pathlib.Path = pathlib.Path(temp_dir, "delivery_mode_default_db.sqlite")
|
||||
reader: Reader = get_reader(custom_location=custom_loc)
|
||||
|
||||
delivery_mode = reader.get_tag((), "delivery_mode", None)
|
||||
assert delivery_mode == "embed", f"Expected default global delivery mode to be 'embed', got: {delivery_mode}"
|
||||
|
||||
reader.close()
|
||||
get_reader.cache_clear()
|
||||
|
||||
|
||||
def test_get_reader_preserves_existing_global_delivery_mode() -> None:
|
||||
"""get_reader should not overwrite an existing global delivery mode value."""
|
||||
get_reader.cache_clear()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
Path.mkdir(Path(temp_dir), exist_ok=True)
|
||||
|
||||
custom_loc: pathlib.Path = pathlib.Path(temp_dir, "delivery_mode_existing_db.sqlite")
|
||||
first_reader: Reader = get_reader(custom_location=custom_loc)
|
||||
first_reader.set_tag((), "delivery_mode", "text") # pyright: ignore[reportArgumentType]
|
||||
first_reader.close()
|
||||
get_reader.cache_clear()
|
||||
|
||||
second_reader: Reader = get_reader(custom_location=custom_loc)
|
||||
delivery_mode = second_reader.get_tag((), "delivery_mode", None)
|
||||
assert delivery_mode == "text", f"Expected existing global delivery mode to stay 'text', got: {delivery_mode}"
|
||||
|
||||
second_reader.close()
|
||||
get_reader.cache_clear()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue