def create_friendship(self, **kwargs): user_id_to_follow = kwargs['user_id'] logger.info(f"create_friendship with user_id: {user_id_to_follow}") if self.scenario == self.SCENARIO_OK: return None elif self.scenario == self.SCENARIO_RETRY_OK: if user_id_to_follow == self.user_id_err and not self.next_retry_ok: self.next_retry_ok = True raise TwythonRateLimitError(error_code=403, msg="Can retry") return None elif self.scenario == self.SCENARIO_RETRY_NOK: if user_id_to_follow == self.user_id_err: raise TwythonRateLimitError(error_code=403, msg="Too many retries") return None elif self.scenario == self.SCENARIO_SKIP: if user_id_to_follow == self.user_id_err: raise TwythonError("Cannot find specified user") return None elif self.scenario == self.SCENARIO_ABORT: if user_id_to_follow == self.user_id_err: raise TwythonError( "401 (Unauthorized), Invalid or expired token") return None else: raise ValueError( f"MockTython has been set with invalid scenario: {self.scenario}" )
class TwitterInterfaceTests(TestCase): @patch('admin_panel_api.twitter_interface.Twython', return_value={}) def test_singleton(self, mock_twython_init): instance1 = TwitterInterface.get_instance() instance2 = TwitterInterface.get_instance() self.assertIsNotNone(instance1) self.assertEqual(instance1, instance2) # Singleton def test_get_tweets_success(self): with patch('admin_panel_api.twitter_interface.Twython.search' ) as mock_search: test_result = {'statuses': []} mock_search.return_value = test_result tweets = TwitterInterface.get_instance().get_tweets() self.assertEqual(tweets, test_result['statuses']) @patch('admin_panel_api.twitter_interface.Twython.search', side_effect=TwythonError('Some error')) def test_get_tweets_error(self, mock_search): try: TwitterInterface.get_instance().get_tweets() except Exception as e: self.assertIsNotNone(e) @patch('admin_panel_api.twitter_interface.Twython.retweet', return_value=True) def test_retweet_success(self, mock_retweet): TwitterInterface.get_instance().retweet('1234') self.assertEqual(mock_retweet.call_count, 1) @patch('admin_panel_api.twitter_interface.Twython.retweet', side_effect=TwythonError('Some error')) def test_retweet_error(self, mock_retweet): try: TwitterInterface.get_instance().retweet('1234') except Exception as e: self.assertIsNotNone(e) @patch('admin_panel_api.twitter_interface.Twython.create_favorite', return_value=True) def test_favorite_success(self, mock_favorite): TwitterInterface.get_instance().favorite('1234') self.assertEqual(mock_favorite.call_count, 1) @patch('admin_panel_api.twitter_interface.Twython.create_favorite', side_effect=TwythonError('Some error')) def test_favorite_error(self, mock_favorite): try: TwitterInterface.get_instance().favorite('1234') except Exception as e: self.assertIsNotNone(e)
def tweet_text(tweet, live=False): InputKun.read_tokens(TOKENSFILE) atd = ACCESS_TOKENS_DIC try: api = Twython(atd['CONSUMER_KEY'], atd['CONSUMER_SECRET'], atd['ACCESS_KEY'], atd['ACCESS_SECRET']) if not live: if len(tweet) > 270: halves = util.split_tweet(tweet) print(halves[0]) print(halves[1]) else: print(tweet) else: if len(tweet) > 270: halves = util.split_tweet(tweet) api.update_status(status=halves[0]) api.update_status(status=halves[1]) else: api.update_status(status=tweet) except TwythonError as e: print(str(e)) # OutputChan.queue_tweet(QUEUEDIR, tweet, image_path, colosseum_str) raise TwythonError("Failed to tweet. Queued tweet")
def test_call_fail(sleep, twython_class, bytesio_class, requests, random, logger): twython = Mock() twython_class.return_value = twython teardown_db(test_db) with MemeOverflow(fake_twitter, fake_imgflip, fake_stack_with_key, test_db) as mo: mock_se_response = Mock( status_code=200, json=Mock(return_value=example_se_response), ) mock_imgflip_get_response = Mock( status_code=200, content=example_imgflip_img_blob, ) requests.get.side_effect = cycle( [mock_se_response, mock_imgflip_get_response]) mock_imgflip_post_response = Mock( status_code=200, json=Mock(return_value=example_imgflip_response), ) requests.post.return_value = mock_imgflip_post_response meme = 'BATMAN_SLAPPING_ROBIN' random.choice.return_value = meme mock_imgflip_response = Mock(content=example_imgflip_img_blob) requests.get.return_value = mock_imgflip_response img_bytes = Mock() bytesio_class.return_value = img_bytes twython.upload_media.return_value = example_twitter_upload_response twython.update_status.side_effect = TwythonError('update status error') mo() assert twython.update_status.call_count == 2 assert sleep.call_count == 2 teardown_db(test_db)
def test_twitter_doesnt_die_upon_lost_connection(slack): twitter = MagicMock() hashtag_reactor = Twitter(slack, twitter_client=twitter) hashtag_reactor.twitter.search.side_effect = TwythonError( 'connection reset') msg = {'text': 'you got a #dadbod', 'channel': 'cat'} hashtag_reactor([msg])
def _produce_friend_ids_names_list(self): # This method iterates through the pages of data (indexed by a cursor) that Twitter # returns when asked for a user's friends lists. It iterates until Twitter # sends an empty next cursor (last page) or until we reach the maximum of iterations # supported by this application. # # Returns: list of dicts containing friendships data. # Each friendship has a user name (screen name) and a user ID users, next_cursor = self._get_friends_curs() friend_ids_names = [] condition = True iterations = 1 while condition: for u in users: friend_ids_names.append((u['screen_name'], u['id'])) if next_cursor > 0 and iterations <= self.MAX_CURSOR_ITERATIONS: users, next_cursor = self._get_friends_curs(curs=next_cursor) iterations += 1 else: condition = False if iterations > self.MAX_CURSOR_ITERATIONS: self.ulog.error( f"Reached {iterations} pagination iterations. This shouldn't happen!" ) raise TwythonError(msg="Too many pages of friends to be retrieved") self.ulog.debug( f"Retrieved full list of {len(friend_ids_names)} friends after {iterations} iterations." ) return friend_ids_names
def test_twitter_resets_upon_lost_connection(slack): twitter = MagicMock() hashtag_reactor = Twitter(slack, twitter_client=twitter) hashtag_reactor.twitter.search.side_effect = TwythonError( 'connection reset') msg = {'text': 'you got a #dadbod', 'channel': 'cat'} hashtag_reactor([msg]) assert hashtag_reactor.twitter.search.call_count == 3 assert hashtag_reactor.slack_client.api_called_with('chat.postMessage')
def test_generate_meme_and_tweet_fail(sleep): teardown_db(test_db) with MemeOverflow(fake_twitter, fake_imgflip, fake_stack_with_key, test_db) as mo: mo.make_meme = Mock(return_value=('img_url', 'meme')) mo.tweet = Mock(side_effect=TwythonError('error')) assert not mo.generate_meme_and_tweet(example_question) assert not mo.db.question_is_known(example_question['question_id']) teardown_db(test_db)
def test_continue_thread_error(app): bill = mock.create_autospec(Bill) prev_tweet_id = '123' app.twitter_bot.tweet_bill.side_effect = TwythonError('Error') no_prev_tweets = {} prev_tweets = continue_thread(bill, app.bills, prev_tweet_id, no_prev_tweets, app.twitter_bot) assert app.bills.insert.call_count == 0 assert prev_tweets == no_prev_tweets
def test_tweet_fail_upload(twython_class, requests, logger): twython = Mock() twython_class.return_value = twython teardown_db(test_db) with MemeOverflow(fake_twitter, fake_imgflip, fake_stack_with_key, test_db) as mo: mock_response = Mock(status_code=200, content=example_imgflip_img_blob) requests.get.return_value = mock_response twython.upload_media.side_effect = TwythonError('upload media error') with pytest.raises(TwythonError): mo.tweet('test', example_imgflip_img_url) logger.error.assert_called_once() teardown_db(test_db)
def test__parse_twithon_error_account_protected(tw_client_ok): logger.info("---------- test__parse_twithon_error_account_protected ----------") user_name = "acc_protected_user_error" err_msg = "already requested to follow" error_returned = TwythonError(msg=err_msg) mock_client = tw_client_ok(user_name) importer = FriendsImporter(mock_client, None, None) is_data_error, reason_for_skipping, irrecoverable_error = importer._parse_twithon_error(error_returned, user_name) assert is_data_error assert reason_for_skipping assert not irrecoverable_error logger.info("========== test__parse_twithon_error_account_protected ============")
def test__parse_twithon_error_user_blocked(tw_client_ok): logger.info("---------- test__parse_twithon_error_user_blocked ----------") user_name = "blocked_user_error" err_msg = "You have been blocked" error_returned = TwythonError(msg=err_msg) mock_client = tw_client_ok(user_name) importer = FriendsImporter(mock_client, None, None) is_data_error, reason_for_skipping, irrecoverable_error = importer._parse_twithon_error(error_returned, user_name) assert is_data_error assert reason_for_skipping assert not irrecoverable_error logger.info("========== test__parse_twithon_error_user_blocked ============")
def test__parse_twithon_error_user_not_found(tw_client_ok): logger.info("---------- test__parse_twithon_error_user_not_found ----------") user_name = "not_found_user_error" err_msg = "Cannot find specified user" error_returned = TwythonError(msg=err_msg) mock_client = tw_client_ok(user_name) importer = FriendsImporter(mock_client, None, None) is_data_error, reason_for_skipping, irrecoverable_error = importer._parse_twithon_error(error_returned, user_name) assert is_data_error assert reason_for_skipping assert not irrecoverable_error logger.info("========== test__parse_twithon_error_user_not_found ============")
def test__parse_twithon_error_irrecoverable_error(tw_client_ok): logger.info("---------- test__parse_twithon_error_irrecoverable_error ----------") user_name = "not_data_user_error" err_msg = "401 (Unauthorized), Invalid or expired token" error_returned = TwythonError(msg=err_msg) mock_client = tw_client_ok(user_name) importer = FriendsImporter(mock_client, None, None) is_data_error, reason_for_skipping, irrecoverable_error = importer._parse_twithon_error(error_returned, user_name) assert not is_data_error assert not reason_for_skipping assert irrecoverable_error logger.info("========== test__parse_twithon_error_irrecoverable_error ============")
def test__parse_twithon_error_not_data_problem(tw_client_ok): logger.info("---------- test__parse_twithon_error_not_data_problem ----------") user_name = "not_data_user_error" err_msg = "Some twitter error" error_returned = TwythonError(msg=err_msg) mock_client = tw_client_ok(user_name) importer = FriendsImporter(mock_client, None, None) is_data_error, reason_for_skipping, irrecoverable_error = importer._parse_twithon_error(error_returned, user_name) assert not is_data_error assert not reason_for_skipping assert not irrecoverable_error logger.info("========== test__parse_twithon_error_not_data_problem ============")
def scrape(self): monitor = ScrapeMonitor(len(self.wachtrij), self.FIRST_ID) n = -1 for opdracht in self.wachtrij: naam, callback = opdracht tweetaantal = 200 data = [] oudsteId = self.LAST_ID pagina = 0 over = 299 try: try: while tweetaantal >= 195 and oudsteId >= self.FIRST_ID: n = n + 1 if n >= len(self.twitters): n = 0 pagina = pagina + 1 newdata = self.twitters[n].get_user_timeline( screen_name=naam, count=200, since_id=self.FIRST_ID, max_id=oudsteId) tweetaantal = len(newdata) if tweetaantal > 0: for tweet in newdata: newid = int(tweet['id_str']) if newid < oudsteId: oudsteId = newid monitor.printSuccess(naam, pagina, tweetaantal, oudsteId, n, over) data.extend(newdata) else: raise TwythonError('No data for this user') over = int(self.twitters[n].get_lastfunction_header( 'x-rate-limit-remaining')) callback(data, naam) except TwythonError, e: monitor.printError(naam, e.message, n, over) if e.error_code == 429: over = 0 else: over = int(self.twitters[n].get_lastfunction_header( 'x-rate-limit-remaining')) if over < 1: reset = int(self.twitters[n].get_lastfunction_header( 'x-rate-limit-reset')) wachttijd = reset - time.time() + 10 monitor.printWachten(reset, n, over) time.sleep(wachttijd) except KeyboardInterrupt: raise
def tweet_image(tweet, image_path, live=False): InputKun.read_tokens(TOKENSFILE) atd = ACCESS_TOKENS_DIC try: api = Twython(atd['CONSUMER_KEY'], atd['CONSUMER_SECRET'], atd['ACCESS_KEY'], atd['ACCESS_SECRET']) photo = open(image_path, 'rb') image_ids = api.upload_media(media=photo) if not live: # Only for testing print(tweet) else: api.update_status(status=tweet, media_ids=image_ids['media_id']) except TwythonError: raise TwythonError("Failed to tweet. Queued tweet")
def authenticate_app(request): callback_url = request.build_absolute_uri(reverse('tw_auth_callback')) twitter = Twython(app_key=APP_KEY, app_secret=APP_SECRET) try: tw_auth = twitter.get_authentication_tokens(callback_url=callback_url, force_login=True) if 'oauth_token' not in tw_auth or 'oauth_token_secret' not in tw_auth: raise TwythonError(msg="Missing OAuth token and secret") except TwythonError as e: return redirect_to_auth_error_view(request, f"Problem authenticating app: {e}") oauth_token = tw_auth['oauth_token'] oauth_token_secret = tw_auth['oauth_token_secret'] redirect_url = tw_auth['auth_url'] # Store the secret token keyed by oauth token for when tw redirects to this app request.session['temp_oauth_store'] = {oauth_token: oauth_token_secret} return redirect_url
def _request(self, url, method="GET", params=None, api_call=None): """Internal request method""" method = method.lower() params = params or {} func = getattr(self.client, method) params, files = (params, None) if "event" in params else _transparent_params(params) requests_args = {} for k, v in self.client_args.items(): # Maybe this should be set as a class variable and only done once? if k in ("timeout", "allow_redirects", "stream", "verify"): requests_args[k] = v if method == "get": requests_args["params"] = params else: requests_args.update({"data": json.dumps(params) if "event" in params else params, "files": files}) try: if method == "get": event = HttpEvent(method, url + "?" + urlencode(params)) else: event = HttpEvent(method, url, urlencode(params)) self.events.append(event) response = func(url, **requests_args) event.status_code = response.status_code event.response_body = response.text except requests.RequestException as e: raise TwythonError(str(e)) content = response.content.decode("utf-8") # create stash for last function intel self._last_call = { "api_call": api_call, "api_error": None, "cookies": response.cookies, "headers": response.headers, "status_code": response.status_code, "url": response.url, "content": content, } # Wrap the json loads in a try, and defer an error # Twitter will return invalid json with an error code in the headers json_error = False if content: try: try: # try to get json content = content.json() except AttributeError: # if unicode detected content = json.loads(content) except ValueError: json_error = True content = {} if response.status_code > 304: # If there is no error message, use a default. errors = content.get("errors", [{"message": "An error occurred processing your request."}]) if errors and isinstance(errors, list): error_message = errors[0]["message"] else: error_message = errors # pragma: no cover self._last_call["api_error"] = error_message ExceptionType = TwythonError if response.status_code == 429: # Twitter API 1.1, always return 429 when rate limit is exceeded ExceptionType = TwythonRateLimitError # pragma: no cover elif response.status_code == 401 or "Bad Authentication data" in error_message: # Twitter API 1.1, returns a 401 Unauthorized or # a 400 "Bad Authentication data" for invalid/expired app keys/user tokens ExceptionType = TwythonAuthError raise ExceptionType( error_message, error_code=response.status_code, retry_after=response.headers.get("retry-after") ) # if we have a json error here, then it's not an official Twitter API error if json_error and response.status_code not in (200, 201, 202): # pragma: no cover raise TwythonError("Response was not valid JSON, unable to decode.") return content
def _request(self, url, method='GET', params=None, api_call=None): """Internal request method""" method = method.lower() params = params or {} func = getattr(self.client, method) params, files = _transparent_params(params) requests_args = {} for k, v in self.client_args.items(): # Maybe this should be set as a class variable and only done once? if k in ('timeout', 'allow_redirects', 'stream', 'verify'): requests_args[k] = v if method == 'get': requests_args['params'] = params else: requests_args.update({ 'data': params, 'files': files, }) try: if method == 'get': event = HttpEvent(method, url + '?' + urlencode(params)) else: event = HttpEvent(method, url, urlencode(params)) self.events.append(event) response = func(url, **requests_args) event.status_code = response.status_code event.response_body = response.text except requests.RequestException as e: raise TwythonError(str(e)) content = response.content.decode('utf-8') # create stash for last function intel self._last_call = { 'api_call': api_call, 'api_error': None, 'cookies': response.cookies, 'headers': response.headers, 'status_code': response.status_code, 'url': response.url, 'content': content, } # Wrap the json loads in a try, and defer an error # Twitter will return invalid json with an error code in the headers json_error = False if content: try: try: # try to get json content = content.json() except AttributeError: # if unicode detected content = json.loads(content) except ValueError: json_error = True content = {} if response.status_code > 304: # If there is no error message, use a default. errors = content.get( 'errors', [{ 'message': 'An error occurred processing your request.' }]) if errors and isinstance(errors, list): error_message = errors[0]['message'] else: error_message = errors # pragma: no cover self._last_call['api_error'] = error_message ExceptionType = TwythonError if response.status_code == 429: # Twitter API 1.1, always return 429 when rate limit is exceeded ExceptionType = TwythonRateLimitError # pragma: no cover elif response.status_code == 401 or 'Bad Authentication data' in error_message: # Twitter API 1.1, returns a 401 Unauthorized or # a 400 "Bad Authentication data" for invalid/expired app keys/user tokens ExceptionType = TwythonAuthError raise ExceptionType( error_message, error_code=response.status_code, retry_after=response.headers.get('retry-after')) # if we have a json error here, then it's not an official Twitter API error if json_error and response.status_code not in ( 200, 201, 202): # pragma: no cover raise TwythonError( 'Response was not valid JSON, unable to decode.') return content
def setUp(self): """ Prepare to run tests on the Twitter interface. Since OGRe requires API keys to run and they cannot be stored conveniently, this test module retrieves them from the OS; however, to prevent OGRe from actually querying the APIs (and subsequently retrieving unpredictable data), a MagicMock object is used to do a dependency injection. This relieves the need for setting environment variables (although they may be necessary in the future). Predictable results are stored in the data directory to be read during these tests. """ self.log = logging.getLogger(__name__) self.log.debug("Initializing a TwitterTest...") self.retriever = OGRe( keys={ "Twitter": { "consumer_key": os.environ.get("TWITTER_CONSUMER_KEY"), "access_token": os.environ.get("TWITTER_ACCESS_TOKEN") } } ) with open("ogre/test/data/Twitter-response-example.json") as tweets: self.tweets = json.load(tweets) depleted_tweets = copy.deepcopy(self.tweets) depleted_tweets["search_metadata"].pop("next_results", None) limit_normal = twitter_limits(2, 1234567890) dependency_injections = { "regular": { "api": { "limit": limit_normal, "return": copy.deepcopy(self.tweets), "effect": None }, "network": { "return": None, "effect": lambda _: StringIO(u"test_image") } }, "malformed_limits": { "api": { "limit": {}, "return": copy.deepcopy(self.tweets), "effect": None }, "network": { "return": None, "effect": lambda _: StringIO(u"test_image") } }, "low_limits": { "api": { "limit": twitter_limits(1, 1234567890), "return": copy.deepcopy(self.tweets), "effect": None }, "network": { "return": None, "effect": lambda _: StringIO(u"test_image") } }, "limited": { "api": { "limit": twitter_limits(0, 1234567890), "return": { "errors": [ { "code": 88, "message": "Rate limit exceeded" } ] }, "effect": None }, "network": { "return": None, "effect": Exception() } }, "imitate": { "api": { "limit": limit_normal, "return": None, "effect": TwythonError("TwythonError") }, "network": { "return": None, "effect": Exception() } }, "complex": { "api": { "limit": limit_normal, "return": { "error": "Sorry, your query is too complex." + " Please reduce complexity and try again." }, "effect": None }, "network": { "return": None, "effect": Exception() } }, "deplete": { "api": { "limit": twitter_limits(1, 1234567890), "return": copy.deepcopy(depleted_tweets), "effect": None }, "network": { "return": StringIO(u"test_image"), "effect": None } } } self.injectors = { "api": {}, "network": {} } for name, dependencies in dependency_injections.items(): api = MagicMock() api().get_application_rate_limit_status.return_value =\ dependencies["api"]["limit"] api().search.return_value = dependencies["api"]["return"] api().search.side_effect = dependencies["api"]["effect"] api.reset_mock() self.injectors["api"][name] = api network = MagicMock() network.return_value = dependencies["network"]["return"] network.side_effect = dependencies["network"]["effect"] network.reset_mock() self.injectors["network"][name] = network
def test_claim( self, mock_verify_credentials, mock_register_webhook, mock_subscribe_to_webhook, mock_delete_webhook, mock_get_webhooks, ): mock_get_webhooks.return_value = [{"id": "webhook_id"}] mock_delete_webhook.return_value = {"ok", True} url = reverse("channels.types.twitter.claim") self.login(self.admin) response = self.client.get(reverse("channels.channel_claim")) self.assertContains(response, "/channels/types/twitter/claim") response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertContains(response, "Connect Twitter") self.assertEqual( list(response.context["form"].fields.keys()), ["api_key", "api_secret", "access_token", "access_token_secret", "env_name", "loc"], ) # try submitting empty form response = self.client.post(url, {}) self.assertEqual(response.status_code, 200) self.assertFormError(response, "form", "api_key", "This field is required.") self.assertFormError(response, "form", "api_secret", "This field is required.") self.assertFormError(response, "form", "access_token", "This field is required.") self.assertFormError(response, "form", "access_token_secret", "This field is required.") # try submitting with invalid credentials mock_verify_credentials.side_effect = TwythonError("Invalid credentials") response = self.client.post( url, {"api_key": "ak", "api_secret": "as", "access_token": "at", "access_token_secret": "ats"} ) self.assertEqual(response.status_code, 200) self.assertFormError(response, "form", None, "The provided Twitter credentials do not appear to be valid.") # error registering webhook mock_verify_credentials.return_value = {"id": "87654", "screen_name": "jimmy"} mock_verify_credentials.side_effect = None mock_register_webhook.side_effect = TwythonError("Exceeded number of webhooks") response = self.client.post( url, { "api_key": "ak", "api_secret": "as", "access_token": "at", "access_token_secret": "ats", "env_name": "production", }, ) self.assertEqual(response.status_code, 200) self.assertFormError(response, "form", None, "Exceeded number of webhooks") # try a valid submission mock_register_webhook.side_effect = None mock_register_webhook.return_value = {"id": "1234567"} response = self.client.post( url, { "api_key": "ak", "api_secret": "as", "access_token": "at", "access_token_secret": "ats", "env_name": "beta", }, ) self.assertEqual(response.status_code, 302) channel = Channel.objects.get(address="jimmy", is_active=True) self.assertEqual( channel.config, { "handle_id": "87654", "api_key": "ak", "api_secret": "as", "access_token": "at", "env_name": "beta", "access_token_secret": "ats", "webhook_id": "1234567", "callback_domain": channel.callback_domain, }, ) self.assertTrue(channel.get_type().has_attachment_support(channel)) mock_register_webhook.assert_called_with( "beta", "https://%s/c/twt/%s/receive" % (channel.callback_domain, channel.uuid) ) mock_subscribe_to_webhook.assert_called_with("beta")
def get_connection(self): """(obj) Returns the current connection made to Twitter.""" if self.connected: return self.connection raise TwythonError("not connected to Twitter")
def _get_friends_list_page_nok(self, users): error_to_raise = TwythonError("Irrecoverable error!") self.data_pages -= 1 next_cursor = self._process_cursor(error_to_raise) result = {'users': users, 'next_cursor': next_cursor} return result