From bf94f3f3e4329919a89208d46e00e9aed0071a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Helle=C5=9Ben?= Date: Mon, 16 Mar 2026 01:10:49 +0100 Subject: [PATCH] Add new webhook detail view --- discord_rss_bot/main.py | 15 ++- discord_rss_bot/templates/index.html | 3 +- .../templates/webhook_entries.html | 92 ++++++++++++-- tests/test_main.py | 115 ++++++++++++++++++ 4 files changed, 214 insertions(+), 11 deletions(-) diff --git a/discord_rss_bot/main.py b/discord_rss_bot/main.py index 56fd7d2..98c05b8 100644 --- a/discord_rss_bot/main.py +++ b/discord_rss_bot/main.py @@ -1473,12 +1473,14 @@ def modify_webhook( old_hook: Annotated[str, Form()], new_hook: Annotated[str, Form()], reader: Annotated[Reader, Depends(get_reader_dependency)], + redirect_to: Annotated[str, Form()] = "", ): """Modify a webhook. Args: old_hook: The webhook to modify. new_hook: The new webhook. + redirect_to: Optional redirect URL after the update. reader: The Reader instance. Returns: @@ -1515,8 +1517,13 @@ def modify_webhook( if webhook == old_hook.strip(): reader.set_tag(feed.url, "webhook", new_hook.strip()) # pyright: ignore[reportArgumentType] - # Redirect to the webhook page. - return RedirectResponse(url="/webhooks", status_code=303) + redirect_url: str = redirect_to.strip() or "/webhooks" + if redirect_to: + redirect_url = redirect_url.replace(urllib.parse.quote(old_hook.strip()), urllib.parse.quote(new_hook.strip())) + redirect_url = redirect_url.replace(old_hook.strip(), new_hook.strip()) + + # Redirect to the requested page. + return RedirectResponse(url=redirect_url, status_code=303) def extract_youtube_video_id(url: str) -> str | None: @@ -1577,6 +1584,8 @@ async def get_webhook_entries( # noqa: C901, PLR0914 if not webhook_name: raise HTTPException(status_code=404, detail=f"Webhook not found: {clean_webhook_url}") + hook_info: WebhookInfo = get_data_from_hook_url(hook_name=webhook_name, hook_url=clean_webhook_url) + # Get all feeds associated with this webhook all_feeds: list[Feed] = list(reader.get_feeds()) webhook_feeds: list[Feed] = [] @@ -1632,8 +1641,10 @@ async def get_webhook_entries( # noqa: C901, PLR0914 context = { "request": request, + "hook_info": hook_info, "webhook_name": webhook_name, "webhook_url": clean_webhook_url, + "webhook_feeds": webhook_feeds, "entries": paginated_entries, "html": html, "last_entry": last_entry, diff --git a/discord_rss_bot/templates/index.html b/discord_rss_bot/templates/index.html index 06b8578..d7db60c 100644 --- a/discord_rss_bot/templates/index.html +++ b/discord_rss_bot/templates/index.html @@ -33,7 +33,8 @@

- {{ hook_from_context.name }} + {{ hook_from_context.name }}

View Latest Entries diff --git a/discord_rss_bot/templates/webhook_entries.html b/discord_rss_bot/templates/webhook_entries.html index eb12487..9798b1a 100644 --- a/discord_rss_bot/templates/webhook_entries.html +++ b/discord_rss_bot/templates/webhook_entries.html @@ -1,20 +1,96 @@ {% extends "base.html" %} {% block title %} - | {{ webhook_name }} - Latest Entries + | {{ webhook_name }} {% endblock title %} {% block content %}
- -

{{ webhook_name }} - Latest Entries ({{ total_entries }} total from {{ feeds_count }} feeds)

- -
-

- {{ webhook_url }} -

+
+
+

{{ webhook_name }}

+

+ {{ total_entries }} total from {{ feeds_count }} feed{{ 's' if feeds_count != 1 else '' }} +

+

+ {{ webhook_url }} +

+
+ +
+
+
+
+
+

Settings

+ +
+ + +
+ + +
+
+ +
+
+
+ + +
+
+
+
+
+

Attached feeds

+ {% if webhook_feeds %} + + {% else %} +

No feeds are attached to this webhook yet.

+ {% endif %} +
{# Rendered HTML content #} {% if entries %} +

Latest entries

{{ html|safe }}
{% if is_show_more_entries_button_visible and last_entry %} None: response: Response = client.get(url="/webhooks") assert response.status_code == 200, f"/webhooks failed: {response.text}" + response = client.get(url="/webhook_entries", params={"webhook_url": webhook_url}) + assert response.status_code == 200, f"/webhook_entries failed: {response.text}" + response: Response = client.get(url="/whitelist", params={"feed_url": encoded_feed_url(feed_url)}) assert response.status_code == 200, f"/whitelist failed: {response.text}" @@ -882,6 +885,32 @@ def test_webhook_entries_no_feeds() -> None: assert "No feeds found" in response.text or "Add feeds" in response.text, "Expected message about no feeds" +def test_webhook_entries_no_feeds_still_shows_webhook_settings() -> None: + """The webhook detail view should show settings/actions even with no attached feeds.""" + client.post(url="/delete_webhook", data={"webhook_url": webhook_url}) + response: Response = client.post( + url="/add_webhook", + data={"webhook_name": webhook_name, "webhook_url": webhook_url}, + ) + assert response.status_code == 200, f"Failed to add webhook: {response.text}" + + response = client.get( + url="/webhook_entries", + params={"webhook_url": webhook_url}, + ) + + assert response.status_code == 200, f"Failed to get /webhook_entries: {response.text}" + assert "Settings" in response.text, "Expected settings card on webhook detail view" + assert "Modify Webhook" in response.text, "Expected modify form on webhook detail view" + assert "Delete Webhook" in response.text, "Expected delete action on webhook detail view" + assert "Back to dashboard" in response.text, "Expected dashboard navigation link" + assert "All webhooks" in response.text, "Expected all webhooks navigation link" + assert f'name="old_hook" value="{webhook_url}"' in response.text, "Expected old_hook hidden input" + assert f'value="/webhook_entries?webhook_url={urllib.parse.quote(webhook_url)}"' in response.text, ( + "Expected modify form to redirect back to the current webhook detail view" + ) + + def test_webhook_entries_with_feeds_no_entries() -> None: """Test webhook_entries endpoint when webhook has feeds but no entries yet.""" # Clean up and create fresh webhook @@ -943,6 +972,38 @@ def test_webhook_entries_with_entries() -> None: assert webhook_name in response.text, "Webhook name not found in response" # Should show entries (the feed has entries) assert "total from" in response.text, "Expected to see entry count" + assert "Modify Webhook" in response.text, "Expected webhook settings to be visible" + assert "Attached feeds" in response.text, "Expected attached feeds section to be visible" + + +def test_webhook_entries_shows_attached_feed_link() -> None: + """The webhook detail view should list attached feeds linking to their feed pages.""" + client.post(url="/delete_webhook", data={"webhook_url": webhook_url}) + response: Response = client.post( + url="/add_webhook", + data={"webhook_name": webhook_name, "webhook_url": webhook_url}, + ) + assert response.status_code == 200, f"Failed to add webhook: {response.text}" + + client.post(url="/remove", data={"feed_url": feed_url}) + response = client.post( + url="/add", + data={"feed_url": feed_url, "webhook_dropdown": webhook_name}, + ) + assert response.status_code == 200, f"Failed to add feed: {response.text}" + + response = client.get( + url="/webhook_entries", + params={"webhook_url": webhook_url}, + ) + + assert response.status_code == 200, f"Failed to get /webhook_entries: {response.text}" + assert f"/feed?feed_url={urllib.parse.quote(feed_url)}" in response.text, ( + "Expected attached feed to link to its feed detail page" + ) + assert "Latest entries" in response.text, "Expected latest entries heading on webhook detail view" + + client.post(url="/remove", data={"feed_url": feed_url}) def test_webhook_entries_multiple_feeds() -> None: @@ -1047,6 +1108,60 @@ def test_webhook_entries_url_encoding() -> None: client.post(url="/remove", data={"feed_url": feed_url}) +def test_dashboard_webhook_name_links_to_webhook_detail() -> None: + """Webhook names on the dashboard should open the webhook detail view.""" + client.post(url="/delete_webhook", data={"webhook_url": webhook_url}) + response: Response = client.post( + url="/add_webhook", + data={"webhook_name": webhook_name, "webhook_url": webhook_url}, + ) + assert response.status_code == 200, f"Failed to add webhook: {response.text}" + + client.post(url="/remove", data={"feed_url": feed_url}) + response = client.post(url="/add", data={"feed_url": feed_url, "webhook_dropdown": webhook_name}) + assert response.status_code == 200, f"Failed to add feed: {response.text}" + + response = client.get(url="/") + assert response.status_code == 200, f"Failed to get /: {response.text}" + + expected_link = f"/webhook_entries?webhook_url={urllib.parse.quote(webhook_url)}" + assert expected_link in response.text, "Expected dashboard webhook link to point to the webhook detail view" + + client.post(url="/remove", data={"feed_url": feed_url}) + + +def test_modify_webhook_redirects_back_to_webhook_detail() -> None: + """Webhook updates from the detail view should redirect back to that view with the new URL.""" + original_webhook_url = "https://discord.com/api/webhooks/1234567890/abcdefghijklmnopqrstuvwxyz" + new_webhook_url = "https://discord.com/api/webhooks/1234567890/updated-token" + + client.post(url="/delete_webhook", data={"webhook_url": original_webhook_url}) + client.post(url="/delete_webhook", data={"webhook_url": new_webhook_url}) + + response = client.post( + url="/add_webhook", + data={"webhook_name": webhook_name, "webhook_url": original_webhook_url}, + ) + assert response.status_code == 200, f"Failed to add webhook: {response.text}" + + no_redirect_client = TestClient(app, follow_redirects=False) + response = no_redirect_client.post( + url="/modify_webhook", + data={ + "old_hook": original_webhook_url, + "new_hook": new_webhook_url, + "redirect_to": f"/webhook_entries?webhook_url={urllib.parse.quote(original_webhook_url)}", + }, + ) + + assert response.status_code == 303, f"Expected 303 redirect, got {response.status_code}: {response.text}" + assert response.headers["location"] == (f"/webhook_entries?webhook_url={urllib.parse.quote(new_webhook_url)}"), ( + f"Unexpected redirect location: {response.headers['location']}" + ) + + client.post(url="/delete_webhook", data={"webhook_url": new_webhook_url}) + + def test_reader_dependency_override_is_used() -> None: """Reader should be injectable and overridable via FastAPI dependency overrides."""