Fix /post_entry
This commit is contained in:
parent
71695c2987
commit
5323245cf6
2 changed files with 126 additions and 4 deletions
|
|
@ -1080,7 +1080,11 @@ def create_html_for_feed( # noqa: C901, PLR0914
|
||||||
)
|
)
|
||||||
|
|
||||||
entry_id: str = urllib.parse.quote(entry.id)
|
entry_id: str = urllib.parse.quote(entry.id)
|
||||||
to_discord_html: str = f"<a class='text-muted' href='/post_entry?entry_id={entry_id}'>Send to Discord</a>"
|
encoded_source_feed_url: str = urllib.parse.quote(source_feed_url)
|
||||||
|
to_discord_html: str = (
|
||||||
|
f"<a class='text-muted' href='/post_entry?entry_id={entry_id}&feed_url={encoded_source_feed_url}'>"
|
||||||
|
"Send to Discord</a>"
|
||||||
|
)
|
||||||
|
|
||||||
# Check if this is a YouTube feed entry and the entry has a link
|
# Check if this is a YouTube feed entry and the entry has a link
|
||||||
is_youtube_feed = "youtube.com/feeds/videos.xml" in entry.feed.url
|
is_youtube_feed = "youtube.com/feeds/videos.xml" in entry.feed.url
|
||||||
|
|
@ -1427,18 +1431,32 @@ async def search(
|
||||||
async def post_entry(
|
async def post_entry(
|
||||||
entry_id: str,
|
entry_id: str,
|
||||||
reader: Annotated[Reader, Depends(get_reader_dependency)],
|
reader: Annotated[Reader, Depends(get_reader_dependency)],
|
||||||
|
feed_url: str = "",
|
||||||
):
|
):
|
||||||
"""Send single entry to Discord.
|
"""Send single entry to Discord.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
entry_id: The entry to send.
|
entry_id: The entry to send.
|
||||||
|
feed_url: Optional feed URL used to disambiguate entries with identical IDs.
|
||||||
reader: The Reader instance.
|
reader: The Reader instance.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
RedirectResponse: Redirect to the feed page.
|
RedirectResponse: Redirect to the feed page.
|
||||||
"""
|
"""
|
||||||
unquoted_entry_id: str = urllib.parse.unquote(entry_id)
|
unquoted_entry_id: str = urllib.parse.unquote(entry_id)
|
||||||
entry: Entry | None = next((entry for entry in reader.get_entries() if entry.id == unquoted_entry_id), None)
|
clean_feed_url: str = urllib.parse.unquote(feed_url.strip()) if feed_url else ""
|
||||||
|
|
||||||
|
# Prefer feed-scoped lookup when feed_url is provided. This avoids ambiguity when
|
||||||
|
# multiple feeds contain entries with the same ID.
|
||||||
|
entry: Entry | None = None
|
||||||
|
if clean_feed_url:
|
||||||
|
entry = next(
|
||||||
|
(entry for entry in reader.get_entries(feed=clean_feed_url) if entry.id == unquoted_entry_id),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
entry = next((entry for entry in reader.get_entries() if entry.id == unquoted_entry_id), None)
|
||||||
|
|
||||||
if entry is None:
|
if entry is None:
|
||||||
return HTMLResponse(status_code=404, content=f"Entry '{entry_id}' not found.")
|
return HTMLResponse(status_code=404, content=f"Entry '{entry_id}' not found.")
|
||||||
|
|
||||||
|
|
@ -1446,8 +1464,8 @@ async def post_entry(
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Redirect to the feed page.
|
# Redirect to the feed page.
|
||||||
clean_feed_url: str = entry.feed.url.strip()
|
redirect_feed_url: str = entry.feed.url.strip()
|
||||||
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(redirect_feed_url)}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/modify_webhook", response_class=HTMLResponse)
|
@app.post("/modify_webhook", response_class=HTMLResponse)
|
||||||
|
|
|
||||||
|
|
@ -480,6 +480,110 @@ def test_update_feed_not_found() -> None:
|
||||||
assert "Feed not found" in response.text
|
assert "Feed not found" in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_entry_send_to_discord() -> None:
|
||||||
|
"""Test that /post_entry sends an entry to Discord and redirects to the feed page.
|
||||||
|
|
||||||
|
Regression test for the bug where the injected reader was not passed to
|
||||||
|
send_entry_to_discord, meaning the dependency-injected reader was silently ignored.
|
||||||
|
"""
|
||||||
|
# Ensure webhook and feed exist.
|
||||||
|
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}"
|
||||||
|
|
||||||
|
# Retrieve an entry from the feed to get a valid entry ID.
|
||||||
|
reader: main_module.Reader = main_module.get_reader_dependency()
|
||||||
|
entries: list[Entry] = list(reader.get_entries(feed=feed_url, limit=1))
|
||||||
|
assert entries, "Feed should have at least one entry to send"
|
||||||
|
entry_to_send: main_module.Entry = entries[0]
|
||||||
|
encoded_id: str = urllib.parse.quote(entry_to_send.id)
|
||||||
|
|
||||||
|
no_redirect_client = TestClient(app, follow_redirects=False)
|
||||||
|
|
||||||
|
# Patch execute_webhook so no real HTTP requests are made to Discord.
|
||||||
|
with patch("discord_rss_bot.feeds.execute_webhook") as mock_execute:
|
||||||
|
response = no_redirect_client.get(
|
||||||
|
url="/post_entry",
|
||||||
|
params={"entry_id": encoded_id, "feed_url": urllib.parse.quote(feed_url)},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 303, f"Expected redirect after sending, got {response.status_code}: {response.text}"
|
||||||
|
location: str = response.headers.get("location", "")
|
||||||
|
assert "feed?feed_url=" in location, f"Should redirect to feed page, got: {location}"
|
||||||
|
assert mock_execute.called, "execute_webhook should have been called to deliver the entry to Discord"
|
||||||
|
|
||||||
|
# Cleanup.
|
||||||
|
client.post(url="/remove", data={"feed_url": feed_url})
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_entry_unknown_id_returns_404() -> None:
|
||||||
|
"""Test that /post_entry returns 404 when the entry ID does not exist."""
|
||||||
|
response: Response = client.get(
|
||||||
|
url="/post_entry",
|
||||||
|
params={"entry_id": "https://nonexistent.example.com/entry-that-does-not-exist"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 404, f"Expected 404 for unknown entry, got {response.status_code}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_entry_uses_feed_url_to_disambiguate_duplicate_ids() -> None:
|
||||||
|
"""When IDs collide across feeds, /post_entry should pick the entry from provided feed_url."""
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class DummyFeed:
|
||||||
|
url: str
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class DummyEntry:
|
||||||
|
id: str
|
||||||
|
feed: DummyFeed
|
||||||
|
feed_url: str
|
||||||
|
|
||||||
|
feed_a = "https://example.com/feed-a.xml"
|
||||||
|
feed_b = "https://example.com/feed-b.xml"
|
||||||
|
shared_id = "https://example.com/shared-entry-id"
|
||||||
|
|
||||||
|
entry_a: Entry = cast("Entry", DummyEntry(id=shared_id, feed=DummyFeed(feed_a), feed_url=feed_a))
|
||||||
|
entry_b: Entry = cast("Entry", DummyEntry(id=shared_id, feed=DummyFeed(feed_b), feed_url=feed_b))
|
||||||
|
|
||||||
|
class StubReader:
|
||||||
|
def get_entries(self, feed: str | None = None) -> list[Entry]:
|
||||||
|
if feed == feed_a:
|
||||||
|
return [entry_a]
|
||||||
|
if feed == feed_b:
|
||||||
|
return [entry_b]
|
||||||
|
return [entry_a, entry_b]
|
||||||
|
|
||||||
|
selected_feed_urls: list[str] = []
|
||||||
|
|
||||||
|
def fake_send_entry_to_discord(entry: Entry, reader: object | None = None) -> None:
|
||||||
|
selected_feed_urls.append(entry.feed.url)
|
||||||
|
|
||||||
|
app.dependency_overrides[get_reader_dependency] = StubReader
|
||||||
|
no_redirect_client = TestClient(app, follow_redirects=False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with patch("discord_rss_bot.main.send_entry_to_discord", side_effect=fake_send_entry_to_discord):
|
||||||
|
response: Response = no_redirect_client.get(
|
||||||
|
url="/post_entry",
|
||||||
|
params={"entry_id": urllib.parse.quote(shared_id), "feed_url": urllib.parse.quote(feed_b)},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 303, f"Expected redirect after sending, got {response.status_code}"
|
||||||
|
assert selected_feed_urls == [feed_b], f"Expected feed-b entry, got: {selected_feed_urls}"
|
||||||
|
|
||||||
|
location = response.headers.get("location", "")
|
||||||
|
assert urllib.parse.quote(feed_b) in location, f"Expected redirect to feed-b page, got: {location}"
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides = {}
|
||||||
|
|
||||||
|
|
||||||
def test_navbar_backup_link_hidden_when_not_configured(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_navbar_backup_link_hidden_when_not_configured(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
"""Test that the backup link is not shown in the navbar when GIT_BACKUP_PATH is not set."""
|
"""Test that the backup link is not shown in the navbar when GIT_BACKUP_PATH is not set."""
|
||||||
# Ensure GIT_BACKUP_PATH is not set
|
# Ensure GIT_BACKUP_PATH is not set
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue