Add webhook text length stuff
Some checks failed
Test and build Docker image / docker (push) Failing after 15s

This commit is contained in:
Joakim Hellsén 2026-07-02 04:40:38 +02:00
commit 85f25263f3
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
9 changed files with 525 additions and 26 deletions

View file

@ -379,6 +379,43 @@ def test_get_feed_media_gallery_image_limit_defaults_to_first_image() -> None:
assert result == 1
@pytest.mark.parametrize(
("tag_value", "expected_limit"),
[
(1, 1),
(25, 25),
(-1, 1),
(99_999, 4000),
("200", 200),
("0", 1),
("unknown", 4000),
],
)
def test_get_feed_webhook_text_length_limit_normalizes_stored_tag(
tag_value: feeds.JsonValue,
expected_limit: int,
) -> None:
reader = MagicMock()
feed = MagicMock()
feed.url = "https://example.com/feed.xml"
reader.get_tag.return_value = tag_value
result = feeds.get_feed_webhook_text_length_limit(reader, feed)
assert result == expected_limit
def test_get_feed_webhook_text_length_limit_defaults_to_discord_limit() -> None:
reader = MagicMock()
feed = MagicMock()
feed.url = "https://example.com/feed.xml"
reader.get_tag.side_effect = lambda resource, key, default=None: default # noqa: ARG005
result = feeds.get_feed_webhook_text_length_limit(reader, feed)
assert result == 4000
def test_create_feed_inherits_global_screenshot_layout() -> None:
reader = MagicMock()
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
@ -435,6 +472,41 @@ def test_create_feed_sets_default_media_gallery_image_limit() -> None:
)
def test_create_feed_sets_default_webhook_text_length_limit() -> 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",
"webhook_text_length_limit",
4000,
)
def test_create_feed_inherits_global_webhook_text_length_limit() -> 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",
"webhook_text_length_limit": 2500,
}.get(key, default)
create_feed(reader, "https://example.com/feed.xml", "Main")
reader.set_tag.assert_any_call(
"https://example.com/feed.xml",
"webhook_text_length_limit",
2500,
)
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
@ -664,6 +736,32 @@ def test_create_screenshot_webhook_falls_back_to_text_on_failure(
)
@patch("discord_rss_bot.feeds.replace_tags_in_text_message")
def test_create_text_webhook_uses_feed_text_length_limit(mock_replace_tags_in_text_message: MagicMock) -> None:
mock_replace_tags_in_text_message.return_value = "start-" + ("x" * 40) + "-end"
entry = MagicMock()
entry.feed.url = "https://example.com/feed.xml"
reader = MagicMock()
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
"custom_message": "{{entry_title}}",
"webhook_text_length_limit": 20,
}.get(key, default)
webhook = feeds.create_text_webhook(
"https://discord.com/api/webhooks/123/abc",
entry,
reader,
use_default_message_on_empty=False,
)
assert webhook.content is not None
assert len(webhook.content) == 20
assert webhook.content.startswith("start-")
assert webhook.content.endswith("-end")
assert "..." in webhook.content
@patch("discord_rss_bot.feeds.fetch_ttvdrops_campaign_media_items", return_value=[])
@patch("discord_rss_bot.feeds.replace_tags_in_embed")
def test_create_embed_webhook_uses_media_gallery_for_entry_images(
@ -671,7 +769,10 @@ def test_create_embed_webhook_uses_media_gallery_for_entry_images(
mock_fetch_ttvdrops_campaign_media_items: MagicMock,
) -> None:
reader = MagicMock()
reader.get_tag.return_value = 10
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
"media_gallery_image_limit": 10,
"webhook_text_length_limit": 4000,
}.get(key, default)
entry = MagicMock()
entry.id = "entry-1"
entry.title = "Entry title"
@ -707,6 +808,38 @@ def test_create_embed_webhook_uses_media_gallery_for_entry_images(
]
@patch("discord_rss_bot.feeds.fetch_ttvdrops_campaign_media_items", return_value=[])
@patch("discord_rss_bot.feeds.replace_tags_in_embed")
def test_create_embed_webhook_uses_feed_text_length_limit_for_media_gallery(
mock_replace_tags_in_embed: MagicMock,
mock_fetch_ttvdrops_campaign_media_items: MagicMock,
) -> None:
reader = MagicMock()
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
"media_gallery_image_limit": 10,
"webhook_text_length_limit": 20,
}.get(key, default)
entry = MagicMock()
entry.id = "entry-1"
entry.title = "Entry title"
entry.link = "https://example.com/entry"
entry.summary = '<img src="https://example.com/summary.jpg" />'
entry.content = [MagicMock(value='<img src="https://example.com/content-1.jpg" />')]
entry.feed.url = "https://example.com/feed.xml"
mock_replace_tags_in_embed.return_value = feeds.CustomEmbed(description="x" * 100)
webhook = feeds.create_embed_webhook("https://discord.com/api/webhooks/123/abc", entry, reader)
assert webhook.flags == 1 << 15
components = get_test_webhook_components(webhook)
text_component = components[0]
assert isinstance(text_component, dict)
assert isinstance(text_component["content"], str)
assert len(text_component["content"]) == 20
assert text_component["content"].endswith("...")
mock_fetch_ttvdrops_campaign_media_items.assert_called_once_with(entry)
@patch("discord_rss_bot.feeds.fetch_ttvdrops_campaign_media_items", return_value=[])
@patch("discord_rss_bot.feeds.replace_tags_in_embed")
def test_create_embed_webhook_can_limit_media_gallery_to_first_image(
@ -714,7 +847,10 @@ def test_create_embed_webhook_can_limit_media_gallery_to_first_image(
mock_fetch_ttvdrops_campaign_media_items: MagicMock,
) -> None:
reader = MagicMock()
reader.get_tag.return_value = 1
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
"media_gallery_image_limit": 1,
"webhook_text_length_limit": 4000,
}.get(key, default)
entry = MagicMock()
entry.id = "entry-1"
entry.title = "Entry title"
@ -744,7 +880,10 @@ def test_create_embed_webhook_can_disable_media_images(
mock_fetch_ttvdrops_campaign_media_items: MagicMock,
) -> None:
reader = MagicMock()
reader.get_tag.return_value = 0
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
"media_gallery_image_limit": 0,
"webhook_text_length_limit": 4000,
}.get(key, default)
entry = MagicMock()
entry.id = "entry-1"
entry.title = "Entry title"
@ -770,6 +909,38 @@ def test_create_embed_webhook_can_disable_media_images(
mock_fetch_ttvdrops_campaign_media_items.assert_not_called()
@patch("discord_rss_bot.feeds.fetch_ttvdrops_campaign_media_items", return_value=[])
@patch("discord_rss_bot.feeds.replace_tags_in_embed")
def test_create_embed_webhook_uses_feed_text_length_limit_for_regular_embed_description(
mock_replace_tags_in_embed: MagicMock,
mock_fetch_ttvdrops_campaign_media_items: MagicMock,
) -> None:
reader = MagicMock()
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
"media_gallery_image_limit": 0,
"webhook_text_length_limit": 20,
}.get(key, default)
entry = MagicMock()
entry.id = "entry-1"
entry.title = "Entry title"
entry.link = "https://example.com/entry"
entry.summary = ""
entry.content = []
entry.feed.url = "https://example.com/feed.xml"
mock_replace_tags_in_embed.return_value = feeds.CustomEmbed(description="x" * 100)
webhook = feeds.create_embed_webhook("https://discord.com/api/webhooks/123/abc", entry, reader)
assert "components" not in webhook.json
embeds = webhook.json.get("embeds")
assert isinstance(embeds, list)
assert isinstance(embeds[0], dict)
assert isinstance(embeds[0].get("description"), str)
assert len(embeds[0]["description"]) == 20
assert embeds[0]["description"].endswith("...")
mock_fetch_ttvdrops_campaign_media_items.assert_not_called()
@patch("discord_rss_bot.feeds.fetch_ttvdrops_campaign_media_items")
@patch("discord_rss_bot.feeds.replace_tags_in_embed")
def test_create_embed_webhook_prefers_ttvdrops_reward_images_and_alt_text(

View file

@ -508,6 +508,26 @@ def test_post_set_global_screenshot_layout_triggers_backup(monkeypatch: pytest.M
assert "Set global screenshot layout to mobile" in commit_message
def test_post_set_global_webhook_text_length_limit_triggers_backup(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""Posting to /set_global_webhook_text_length_limit 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_webhook_text_length_limit", data={"text_length_limit": "2500"})
assert response.status_code == 200, f"Failed to set global webhook text length limit: {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 webhook text length limit to 2500" 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

@ -283,8 +283,13 @@ def test_get() -> None:
assert "This feed" in response.text
assert "Screenshot Delivery" in response.text
assert "Image Delivery" in response.text
assert "Text Delivery" in response.text
assert "main embed" in response.text
assert "2000 characters" in response.text
assert 'type="range"' in response.text
assert 'max="10"' in response.text
assert 'id="text_length_limit"' in response.text
assert 'max="4000"' in response.text
response: Response = client.get(url="/")
assert response.status_code == 200, f"/ failed: {response.text}"
@ -603,6 +608,10 @@ def test_settings_page_shows_screenshot_layout_setting() -> None:
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 "Default webhook text limit for new feeds" in response.text
assert "regular embed mode" in response.text
assert "2000 characters" in response.text
assert 'id="global_text_length_limit"' in response.text
assert "uv run playwright install chromium" in response.text
@ -615,6 +624,18 @@ def test_set_global_delivery_mode() -> None:
assert re.search(r"<option\s+value=\"text\"[^>]*\bselected\b", response.text)
def test_set_global_webhook_text_length_limit() -> None:
response: Response = client.post(
url="/set_global_webhook_text_length_limit",
data={"text_length_limit": "2500"},
)
assert response.status_code == 200, f"Failed to set global webhook text length limit: {response.text}"
response = client.get(url="/settings")
assert response.status_code == 200, f"/settings failed after setting webhook text length limit: {response.text}"
assert 'value="2500"' in 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}"
@ -742,6 +763,41 @@ def test_set_feed_media_gallery_image_limit_route_updates_stored_tag() -> None:
app.dependency_overrides = {}
def test_set_feed_webhook_text_length_limit_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], int] = {}
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: int) -> None:
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_webhook_text_length_limit",
data={"feed_url": stub_reader.feed.url, "text_length_limit": "2500"},
follow_redirects=False,
)
assert response.status_code == 303, f"/set_feed_webhook_text_length_limit failed: {response.text}"
assert stub_reader.tags[stub_reader.feed.url, "webhook_text_length_limit"] == 2500
finally:
app.dependency_overrides = {}
def test_sent_webhooks_view_shows_saved_records() -> None:
@dataclass(slots=True)
class DummyFeed:

