From c8d4cd0a607193eabc9a0660a7287965edbeb8a4 Mon Sep 17 00:00:00 2001 From: iloveyamlfries <288737474+iloveyamlfries@users.noreply.github.com> Date: Sat, 13 Jun 2026 02:06:43 -0500 Subject: [PATCH] Fix Reddit comment API payload --- rdt_cli/client.py | 40 +++++++++++++++++++++++++++++++++++++- rdt_cli/commands/social.py | 11 ++--------- tests/test_cli.py | 24 +++++++++++++++++++++-- tests/test_client.py | 32 ++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 12 deletions(-) diff --git a/rdt_cli/client.py b/rdt_cli/client.py index 7762976..4833201 100644 --- a/rdt_cli/client.py +++ b/rdt_cli/client.py @@ -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 @@ -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 ─────────────────────────────────────────── diff --git a/rdt_cli/commands/social.py b/rdt_cli/commands/social.py index 9c4758f..5b17492 100644 --- a/rdt_cli/commands/social.py +++ b/rdt_cli/commands/social.py @@ -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: @@ -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 @@ -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) @@ -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() @@ -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}") diff --git a/tests/test_cli.py b/tests/test_cli.py index 50e7df2..63ce240 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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__ @@ -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 @@ -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 ────────────────────────────────────────── @@ -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 - diff --git a/tests/test_client.py b/tests/test_client.py index d6bbf71..0fd4bb6 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -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")