diff --git a/discord_rss_bot/custom_message.py b/discord_rss_bot/custom_message.py index e5f1b14..4a5c819 100644 --- a/discord_rss_bot/custom_message.py +++ b/discord_rss_bot/custom_message.py @@ -38,6 +38,7 @@ class CustomEmbed: thumbnail_url: str = "" footer_text: str = "" footer_icon_url: str = "" + show_steam_game_icon_in_thumbnail: bool = False def try_to_replace(custom_message: str, template: str, replace_with: str) -> str: @@ -398,7 +399,7 @@ def save_embed(reader: Reader, feed: Feed, embed: CustomEmbed) -> None: feed: The feed to set the tag in. embed: The embed to set. """ - embed_dict: dict[str, str | int] = { + embed_dict: dict[str, str | int | bool] = { "title": embed.title, "description": embed.description, "color": embed.color, @@ -409,6 +410,7 @@ def save_embed(reader: Reader, feed: Feed, embed: CustomEmbed) -> None: "thumbnail_url": embed.thumbnail_url, "footer_text": embed.footer_text, "footer_icon_url": embed.footer_icon_url, + "show_steam_game_icon_in_thumbnail": embed.show_steam_game_icon_in_thumbnail, } reader.set_tag(feed, "embed", json.dumps(embed_dict)) # pyright: ignore[reportArgumentType] @@ -428,7 +430,7 @@ def get_embed(reader: Reader, feed: Feed) -> CustomEmbed: if embed: if not isinstance(embed, str): return get_embed_data(embed) # pyright: ignore[reportArgumentType] - embed_data: dict[str, str | int] = json.loads(embed) + embed_data: dict[str, str | int | bool] = json.loads(embed) return get_embed_data(embed_data) return CustomEmbed( @@ -442,10 +444,22 @@ def get_embed(reader: Reader, feed: Feed) -> CustomEmbed: thumbnail_url="", footer_text="", footer_icon_url="", + show_steam_game_icon_in_thumbnail=False, ) -def get_embed_data(embed_data: dict[str, str | int]) -> CustomEmbed: +def coerce_embed_bool(value: object) -> bool: + """Normalize stored embed booleans from JSON or form-like values.""" + if isinstance(value, bool): + return value + if isinstance(value, int): + return value != 0 + if isinstance(value, str): + return value.strip().lower() in {"1", "true", "on", "yes"} + return False + + +def get_embed_data(embed_data: dict[str, str | int | bool]) -> CustomEmbed: """Get embed data from embed_data. Args: @@ -464,6 +478,9 @@ def get_embed_data(embed_data: dict[str, str | int]) -> CustomEmbed: thumbnail_url: str = str(embed_data.get("thumbnail_url", "")) footer_text: str = str(embed_data.get("footer_text", "")) footer_icon_url: str = str(embed_data.get("footer_icon_url", "")) + show_steam_game_icon_in_thumbnail: bool = coerce_embed_bool( + embed_data.get("show_steam_game_icon_in_thumbnail", False), + ) return CustomEmbed( title=title, @@ -476,4 +493,5 @@ def get_embed_data(embed_data: dict[str, str | int]) -> CustomEmbed: thumbnail_url=thumbnail_url, footer_text=footer_text, footer_icon_url=footer_icon_url, + show_steam_game_icon_in_thumbnail=show_steam_game_icon_in_thumbnail, ) diff --git a/discord_rss_bot/feeds.py b/discord_rss_bot/feeds.py index ffdbfb3..a75629e 100644 --- a/discord_rss_bot/feeds.py +++ b/discord_rss_bot/feeds.py @@ -12,6 +12,7 @@ import re import time from collections.abc import Callable from contextlib import suppress +from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import Literal @@ -186,6 +187,123 @@ def extract_domain(url: str) -> str: # noqa: PLR0911 return "Other" +STEAM_STORE_APP_ID_PATH_PREFIXES: tuple[tuple[str, ...], ...] = ( + ("feeds", "news", "app"), + ("news", "app"), + ("app",), +) + + +def is_steam_feed_url(url: str) -> bool: + """Return whether a feed URL belongs to Steam.""" + if not url: + return False + + try: + parsed_url: ParseResult = urlparse(url) + except ValueError: + return False + + normalized_netloc: str = parsed_url.netloc.lower().removeprefix("www.") + return normalized_netloc in frozenset({"store.steampowered.com", "steamcommunity.com"}) + + +def _extract_steam_app_id_from_path(normalized_netloc: str, path_segments: list[str]) -> str | None: + if normalized_netloc == "store.steampowered.com": + for prefix in STEAM_STORE_APP_ID_PATH_PREFIXES: + prefix_length: int = len(prefix) + if len(path_segments) > prefix_length and tuple(path_segments[:prefix_length]) == prefix: + app_id: str = path_segments[prefix_length] + if app_id.isdigit(): + return app_id + + if normalized_netloc == "steamcommunity.com" and len(path_segments) > 1: + app_type: str = path_segments[0] + app_id = path_segments[1] + if app_type in frozenset({"games", "app"}) and app_id.isdigit(): + return app_id + + return None + + +def _extract_steam_app_id_from_query(parsed_url: ParseResult) -> str | None: + query_params: dict[str, list[str]] = parse_qs(parsed_url.query) + for key in ("appid", "app_id", "appids"): + for value in query_params.get(key, []): + cleaned_value: str = value.strip() + if cleaned_value.isdigit(): + return cleaned_value + return None + + +def extract_steam_app_id_from_url(url: str) -> str | None: + """Extract a Steam application ID from a Steam URL when present. + + Args: + url: The Steam URL to inspect. + + Returns: + str | None: The application ID when present in the path or query string, otherwise None. + """ + if not url: + return None + + try: + parsed_url: ParseResult = urlparse(url) + except ValueError: + return None + + normalized_netloc: str = parsed_url.netloc.lower().removeprefix("www.") + path_segments: list[str] = [segment for segment in parsed_url.path.split("/") if segment] + return _extract_steam_app_id_from_path(normalized_netloc, path_segments) or _extract_steam_app_id_from_query( + parsed_url + ) + + +def get_local_steam_game_icon_file(app_id: str) -> WebhookFile | None: + """Return a local Steam game icon file for Discord upload when available.""" + icon_path: Path = Path(__file__).resolve().parent.parent / "icons" / f"{app_id}.png" + if not icon_path.is_file(): + return None + + try: + icon_bytes: bytes = icon_path.read_bytes() + except OSError: + logger.exception("Failed to read local Steam icon for app %s from %s", app_id, icon_path) + return None + + if not icon_bytes: + logger.warning("Local Steam icon file is empty for app %s: %s", app_id, icon_path) + return None + + content_hash: str = hashlib.sha256(icon_bytes).hexdigest()[:12] + return WebhookFile( + filename=f"steam-app-{app_id}-{content_hash}.png", + content=icon_bytes, + ) + + +def get_steam_game_thumbnail(entry: Entry) -> tuple[str | None, WebhookFile | None]: + """Return the preferred Steam thumbnail source for an entry. + + Returns: + tuple[str | None, WebhookFile | None]: Thumbnail URL and optional uploaded file. + """ + feed_url: str = str(getattr(entry.feed, "url", "") or "") + if not is_steam_feed_url(feed_url): + return None, None + + app_id: str | None = extract_steam_app_id_from_url(feed_url) or extract_steam_app_id_from_url(str(entry.link or "")) + if not app_id: + return None, None + + local_icon_file: WebhookFile | None = get_local_steam_game_icon_file(app_id) + if local_icon_file: + return f"attachment://{local_icon_file.filename}", local_icon_file + + return f"https://cdn.cloudflare.steamstatic.com/steam/apps/{app_id}/capsule_sm_120.jpg", None + + def send_entry_to_discord(entry: Entry, reader: Reader) -> str | None: """Send a single entry to Discord. @@ -1719,6 +1837,10 @@ def create_embed_webhook( # noqa: C901, PLR0912 custom_embed: CustomEmbed = replace_tags_in_embed(feed=feed, entry=entry, reader=reader) media_gallery_image_limit: int = get_feed_media_gallery_image_limit(reader, feed) webhook_text_length_limit: int = get_feed_webhook_text_length_limit(reader, feed) + steam_game_thumbnail_url: str | None = None + steam_game_thumbnail_file: WebhookFile | None = None + if custom_embed.show_steam_game_icon_in_thumbnail: + steam_game_thumbnail_url, steam_game_thumbnail_file = get_steam_game_thumbnail(entry) if media_gallery_image_limit == 0: custom_embed.image_url = "" custom_embed.thumbnail_url = "" @@ -1771,8 +1893,9 @@ def create_embed_webhook( # noqa: C901, PLR0912 icon_url=custom_embed.author_icon_url, ) - if custom_embed.thumbnail_url: - discord_embed.set_thumbnail(url=custom_embed.thumbnail_url) + embed_thumbnail_url: str = steam_game_thumbnail_url or custom_embed.thumbnail_url + if embed_thumbnail_url: + discord_embed.set_thumbnail(url=embed_thumbnail_url) if custom_embed.image_url: discord_embed.set_image(url=custom_embed.image_url) @@ -1786,6 +1909,9 @@ def create_embed_webhook( # noqa: C901, PLR0912 if custom_embed.footer_icon_url and not custom_embed.footer_text: discord_embed.set_footer(text="-", icon_url=custom_embed.footer_icon_url) + if steam_game_thumbnail_file: + webhook.add_file(file=steam_game_thumbnail_file.content, filename=steam_game_thumbnail_file.filename) + webhook.add_embed(discord_embed) return webhook diff --git a/discord_rss_bot/main.py b/discord_rss_bot/main.py index 746cd3e..acc07d9 100644 --- a/discord_rss_bot/main.py +++ b/discord_rss_bot/main.py @@ -63,6 +63,7 @@ from discord_rss_bot.feeds import get_feed_media_gallery_image_limit from discord_rss_bot.feeds import get_feed_webhook_text_length_limit from discord_rss_bot.feeds import get_screenshot_layout from discord_rss_bot.feeds import get_sent_webhook_records +from discord_rss_bot.feeds import is_steam_feed_url from discord_rss_bot.feeds import send_entry_to_discord from discord_rss_bot.feeds import send_to_discord from discord_rss_bot.feeds import update_feed_and_collect_modified_entries @@ -1221,7 +1222,7 @@ async def get_embed_page( # Get previous data, this is used when creating the form. embed: CustomEmbed = get_embed(reader, feed) - context: dict[str, Request | Feed | str | Entry | CustomEmbed] = { + context: dict[str, Request | Feed | str | Entry | CustomEmbed | bool] = { "request": request, "feed": feed, "title": embed.title, @@ -1234,6 +1235,7 @@ async def get_embed_page( "author_icon_url": embed.author_icon_url, "footer_text": embed.footer_text, "footer_icon_url": embed.footer_icon_url, + "show_steam_game_icon_in_thumbnail": embed.show_steam_game_icon_in_thumbnail, } if custom_embed := get_embed(reader, feed): context["custom_embed"] = custom_embed @@ -1258,6 +1260,7 @@ async def post_embed( # noqa: C901 author_icon_url: Annotated[str, Form()] = "", footer_text: Annotated[str, Form()] = "", footer_icon_url: Annotated[str, Form()] = "", + show_steam_game_icon_in_thumbnail: Annotated[bool, Form()] = False, ) -> RedirectResponse: """Set the embed settings. @@ -1273,6 +1276,7 @@ async def post_embed( # noqa: C901 author_icon_url: The author icon url of the embed. footer_text: The footer text of the embed. footer_icon_url: The footer icon url of the embed. + show_steam_game_icon_in_thumbnail: Whether to use the Steam game icon as the embed thumbnail. reader: The Reader instance. Returns: @@ -1303,6 +1307,8 @@ async def post_embed( # noqa: C901 custom_embed.footer_text = footer_text if footer_icon_url != custom_embed.footer_icon_url: custom_embed.footer_icon_url = footer_icon_url + if show_steam_game_icon_in_thumbnail != custom_embed.show_steam_game_icon_in_thumbnail: + custom_embed.show_steam_game_icon_in_thumbnail = show_steam_game_icon_in_thumbnail # Save the data. save_embed(reader, feed, custom_embed) @@ -1855,6 +1861,7 @@ async def get_feed( # noqa: C901, PLR0912, PLR0914, PLR0915 "webhook_text_length_limit": get_feed_webhook_text_length_limit(reader, feed), "max_webhook_text_length_limit": 4000, "save_sent_webhooks": feed_saves_sent_webhooks(reader, feed), + "is_steam_feed": is_steam_feed_url(feed.url), } return templates.TemplateResponse(request=request, name="feed.html", context=context) @@ -1921,6 +1928,7 @@ async def get_feed( # noqa: C901, PLR0912, PLR0914, PLR0915 "webhook_text_length_limit": get_feed_webhook_text_length_limit(reader, feed), "max_webhook_text_length_limit": 4000, "save_sent_webhooks": feed_saves_sent_webhooks(reader, feed), + "is_steam_feed": is_steam_feed_url(feed.url), } return templates.TemplateResponse(request=request, name="feed.html", context=context) diff --git a/discord_rss_bot/templates/embed.html b/discord_rss_bot/templates/embed.html index 4bcc3a7..c2f4595 100644 --- a/discord_rss_bot/templates/embed.html +++ b/discord_rss_bot/templates/embed.html @@ -245,6 +245,17 @@ +
+ + +
diff --git a/discord_rss_bot/templates/feed.html b/discord_rss_bot/templates/feed.html index 76c8434..dcefd1a 100644 --- a/discord_rss_bot/templates/feed.html +++ b/discord_rss_bot/templates/feed.html @@ -338,6 +338,9 @@ {% endif %} + {% if is_steam_feed %} +
Steam feeds can enable thumbnails in embed settings.
+ {% endif %}