View file

@ -192,6 +192,48 @@ def test_get_reader_preserves_existing_global_delivery_mode() -> None:
get_reader.cache_clear()
def test_get_reader_sets_default_global_webhook_text_length_limit() -> None:
"""get_reader should initialize global webhook text length limit to 4000 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, "webhook_text_length_limit_default_db.sqlite")
reader: Reader = get_reader(custom_location=custom_loc)
webhook_text_length_limit = reader.get_tag((), "webhook_text_length_limit", None)
assert webhook_text_length_limit == 4000, (
f"Expected default global webhook text length limit to be 4000, got: {webhook_text_length_limit}"
)
reader.close()
get_reader.cache_clear()
def test_get_reader_preserves_existing_global_webhook_text_length_limit() -> None:
"""get_reader should not overwrite an existing global webhook text length limit 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, "webhook_text_length_limit_existing_db.sqlite")
first_reader: Reader = get_reader(custom_location=custom_loc)
first_reader.set_tag((), "webhook_text_length_limit", 2500) # pyright: ignore[reportArgumentType]
first_reader.close()
get_reader.cache_clear()
second_reader: Reader = get_reader(custom_location=custom_loc)
webhook_text_length_limit = second_reader.get_tag((), "webhook_text_length_limit", None)
assert webhook_text_length_limit == 2500, (
f"Expected existing global webhook text length limit to stay 2500, got: {webhook_text_length_limit}"
)
second_reader.close()
get_reader.cache_clear()
def test_get_reader_enables_autodiscover_plugin() -> None:
"""get_reader should store advertised feed links when HTML parsing fails."""
get_reader.cache_clear()