Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion rdt_cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,35 @@ def _post(self, url: str, data: dict[str, Any] | None = None) -> Any:
"""POST request."""
return self._write_request("POST", url, data=data)

@staticmethod
def _format_api_errors(errors: Any) -> str:
"""Flatten Reddit's json.errors list into a readable message."""
if not isinstance(errors, list):
return str(errors)

messages: list[str] = []
for error in errors:
if isinstance(error, (list, tuple)):
message = ": ".join(str(part) for part in error if part)
else:
message = str(error)
if message:
messages.append(message)
return "; ".join(messages) or "Reddit API returned an error"

def _raise_for_api_errors(self, response: Any) -> None:
"""Raise when an old Reddit API response contains json.errors."""
if not isinstance(response, dict):
return

json_response = response.get("json")
if not isinstance(json_response, dict):
return

errors = json_response.get("errors")
if errors:
raise RedditApiError(self._format_api_errors(errors), response=response)

# ── Listing helpers ─────────────────────────────────────────────

@staticmethod
Expand Down Expand Up @@ -349,7 +378,16 @@ def subscribe(self, subreddit: str, action: str = "sub") -> dict:

def post_comment(self, parent_fullname: str, text: str) -> dict:
"""Post a comment."""
return self._post(COMMENT_URL, data={"parent": parent_fullname, "text": text})
response = self._post(
COMMENT_URL,
data={
"api_type": "json",
"thing_id": parent_fullname,
"text": text,
},
)
self._raise_for_api_errors(response)
return response

# ── Subscription feed ───────────────────────────────────────────

Expand Down
11 changes: 2 additions & 9 deletions rdt_cli/commands/social.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# ── Helpers ─────────────────────────────────────────────────────────


def _resolve_fullname(id_or_index: str) -> str | None:
def _resolve_fullname(id_or_index: str) -> str:
"""Resolve an ID or short-index to a Reddit fullname (t3_xxx).

Accepts:
Expand All @@ -31,8 +31,7 @@ def _resolve_fullname(id_or_index: str) -> str | None:
pid = item.get("id", "")
if pid:
return f"t3_{pid}"
console.print(f"[yellow]Index {idx} not found in cache[/yellow]")
return None
raise click.ClickException(f"Index {idx} not found in cache")
except ValueError:
pass

Expand Down Expand Up @@ -65,8 +64,6 @@ def upvote(id_or_index: str, undo: bool, down: bool) -> None:
with RedditClient(cred) as client:
client.validate_session()
fullname = _resolve_fullname(id_or_index)
if not fullname:
return
direction = 0 if undo else (-1 if down else 1)
action_label = "Unvoted" if undo else ("⬇ Downvoted" if down else "⬆ Upvoted")
client.vote(fullname, direction=direction)
Expand Down Expand Up @@ -94,8 +91,6 @@ def save(id_or_index: str, undo: bool) -> None:
with RedditClient(cred) as client:
client.validate_session()
fullname = _resolve_fullname(id_or_index)
if not fullname:
return
if undo:
client.unsave_item(fullname)
write_delay()
Expand Down Expand Up @@ -153,8 +148,6 @@ def comment(id_or_index: str, text: str) -> None:
with RedditClient(cred) as client:
client.validate_session()
fullname = _resolve_fullname(id_or_index)
if not fullname:
return
client.post_comment(fullname, text)
write_delay()
console.print(f"[green]✅ Comment posted[/green] on {fullname}")
Expand Down
24 changes: 22 additions & 2 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from unittest.mock import patch

import pytest
from click import ClickException
from click.testing import CliRunner

from rdt_cli import __version__
Expand Down Expand Up @@ -593,7 +594,8 @@ def test_index_no_cache(self, tmp_path, monkeypatch):
from rdt_cli import index_cache
monkeypatch.setattr(index_cache, "INDEX_CACHE_FILE", tmp_path / "none.json")
from rdt_cli.commands.social import _resolve_fullname
assert _resolve_fullname("3") is None
with pytest.raises(ClickException, match="Index 3 not found in cache"):
_resolve_fullname("3")

def test_index_with_cache(self, tmp_path, monkeypatch):
from rdt_cli import index_cache
Expand All @@ -616,6 +618,25 @@ def test_index_with_cache_no_name(self, tmp_path, monkeypatch):
assert _resolve_fullname("1") == "t3_ccc"


class TestSocialCommands:
def test_comment_missing_cached_index_exits_nonzero(self, tmp_path, monkeypatch):
from rdt_cli import index_cache
from rdt_cli.auth import Credential

monkeypatch.setattr(index_cache, "INDEX_CACHE_FILE", tmp_path / "none.json")

with patch("rdt_cli.commands.social.require_auth", return_value=Credential(cookies={"reddit_session": "abc"})):
with patch("rdt_cli.commands.social.RedditClient") as mock_client_class:
mock_client = mock_client_class.return_value.__enter__.return_value
mock_client.validate_session.return_value = {"authenticated": True}

result = runner.invoke(cli, ["comment", "99", "Useful context."])

assert result.exit_code != 0
assert "Index 99 not found in cache" in result.output
mock_client.post_comment.assert_not_called()


# ── Mocked browse commands ──────────────────────────────────────────


Expand Down Expand Up @@ -971,4 +992,3 @@ def test_show_help_shows_compact(self):
result = runner.invoke(cli, ["show", "--help"])
assert result.exit_code == 0
assert "--compact" in result.output

32 changes: 32 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,35 @@ def test_get_more_comments_uses_api_morechildren() -> None:
"raw_json": 1,
},
)


def test_post_comment_uses_reddit_comment_payload() -> None:
cred = Credential(cookies={"reddit_session": "abc"})
with RedditClient(cred) as client:
with patch.object(client, "_post", return_value={"json": {"errors": []}}) as mock_post:
data = client.post_comment("t3_abc123", "Useful context.")

assert data == {"json": {"errors": []}}
mock_post.assert_called_once_with(
"/api/comment",
data={
"api_type": "json",
"thing_id": "t3_abc123",
"text": "Useful context.",
},
)


def test_post_comment_raises_on_reddit_api_errors() -> None:
cred = Credential(cookies={"reddit_session": "abc"})
response = {"json": {"errors": [["BAD_THING", "bad target", "thing_id"]]}}

with RedditClient(cred) as client:
with patch.object(client, "_post", return_value=response):
try:
client.post_comment("t3_abc123", "Useful context.")
except RedditApiError as exc:
assert "BAD_THING" in str(exc)
assert exc.response == response
else:
raise AssertionError("expected RedditApiError")