Compare commits

..

No commits in common. "aa8a74ba67da1d28684769b47197dd4577cff104" and "9ec0166e7f01d4e2487813e9d704c63d9521521e" have entirely different histories.

5 changed files with 44 additions and 374 deletions

View file

@ -114,13 +114,6 @@ def get_reader_dependency() -> Reader:
return get_reader() return get_reader()
def has_webhooks() -> bool:
"""Return whether at least one global webhook is configured."""
reader: Reader = get_reader()
webhooks = list(reader.get_tag((), "webhooks", []))
return bool(webhooks)
# Time constants for relative time formatting # Time constants for relative time formatting
SECONDS_PER_MINUTE = 60 SECONDS_PER_MINUTE = 60
SECONDS_PER_HOUR = 3600 SECONDS_PER_HOUR = 3600
@ -192,7 +185,6 @@ templates.env.filters["encode_url"] = lambda url: urllib.parse.quote(str(url)) i
templates.env.filters["discord_markdown"] = markdownify # pyright: ignore[reportArgumentType] templates.env.filters["discord_markdown"] = markdownify # pyright: ignore[reportArgumentType]
templates.env.filters["relative_time"] = relative_time templates.env.filters["relative_time"] = relative_time
templates.env.globals["get_backup_path"] = get_backup_path # pyright: ignore[reportArgumentType] templates.env.globals["get_backup_path"] = get_backup_path # pyright: ignore[reportArgumentType]
templates.env.globals["has_webhooks"] = has_webhooks # pyright: ignore[reportArgumentType]
@app.post("/add_webhook") @app.post("/add_webhook")
@ -221,15 +213,11 @@ async def post_add_webhook(
# Example: [{"name": "webhook_name", "url": "webhook_url"}] # Example: [{"name": "webhook_name", "url": "webhook_url"}]
webhooks = cast("list[dict[str, str]]", webhooks) webhooks = cast("list[dict[str, str]]", webhooks)
clean_webhook_url: str = webhook_url.strip()
if not is_url_valid(clean_webhook_url):
raise HTTPException(status_code=400, detail="Invalid webhook URL")
# Only add the webhook if it doesn't already exist. # Only add the webhook if it doesn't already exist.
stripped_webhook_name = webhook_name.strip() stripped_webhook_name = webhook_name.strip()
if all(webhook["name"] != stripped_webhook_name for webhook in webhooks): if all(webhook["name"] != stripped_webhook_name for webhook in webhooks):
# Add the new webhook to the list of webhooks. # Add the new webhook to the list of webhooks.
webhooks.append({"name": webhook_name.strip(), "url": clean_webhook_url}) webhooks.append({"name": webhook_name.strip(), "url": webhook_url.strip()})
reader.set_tag((), "webhooks", webhooks) # pyright: ignore[reportArgumentType] reader.set_tag((), "webhooks", webhooks) # pyright: ignore[reportArgumentType]
@ -312,52 +300,6 @@ async def post_create_feed(
return RedirectResponse(url=f"/feed?feed_url={urllib.parse.quote(clean_feed_url)}", status_code=303) return RedirectResponse(url=f"/feed?feed_url={urllib.parse.quote(clean_feed_url)}", status_code=303)
@app.post("/attach_feed_webhook")
async def post_attach_feed_webhook(
feed_url: Annotated[str, Form()],
webhook_dropdown: Annotated[str, Form()],
reader: Annotated[Reader, Depends(get_reader_dependency)],
redirect_to: Annotated[str, Form()] = "",
) -> RedirectResponse:
"""Attach an existing feed to one of the configured webhooks.
Args:
feed_url: The feed URL to update.
webhook_dropdown: The webhook name selected from the dropdown.
reader: The Reader instance.
redirect_to: Optional redirect URL after update.
Returns:
RedirectResponse: Redirect to index or feed page.
Raises:
HTTPException: If feed or webhook cannot be found.
"""
clean_feed_url: str = urllib.parse.unquote(feed_url.strip())
selected_webhook_name: str = webhook_dropdown.strip()
try:
reader.get_feed(clean_feed_url)
except FeedNotFoundError as e:
raise HTTPException(status_code=404, detail="Feed not found") from e
webhook_url: str = ""
hooks = cast("list[dict[str, str]]", list(reader.get_tag((), "webhooks", [])))
for hook in hooks:
if hook.get("name") == selected_webhook_name:
webhook_url = hook.get("url", "").strip()
break
if not webhook_url:
raise HTTPException(status_code=404, detail="Webhook not found")
reader.set_tag(clean_feed_url, "webhook", webhook_url) # pyright: ignore[reportArgumentType]
commit_state_change(reader, f"Attach feed {clean_feed_url} to webhook {selected_webhook_name}")
redirect_url: str = redirect_to.strip() or f"/feed?feed_url={urllib.parse.quote(clean_feed_url)}"
return RedirectResponse(url=redirect_url, status_code=303)
@app.post("/pause") @app.post("/pause")
async def post_pause_feed( async def post_pause_feed(
feed_url: Annotated[str, Form()], feed_url: Annotated[str, Form()],
@ -1116,14 +1058,6 @@ async def get_feed( # noqa: C901, PLR0912, PLR0914, PLR0915
except FeedNotFoundError as e: except FeedNotFoundError as e:
raise HTTPException(status_code=404, detail=f"Feed '{clean_feed_url}' not found.\n\n{e}") from e raise HTTPException(status_code=404, detail=f"Feed '{clean_feed_url}' not found.\n\n{e}") from e
webhooks: list[dict[str, str]] = cast("list[dict[str, str]]", list(reader.get_tag((), "webhooks", [])))
current_webhook_url: str = str(reader.get_tag(feed.url, "webhook", "")).strip()
current_webhook_name: str = ""
for hook in webhooks:
if hook.get("url", "").strip() == current_webhook_url:
current_webhook_name = hook.get("name", "").strip()
break
# Only show button if more than 10 entries. # Only show button if more than 10 entries.
total_entries: int = reader.get_entry_counts(feed=feed).total or 0 total_entries: int = reader.get_entry_counts(feed=feed).total or 0
is_show_more_entries_button_visible: bool = total_entries > entries_per_page is_show_more_entries_button_visible: bool = total_entries > entries_per_page
@ -1169,9 +1103,6 @@ async def get_feed( # noqa: C901, PLR0912, PLR0914, PLR0915
"total_entries": total_entries, "total_entries": total_entries,
"feed_interval": feed_interval, "feed_interval": feed_interval,
"global_interval": global_interval, "global_interval": global_interval,
"webhooks": webhooks,
"current_webhook_url": current_webhook_url,
"current_webhook_name": current_webhook_name,
} }
return templates.TemplateResponse(request=request, name="feed.html", context=context) return templates.TemplateResponse(request=request, name="feed.html", context=context)
@ -1228,9 +1159,6 @@ async def get_feed( # noqa: C901, PLR0912, PLR0914, PLR0915
"total_entries": total_entries, "total_entries": total_entries,
"feed_interval": feed_interval, "feed_interval": feed_interval,
"global_interval": global_interval, "global_interval": global_interval,
"webhooks": webhooks,
"current_webhook_url": current_webhook_url,
"current_webhook_name": current_webhook_name,
} }
return templates.TemplateResponse(request=request, name="feed.html", context=context) return templates.TemplateResponse(request=request, name="feed.html", context=context)
@ -1372,30 +1300,20 @@ def get_data_from_hook_url(hook_name: str, hook_url: str) -> WebhookInfo:
Returns: Returns:
WebhookInfo: The webhook username, avatar, guild id, etc. WebhookInfo: The webhook username, avatar, guild id, etc.
""" """
clean_hook_url: str = hook_url.strip() our_hook: WebhookInfo = WebhookInfo(custom_name=hook_name, url=hook_url)
our_hook: WebhookInfo = WebhookInfo(custom_name=hook_name, url=clean_hook_url)
# Keep /webhooks usable even if a malformed webhook URL was saved. if hook_url:
if not clean_hook_url or not is_url_valid(clean_hook_url): response: Response = httpx.get(hook_url)
logger.warning("Skipping webhook metadata fetch for invalid URL: %s", clean_hook_url) if response.is_success:
return our_hook webhook_json = json.loads(response.text)
our_hook.webhook_type = webhook_json["type"] or None
try: our_hook.webhook_id = webhook_json["id"] or None
response: Response = httpx.get(clean_hook_url, timeout=10.0) our_hook.name = webhook_json["name"] or None
except httpx.HTTPError as e: our_hook.avatar = webhook_json["avatar"] or None
logger.warning("Failed to fetch webhook metadata for %s: %s", clean_hook_url, e) our_hook.channel_id = webhook_json["channel_id"] or None
return our_hook our_hook.guild_id = webhook_json["guild_id"] or None
our_hook.token = webhook_json["token"] or None
if response.is_success: our_hook.avatar_mod = int(webhook_json["channel_id"] or 0) % 5
webhook_json = json.loads(response.text)
our_hook.webhook_type = webhook_json["type"] or None
our_hook.webhook_id = webhook_json["id"] or None
our_hook.name = webhook_json["name"] or None
our_hook.avatar = webhook_json["avatar"] or None
our_hook.channel_id = webhook_json["channel_id"] or None
our_hook.guild_id = webhook_json["guild_id"] or None
our_hook.token = webhook_json["token"] or None
our_hook.avatar_mod = int(webhook_json["channel_id"] or 0) % 5
return our_hook return our_hook
@ -1731,9 +1649,6 @@ def modify_webhook(
webhooks = cast("list[dict[str, str]]", webhooks) webhooks = cast("list[dict[str, str]]", webhooks)
old_hook_clean: str = old_hook.strip() old_hook_clean: str = old_hook.strip()
new_hook_clean: str = new_hook.strip() new_hook_clean: str = new_hook.strip()
if not is_url_valid(new_hook_clean):
raise HTTPException(status_code=400, detail="Invalid webhook URL")
webhook_modified: bool = False webhook_modified: bool = False
for hook in webhooks: for hook in webhooks:

View file

@ -5,7 +5,7 @@
{% block content %} {% block content %}
<div class="row g-3 feed-page"> <div class="row g-3 feed-page">
<div class="col-12"> <div class="col-12">
<article class="card border border-dark shadow-sm text-light rounded-0"> <article class="card border border-dark shadow-sm text-light">
<div class="card-body p-3 p-md-4 text-light"> <div class="card-body p-3 p-md-4 text-light">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3"> <div class="d-flex flex-wrap justify-content-between align-items-start gap-3">
<div class="feed-page__content"> <div class="feed-page__content">
@ -105,7 +105,7 @@
</form> </form>
{% if screenshot_layout == "mobile" %} {% if screenshot_layout == "mobile" %}
<form action="/use_screenshot_desktop" method="post" class="d-inline"> <form action="/use_screenshot_desktop" method="post" class="d-inline">
<button class="btn btn-outline-light btn-sm" <button class="btn btn-outline-secondary btn-sm"
name="feed_url" name="feed_url"
value="{{ feed.url }}">Use desktop screenshot layout</button> value="{{ feed.url }}">Use desktop screenshot layout</button>
</form> </form>
@ -138,18 +138,18 @@
</section> </section>
<section class="mt-4 pt-3 border-top border-secondary-subtle"> <section class="mt-4 pt-3 border-top border-secondary-subtle">
<h3 class="h6 text-uppercase text-muted mb-3">Customization</h3> <h3 class="h6 text-uppercase text-muted mb-3">Customization</h3>
<div class="d-flex flex-wrap gap-2"> <div class="d-flex flex-column align-items-start gap-2">
<a class="btn btn-sm btn-outline-light" <a class="text-muted text-decoration-none"
href="/whitelist?feed_url={{ feed.url|encode_url }}">Whitelist</a> href="/whitelist?feed_url={{ feed.url|encode_url }}">Whitelist</a>
<a class="btn btn-sm btn-outline-light" <a class="text-muted text-decoration-none"
href="/blacklist?feed_url={{ feed.url|encode_url }}">Blacklist</a> href="/blacklist?feed_url={{ feed.url|encode_url }}">Blacklist</a>
<a class="btn btn-sm btn-outline-light" <a class="text-muted text-decoration-none"
href="/custom?feed_url={{ feed.url|encode_url }}"> href="/custom?feed_url={{ feed.url|encode_url }}">
Customize message Customize message
{% if delivery_mode == "text" %}(Currently active){% endif %} {% if delivery_mode == "text" %}(Currently active){% endif %}
</a> </a>
{% if not "youtube.com/feeds/videos.xml" in feed.url %} {% if not "youtube.com/feeds/videos.xml" in feed.url %}
<a class="btn btn-sm btn-outline-light" <a class="text-muted text-decoration-none"
href="/embed?feed_url={{ feed.url|encode_url }}"> href="/embed?feed_url={{ feed.url|encode_url }}">
Customize embed Customize embed
{% if delivery_mode == "embed" %}(Currently active){% endif %} {% if delivery_mode == "embed" %}(Currently active){% endif %}
@ -171,64 +171,23 @@
</div> </div>
</form> </form>
</section> </section>
<section class="mt-4 pt-3 border-top border-secondary-subtle">
<h3 class="h6 text-uppercase text-muted mb-3">Webhook</h3>
{% if current_webhook_name %}
<p class="text-muted mb-3">
Current webhook:
<strong>{{ current_webhook_name }}</strong>
</p>
{% elif current_webhook_url %}
<p class="text-warning mb-3">This feed references a missing webhook. Choose a webhook below to reattach it.</p>
{% else %}
<p class="text-warning mb-3">No webhook is attached to this feed yet.</p>
{% endif %}
{% if webhooks %}
<form action="/attach_feed_webhook"
method="post"
class="d-flex flex-wrap align-items-center gap-2 mb-0">
<input type="hidden" name="feed_url" value="{{ feed.url }}" />
<input type="hidden"
name="redirect_to"
value="/feed?feed_url={{ feed.url|encode_url }}" />
<select name="webhook_dropdown"
class="form-select form-select-sm bg-dark border-dark text-muted"
required>
<option value=""
disabled
{% if not current_webhook_name %}selected{% endif %}>
Select webhook...
</option>
{% for hook in webhooks %}
<option value="{{ hook.name }}"
{% if hook.name == current_webhook_name %}selected{% endif %}>
{{ hook.name }}
</option>
{% endfor %}
</select>
<button class="btn btn-outline-light btn-sm" type="submit">Save webhook</button>
</form>
{% else %}
<p class="text-muted mb-0">Add a webhook first to attach this feed.</p>
{% endif %}
</section>
<section class="mt-4 pt-3 border-top border-secondary-subtle"> <section class="mt-4 pt-3 border-top border-secondary-subtle">
<h3 class="h6 text-uppercase text-muted mb-3">Feed Information</h3> <h3 class="h6 text-uppercase text-muted mb-3">Feed Information</h3>
<div class="row g-2 text-muted small"> <div class="row g-2 text-muted small">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<div class="p-2">Added: {{ feed.added | relative_time }}</div> <div class="p-2 border border-secondary rounded">Added: {{ feed.added | relative_time }}</div>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<div class="p-2">Last Updated: {{ feed.last_updated | relative_time }}</div> <div class="p-2 border border-secondary rounded">Last Updated: {{ feed.last_updated | relative_time }}</div>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<div class="p-2">Last Retrieved: {{ feed.last_retrieved | relative_time }}</div> <div class="p-2 border border-secondary rounded">Last Retrieved: {{ feed.last_retrieved | relative_time }}</div>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<div class="p-2">Next Update: {{ feed.update_after | relative_time }}</div> <div class="p-2 border border-secondary rounded">Next Update: {{ feed.update_after | relative_time }}</div>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<div class="p-2"> <div class="p-2 border border-secondary rounded">
Updates: Updates:
<span class="badge {{ 'bg-success' if feed.updates_enabled else 'bg-danger' }}"> <span class="badge {{ 'bg-success' if feed.updates_enabled else 'bg-danger' }}">
{{ 'Enabled' if feed.updates_enabled else 'Disabled' }} {{ 'Enabled' if feed.updates_enabled else 'Disabled' }}
@ -288,7 +247,7 @@
</article> </article>
</div> </div>
<div class="col-12"> <div class="col-12">
<section class="card border border-dark shadow-sm text-light rounded-0"> <section class="card border border-dark shadow-sm text-light">
<div class="card-header bg-transparent text-muted border-secondary">Rendered HTML content</div> <div class="card-header bg-transparent text-muted border-secondary">Rendered HTML content</div>
<div class="card-body p-0"> <div class="card-body p-0">
<pre class="m-0 p-3 feed-page__pre">{{ html|safe }}</pre> <pre class="m-0 p-3 feed-page__pre">{{ html|safe }}</pre>

View file

@ -133,41 +133,20 @@
<ul class="list-group text-danger"> <ul class="list-group text-danger">
Feeds without attached webhook: Feeds without attached webhook:
{% for feed in feeds_without_attached_webhook %} {% for feed in feeds_without_attached_webhook %}
<li class="list-group-item bg-dark border-dark text-danger"> <a class="text-muted" href="/feed?feed_url={{ feed.url|encode_url }}">
<div class="d-flex flex-wrap align-items-center gap-2"> {# Display username@youtube for YouTube feeds #}
<a class="text-muted" href="/feed?feed_url={{ feed.url|encode_url }}"> {% if "youtube.com/feeds/videos.xml" in feed.url %}
{# Display username@youtube for YouTube feeds #} {% if "user=" in feed.url %}
{% if "youtube.com/feeds/videos.xml" in feed.url %} {{ feed.url.split("user=")[1] }}@youtube
{% if "user=" in feed.url %} {% elif "channel_id=" in feed.url %}
{{ feed.url.split("user=")[1] }}@youtube {{ feed.title if feed.title else feed.url.split("channel_id=")[1] }}@youtube
{% elif "channel_id=" in feed.url %}
{{ feed.title if feed.title else feed.url.split("channel_id=")[1] }}@youtube
{% else %}
{{ feed.url }}
{% endif %}
{% else %}
{{ feed.url }}
{% endif %}
</a>
{% if webhooks %}
<form action="/attach_feed_webhook"
method="post"
class="d-flex flex-wrap align-items-center gap-2 ms-md-auto">
<input type="hidden" name="feed_url" value="{{ feed.url }}" />
<input type="hidden" name="redirect_to" value="/" />
<select name="webhook_dropdown"
class="form-select form-select-sm bg-dark border-dark text-muted"
required>
<option value="" selected disabled>Select webhook...</option>
{% for hook in webhooks %}<option value="{{ hook.name }}">{{ hook.name }}</option>{% endfor %}
</select>
<button class="btn btn-outline-light btn-sm" type="submit">Attach</button>
</form>
{% else %} {% else %}
<span class="text-muted small">Add a webhook first to attach this feed.</span> {{ feed.url }}
{% endif %} {% endif %}
</div> {% else %}
</li> {{ feed.url }}
{% endif %}
</a>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>

View file

@ -12,12 +12,10 @@
<a class="nav-link" href="/">Feeds</a> <a class="nav-link" href="/">Feeds</a>
</li> </li>
<li class="nav-item nav-link d-none d-md-block">|</li> <li class="nav-item nav-link d-none d-md-block">|</li>
{% if has_webhooks() %} <li class="nav-item">
<li class="nav-item"> <a class="nav-link" href="/add">Add feed</a>
<a class="nav-link" href="/add">Add feed</a> </li>
</li> <li class="nav-item nav-link d-none d-md-block">|</li>
<li class="nav-item nav-link d-none d-md-block">|</li>
{% endif %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/webhooks">Webhooks</a> <a class="nav-link" href="/webhooks">Webhooks</a>
</li> </li>

View file

@ -25,7 +25,6 @@ if TYPE_CHECKING:
import pytest import pytest
from httpx import Response from httpx import Response
from reader import Entry from reader import Entry
from reader import Reader
client: TestClient = TestClient(app) client: TestClient = TestClient(app)
webhook_name: str = "Hello, I am a webhook!" webhook_name: str = "Hello, I am a webhook!"
@ -89,56 +88,6 @@ def test_add_webhook() -> None:
assert webhook_name in response.text, f"Webhook not found in /webhooks: {response.text}" assert webhook_name in response.text, f"Webhook not found in /webhooks: {response.text}"
def test_add_webhook_rejects_invalid_url() -> None:
"""Adding a webhook with a non-URL value should fail validation."""
response: Response = client.post(
url="/add_webhook",
data={"webhook_name": "Invalid URL Hook", "webhook_url": "not-a-url"},
)
assert response.status_code == 400, f"Expected invalid webhook URL to be rejected: {response.text}"
assert "Invalid webhook URL" in response.text
def test_add_webhook_allows_valid_url_after_invalid_attempt() -> None:
"""A rejected invalid webhook URL should not prevent a later valid add."""
response: Response = client.post(url="/delete_webhook", data={"webhook_url": webhook_url})
assert response.status_code == 200, f"Failed to delete webhook: {response.text}"
response = client.post(
url="/add_webhook",
data={"webhook_name": "Invalid URL Hook", "webhook_url": "not-a-url"},
)
assert response.status_code == 400, f"Expected invalid webhook URL to be rejected: {response.text}"
assert "Invalid webhook URL" in response.text
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 after invalid attempt: {response.text}"
response = client.get(url="/webhooks")
assert response.status_code == 200, f"Failed to get /webhooks: {response.text}"
assert webhook_name in response.text, f"Webhook not found in /webhooks: {response.text}"
response = client.post(url="/delete_webhook", data={"webhook_url": webhook_url})
assert response.status_code == 200, f"Failed to delete webhook: {response.text}"
def test_webhooks_page_handles_invalid_stored_webhook_url() -> None:
"""/webhooks should render even if a malformed webhook URL is present in storage."""
reader: Reader = get_reader_dependency()
malformed_webhook_name = "Malformed hook"
malformed_webhook_url = "definitely-not-a-url"
reader.set_tag((), "webhooks", [{"name": malformed_webhook_name, "url": malformed_webhook_url}]) # pyright: ignore[reportArgumentType]
response: Response = client.get(url="/webhooks")
assert response.status_code == 200, f"/webhooks should not crash for malformed URLs: {response.text}"
assert malformed_webhook_name in response.text
def test_create_feed() -> None: def test_create_feed() -> None:
"""Test the /create_feed page.""" """Test the /create_feed page."""
# Ensure webhook exists for this test regardless of test order. # Ensure webhook exists for this test regardless of test order.
@ -248,28 +197,6 @@ def test_add_page_shows_global_default_delivery_mode_hint() -> None:
assert "text" in response.text assert "text" in response.text
def test_navbar_add_feed_visible_only_when_webhooks_exist() -> None:
reader: Reader = get_reader_dependency()
reader.set_tag((), "webhooks", []) # pyright: ignore[reportArgumentType]
response: Response = client.get(url="/")
assert response.status_code == 200, f"/ failed: {response.text}"
assert '<a class="nav-link" href="/add">Add feed</a>' not in response.text
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="/")
assert response.status_code == 200, f"/ failed: {response.text}"
assert '<a class="nav-link" href="/add">Add feed</a>' in response.text
cleanup_response: Response = client.post(url="/delete_webhook", data={"webhook_url": webhook_url})
assert cleanup_response.status_code == 200, f"Failed to clean up webhook: {cleanup_response.text}"
def test_c3kay_feed_delivery_mode_toggle_routes_update_stored_tags() -> None: def test_c3kay_feed_delivery_mode_toggle_routes_update_stored_tags() -> None:
reader = get_reader_dependency() reader = get_reader_dependency()
c3kay_feed_url = "https://feeds.c3kay.de/hoyolab-ui-toggle-test.xml" c3kay_feed_url = "https://feeds.c3kay.de/hoyolab-ui-toggle-test.xml"
@ -609,114 +536,6 @@ def test_delete_webhook() -> None:
assert webhook_name not in response3.text, f"Webhook found in /webhooks: {response3.text}" assert webhook_name not in response3.text, f"Webhook found in /webhooks: {response3.text}"
def test_attach_feed_webhook_from_index() -> None:
"""Feeds without attached webhook should be attachable from the index page."""
original_webhook_name = "original-webhook"
original_webhook_url = "https://discord.com/api/webhooks/111/original"
replacement_webhook_name = "replacement-webhook"
replacement_webhook_url = "https://discord.com/api/webhooks/222/replacement"
# Start clean.
client.post(url="/remove", data={"feed_url": feed_url})
client.post(url="/delete_webhook", data={"webhook_url": original_webhook_url})
client.post(url="/delete_webhook", data={"webhook_url": replacement_webhook_url})
# Add a webhook and a feed attached to it.
response = client.post(
url="/add_webhook",
data={"webhook_name": original_webhook_name, "webhook_url": original_webhook_url},
)
assert response.status_code == 200, f"Failed to add original webhook: {response.text}"
response = client.post(url="/add", data={"feed_url": feed_url, "webhook_dropdown": original_webhook_name})
assert response.status_code == 200, f"Failed to add feed: {response.text}"
# Remove the original webhook so feed becomes "without attached webhook".
response = client.post(url="/delete_webhook", data={"webhook_url": original_webhook_url})
assert response.status_code == 200, f"Failed to delete original webhook: {response.text}"
# Add a replacement webhook we can attach to.
response = client.post(
url="/add_webhook",
data={"webhook_name": replacement_webhook_name, "webhook_url": replacement_webhook_url},
)
assert response.status_code == 200, f"Failed to add replacement webhook: {response.text}"
# The feed should now be listed in "Feeds without attached webhook" section.
response = client.get(url="/")
assert response.status_code == 200, f"Failed to get /: {response.text}"
assert "Feeds without attached webhook:" in response.text
assert "/attach_feed_webhook" in response.text
# Attach the feed to the new webhook.
response = client.post(
url="/attach_feed_webhook",
data={"feed_url": feed_url, "webhook_dropdown": replacement_webhook_name, "redirect_to": "/"},
)
assert response.status_code == 200, f"Failed to attach feed to webhook: {response.text}"
reader = get_reader_dependency()
assert reader.get_tag(feed_url, "webhook", "") == replacement_webhook_url
# Cleanup.
client.post(url="/remove", data={"feed_url": feed_url})
client.post(url="/delete_webhook", data={"webhook_url": replacement_webhook_url})
def test_attach_feed_webhook_from_feed_page() -> None:
"""Feed detail page should allow attaching/replacing webhook directly."""
original_webhook_name = "feed-page-original-webhook"
original_webhook_url = "https://discord.com/api/webhooks/333/original"
replacement_webhook_name = "feed-page-replacement-webhook"
replacement_webhook_url = "https://discord.com/api/webhooks/444/replacement"
# Start clean.
client.post(url="/remove", data={"feed_url": feed_url})
client.post(url="/delete_webhook", data={"webhook_url": original_webhook_url})
client.post(url="/delete_webhook", data={"webhook_url": replacement_webhook_url})
# Create two webhooks and attach feed to original.
response = client.post(
url="/add_webhook",
data={"webhook_name": original_webhook_name, "webhook_url": original_webhook_url},
)
assert response.status_code == 200, f"Failed to add original webhook: {response.text}"
response = client.post(
url="/add_webhook",
data={"webhook_name": replacement_webhook_name, "webhook_url": replacement_webhook_url},
)
assert response.status_code == 200, f"Failed to add replacement webhook: {response.text}"
response = client.post(url="/add", data={"feed_url": feed_url, "webhook_dropdown": original_webhook_name})
assert response.status_code == 200, f"Failed to add feed: {response.text}"
# Feed page should show the webhook form and current webhook label.
response = client.get(url="/feed", params={"feed_url": feed_url})
assert response.status_code == 200, f"Failed to get /feed: {response.text}"
assert "Current webhook:" in response.text
assert "/attach_feed_webhook" in response.text
# Reattach to replacement webhook via endpoint used by feed page form.
response = client.post(
url="/attach_feed_webhook",
data={
"feed_url": feed_url,
"webhook_dropdown": replacement_webhook_name,
"redirect_to": f"/feed?feed_url={urllib.parse.quote(feed_url)}",
},
)
assert response.status_code == 200, f"Failed to reattach feed webhook: {response.text}"
reader = get_reader_dependency()
assert reader.get_tag(feed_url, "webhook", "") == replacement_webhook_url
# Cleanup.
client.post(url="/remove", data={"feed_url": feed_url})
client.post(url="/delete_webhook", data={"webhook_url": original_webhook_url})
client.post(url="/delete_webhook", data={"webhook_url": replacement_webhook_url})
def test_update_feed_not_found() -> None: def test_update_feed_not_found() -> None:
"""Test updating a non-existent feed.""" """Test updating a non-existent feed."""
# Generate a feed URL that does not exist # Generate a feed URL that does not exist