Feed URL

diff --git a/icons/570.png b/icons/570.png new file mode 100644 index 0000000..e97fc97 Binary files /dev/null and b/icons/570.png differ diff --git a/tests/test_custom_message.py b/tests/test_custom_message.py index 38cf649..ecd744a 100644 --- a/tests/test_custom_message.py +++ b/tests/test_custom_message.py @@ -355,6 +355,7 @@ def test_save_embed_serializes_embed_and_writes_feed_tag() -> None: thumbnail_url="https://example.com/thumb.png", footer_text="Footer", footer_icon_url="https://example.com/footer.png", + show_steam_game_icon_in_thumbnail=True, ) save_embed(reader=reader, feed=feed, embed=embed) @@ -365,6 +366,7 @@ def test_save_embed_serializes_embed_and_writes_feed_tag() -> None: parsed = typing.cast("dict[str, str]", __import__("json").loads(call_args[2])) assert parsed["title"] == "Title" assert parsed["footer_icon_url"] == "https://example.com/footer.png" + assert parsed["show_steam_game_icon_in_thumbnail"] is True def test_get_embed_returns_default_embed_when_tag_is_empty() -> None: @@ -420,8 +422,10 @@ def test_get_embed_data_coerces_values_to_strings() -> None: "thumbnail_url": 8, "footer_text": 9, "footer_icon_url": 10, + "show_steam_game_icon_in_thumbnail": "true", }, ) assert embed.title == "1" assert embed.footer_icon_url == "10" + assert embed.show_steam_game_icon_in_thumbnail is True diff --git a/tests/test_feeds.py b/tests/test_feeds.py index 793fcbf..f5e87b9 100644 --- a/tests/test_feeds.py +++ b/tests/test_feeds.py @@ -379,6 +379,21 @@ def test_get_feed_media_gallery_image_limit_defaults_to_first_image() -> None: assert result == 1 +@pytest.mark.parametrize( + ("url", "expected_app_id"), + [ + ("https://store.steampowered.com/feeds/news/app/570/?cc=us&l=english", "570"), + ("https://store.steampowered.com/news/app/440/view/1234567890", "440"), + ("https://store.steampowered.com/app/730/CounterStrike_2/", "730"), + ("https://steamcommunity.com/games/570/rss/", "570"), + ("https://steamcommunity.com/app/730/announcements/detail/1234567890", "730"), + ("https://example.com/feed.xml", None), + ], +) +def test_extract_steam_app_id_from_url(url: str, expected_app_id: str | None) -> None: + assert feeds.extract_steam_app_id_from_url(url) == expected_app_id + + @pytest.mark.parametrize( ("tag_value", "expected_limit"), [ @@ -909,6 +924,122 @@ 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_can_use_steam_game_icon_thumbnail( + 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": 4000, + }.get(key, default) + entry = MagicMock() + entry.id = "entry-steam-1" + entry.title = "Dota 2 patch notes" + entry.link = "https://steamcommunity.com/games/570/announcements/detail/1234567890" + entry.summary = "" + entry.content = [] + entry.feed.url = "https://store.steampowered.com/feeds/news/app/570/?cc=us&l=english" + mock_replace_tags_in_embed.return_value = feeds.CustomEmbed( + description="Steam news", + thumbnail_url="https://example.com/custom-thumb.jpg", + show_steam_game_icon_in_thumbnail=True, + ) + + with patch("discord_rss_bot.feeds.Path.is_file", return_value=False): + 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 embeds[0]["thumbnail"] == { + "url": "https://cdn.cloudflare.steamstatic.com/steam/apps/570/capsule_sm_120.jpg", + } + assert webhook.files == [] + 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_prefers_local_steam_game_icon_thumbnail( + mock_replace_tags_in_embed: MagicMock, + mock_fetch_ttvdrops_campaign_media_items: MagicMock, +) -> None: + local_icon_bytes = b"local-steam-icon" + + reader = MagicMock() + 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-steam-local-1" + entry.title = "Dota 2 patch notes" + entry.link = "https://steamcommunity.com/games/570/announcements/detail/1234567890" + entry.summary = "" + entry.content = [] + entry.feed.url = "https://store.steampowered.com/feeds/news/app/570/?cc=us&l=english" + mock_replace_tags_in_embed.return_value = feeds.CustomEmbed( + description="Steam news", + thumbnail_url="https://example.com/custom-thumb.jpg", + show_steam_game_icon_in_thumbnail=True, + ) + + with ( + patch("discord_rss_bot.feeds.Path.is_file", return_value=True), + patch("discord_rss_bot.feeds.Path.read_bytes", return_value=local_icon_bytes), + ): + webhook = feeds.create_embed_webhook("https://discord.com/api/webhooks/123/abc", entry, reader) + + embeds = webhook.json.get("embeds") + assert isinstance(embeds, list) + assert isinstance(embeds[0], dict) + assert len(webhook.files) == 1 + uploaded_icon = webhook.files[0] + assert uploaded_icon.content == local_icon_bytes + assert uploaded_icon.filename.startswith("steam-app-570-") + assert uploaded_icon.filename.endswith(".png") + assert embeds[0]["thumbnail"] == {"url": f"attachment://{uploaded_icon.filename}"} + 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_does_not_inject_steam_thumbnail_when_app_id_is_missing( + 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": 4000, + }.get(key, default) + entry = MagicMock() + entry.id = "entry-steam-2" + entry.title = "Steam group post" + entry.link = "https://steamcommunity.com/groups/example/announcements/detail/1234567890" + entry.summary = "" + entry.content = [] + entry.feed.url = "https://steamcommunity.com/groups/example/rss/" + mock_replace_tags_in_embed.return_value = feeds.CustomEmbed( + description="Steam group news", + thumbnail_url="https://example.com/custom-thumb.jpg", + show_steam_game_icon_in_thumbnail=True, + ) + + 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 "thumbnail" not in embeds[0] + 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( @@ -936,8 +1067,8 @@ def test_create_embed_webhook_uses_feed_text_length_limit_for_regular_embed_desc 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("...") + assert len(embeds[0]["description"]) == 20 # pyright: ignore[reportArgumentType] + assert embeds[0]["description"].endswith("...") # pyright: ignore[reportOptionalMemberAccess, reportAttributeAccessIssue] mock_fetch_ttvdrops_campaign_media_items.assert_not_called() diff --git a/tests/test_main.py b/tests/test_main.py index b378a29..710499d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -303,6 +303,60 @@ def test_get() -> None: assert response.status_code == 200, f"/whitelist failed: {response.text}" +def test_feed_page_shows_steam_thumbnail_hint_for_steam_feeds() -> None: + @dataclass(slots=True) + class DummyFeed: + url: str + title: str + updates_enabled: bool = True + last_exception: None = None + added: None = None + last_updated: None = None + last_retrieved: None = None + update_after: None = None + + class StubReader: + def __init__(self) -> None: + self.feed = DummyFeed( + url="https://store.steampowered.com/feeds/news/app/570/?cc=US&l=english", + title="Dota 2", + ) + + def get_feed(self, feed_url: str) -> DummyFeed: + assert feed_url == self.feed.url + return self.feed + + def get_tag(self, _resource: object, key: str, default: TestTagValue = None) -> TestTagValue: + return { + "webhooks": [], + "delivery_mode": "embed", + "screenshot_layout": "desktop", + "media_gallery_image_limit": 0, + "webhook_text_length_limit": 4000, + "save_sent_webhooks": True, + }.get(key, default) + + def get_entry_counts(self, **_kwargs: TestKwargValue) -> SimpleNamespace: + return SimpleNamespace(total=0) + + def get_entries(self, **_kwargs: TestKwargValue) -> list[Entry]: + return [] + + def get_feed_counts(self, **_kwargs: TestKwargValue) -> SimpleNamespace: + return SimpleNamespace() + + stub = StubReader() + app.dependency_overrides[get_reader_dependency] = lambda: stub + + try: + with patch("discord_rss_bot.main.create_html_for_feed", return_value=""): + response: Response = client.get(url="/feed", params={"feed_url": stub.feed.url}) + + assert response.status_code == 200, f"/feed failed: {response.text}" + finally: + app.dependency_overrides = {} + + def test_blacklist_page_uses_live_preview_layout() -> None: ensure_preview_feed_exists() @@ -2562,6 +2616,7 @@ def test_post_embed_saves_all_fields() -> None: "thumbnail_url": "https://example.com/thumb.png", "footer_text": "Footer Text", "footer_icon_url": "https://example.com/footer.png", + "show_steam_game_icon_in_thumbnail": "true", }, follow_redirects=False, ) @@ -2584,6 +2639,7 @@ def test_post_embed_saves_all_fields() -> None: assert saved["thumbnail_url"] == "https://example.com/thumb.png" assert saved["footer_text"] == "Footer Text" assert saved["footer_icon_url"] == "https://example.com/footer.png" + assert saved["show_steam_game_icon_in_thumbnail"] is True finally: app.dependency_overrides = {} @@ -2603,6 +2659,7 @@ def test_post_embed_allows_clearing_description() -> None: "thumbnail_url": "", "footer_text": "", "footer_icon_url": "", + "show_steam_game_icon_in_thumbnail": False, }) stub = _make_stub_reader_for_embed(stored_embed=existing) app.dependency_overrides[get_reader_dependency] = lambda: stub @@ -2660,6 +2717,7 @@ def test_post_embed_allows_clearing_all_fields() -> None: "thumbnail_url": "https://old.example.com/thumb.png", "footer_text": "Old Footer", "footer_icon_url": "https://old.example.com/footer.png", + "show_steam_game_icon_in_thumbnail": True, }) stub = _make_stub_reader_for_embed(stored_embed=existing) app.dependency_overrides[get_reader_dependency] = lambda: stub @@ -2699,6 +2757,7 @@ def test_post_embed_allows_clearing_all_fields() -> None: assert not saved["thumbnail_url"] assert not saved["footer_text"] assert not saved["footer_icon_url"] + assert saved["show_steam_game_icon_in_thumbnail"] is False finally: app.dependency_overrides = {} @@ -2716,6 +2775,7 @@ def test_post_embed_untouched_fields_retain_values() -> None: "thumbnail_url": "", "footer_text": "Old Footer", "footer_icon_url": "", + "show_steam_game_icon_in_thumbnail": False, }) stub = _make_stub_reader_for_embed(stored_embed=existing) app.dependency_overrides[get_reader_dependency] = lambda: stub @@ -2755,6 +2815,7 @@ def test_post_embed_untouched_fields_retain_values() -> None: assert saved["author_name"] == "Author" assert saved["author_url"] == "https://a.example.com" assert saved["footer_text"] == "Old Footer" + assert saved["show_steam_game_icon_in_thumbnail"] is False finally: app.dependency_overrides = {} @@ -2792,6 +2853,7 @@ def test_post_embed_saves_empty_description_when_no_prior_embed_exists() -> None saved: dict[str, str] = json.loads(json_arg) assert saved["title"] == "Just a Title" assert not saved["description"], f"Expected empty description, got {saved['description']!r}" + assert saved["show_steam_game_icon_in_thumbnail"] is False finally: app.dependency_overrides = {}