Skip to main content
Glama
test_normalizer.py19.4 kB
""" Unit tests for Reddit response normalizer. Tests the ResponseNormalizer class and normalization functions. """ import pytest from unittest.mock import MagicMock, Mock from datetime import datetime from src.reddit.normalizer import ( ResponseNormalizer, normalizer, normalize_post, normalize_comment, normalize_user, normalize_subreddit, ) class TestResponseNormalizer: """Test ResponseNormalizer class.""" def test_singleton_instance(self): """Test that normalizer singleton exists.""" assert normalizer is not None assert isinstance(normalizer, ResponseNormalizer) class TestNormalizePost: """Test post normalization.""" def create_mock_submission(self, **kwargs): """Create a mock PRAW submission.""" defaults = { 'id': 'test123', 'title': 'Test Post', 'author': MagicMock(name='testuser'), 'subreddit': MagicMock(display_name='python'), 'created_utc': 1699200000.0, 'score': 100, 'upvote_ratio': 0.95, 'num_comments': 50, 'url': 'https://reddit.com/r/python/test', 'permalink': '/r/python/comments/test123/test_post/', 'selftext': 'This is a test post', 'link_flair_text': 'Discussion', 'is_self': True, 'is_video': False, 'over_18': False, 'spoiler': False, 'stickied': False, 'locked': False, 'archived': False, } defaults.update(kwargs) mock_submission = MagicMock() for key, value in defaults.items(): setattr(mock_submission, key, value) return mock_submission def test_normalize_basic_post(self): """Test normalizing a basic post.""" submission = self.create_mock_submission() result = normalize_post(submission) assert result['id'] == 'test123' assert result['type'] == 'post' assert result['title'] == 'Test Post' assert result['author'] == 'testuser' assert result['subreddit'] == 'python' assert result['score'] == 100 assert result['upvote_ratio'] == 0.95 assert result['num_comments'] == 50 def test_normalize_post_with_deleted_author(self): """Test normalizing post with deleted author.""" submission = self.create_mock_submission(author=None) result = normalize_post(submission) assert result['author'] == '[deleted]' def test_normalize_post_with_long_selftext(self): """Test that long selftext is truncated.""" long_text = 'a' * 2000 submission = self.create_mock_submission(selftext=long_text) result = normalize_post(submission) assert len(result['selftext']) == 1000 assert result['selftext'] == 'a' * 1000 def test_normalize_post_with_empty_selftext(self): """Test normalizing post with no selftext.""" submission = self.create_mock_submission(selftext='') result = normalize_post(submission) assert result['selftext'] == '' def test_normalize_post_permalink_format(self): """Test that permalink is properly formatted.""" submission = self.create_mock_submission( permalink='/r/test/comments/123/title/' ) result = normalize_post(submission) assert result['permalink'].startswith('https://reddit.com') assert '/r/test/comments/123/' in result['permalink'] def test_normalize_video_post(self): """Test normalizing video post.""" submission = self.create_mock_submission( is_video=True, is_self=False ) result = normalize_post(submission) assert result['is_video'] is True assert result['is_self'] is False def test_normalize_nsfw_post(self): """Test normalizing NSFW post.""" submission = self.create_mock_submission(over_18=True) result = normalize_post(submission) assert result['over_18'] is True def test_normalize_stickied_post(self): """Test normalizing stickied post.""" submission = self.create_mock_submission(stickied=True) result = normalize_post(submission) assert result['stickied'] is True def test_normalize_post_batch(self): """Test normalizing multiple posts.""" submissions = [ self.create_mock_submission(id='post1', title='Post 1'), self.create_mock_submission(id='post2', title='Post 2'), self.create_mock_submission(id='post3', title='Post 3'), ] results = ResponseNormalizer.normalize_post_batch(submissions) assert len(results) == 3 assert results[0]['id'] == 'post1' assert results[1]['id'] == 'post2' assert results[2]['id'] == 'post3' class TestNormalizeComment: """Test comment normalization.""" def create_mock_comment(self, **kwargs): """Create a mock PRAW comment.""" defaults = { 'id': 'comment123', 'author': MagicMock(name='commenter'), 'body': 'This is a comment', 'score': 10, 'created_utc': 1699200000.0, 'depth': 0, 'parent_id': 't3_post123', 'is_submitter': False, 'stickied': False, 'distinguished': None, 'edited': False, 'controversiality': 0, } defaults.update(kwargs) mock_comment = MagicMock() for key, value in defaults.items(): setattr(mock_comment, key, value) return mock_comment def test_normalize_basic_comment(self): """Test normalizing a basic comment.""" comment = self.create_mock_comment() result = normalize_comment(comment) assert result['id'] == 'comment123' assert result['type'] == 'comment' assert result['author'] == 'commenter' assert result['body'] == 'This is a comment' assert result['score'] == 10 assert result['depth'] == 0 assert result['parent_id'] == 't3_post123' def test_normalize_deleted_comment(self): """Test normalizing deleted comment.""" comment = self.create_mock_comment(author=None) result = normalize_comment(comment) assert result['author'] == '[deleted]' def test_normalize_removed_comment(self): """Test normalizing removed comment.""" comment = self.create_mock_comment(body='') result = normalize_comment(comment) assert result['body'] == '[removed]' def test_normalize_edited_comment(self): """Test normalizing edited comment.""" edit_timestamp = 1699210000.0 comment = self.create_mock_comment(edited=edit_timestamp) result = normalize_comment(comment) assert result['edited'] == int(edit_timestamp) def test_normalize_unedited_comment(self): """Test normalizing unedited comment.""" comment = self.create_mock_comment(edited=False) result = normalize_comment(comment) assert result['edited'] is False def test_normalize_submitter_comment(self): """Test normalizing comment by post author.""" comment = self.create_mock_comment(is_submitter=True) result = normalize_comment(comment) assert result['is_submitter'] is True def test_normalize_distinguished_comment(self): """Test normalizing distinguished comment.""" comment = self.create_mock_comment(distinguished='moderator') result = normalize_comment(comment) assert result['distinguished'] == 'moderator' def test_normalize_nested_comment(self): """Test normalizing nested comment.""" comment = self.create_mock_comment( depth=2, parent_id='t1_parent_comment' ) result = normalize_comment(comment) assert result['depth'] == 2 assert result['parent_id'] == 't1_parent_comment' def test_normalize_comment_without_depth(self): """Test normalizing comment without depth attribute.""" comment = MagicMock() comment.id = 'test' comment.author = MagicMock(name='user') comment.body = 'test' comment.score = 1 comment.created_utc = 1699200000.0 comment.parent_id = 't3_post' comment.is_submitter = False comment.stickied = False comment.distinguished = None comment.edited = False # No depth attribute delattr(comment, 'depth') result = normalize_comment(comment) assert result['depth'] == 0 # Default value def test_normalize_comment_batch(self): """Test normalizing multiple comments.""" comments = [ self.create_mock_comment(id='c1', body='Comment 1'), self.create_mock_comment(id='c2', body='Comment 2'), ] results = ResponseNormalizer.normalize_comment_batch(comments) assert len(results) == 2 assert results[0]['id'] == 'c1' assert results[1]['id'] == 'c2' class TestNormalizeUser: """Test user normalization.""" def create_mock_redditor(self, **kwargs): """Create a mock PRAW redditor.""" defaults = { 'name': 'testuser', 'id': 'user123', 'created_utc': 1699200000.0, 'link_karma': 1000, 'comment_karma': 5000, 'is_gold': False, 'is_mod': False, 'has_verified_email': True, 'icon_img': 'https://reddit.com/icon.png', } defaults.update(kwargs) mock_redditor = MagicMock() for key, value in defaults.items(): setattr(mock_redditor, key, value) return mock_redditor def test_normalize_basic_user(self): """Test normalizing a basic user.""" redditor = self.create_mock_redditor() result = normalize_user(redditor) assert result['username'] == 'testuser' assert result['id'] == 'user123' assert result['link_karma'] == 1000 assert result['comment_karma'] == 5000 assert result['is_gold'] is False assert result['is_mod'] is False def test_normalize_gold_user(self): """Test normalizing Reddit Gold user.""" redditor = self.create_mock_redditor(is_gold=True) result = normalize_user(redditor) assert result['is_gold'] is True def test_normalize_moderator_user(self): """Test normalizing moderator user.""" redditor = self.create_mock_redditor(is_mod=True) result = normalize_user(redditor) assert result['is_mod'] is True def test_normalize_user_without_optional_fields(self): """Test normalizing user without optional fields.""" redditor = MagicMock() redditor.name = 'user' redditor.created_utc = 1699200000.0 redditor.link_karma = 100 redditor.comment_karma = 200 # Remove optional attributes for attr in ['id', 'is_gold', 'is_mod', 'has_verified_email', 'icon_img']: if hasattr(redditor, attr): delattr(redditor, attr) result = normalize_user(redditor) assert result['username'] == 'user' assert result['id'] is None assert result['is_gold'] is False assert result['is_mod'] is False class TestNormalizeSubreddit: """Test subreddit normalization.""" def create_mock_subreddit(self, **kwargs): """Create a mock PRAW subreddit.""" defaults = { 'display_name': 'python', 'id': 'sub123', 'title': 'Python Programming', 'public_description': 'Learn Python', 'subscribers': 1000000, 'active_user_count': 5000, 'created_utc': 1699200000.0, 'over18': False, 'url': '/r/python/', 'icon_img': 'https://reddit.com/icon.png', 'community_icon': 'https://reddit.com/community.png', 'submission_type': 'any', } defaults.update(kwargs) mock_subreddit = MagicMock() for key, value in defaults.items(): setattr(mock_subreddit, key, value) return mock_subreddit def test_normalize_basic_subreddit(self): """Test normalizing a basic subreddit.""" subreddit = self.create_mock_subreddit() result = normalize_subreddit(subreddit) assert result['name'] == 'python' assert result['id'] == 'sub123' assert result['title'] == 'Python Programming' assert result['description'] == 'Learn Python' assert result['subscribers'] == 1000000 assert result['active_users'] == 5000 assert result['over18'] is False def test_normalize_nsfw_subreddit(self): """Test normalizing NSFW subreddit.""" subreddit = self.create_mock_subreddit(over18=True) result = normalize_subreddit(subreddit) assert result['over18'] is True def test_normalize_subreddit_url_format(self): """Test that subreddit URL is properly formatted.""" subreddit = self.create_mock_subreddit(url='/r/python/') result = normalize_subreddit(subreddit) assert result['url'].startswith('https://reddit.com') assert '/r/python/' in result['url'] class TestAddMetadata: """Test metadata addition to responses.""" def test_add_basic_metadata(self): """Test adding basic metadata.""" data = {"test": "data"} result = ResponseNormalizer.add_metadata(data) assert 'data' in result assert 'metadata' in result assert result['data'] == data assert 'timestamp' in result['metadata'] def test_add_cache_metadata(self): """Test adding cache-related metadata.""" data = {"test": "data"} result = ResponseNormalizer.add_metadata( data, cached=True, cache_age_seconds=120 ) assert result['metadata']['cached'] is True assert result['metadata']['cache_age_seconds'] == 120 def test_add_rate_limit_metadata(self): """Test adding rate limit metadata.""" data = {"test": "data"} result = ResponseNormalizer.add_metadata( data, rate_limit_remaining=85 ) assert result['metadata']['rate_limit_remaining'] == 85 def test_add_performance_metadata(self): """Test adding performance metadata.""" data = {"test": "data"} result = ResponseNormalizer.add_metadata( data, execution_time_ms=123.45, reddit_api_calls=2 ) assert result['metadata']['execution_time_ms'] == 123.45 assert result['metadata']['reddit_api_calls'] == 2 def test_add_all_metadata(self): """Test adding all metadata fields.""" data = {"test": "data"} result = ResponseNormalizer.add_metadata( data, cached=True, cache_age_seconds=300, rate_limit_remaining=95, execution_time_ms=50.0, reddit_api_calls=1 ) metadata = result['metadata'] assert metadata['cached'] is True assert metadata['cache_age_seconds'] == 300 assert metadata['rate_limit_remaining'] == 95 assert metadata['execution_time_ms'] == 50.0 assert metadata['reddit_api_calls'] == 1 assert 'timestamp' in metadata def test_metadata_timestamp_format(self): """Test that timestamp is in ISO format.""" data = {"test": "data"} result = ResponseNormalizer.add_metadata(data) timestamp = result['metadata']['timestamp'] # Should be parseable as ISO format parsed = datetime.fromisoformat(timestamp) assert isinstance(parsed, datetime) class TestConvenienceFunctions: """Test convenience normalization functions.""" def test_normalize_post_function(self): """Test normalize_post convenience function.""" mock_submission = MagicMock() mock_submission.id = 'test' mock_submission.title = 'Test' mock_submission.author = MagicMock(name='user') mock_submission.subreddit = MagicMock(display_name='test') mock_submission.created_utc = 1699200000.0 mock_submission.score = 10 mock_submission.upvote_ratio = 0.9 mock_submission.num_comments = 5 mock_submission.url = 'https://reddit.com' mock_submission.permalink = '/r/test/test' mock_submission.selftext = 'text' mock_submission.link_flair_text = None mock_submission.is_self = True mock_submission.is_video = False mock_submission.over_18 = False mock_submission.spoiler = False mock_submission.stickied = False mock_submission.locked = False mock_submission.archived = False result = normalize_post(mock_submission) assert result['id'] == 'test' assert result['type'] == 'post' def test_normalize_comment_function(self): """Test normalize_comment convenience function.""" mock_comment = MagicMock() mock_comment.id = 'test' mock_comment.author = MagicMock(name='user') mock_comment.body = 'comment' mock_comment.score = 5 mock_comment.created_utc = 1699200000.0 mock_comment.depth = 0 mock_comment.parent_id = 't3_post' mock_comment.is_submitter = False mock_comment.stickied = False mock_comment.distinguished = None mock_comment.edited = False mock_comment.controversiality = 0 result = normalize_comment(mock_comment) assert result['id'] == 'test' assert result['type'] == 'comment' def test_normalize_user_function(self): """Test normalize_user convenience function.""" mock_redditor = MagicMock() mock_redditor.name = 'testuser' mock_redditor.id = 'user123' mock_redditor.created_utc = 1699200000.0 mock_redditor.link_karma = 100 mock_redditor.comment_karma = 200 mock_redditor.is_gold = False mock_redditor.is_mod = False mock_redditor.has_verified_email = True mock_redditor.icon_img = None result = normalize_user(mock_redditor) assert result['username'] == 'testuser' def test_normalize_subreddit_function(self): """Test normalize_subreddit convenience function.""" mock_subreddit = MagicMock() mock_subreddit.display_name = 'python' mock_subreddit.id = 'sub123' mock_subreddit.title = 'Python' mock_subreddit.public_description = 'Python programming' mock_subreddit.subscribers = 1000 mock_subreddit.active_user_count = 100 mock_subreddit.created_utc = 1699200000.0 mock_subreddit.over18 = False mock_subreddit.url = '/r/python/' mock_subreddit.icon_img = None mock_subreddit.community_icon = None mock_subreddit.submission_type = 'any' result = normalize_subreddit(mock_subreddit) assert result['name'] == 'python'

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/padak/apify-actor-reddit-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server