import httpx import pytest from arcade.sdk.errors import ToolExecutionError from arcade_reddit.client import RedditClient from arcade_reddit.utils import ( create_fullname_for_comment, create_fullname_for_multiple_posts, create_fullname_for_post, create_path_for_post, parse_get_content_of_multiple_posts_response, parse_get_content_of_post_response, parse_get_posts_in_subreddit_response, parse_get_top_level_comments_response, parse_subreddit_rules_response, parse_user_posts_response, resolve_subreddit_access, ) class DummyRedditClientResponse: def __init__(self, status_code): self.status_code = status_code @pytest.mark.parametrize( "identifier, expected", [ ("abcdef", "t1_abcdef"), ("https://www.reddit.com/r/test/comments/1234567890/comment/1abcdef/", "t1_1abcdef"), ("t1_abcdef", "t1_abcdef"), ("/r/test/comments/1234567890/comment/1abcdef/", "t1_1abcdef"), ("not-an-id", pytest.raises(ToolExecutionError)), # Fullname: Invalid (not a Reddit comment id, but a Reddit post id) ("t2_abcdef", pytest.raises(ToolExecutionError)), # URL: Invalid (not a Reddit url to a comment, but to a post) ("https://www.reddit.com/r/test/comments/1234567890", pytest.raises(ToolExecutionError)), # Permalink: Invalid (not a Reddit permalink to a comment, but to a post) ("/r/test/comments/1234567890", pytest.raises(ToolExecutionError)), ], ) def test_create_fullname_for_comment(identifier, expected) -> None: if isinstance(expected, str): assert create_fullname_for_comment(identifier) == expected else: with expected: create_fullname_for_comment(identifier) @pytest.mark.parametrize( "identifier, expected", [ ("https://www.reddit.com/r/test/comments/1234abc/", "/r/test/comments/1234abc/"), ("1234abc", "/comments/1234abc"), ("/r/test/comments/1234abc/", "/r/test/comments/1234abc/"), ("t3_1234abc", "/comments/1234abc"), # URL: invalid (non-reddit domain) ("https://www.example.com/r/test/comments/1234abc/", pytest.raises(ToolExecutionError)), # Post ID: invalid (non-alphanumeric character) ("12!abc", pytest.raises(ToolExecutionError)), # Permalink: invalid (missing the leading "/" so not recognized as a proper permalink) ("r/test/comments/1234abc/", pytest.raises(ToolExecutionError)), # Fullname: invalid (contains an illegal character) ("t3_1234*abc", pytest.raises(ToolExecutionError)), ], ) def test_create_path_for_post(identifier, expected): if isinstance(expected, str): result = create_path_for_post(identifier) assert result == expected else: with expected: create_path_for_post(identifier) @pytest.mark.parametrize( "identifier, expected", [ ("https://www.reddit.com/r/test/comments/1234abc/", "t3_1234abc"), ("1234abc", "t3_1234abc"), ("/r/test/comments/1234abc/", "t3_1234abc"), ("t3_1234abc", "t3_1234abc"), # URL: invalid (missing "/comments/" segment) ("https://www.reddit.com/r/test/1234abc/", pytest.raises(ToolExecutionError)), # Post ID: invalid (non-alphanumeric) ("12!abc", pytest.raises(ToolExecutionError)), # Permalink: invalid (missing "/comments/" segment) ("/r/test/1234abc/", pytest.raises(ToolExecutionError)), # Fullname: invalid (type prefix is for a message, not a post ("t4_1234abc", pytest.raises(ToolExecutionError)), ], ) def test_create_fullname_for_post(identifier, expected): if isinstance(expected, str): result = create_fullname_for_post(identifier) assert result == expected else: with expected: create_fullname_for_post(identifier) @pytest.mark.parametrize( "identifiers, expected_fullnames, expected_warnings", [ ([], [], []), ( ["t3_1234abc", "https://www.reddit.com/r/test/comments/1234abc/"], ["t3_1234abc", "t3_1234abc"], [], ), ( ["t3_1234abc", "not-a-valid-identifier"], ["t3_1234abc"], [ { "message": "'not-a-valid-identifier' is not a valid Reddit post identifier.", "identifier": "not-a-valid-identifier", } ], ), ( ["inv@lid", "not-a-valid-identifier"], [], [ { "message": "'inv@lid' is not a valid Reddit post identifier.", "identifier": "inv@lid", }, { "message": "'not-a-valid-identifier' is not a valid Reddit post identifier.", "identifier": "not-a-valid-identifier", }, ], ), ], ) def test_create_fullname_for_multiple_posts(identifiers, expected_fullnames, expected_warnings): actual_fullnames, actual_warnings = create_fullname_for_multiple_posts(identifiers) assert actual_fullnames == expected_fullnames assert actual_warnings == expected_warnings def test_parse_get_posts_in_subreddit_response_empty(): data = {} expected = {"cursor": None, "posts": []} result = parse_get_posts_in_subreddit_response(data) assert result == expected def test_parse_get_content_of_post_response_empty_and_malformed(): data = [] result = parse_get_content_of_post_response(data) assert result == {} data = None result = parse_get_content_of_post_response(data) assert result == {} # missing expected keys data = [{}] expected = { "id": None, "name": None, "title": None, "author": None, "subreddit": None, "created_utc": None, "num_comments": None, "score": None, "upvote_ratio": None, "upvotes": None, "permalink": None, "url": None, "is_video": None, "body": None, } result = parse_get_content_of_post_response(data) assert result == expected def test_parse_get_content_of_multiple_posts_response_empty_and_malformed(): expected = [] data = {} result = parse_get_content_of_multiple_posts_response(data) assert result == expected data = None result = parse_get_content_of_multiple_posts_response(data) assert result == expected data = [{}] result = parse_get_content_of_multiple_posts_response(data) assert result == expected def test_parse_get_top_level_comments_response_missing_data(): data = [{}] expected = {"comments": [], "num_comments": 0} result = parse_get_top_level_comments_response(data) assert result == expected @pytest.mark.asyncio @pytest.mark.parametrize( "status_code, expected_result, expected_exception", [ (None, {"exists": True, "accessible": True}, None), (404, {"exists": False, "accessible": False}, None), (302, {"exists": False, "accessible": False}, None), (403, {"exists": True, "accessible": False}, None), (500, None, httpx.HTTPStatusError), ], ) async def test_resolve_subreddit_access(status_code, expected_result, expected_exception): class DummyRedditClient(RedditClient): async def get(self, path: str, **kwargs): if status_code is not None: raise httpx.HTTPStatusError( "Error", request=None, response=DummyRedditClientResponse(status_code) ) return "dummy_success" client = DummyRedditClient(token="dummy") # noqa: S106 if expected_exception: with pytest.raises(expected_exception): await resolve_subreddit_access(client, "testsub") else: result = await resolve_subreddit_access(client, "testsub") assert result == expected_result @pytest.mark.parametrize( "data, expected", [ ( { "rules": [ {"priority": 1, "short_name": "Rule1", "description": "Desc1"}, {"priority": 2, "short_name": "Rule2", "description": "Desc2"}, ] }, { "rules": [ {"priority": 1, "title": "Rule1", "body": "Desc1"}, {"priority": 2, "title": "Rule2", "body": "Desc2"}, ] }, ), ({}, {"rules": []}), ], ) def test_parse_subreddit_rules_response(data, expected): result = parse_subreddit_rules_response(data) assert result == expected @pytest.mark.asyncio @pytest.mark.parametrize( "posts_data, expected_cursor, expected_posts", [ ( { "data": { "after": "cursor123", "children": [ { "data": { "id": "1", "name": "t3_1", "title": "Post 1", "author": "user1", "subreddit": "subreddit1", "created_utc": 1712345678, "num_comments": 10, "score": 100, "upvote_ratio": 0.5, "ups": 50, "permalink": "permalink1", "url": "url1", "is_video": False, "selftext": "This is the body of post 1", "all_awardings": [], "allow_live_comments": False, "approved": False, "approved_at_utc": None, } }, ], } }, "cursor123", [ { "id": "1", "name": "t3_1", "title": "Post 1", "author": "user1", "subreddit": "subreddit1", "created_utc": 1712345678, "num_comments": 10, "score": 100, "upvote_ratio": 0.5, "upvotes": 50, "permalink": "permalink1", "url": "url1", "is_video": False, }, ], ), ({"data": {"after": None, "children": []}}, None, []), ], ) async def test_parse_user_posts_response_without_body(posts_data, expected_cursor, expected_posts): dummy_context = object() result = await parse_user_posts_response(dummy_context, posts_data, include_body=False) if expected_cursor: expected = {"cursor": expected_cursor, "posts": expected_posts} else: expected = {"posts": expected_posts} assert result == expected