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

This commit is contained in:
Joakim Hellsén 2026-04-10 00:32:02 +02:00
commit 9ec0166e7f
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
14 changed files with 1571 additions and 241 deletions

View file

@ -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")

View file

@ -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"

View file

@ -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.

View file

@ -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()