From 222f20525c2cbe6e93313b55daec4bca78f30131 Mon Sep 17 00:00:00 2001 From: Chris Gerber Date: Wed, 20 Nov 2024 15:01:40 -0800 Subject: [PATCH] feat: twitter langchain actions tests (#53) * first pass implementing tests for twitter langchain * formatting * implementing feedback --- .../actions/social/twitter/account_details.py | 2 +- .../social/twitter/post_tweet_reply.py | 4 +- twitter-langchain/examples/chatbot/chatbot.py | 1 - twitter-langchain/tests/__init__.py | 0 twitter-langchain/tests/actions/__init__.py | 0 .../tests/actions/test_account_details.py | 44 +++++++++++++ .../tests/actions/test_account_mentions.py | 60 ++++++++++++++++++ .../tests/actions/test_post_tweet.py | 56 +++++++++++++++++ .../tests/actions/test_post_tweet_reply.py | 62 +++++++++++++++++++ twitter-langchain/tests/conftest.py | 7 +++ .../tests/factories/twitter_factory.py | 15 +++++ 11 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 twitter-langchain/tests/__init__.py create mode 100644 twitter-langchain/tests/actions/__init__.py create mode 100644 twitter-langchain/tests/actions/test_account_details.py create mode 100644 twitter-langchain/tests/actions/test_account_mentions.py create mode 100644 twitter-langchain/tests/actions/test_post_tweet.py create mode 100644 twitter-langchain/tests/actions/test_post_tweet_reply.py create mode 100644 twitter-langchain/tests/conftest.py create mode 100644 twitter-langchain/tests/factories/twitter_factory.py diff --git a/cdp-agentkit-core/cdp_agentkit_core/actions/social/twitter/account_details.py b/cdp-agentkit-core/cdp_agentkit_core/actions/social/twitter/account_details.py index 5af2281f2..01edbb920 100644 --- a/cdp-agentkit-core/cdp_agentkit_core/actions/social/twitter/account_details.py +++ b/cdp-agentkit-core/cdp_agentkit_core/actions/social/twitter/account_details.py @@ -42,7 +42,7 @@ def account_details(client: tweepy.Client) -> str: message = f"""Successfully retrieved authenticated user account details:\n{dumps(response)}""" except tweepy.errors.TweepyException as e: - message = f"Error retrieving authenticated user account details: {e}" + message = f"Error retrieving authenticated user account details:\n{e}" return message diff --git a/cdp-agentkit-core/cdp_agentkit_core/actions/social/twitter/post_tweet_reply.py b/cdp-agentkit-core/cdp_agentkit_core/actions/social/twitter/post_tweet_reply.py index 52e5c9f88..346159fec 100644 --- a/cdp-agentkit-core/cdp_agentkit_core/actions/social/twitter/post_tweet_reply.py +++ b/cdp-agentkit-core/cdp_agentkit_core/actions/social/twitter/post_tweet_reply.py @@ -10,7 +10,7 @@ This tool will post a reply to a tweet on Twitter. The tool takes the text of the reply and the tweet id to reply to as input. Tweets can be maximum 280 characters. A successful response will return a message with the api response as a json payload: - {"data": {"id": "0123456789012345678", "text": "hellllloooo!", "edit_history_tweet_ids": ["1234567890123456789"]}} + {"data": {"id": "0123456789012345678", "text": "So good to be here!", "edit_history_tweet_ids": ["1234567890123456789"]}} A failure response will return a message with the tweepy client api request error: You are not allowed to create a Tweet with duplicate content. @@ -47,7 +47,7 @@ def post_tweet_reply(client: tweepy.Client, tweet_id: str, tweet_reply: str) -> message = "" try: - response = client.create_tweet(text=tweet_reply, in_reply_to_tweet_id=tweet_id) + response = client.create_tweet(in_reply_to_tweet_id=tweet_id, text=tweet_reply) message = f"Successfully posted reply to Twitter:\n{dumps(response)}" except tweepy.errors.TweepyException as e: message = f"Error posting reply to Twitter:\n{e}" diff --git a/twitter-langchain/examples/chatbot/chatbot.py b/twitter-langchain/examples/chatbot/chatbot.py index 3220fa351..c2c4fd497 100644 --- a/twitter-langchain/examples/chatbot/chatbot.py +++ b/twitter-langchain/examples/chatbot/chatbot.py @@ -1,4 +1,3 @@ -import os import sys import time diff --git a/twitter-langchain/tests/__init__.py b/twitter-langchain/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/twitter-langchain/tests/actions/__init__.py b/twitter-langchain/tests/actions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/twitter-langchain/tests/actions/test_account_details.py b/twitter-langchain/tests/actions/test_account_details.py new file mode 100644 index 000000000..e590179b1 --- /dev/null +++ b/twitter-langchain/tests/actions/test_account_details.py @@ -0,0 +1,44 @@ +from json import dumps +from unittest.mock import patch + +import tweepy +from cdp_agentkit_core.actions.social.twitter.account_details import ( + account_details, +) + +MOCK_ID = 1234 +MOCK_NAME = "Test Account" +MOCK_USERNAME = "testaccount" + + +def test_account_details_success(tweepy_factory): + mock_client = tweepy_factory() + mock_client_result = { + "data": { + "id": MOCK_ID, + "name": MOCK_USERNAME, + "username": MOCK_USERNAME, + }, + } + + expected_result = mock_client_result.copy() + expected_result["data"]["url"] = f"https://x.com/{MOCK_USERNAME}" + expected_response = f"Successfully retrieved authenticated user account details:\n{dumps(expected_result)}" + + with patch.object(mock_client, "get_me", return_value=mock_client_result) as mock_get_me: + response = account_details(mock_client) + + assert response == expected_response + mock_get_me.assert_called_once_with() + + +def test_account_details_failure(tweepy_factory): + mock_client = tweepy_factory() + + expected_result = tweepy.errors.TweepyException("Tweepy Error") + expected_response = f"Error retrieving authenticated user account details:\n{expected_result}" + + with patch.object(mock_client, "get_me", side_effect=expected_result) as mock_tweepy_get_me: + response = account_details(mock_client) + + assert response == expected_response diff --git a/twitter-langchain/tests/actions/test_account_mentions.py b/twitter-langchain/tests/actions/test_account_mentions.py new file mode 100644 index 000000000..a07f59244 --- /dev/null +++ b/twitter-langchain/tests/actions/test_account_mentions.py @@ -0,0 +1,60 @@ +from json import dumps +from unittest.mock import patch + +import pytest +import tweepy +from cdp_agentkit_core.actions.social.twitter.account_mentions import ( + AccountMentionsInput, + account_mentions, +) + +MOCK_ACCOUNT_ID = "1234" + +def test_account_mentions_input_model_valid(): + """Test that AccountMentionsInput accepts valid parameters.""" + input_model = AccountMentionsInput( + account_id=MOCK_ACCOUNT_ID, + ) + + assert input_model.account_id == MOCK_ACCOUNT_ID + + +def test_account_mentions_input_model_missing_params(): + """Test that AccountMentionsInput raises error when params are missing.""" + with pytest.raises(ValueError): + AccountMentionsInput() + +def test_account_mentions_success(tweepy_factory): + mock_client = tweepy_factory() + mock_client_result = { + "data": [ + { + "id": "1857479287504584856", + "edit_history_tweet_ids": ["1857479287504584856"], + "text": "@CDPAgentKit reply", + }, + ], + "meta": { + "result_count": 1, + "newest_id": "1857479287504584856", + "oldest_id": "1857479287504584856", + }, + } + + expected_response = f"Successfully retrieved authenticated user account mentions:\n{dumps(mock_client_result)}" + + with patch.object(mock_client, "get_users_mentions", return_value=mock_client_result) as mock_get_users_mentions: + response = account_mentions(mock_client, MOCK_ACCOUNT_ID) + + assert response == expected_response + mock_get_users_mentions.assert_called_once_with(MOCK_ACCOUNT_ID) + +def test_account_mentions_failure(tweepy_factory): + mock_client = tweepy_factory() + + expected_result = tweepy.errors.TweepyException("Tweepy Error") + expected_response = f"Error retrieving authenticated user account mentions:\n{expected_result}" + + with patch.object(mock_client, "get_users_mentions", side_effect=expected_result) as mock_get_users_mentions: + response = account_mentions(mock_client, MOCK_ACCOUNT_ID) + assert response == expected_response diff --git a/twitter-langchain/tests/actions/test_post_tweet.py b/twitter-langchain/tests/actions/test_post_tweet.py new file mode 100644 index 000000000..25df5c8db --- /dev/null +++ b/twitter-langchain/tests/actions/test_post_tweet.py @@ -0,0 +1,56 @@ +from json import dumps +from unittest.mock import patch + +import pytest +import tweepy +from cdp_agentkit_core.actions.social.twitter.post_tweet import ( + PostTweetInput, + post_tweet, +) + +MOCK_TWEET = "hello, world!" + +def test_post_tweet_input_model_valid(): + """Test that PostTweetInput accepts valid parameters.""" + input_model = PostTweetInput( + tweet=MOCK_TWEET, + ) + + assert input_model.tweet == MOCK_TWEET + + +def test_post_tweet_input_model_missing_params(): + """Test that PostTweetInput raises error when params are missing.""" + with pytest.raises(ValueError): + PostTweetInput() + +def test_post_tweet_success(tweepy_factory): + mock_client = tweepy_factory() + mock_client_result = { + "data": { + "text": "hello, world!", + "id": "0123456789012345678", + "edit_history_tweet_ids": ["0123456789012345678"], + } + } + + expected_response = f"Successfully posted to Twitter:\n{dumps(mock_client_result)}" + + with patch.object(mock_client, "create_tweet", return_value=mock_client_result) as mock_create_tweet: + response = post_tweet(mock_client, MOCK_TWEET) + + assert response == expected_response + mock_create_tweet.assert_called_once_with(text=MOCK_TWEET) + + +def test_post_tweet_failure(tweepy_factory): + mock_client = tweepy_factory() + + expected_result = tweepy.errors.TweepyException("Tweepy Error") + expected_response = f"Error posting to Twitter:\n{expected_result}" + + with patch.object(mock_client, "create_tweet", side_effect=expected_result) as mock_tweepy_create_tweet: + response = post_tweet(mock_client, MOCK_TWEET) + assert response == expected_response + + diff --git a/twitter-langchain/tests/actions/test_post_tweet_reply.py b/twitter-langchain/tests/actions/test_post_tweet_reply.py new file mode 100644 index 000000000..59e9d03f5 --- /dev/null +++ b/twitter-langchain/tests/actions/test_post_tweet_reply.py @@ -0,0 +1,62 @@ +from json import dumps +from unittest.mock import patch + +import pytest +import tweepy +from cdp_agentkit_core.actions.social.twitter.post_tweet_reply import ( + PostTweetReplyInput, + post_tweet_reply, +) + +MOCK_TWEET_ID = "1234" +MOCK_TWEET_REPLY = "So good to be here!" + +def test_post_tweet_input_model_valid(): + """Test that PostTweetReplyInput accepts valid parameters.""" + input_model = PostTweetReplyInput( + tweet_id=MOCK_TWEET_ID, + tweet_reply=MOCK_TWEET_REPLY, + ) + + assert input_model.tweet_id == MOCK_TWEET_ID + assert input_model.tweet_reply == MOCK_TWEET_REPLY + + +def test_post_tweet_input_model_missing_params(): + """Test that PostTweetReplyInput raises error when params are missing.""" + with pytest.raises(ValueError): + PostTweetReplyInput() + +def test_post_tweet_success(tweepy_factory): + mock_client = tweepy_factory() + mock_client_result = { + "data": { + "id": "0123456789012345678", + "text": "So good to be here!", + "edit_history_tweet_ids": ["1234567890123456789"], + } + } + + expected_response = f"Successfully posted reply to Twitter:\n{dumps(mock_client_result)}" + + with patch.object(mock_client, "create_tweet", return_value=mock_client_result) as mock_create_tweet: + response = post_tweet_reply(mock_client, MOCK_TWEET_ID, MOCK_TWEET_REPLY) + + assert response == expected_response + mock_create_tweet.assert_called_once_with( + in_reply_to_tweet_id=MOCK_TWEET_ID, + text=MOCK_TWEET_REPLY, + ) + + +def test_post_tweet_failure(tweepy_factory): + mock_client = tweepy_factory() + + expected_result = tweepy.errors.TweepyException("Tweepy Error") + expected_response = f"Error posting reply to Twitter:\n{expected_result}" + + with patch.object(mock_client, "create_tweet", side_effect=expected_result) as mock_tweepy_create_tweet: + response = post_tweet_reply(mock_client, MOCK_TWEET_ID, MOCK_TWEET_REPLY) + assert response == expected_response + + diff --git a/twitter-langchain/tests/conftest.py b/twitter-langchain/tests/conftest.py new file mode 100644 index 000000000..627976e60 --- /dev/null +++ b/twitter-langchain/tests/conftest.py @@ -0,0 +1,7 @@ +import os + +factory_modules = [ + f[:-3] for f in os.listdir("./tests/factories") if f.endswith(".py") and f != "__init__.py" +] + +pytest_plugins = [f"tests.factories.{module_name}" for module_name in factory_modules] diff --git a/twitter-langchain/tests/factories/twitter_factory.py b/twitter-langchain/tests/factories/twitter_factory.py new file mode 100644 index 000000000..44aad5504 --- /dev/null +++ b/twitter-langchain/tests/factories/twitter_factory.py @@ -0,0 +1,15 @@ +from unittest.mock import MagicMock + +import pytest +import tweepy + + +@pytest.fixture +def tweepy_factory(): + """Create and return a factory for tweepy mock fixtures.""" + + def _create_tweepy(): + tweepy_mock = MagicMock(spec=tweepy.Client) + return tweepy_mock + + return _create_tweepy