diff --git a/mobilizon_reshare/publishers/platforms/facebook.py b/mobilizon_reshare/publishers/platforms/facebook.py index d7d9c2d..b703039 100644 --- a/mobilizon_reshare/publishers/platforms/facebook.py +++ b/mobilizon_reshare/publishers/platforms/facebook.py @@ -13,6 +13,8 @@ from mobilizon_reshare.publishers.abstract import ( from mobilizon_reshare.publishers.exceptions import ( InvalidCredentials, InvalidEvent, + InvalidMessage, + PublisherError, ) @@ -37,7 +39,8 @@ class FacebookFormatter(AbstractEventFormatter): self._log_error("No text was found", raise_error=InvalidEvent) def _validate_message(self, message) -> None: - pass + if len(message) >= 63200: + self._log_error("Message is too long", raise_error=InvalidMessage) def _preprocess_event(self, event: MobilizonEvent): event.description = html_to_plaintext(event.description) @@ -52,18 +55,23 @@ class FacebookPlatform(AbstractPlatform): name = "facebook" - def _get_api(self): + def _get_api(self) -> facebook.GraphAPI: return facebook.GraphAPI( access_token=self.conf["page_access_token"], version="8.0" ) def _send(self, message: str, event: Optional[MobilizonEvent] = None): - self._get_api().put_object( - parent_object="me", - connection_name="feed", - message=message, - link=event.mobilizon_link if event else None, - ) + try: + self._get_api().put_object( + parent_object="me", + connection_name="feed", + message=message, + link=event.mobilizon_link if event else None, + ) + except GraphAPIError: + self._log_error( + "Facebook send failed", raise_error=PublisherError, + ) def validate_credentials(self): @@ -76,7 +84,7 @@ class FacebookPlatform(AbstractPlatform): raise_error=InvalidCredentials, ) - self._log_debug("Facebook credentials are valid") + self._log_debug("Facebook credentials are valid") def _validate_response(self, response): pass diff --git a/mobilizon_reshare/publishers/platforms/telegram.py b/mobilizon_reshare/publishers/platforms/telegram.py index 9a4a8d4..10919b5 100644 --- a/mobilizon_reshare/publishers/platforms/telegram.py +++ b/mobilizon_reshare/publishers/platforms/telegram.py @@ -1,3 +1,4 @@ +import re from typing import Optional import pkg_resources @@ -46,7 +47,11 @@ class TelegramFormatter(AbstractEventFormatter): self._log_error("Message is too long", raise_error=InvalidMessage) def _preprocess_message(self, message: str) -> str: - + """ + This function converts HTML5 to Telegram's HTML dialect + :param message: a HTML5 string + :return: a HTML string compatible with Telegram + """ html = BeautifulSoup(message, "html.parser") # replacing paragraphs for tag in html.findAll(["p", "br"]): @@ -70,7 +75,8 @@ class TelegramFormatter(AbstractEventFormatter): # cleaning html trailing whitespace for tag in html.findAll("a"): tag["href"] = tag["href"].replace(" ", "").strip().lstrip() - return str(html) + s = str(html) + return re.sub(r"\n{2,}", "\n\n", s).strip() # remove multiple newlines class TelegramPlatform(AbstractPlatform): diff --git a/mobilizon_reshare/publishers/platforms/twitter.py b/mobilizon_reshare/publishers/platforms/twitter.py index 7490cc9..0a1275c 100644 --- a/mobilizon_reshare/publishers/platforms/twitter.py +++ b/mobilizon_reshare/publishers/platforms/twitter.py @@ -11,7 +11,6 @@ from mobilizon_reshare.publishers.abstract import ( ) from mobilizon_reshare.publishers.exceptions import ( InvalidCredentials, - InvalidEvent, PublisherError, InvalidMessage, ) @@ -33,9 +32,7 @@ class TwitterFormatter(AbstractEventFormatter): ) def _validate_event(self, event: MobilizonEvent) -> None: - text = event.description - if not (text and text.strip()): - self._log_error("No text was found", raise_error=InvalidEvent) + pass # pragma: no cover def _validate_message(self, message) -> None: # TODO this is not precise. It should count the characters according to Twitter's logic but @@ -76,7 +73,7 @@ class TwitterPlatform(AbstractPlatform): ) def _validate_response(self, res: Status) -> dict: - pass + pass # pragma: no cover class TwitterPublisher(TwitterPlatform): diff --git a/tests/formatting/test_output_format.py b/tests/formatting/test_output_format.py index 7abbd7a..563cbbe 100644 --- a/tests/formatting/test_output_format.py +++ b/tests/formatting/test_output_format.py @@ -25,7 +25,8 @@ end_date = begin_date.shift(hours=1) def event() -> MobilizonEvent: return MobilizonEvent( name="test event", - description="

description of the event

", + description="

description of the event

another header

", + # "", begin_datetime=begin_date, end_datetime=end_date, mobilizon_link="http://some_link.com/123", @@ -49,7 +50,7 @@ def event() -> MobilizonEvent: 📍 location -description of the event +description of the event another header Link: http://some_link.com/123 """, @@ -61,7 +62,9 @@ Link: http://some_link.com/123 🕒 01 January, {begin_date.format('HH:mm')} - 01 January, {end_date.format('HH:mm')} 📍 location -description of the event +description of the event + +another header Link""", ], diff --git a/tests/publishers/test_facebook.py b/tests/publishers/test_facebook.py new file mode 100644 index 0000000..51a12dd --- /dev/null +++ b/tests/publishers/test_facebook.py @@ -0,0 +1,64 @@ +from logging import DEBUG +from unittest.mock import patch + +import pytest +from facebook import GraphAPI, GraphAPIError + +from mobilizon_reshare.publishers.exceptions import ( + InvalidEvent, + InvalidMessage, + PublisherError, +) +from mobilizon_reshare.publishers.platforms.facebook import ( + FacebookFormatter, + FacebookPublisher, +) + + +def test_message_length_success(event): + message = "a" * 500 + event.description = message + assert FacebookFormatter().validate_event(event) is None + + +def test_message_length_failure(event): + message = "a" * 80000 + event.description = message + + with pytest.raises(InvalidMessage): + FacebookFormatter().validate_event(event) + + +def test_event_validation(event): + event.description = None + with pytest.raises(InvalidEvent): + FacebookFormatter().validate_event(event) + + +def test_send_error(event): + + with patch.object( + GraphAPI, "put_object", side_effect=GraphAPIError("some error") + ) as mock: + with pytest.raises(PublisherError): + FacebookPublisher().send("abc", event) + mock.assert_called() + + +def test_validate_credentials_error(event): + + with patch.object( + GraphAPI, "get_object", side_effect=GraphAPIError("some error") + ) as mock: + with pytest.raises(PublisherError): + FacebookPublisher().validate_credentials() + mock.assert_called() + + +def test_validate_credentials(event, caplog): + + with patch.object(GraphAPI, "get_object", return_value=None) as mock: + with caplog.at_level(DEBUG): + FacebookPublisher().validate_credentials() + mock.assert_called() + assert "Facebook credentials are valid" in caplog.text diff --git a/tests/publishers/test_twitter.py b/tests/publishers/test_twitter.py new file mode 100644 index 0000000..362f298 --- /dev/null +++ b/tests/publishers/test_twitter.py @@ -0,0 +1,44 @@ +from unittest.mock import patch + +import pytest +from tweepy import API, TweepyException + +from mobilizon_reshare.publishers.exceptions import ( + InvalidMessage, + InvalidCredentials, + PublisherError, +) +from mobilizon_reshare.publishers.platforms.twitter import ( + TwitterFormatter, + TwitterPublisher, +) + + +def test_message_length_success(event): + message = "a" * 300 + event.description = message + assert TwitterFormatter().validate_event(event) is None + + +def test_message_length_failure(event): + message = "a" * 10000 + event.name = message + + with pytest.raises(InvalidMessage): + TwitterFormatter().validate_event(event) + + +def test_validate_credentials_error(): + with patch.object(API, "verify_credentials", return_value=False) as mock: + with pytest.raises(InvalidCredentials): + TwitterPublisher().validate_credentials() + mock.assert_called() + + +def test_send_error(event): + with patch.object( + API, "update_status", side_effect=TweepyException("some error") + ) as mock: + with pytest.raises(PublisherError): + TwitterPublisher().send("abc", event) + mock.assert_called()