Refactor publication (#65)
* decoupled notifiers from event * stub * publishers working * fixed format CLI * fixed unit tests * renamed abstractnotifier * added another excluded character
This commit is contained in:
parent
6f81522ad0
commit
b6b2402767
|
@ -1,34 +0,0 @@
|
|||
[default.publisher.telegram]
|
||||
active=true
|
||||
chat_id="xxx"
|
||||
msg_template_path="xxx"
|
||||
token="xxx"
|
||||
username="xxx"
|
||||
[default.publisher.facebook]
|
||||
active=false
|
||||
[default.publisher.zulip]
|
||||
active=true
|
||||
chat_id="xxx"
|
||||
subject="xxx"
|
||||
bot_token="xxx"
|
||||
bot_email="xxx"
|
||||
[default.publisher.twitter]
|
||||
active=false
|
||||
[default.publisher.mastodon]
|
||||
active=false
|
||||
|
||||
[default.notifier.telegram]
|
||||
active=true
|
||||
chat_id="xxx"
|
||||
token="xxx"
|
||||
username="xxx"
|
||||
[default.notifier.zulip]
|
||||
active=true
|
||||
chat_id="xxx"
|
||||
subject="xxx"
|
||||
bot_token="xxx"
|
||||
bot_email="xxx"
|
||||
[default.notifier.twitter]
|
||||
active=false
|
||||
[default.notifier.mastodon]
|
||||
active=false
|
|
@ -2,10 +2,10 @@ import click
|
|||
|
||||
from mobilizon_reshare.event.event import MobilizonEvent
|
||||
from mobilizon_reshare.models.event import Event
|
||||
from mobilizon_reshare.publishers.coordinator import PublisherCoordinator
|
||||
from mobilizon_reshare.publishers.platforms.platform_mapping import get_formatter_class
|
||||
|
||||
|
||||
async def format_event(event_id, publisher):
|
||||
async def format_event(event_id, publisher_name: str):
|
||||
event = await Event.get_or_none(mobilizon_id=event_id).prefetch_related(
|
||||
"publications__publisher"
|
||||
)
|
||||
|
@ -13,5 +13,5 @@ async def format_event(event_id, publisher):
|
|||
click.echo(f"Event with mobilizon_id {event_id} not found.")
|
||||
return
|
||||
event = MobilizonEvent.from_model(event)
|
||||
message = PublisherCoordinator.get_formatted_message(event, publisher)
|
||||
message = get_formatter_class(publisher_name)().get_message_from_event(event)
|
||||
click.echo(message)
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import logging.config
|
||||
from functools import partial
|
||||
|
||||
from mobilizon_reshare.event.event_selection_strategies import select_event_to_publish
|
||||
from mobilizon_reshare.mobilizon.events import get_unpublished_events
|
||||
from mobilizon_reshare.models.publication import PublicationStatus
|
||||
from mobilizon_reshare.publishers.abstract import EventPublication
|
||||
from mobilizon_reshare.publishers.coordinator import (
|
||||
PublicationFailureNotifiersCoordinator,
|
||||
)
|
||||
|
@ -43,16 +45,24 @@ async def main():
|
|||
)
|
||||
|
||||
if event:
|
||||
|
||||
waiting_publications = await publications_with_status(
|
||||
status=PublicationStatus.WAITING,
|
||||
event_mobilizon_id=event.mobilizon_id,
|
||||
)
|
||||
logger.debug(f"Event to publish found: {event.name}")
|
||||
report = PublisherCoordinator(event, waiting_publications).run()
|
||||
await save_publication_report(report, waiting_publications)
|
||||
PublicationFailureNotifiersCoordinator(event, report).notify_failures()
|
||||
|
||||
return 0 if report.successful else 1
|
||||
waiting_publications_models = await publications_with_status(
|
||||
status=PublicationStatus.WAITING, event_mobilizon_id=event.mobilizon_id,
|
||||
)
|
||||
waiting_publications = list(
|
||||
map(
|
||||
partial(EventPublication.from_orm, event=event),
|
||||
waiting_publications_models.values(),
|
||||
)
|
||||
)
|
||||
|
||||
reports = PublisherCoordinator(waiting_publications).run()
|
||||
|
||||
await save_publication_report(reports, waiting_publications_models)
|
||||
for _, report in reports.reports.items():
|
||||
PublicationFailureNotifiersCoordinator(report).notify_failure()
|
||||
|
||||
return 0 if reports.successful else 1
|
||||
else:
|
||||
return 0
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import inspect
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from uuid import UUID
|
||||
|
||||
from dynaconf.utils.boxing import DynaBox
|
||||
from jinja2 import Environment, FileSystemLoader, Template
|
||||
from requests import Response
|
||||
|
||||
from mobilizon_reshare.config.config import get_settings
|
||||
from mobilizon_reshare.event.event import MobilizonEvent
|
||||
from mobilizon_reshare.models.publication import Publication as PublicationModel
|
||||
from .exceptions import PublisherError, InvalidAttribute
|
||||
|
||||
JINJA_ENV = Environment(loader=FileSystemLoader("/"))
|
||||
|
@ -15,52 +17,7 @@ JINJA_ENV = Environment(loader=FileSystemLoader("/"))
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AbstractNotifier(ABC):
|
||||
"""
|
||||
Generic notifier class.
|
||||
Shall be inherited from specific subclasses that will manage validation
|
||||
process for messages and credentials, text formatting, posting, etc.
|
||||
|
||||
Attributes:
|
||||
- ``message``: a formatted ``str``
|
||||
"""
|
||||
|
||||
# Non-abstract subclasses should define ``_conf`` as a 2-tuple, where the
|
||||
# first element is the type of class (either 'notifier' or 'publisher') and
|
||||
# the second the name of its service (ie: 'facebook', 'telegram')
|
||||
_conf = tuple()
|
||||
|
||||
def __repr__(self):
|
||||
return type(self).__name__
|
||||
|
||||
__str__ = __repr__
|
||||
|
||||
@property
|
||||
def conf(self) -> DynaBox:
|
||||
"""
|
||||
Retrieves class's settings.
|
||||
"""
|
||||
cls = type(self)
|
||||
if cls in (AbstractPublisher, AbstractNotifier):
|
||||
raise InvalidAttribute(
|
||||
"Abstract classes cannot access notifiers/publishers' settings"
|
||||
)
|
||||
try:
|
||||
t, n = cls._conf or tuple()
|
||||
return get_settings()[t][n]
|
||||
except (KeyError, ValueError):
|
||||
raise InvalidAttribute(
|
||||
f"Class {cls.__name__} has invalid ``_conf`` attribute"
|
||||
f" (should be 2-tuple)"
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def send(self, message):
|
||||
"""
|
||||
Sends a message to the target channel
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
class LoggerMixin:
|
||||
def _log_debug(self, msg, *args, **kwargs):
|
||||
self.__log(logging.DEBUG, msg, *args, **kwargs)
|
||||
|
||||
|
@ -82,6 +39,65 @@ class AbstractNotifier(ABC):
|
|||
if raise_error is not None:
|
||||
raise raise_error(msg)
|
||||
|
||||
|
||||
class ConfLoaderMixin:
|
||||
_conf = tuple()
|
||||
|
||||
@property
|
||||
def conf(self) -> DynaBox:
|
||||
"""
|
||||
Retrieves class's settings.
|
||||
"""
|
||||
cls = type(self)
|
||||
|
||||
try:
|
||||
t, n = cls._conf or tuple()
|
||||
return get_settings()[t][n]
|
||||
except (KeyError, ValueError):
|
||||
raise InvalidAttribute(
|
||||
f"Class {cls.__name__} has invalid ``_conf`` attribute"
|
||||
f" (should be 2-tuple)"
|
||||
)
|
||||
|
||||
|
||||
class AbstractPlatform(ABC, LoggerMixin, ConfLoaderMixin):
|
||||
"""
|
||||
Generic notifier class.
|
||||
Shall be inherited from specific subclasses that will manage validation
|
||||
process for messages and credentials, text formatting, posting, etc.
|
||||
|
||||
Attributes:
|
||||
- ``message``: a formatted ``str``
|
||||
"""
|
||||
|
||||
# Non-abstract subclasses should define ``_conf`` as a 2-tuple, where the
|
||||
# first element is the type of class (either 'notifier' or 'publisher') and
|
||||
# the second the name of its service (ie: 'facebook', 'telegram')
|
||||
|
||||
def __repr__(self):
|
||||
return type(self).__name__
|
||||
|
||||
__str__ = __repr__
|
||||
|
||||
@abstractmethod
|
||||
def _send(self, message: str):
|
||||
raise NotImplementedError
|
||||
|
||||
def send(self, message: str):
|
||||
"""
|
||||
Sends a message to the target channel
|
||||
"""
|
||||
message = self._preprocess_message(message)
|
||||
response = self._send(message)
|
||||
self._validate_response(response)
|
||||
|
||||
def _preprocess_message(self, message: str):
|
||||
return message
|
||||
|
||||
@abstractmethod
|
||||
def _validate_response(self, response):
|
||||
raise NotImplementedError
|
||||
|
||||
def are_credentials_valid(self) -> bool:
|
||||
try:
|
||||
self.validate_credentials()
|
||||
|
@ -98,59 +114,10 @@ class AbstractNotifier(ABC):
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AbstractEventFormatter(LoggerMixin, ConfLoaderMixin):
|
||||
@abstractmethod
|
||||
def publish(self) -> None:
|
||||
"""
|
||||
Publishes the actual post on social media.
|
||||
Should raise ``PublisherError`` (or one of its subclasses) if
|
||||
anything goes wrong.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_message_valid(self) -> bool:
|
||||
try:
|
||||
self.validate_message()
|
||||
except PublisherError:
|
||||
return False
|
||||
return True
|
||||
|
||||
@abstractmethod
|
||||
def validate_message(self) -> None:
|
||||
"""
|
||||
Validates notifier's message.
|
||||
Should raise ``PublisherError`` (or one of its subclasses) if message
|
||||
is not valid.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AbstractPublisher(AbstractNotifier):
|
||||
"""
|
||||
Generic publisher class.
|
||||
Shall be inherited from specific subclasses that will manage validation
|
||||
process for events and credentials, text formatting, posting, etc.
|
||||
|
||||
Attributes:
|
||||
- ``event``: a ``MobilizonEvent`` containing every useful info from
|
||||
the event
|
||||
- ``message``: a formatted ``str``
|
||||
"""
|
||||
|
||||
_conf = tuple()
|
||||
|
||||
def __init__(self, event: MobilizonEvent):
|
||||
self.event = event
|
||||
super().__init__()
|
||||
|
||||
def is_event_valid(self) -> bool:
|
||||
try:
|
||||
self.validate_event()
|
||||
except PublisherError:
|
||||
return False
|
||||
return True
|
||||
|
||||
@abstractmethod
|
||||
def validate_event(self) -> None:
|
||||
def validate_event(self, message) -> None:
|
||||
"""
|
||||
Validates publisher's event.
|
||||
Should raise ``PublisherError`` (or one of its subclasses) if event
|
||||
|
@ -158,18 +125,18 @@ class AbstractPublisher(AbstractNotifier):
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _preprocess_event(self):
|
||||
def _preprocess_event(self, event):
|
||||
"""
|
||||
Allows publishers to preprocess events before feeding them to the template
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_message_from_event(self) -> str:
|
||||
def get_message_from_event(self, event) -> str:
|
||||
"""
|
||||
Retrieves a message from the event itself.
|
||||
"""
|
||||
self._preprocess_event()
|
||||
return self.event.format(self.get_message_template())
|
||||
event = self._preprocess_event(event)
|
||||
return event.format(self.get_message_template())
|
||||
|
||||
def get_message_template(self) -> Template:
|
||||
"""
|
||||
|
@ -178,17 +145,45 @@ class AbstractPublisher(AbstractNotifier):
|
|||
template_path = self.conf.msg_template_path or self.default_template_path
|
||||
return JINJA_ENV.get_template(template_path)
|
||||
|
||||
@abstractmethod
|
||||
def _send(self, message) -> Response:
|
||||
pass
|
||||
def is_message_valid(self, event: MobilizonEvent) -> bool:
|
||||
try:
|
||||
self.validate_message(self.get_message_from_event(event))
|
||||
except PublisherError:
|
||||
return False
|
||||
return True
|
||||
|
||||
@abstractmethod
|
||||
def _validate_response(self, response: Response) -> None:
|
||||
pass
|
||||
def validate_message(self, message: str) -> None:
|
||||
"""
|
||||
Validates notifier's message.
|
||||
Should raise ``PublisherError`` (or one of its subclasses) if message
|
||||
is not valid.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def send(self, message):
|
||||
res = self._send(message)
|
||||
self._validate_response(res)
|
||||
def is_event_valid(self, event) -> bool:
|
||||
try:
|
||||
self.validate_event(event)
|
||||
except PublisherError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def publish(self) -> None:
|
||||
self.send(message=self.get_message_from_event())
|
||||
|
||||
@dataclass
|
||||
class EventPublication:
|
||||
event: MobilizonEvent
|
||||
id: UUID
|
||||
publisher: AbstractPlatform
|
||||
formatter: AbstractEventFormatter
|
||||
|
||||
@classmethod
|
||||
def from_orm(cls, model: PublicationModel, event: MobilizonEvent):
|
||||
# imported here to avoid circular dependencies
|
||||
from mobilizon_reshare.publishers.platforms.platform_mapping import (
|
||||
get_publisher_class,
|
||||
get_formatter_class,
|
||||
)
|
||||
|
||||
publisher = get_publisher_class(model.publisher.name)()
|
||||
formatter = get_formatter_class(model.publisher.name)()
|
||||
return cls(event, model.id, publisher, formatter)
|
||||
|
|
|
@ -1,32 +1,16 @@
|
|||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from mobilizon_reshare.event.event import MobilizonEvent
|
||||
from mobilizon_reshare.models.publication import Publication
|
||||
from mobilizon_reshare.models.publication import PublicationStatus
|
||||
from mobilizon_reshare.publishers import get_active_notifiers, get_active_publishers
|
||||
from mobilizon_reshare.publishers.abstract import AbstractPublisher
|
||||
from mobilizon_reshare.publishers import get_active_notifiers
|
||||
from mobilizon_reshare.publishers.abstract import EventPublication
|
||||
from mobilizon_reshare.publishers.exceptions import PublisherError
|
||||
from mobilizon_reshare.publishers.telegram import TelegramPublisher
|
||||
from mobilizon_reshare.publishers.zulip import ZulipPublisher
|
||||
from mobilizon_reshare.publishers.platforms.platform_mapping import get_notifier_class
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
name_to_publisher_class = {"telegram": TelegramPublisher, "zulip": ZulipPublisher}
|
||||
|
||||
|
||||
class BuildPublisherMixin:
|
||||
@staticmethod
|
||||
def build_publishers(
|
||||
event: MobilizonEvent, publisher_names
|
||||
) -> dict[str, AbstractPublisher]:
|
||||
|
||||
return {
|
||||
publisher_name: name_to_publisher_class[publisher_name](event)
|
||||
for publisher_name in publisher_names
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class PublicationReport:
|
||||
|
@ -37,7 +21,7 @@ class PublicationReport:
|
|||
|
||||
@dataclass
|
||||
class PublisherCoordinatorReport:
|
||||
publishers: dict[UUID, AbstractPublisher]
|
||||
publications: List[EventPublication]
|
||||
reports: dict[UUID, PublicationReport] = field(default_factory={})
|
||||
|
||||
@property
|
||||
|
@ -47,118 +31,107 @@ class PublisherCoordinatorReport:
|
|||
)
|
||||
|
||||
|
||||
class PublisherCoordinator(BuildPublisherMixin):
|
||||
def __init__(self, event: MobilizonEvent, publications: dict[UUID, Publication]):
|
||||
publishers = self.build_publishers(event, get_active_publishers())
|
||||
self.publishers_by_publication_id = {
|
||||
publication_id: publishers[publication.publisher.name]
|
||||
for publication_id, publication in publications.items()
|
||||
}
|
||||
class PublisherCoordinator:
|
||||
def __init__(self, publications: List[EventPublication]):
|
||||
self.publications = publications
|
||||
|
||||
def run(self) -> PublisherCoordinatorReport:
|
||||
errors = self._validate()
|
||||
if errors:
|
||||
return PublisherCoordinatorReport(
|
||||
reports=errors, publishers=self.publishers_by_publication_id
|
||||
reports=errors, publications=self.publications
|
||||
)
|
||||
|
||||
return self._post()
|
||||
|
||||
def _make_successful_report(self, failed_ids):
|
||||
return {
|
||||
publication_id: PublicationReport(
|
||||
publication.id: PublicationReport(
|
||||
status=PublicationStatus.COMPLETED,
|
||||
reason="",
|
||||
publication_id=publication_id,
|
||||
publication_id=publication.id,
|
||||
)
|
||||
for publication_id in self.publishers_by_publication_id
|
||||
if publication_id not in failed_ids
|
||||
for publication in self.publications
|
||||
if publication.id not in failed_ids
|
||||
}
|
||||
|
||||
def _post(self):
|
||||
failed_publishers_reports = {}
|
||||
for publication_id, p in self.publishers_by_publication_id.items():
|
||||
|
||||
for publication in self.publications:
|
||||
|
||||
try:
|
||||
p.publish()
|
||||
message = publication.formatter.get_message_from_event(
|
||||
publication.event
|
||||
)
|
||||
publication.publisher.send(message)
|
||||
except PublisherError as e:
|
||||
failed_publishers_reports[publication_id] = PublicationReport(
|
||||
failed_publishers_reports[publication.id] = PublicationReport(
|
||||
status=PublicationStatus.FAILED,
|
||||
reason=str(e),
|
||||
publication_id=publication_id,
|
||||
publication_id=publication.id,
|
||||
)
|
||||
|
||||
reports = failed_publishers_reports | self._make_successful_report(
|
||||
failed_publishers_reports.keys()
|
||||
)
|
||||
return PublisherCoordinatorReport(
|
||||
publishers=self.publishers_by_publication_id, reports=reports
|
||||
publications=self.publications, reports=reports
|
||||
)
|
||||
|
||||
def _validate(self):
|
||||
errors: dict[UUID, PublicationReport] = {}
|
||||
for publication_id, p in self.publishers_by_publication_id.items():
|
||||
|
||||
for publication in self.publications:
|
||||
|
||||
reason = []
|
||||
if not p.are_credentials_valid():
|
||||
if not publication.publisher.are_credentials_valid():
|
||||
reason.append("Invalid credentials")
|
||||
if not p.is_event_valid():
|
||||
if not publication.formatter.is_event_valid(publication.event):
|
||||
reason.append("Invalid event")
|
||||
if not p.is_message_valid():
|
||||
if not publication.formatter.is_message_valid(publication.event):
|
||||
reason.append("Invalid message")
|
||||
|
||||
if len(reason) > 0:
|
||||
errors[publication_id] = PublicationReport(
|
||||
errors[publication.id] = PublicationReport(
|
||||
status=PublicationStatus.FAILED,
|
||||
reason=", ".join(reason),
|
||||
publication_id=publication_id,
|
||||
publication_id=publication.id,
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
@staticmethod
|
||||
def get_formatted_message(event: MobilizonEvent, publisher: str) -> str:
|
||||
"""
|
||||
Returns the formatted message for a given event and publisher.
|
||||
"""
|
||||
if publisher not in name_to_publisher_class:
|
||||
raise ValueError(
|
||||
f"Publisher {publisher} does not exist.\nSupported publishers: "
|
||||
f"{', '.join(list(name_to_publisher_class.keys()))}"
|
||||
)
|
||||
|
||||
return name_to_publisher_class[publisher](event).get_message_from_event()
|
||||
class AbstractNotifiersCoordinator:
|
||||
def __init__(self, message: str, notifiers=None):
|
||||
self.message = message
|
||||
self.notifiers = notifiers or [
|
||||
get_notifier_class(notifier)() for notifier in get_active_notifiers()
|
||||
]
|
||||
|
||||
|
||||
class AbstractNotifiersCoordinator(BuildPublisherMixin):
|
||||
def __init__(self, event: MobilizonEvent):
|
||||
self.event = event
|
||||
self.notifiers = self.build_publishers(event, get_active_notifiers())
|
||||
|
||||
def send_to_all(self, message):
|
||||
def send_to_all(self):
|
||||
# TODO: failure to notify should fail safely and write to a dedicated log
|
||||
for notifier in self.notifiers.values():
|
||||
notifier.send(message)
|
||||
for notifier in self.notifiers:
|
||||
notifier.send(self.message)
|
||||
|
||||
|
||||
class PublicationFailureNotifiersCoordinator(AbstractNotifiersCoordinator):
|
||||
def __init__(
|
||||
self,
|
||||
event: MobilizonEvent,
|
||||
publisher_coordinator_report: PublisherCoordinatorReport,
|
||||
):
|
||||
self.report = publisher_coordinator_report
|
||||
super(PublicationFailureNotifiersCoordinator, self).__init__(event)
|
||||
def __init__(self, report: PublicationReport, notifiers=None):
|
||||
self.report = report
|
||||
super(PublicationFailureNotifiersCoordinator, self).__init__(
|
||||
message=self.build_failure_message(), notifiers=notifiers
|
||||
)
|
||||
|
||||
def build_failure_message(self, report: PublicationReport):
|
||||
def build_failure_message(self):
|
||||
report = self.report
|
||||
return (
|
||||
f"Publication {report.publication_id} failed with status: {report.status}.\n"
|
||||
f"Reason: {report.reason}"
|
||||
)
|
||||
|
||||
def notify_failures(self):
|
||||
for publication_id, report in self.report.reports.items():
|
||||
|
||||
logger.info(
|
||||
f"Sending failure notifications for publication: {publication_id}"
|
||||
)
|
||||
if report.status == PublicationStatus.FAILED:
|
||||
self.send_to_all(self.build_failure_message(report))
|
||||
def notify_failure(self):
|
||||
logger.info(
|
||||
f"Sending failure notifications for publication: {self.report.publication_id}"
|
||||
)
|
||||
if self.report.status == PublicationStatus.FAILED:
|
||||
self.send_to_all()
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
from mobilizon_reshare.publishers.platforms.telegram import (
|
||||
TelegramPublisher,
|
||||
TelegramFormatter,
|
||||
TelegramNotifier,
|
||||
)
|
||||
from mobilizon_reshare.publishers.platforms.zulip import (
|
||||
ZulipPublisher,
|
||||
ZulipFormatter,
|
||||
ZulipNotifier,
|
||||
)
|
||||
|
||||
name_to_publisher_class = {
|
||||
"telegram": TelegramPublisher,
|
||||
"zulip": ZulipPublisher,
|
||||
}
|
||||
name_to_formatter_class = {
|
||||
"telegram": TelegramFormatter,
|
||||
"zulip": ZulipFormatter,
|
||||
}
|
||||
name_to_notifier_class = {
|
||||
"telegram": TelegramNotifier,
|
||||
"zulip": ZulipNotifier,
|
||||
}
|
||||
|
||||
|
||||
def get_notifier_class(platform):
|
||||
return name_to_notifier_class[platform]
|
||||
|
||||
|
||||
def get_publisher_class(platform):
|
||||
return name_to_publisher_class[platform]
|
||||
|
||||
|
||||
def get_formatter_class(platform):
|
||||
return name_to_formatter_class[platform]
|
|
@ -2,8 +2,12 @@ import pkg_resources
|
|||
import requests
|
||||
from requests import Response
|
||||
|
||||
from mobilizon_reshare.event.event import MobilizonEvent
|
||||
from mobilizon_reshare.formatting.description import html_to_markdown
|
||||
from mobilizon_reshare.publishers.abstract import AbstractPublisher
|
||||
from mobilizon_reshare.publishers.abstract import (
|
||||
AbstractEventFormatter,
|
||||
AbstractPlatform,
|
||||
)
|
||||
from mobilizon_reshare.publishers.exceptions import (
|
||||
InvalidBot,
|
||||
InvalidCredentials,
|
||||
|
@ -12,35 +16,50 @@ from mobilizon_reshare.publishers.exceptions import (
|
|||
)
|
||||
|
||||
|
||||
class TelegramPublisher(AbstractPublisher):
|
||||
"""
|
||||
Telegram publisher class.
|
||||
"""
|
||||
|
||||
_conf = ("publisher", "telegram")
|
||||
class TelegramFormatter(AbstractEventFormatter):
|
||||
default_template_path = pkg_resources.resource_filename(
|
||||
"mobilizon_reshare.publishers.templates", "telegram.tmpl.j2"
|
||||
)
|
||||
|
||||
def _escape_message(self, message: str) -> str:
|
||||
_conf = ("publisher", "telegram")
|
||||
|
||||
@staticmethod
|
||||
def escape_message(message: str) -> str:
|
||||
message = (
|
||||
message.replace("-", "\\-")
|
||||
.replace(".", "\\.")
|
||||
.replace("(", "\\(")
|
||||
.replace("!", "\\!")
|
||||
.replace(")", "\\)")
|
||||
.replace("#", "")
|
||||
)
|
||||
return message
|
||||
|
||||
def _send(self, message: str) -> Response:
|
||||
return requests.post(
|
||||
url=f"https://api.telegram.org/bot{self.conf.token}/sendMessage",
|
||||
json={
|
||||
"chat_id": self.conf.chat_id,
|
||||
"text": self._escape_message(message),
|
||||
"parse_mode": "markdownv2",
|
||||
},
|
||||
)
|
||||
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)
|
||||
|
||||
def get_message_from_event(self, event: MobilizonEvent) -> str:
|
||||
return super(TelegramFormatter, self).get_message_from_event(event)
|
||||
|
||||
def validate_message(self, message: str) -> None:
|
||||
# TODO implement
|
||||
pass
|
||||
|
||||
def _preprocess_event(self, event: MobilizonEvent):
|
||||
event.description = html_to_markdown(event.description)
|
||||
event.name = html_to_markdown(event.name)
|
||||
return event
|
||||
|
||||
|
||||
class TelegramPlatform(AbstractPlatform):
|
||||
"""
|
||||
Telegram publisher class.
|
||||
"""
|
||||
|
||||
def _preprocess_message(self, message: str):
|
||||
return TelegramFormatter.escape_message(message)
|
||||
|
||||
def validate_credentials(self):
|
||||
conf = self.conf
|
||||
|
@ -67,13 +86,19 @@ class TelegramPublisher(AbstractPublisher):
|
|||
"Found a different bot than the expected one", raise_error=InvalidBot,
|
||||
)
|
||||
|
||||
def validate_event(self) -> None:
|
||||
text = self.event.description
|
||||
if not (text and text.strip()):
|
||||
self._log_error("No text was found", raise_error=InvalidEvent)
|
||||
def _send(self, message) -> Response:
|
||||
return requests.post(
|
||||
url=f"https://api.telegram.org/bot{self.conf.token}/sendMessage",
|
||||
json={
|
||||
"chat_id": self.conf.chat_id,
|
||||
"text": message,
|
||||
"parse_mode": "markdownv2",
|
||||
},
|
||||
)
|
||||
|
||||
def _validate_response(self, res):
|
||||
try:
|
||||
|
||||
res.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self._log_error(
|
||||
|
@ -95,10 +120,12 @@ class TelegramPublisher(AbstractPublisher):
|
|||
|
||||
return data
|
||||
|
||||
def validate_message(self) -> None:
|
||||
# TODO implement
|
||||
pass
|
||||
|
||||
def _preprocess_event(self):
|
||||
self.event.description = html_to_markdown(self.event.description)
|
||||
self.event.name = html_to_markdown(self.event.name)
|
||||
class TelegramPublisher(TelegramPlatform):
|
||||
|
||||
_conf = ("publisher", "telegram")
|
||||
|
||||
|
||||
class TelegramNotifier(TelegramPlatform):
|
||||
|
||||
_conf = ("notifier", "telegram")
|
|
@ -3,8 +3,12 @@ import requests
|
|||
from requests import Response
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from mobilizon_reshare.event.event import MobilizonEvent
|
||||
from mobilizon_reshare.formatting.description import html_to_markdown
|
||||
from mobilizon_reshare.publishers.abstract import AbstractPublisher
|
||||
from mobilizon_reshare.publishers.abstract import (
|
||||
AbstractPlatform,
|
||||
AbstractEventFormatter,
|
||||
)
|
||||
from mobilizon_reshare.publishers.exceptions import (
|
||||
InvalidBot,
|
||||
InvalidCredentials,
|
||||
|
@ -14,7 +18,28 @@ from mobilizon_reshare.publishers.exceptions import (
|
|||
)
|
||||
|
||||
|
||||
class ZulipPublisher(AbstractPublisher):
|
||||
class ZulipFormatter(AbstractEventFormatter):
|
||||
|
||||
_conf = ("publisher", "zulip")
|
||||
default_template_path = pkg_resources.resource_filename(
|
||||
"mobilizon_reshare.publishers.templates", "zulip.tmpl.j2"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
def validate_message(self, message) -> None:
|
||||
pass
|
||||
|
||||
def _preprocess_event(self, event: MobilizonEvent):
|
||||
event.description = html_to_markdown(event.description)
|
||||
event.name = html_to_markdown(event.name)
|
||||
return event
|
||||
|
||||
|
||||
class ZulipPlatform(AbstractPlatform):
|
||||
"""
|
||||
Zulip publisher class.
|
||||
"""
|
||||
|
@ -32,11 +57,7 @@ class ZulipPublisher(AbstractPublisher):
|
|||
return requests.post(
|
||||
url=self.api_uri + "messages",
|
||||
auth=HTTPBasicAuth(self.conf.bot_email, self.conf.bot_token),
|
||||
data={
|
||||
"type": "private",
|
||||
"to": f"[{self.user_id}]",
|
||||
"content": message,
|
||||
},
|
||||
data={"type": "private", "to": f"[{self.user_id}]", "content": message},
|
||||
)
|
||||
|
||||
def _send(self, message: str) -> Response:
|
||||
|
@ -68,8 +89,7 @@ class ZulipPublisher(AbstractPublisher):
|
|||
err.append("bot email")
|
||||
if err:
|
||||
self._log_error(
|
||||
", ".join(err) + " is/are missing",
|
||||
raise_error=InvalidCredentials,
|
||||
", ".join(err) + " is/are missing", raise_error=InvalidCredentials,
|
||||
)
|
||||
|
||||
res = requests.get(
|
||||
|
@ -80,8 +100,7 @@ class ZulipPublisher(AbstractPublisher):
|
|||
|
||||
if not data["is_bot"]:
|
||||
self._log_error(
|
||||
"These user is not a bot",
|
||||
raise_error=InvalidBot,
|
||||
"These user is not a bot", raise_error=InvalidBot,
|
||||
)
|
||||
|
||||
if not bot_email == data["email"]:
|
||||
|
@ -92,13 +111,7 @@ class ZulipPublisher(AbstractPublisher):
|
|||
raise_error=InvalidBot,
|
||||
)
|
||||
|
||||
def validate_event(self) -> None:
|
||||
text = self.event.description
|
||||
if not (text and text.strip()):
|
||||
self._log_error("No text was found", raise_error=InvalidEvent)
|
||||
|
||||
def validate_message(self) -> None:
|
||||
# We don't need this for Zulip.
|
||||
pass
|
||||
|
||||
def _validate_response(self, res) -> dict:
|
||||
|
@ -113,12 +126,17 @@ class ZulipPublisher(AbstractPublisher):
|
|||
|
||||
if data["result"] == "error":
|
||||
self._log_error(
|
||||
f"{res.status_code} Error - {data['msg']}",
|
||||
raise_error=ZulipError,
|
||||
f"{res.status_code} Error - {data['msg']}", raise_error=ZulipError,
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
def _preprocess_event(self):
|
||||
self.event.description = html_to_markdown(self.event.description)
|
||||
self.event.name = html_to_markdown(self.event.name)
|
||||
|
||||
class ZulipPublisher(ZulipPlatform):
|
||||
|
||||
_conf = ("publisher", "zulip")
|
||||
|
||||
|
||||
class ZulipNotifier(ZulipPlatform):
|
||||
|
||||
_conf = ("notifier", "zulip")
|
|
@ -5,13 +5,15 @@ import arrow
|
|||
import pytest
|
||||
|
||||
from mobilizon_reshare.event.event import MobilizonEvent
|
||||
from mobilizon_reshare.publishers.abstract import AbstractPublisher
|
||||
from mobilizon_reshare.publishers.abstract import (
|
||||
AbstractPlatform,
|
||||
AbstractEventFormatter,
|
||||
)
|
||||
from mobilizon_reshare.publishers.exceptions import PublisherError, InvalidResponse
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_event():
|
||||
|
||||
now = arrow.now()
|
||||
return MobilizonEvent(
|
||||
**{
|
||||
|
@ -26,66 +28,78 @@ def test_event():
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_publisher_valid(event):
|
||||
class MockPublisher(AbstractPublisher):
|
||||
def validate_event(self) -> None:
|
||||
def mock_formatter_valid():
|
||||
class MockFormatter(AbstractEventFormatter):
|
||||
def validate_event(self, event) -> None:
|
||||
pass
|
||||
|
||||
def get_message_from_event(self) -> str:
|
||||
return self.event.description
|
||||
def get_message_from_event(self, event) -> str:
|
||||
return event.description
|
||||
|
||||
def validate_credentials(self) -> None:
|
||||
pass
|
||||
|
||||
def validate_message(self) -> None:
|
||||
def validate_message(self, event) -> None:
|
||||
pass
|
||||
|
||||
def _send(self, message):
|
||||
pass
|
||||
|
||||
def _validate_response(self, response) -> None:
|
||||
pass
|
||||
|
||||
return MockPublisher(event)
|
||||
return MockFormatter()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_publisher_invalid(event):
|
||||
class MockPublisher(AbstractPublisher):
|
||||
def validate_event(self) -> None:
|
||||
def mock_formatter_invalid():
|
||||
class MockFormatter(AbstractEventFormatter):
|
||||
def validate_event(self, event) -> None:
|
||||
raise PublisherError("Invalid event")
|
||||
|
||||
def get_message_from_event(self) -> str:
|
||||
def get_message_from_event(self, event) -> str:
|
||||
return ""
|
||||
|
||||
def validate_credentials(self) -> None:
|
||||
raise PublisherError("Invalid credentials")
|
||||
|
||||
def validate_message(self) -> None:
|
||||
def validate_message(self, event) -> None:
|
||||
raise PublisherError("Invalid message")
|
||||
|
||||
def _send(self, message):
|
||||
pass
|
||||
|
||||
def _validate_response(self, response) -> None:
|
||||
pass
|
||||
|
||||
return MockPublisher(event)
|
||||
return MockFormatter()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_publisher_invalid_response(mock_publisher_invalid, event):
|
||||
class MockPublisher(type(mock_publisher_invalid)):
|
||||
def validate_event(self) -> None:
|
||||
def mock_publisher_valid():
|
||||
class MockPublisher(AbstractPlatform):
|
||||
def _send(self, message):
|
||||
pass
|
||||
|
||||
def _validate_response(self, response):
|
||||
pass
|
||||
|
||||
def validate_credentials(self) -> None:
|
||||
pass
|
||||
|
||||
def validate_message(self) -> None:
|
||||
return MockPublisher()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_publisher_invalid():
|
||||
class MockPublisher(AbstractPlatform):
|
||||
def _send(self, message):
|
||||
pass
|
||||
|
||||
def _validate_response(self, response) -> None:
|
||||
def _validate_response(self, response):
|
||||
return InvalidResponse("error")
|
||||
|
||||
def validate_credentials(self) -> None:
|
||||
raise PublisherError("error")
|
||||
|
||||
return MockPublisher()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_publisher_invalid_response():
|
||||
class MockPublisher(AbstractPlatform):
|
||||
def _send(self, message):
|
||||
pass
|
||||
|
||||
def _validate_response(self, response):
|
||||
raise InvalidResponse("Invalid response")
|
||||
|
||||
return MockPublisher(event)
|
||||
def validate_credentials(self) -> None:
|
||||
pass
|
||||
|
||||
return MockPublisher()
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
def test_are_credentials_valid(test_event, mock_publisher_valid):
|
||||
assert mock_publisher_valid.are_credentials_valid()
|
||||
|
||||
|
||||
def test_are_credentials_valid_false(mock_publisher_invalid):
|
||||
assert not mock_publisher_invalid.are_credentials_valid()
|
||||
|
||||
|
||||
def test_is_event_valid(mock_publisher_valid):
|
||||
assert mock_publisher_valid.is_event_valid()
|
||||
|
||||
|
||||
def test_is_event_valid_false(mock_publisher_invalid):
|
||||
assert not mock_publisher_invalid.is_event_valid()
|
||||
|
||||
|
||||
def test_is_message_valid(mock_publisher_valid):
|
||||
assert mock_publisher_valid.is_message_valid()
|
||||
|
||||
|
||||
def test_is_message_valid_false(mock_publisher_invalid):
|
||||
assert not mock_publisher_invalid.is_message_valid()
|
|
@ -0,0 +1,14 @@
|
|||
def test_is_event_valid(mock_formatter_valid, event):
|
||||
assert mock_formatter_valid.is_event_valid(event)
|
||||
|
||||
|
||||
def test_is_event_valid_false(mock_formatter_invalid, event):
|
||||
assert not mock_formatter_invalid.is_event_valid(event)
|
||||
|
||||
|
||||
def test_is_message_valid(mock_formatter_valid, event):
|
||||
assert mock_formatter_valid.is_message_valid(event)
|
||||
|
||||
|
||||
def test_is_message_valid_false(mock_formatter_invalid, event):
|
||||
assert not mock_formatter_invalid.is_message_valid(event)
|
|
@ -3,17 +3,19 @@ from uuid import UUID
|
|||
import pytest
|
||||
from asynctest import MagicMock
|
||||
|
||||
from mobilizon_reshare.config.config import get_settings
|
||||
from mobilizon_reshare.event.event import MobilizonEvent
|
||||
from mobilizon_reshare.models.publication import PublicationStatus, Publication
|
||||
from mobilizon_reshare.models.publication import (
|
||||
PublicationStatus,
|
||||
Publication as PublicationModel,
|
||||
)
|
||||
from mobilizon_reshare.models.publisher import Publisher
|
||||
from mobilizon_reshare.publishers.abstract import EventPublication
|
||||
from mobilizon_reshare.publishers.coordinator import (
|
||||
PublisherCoordinatorReport,
|
||||
PublicationReport,
|
||||
PublisherCoordinator,
|
||||
PublicationFailureNotifiersCoordinator,
|
||||
)
|
||||
from mobilizon_reshare.publishers.telegram import TelegramPublisher
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -37,37 +39,38 @@ def test_publication_report_successful(statuses, successful):
|
|||
|
||||
@pytest.fixture
|
||||
@pytest.mark.asyncio
|
||||
async def mock_publication(
|
||||
async def mock_publications(
|
||||
num_publications: int,
|
||||
test_event: MobilizonEvent,
|
||||
mock_publisher_valid,
|
||||
mock_formatter_valid,
|
||||
):
|
||||
event = test_event.to_model()
|
||||
await event.save()
|
||||
publisher = Publisher(name="telegram")
|
||||
await publisher.save()
|
||||
publication = Publication(
|
||||
id=UUID(int=1),
|
||||
status=PublicationStatus.WAITING,
|
||||
event=event,
|
||||
publisher=publisher,
|
||||
timestamp=None,
|
||||
reason=None,
|
||||
)
|
||||
await publication.save()
|
||||
return publication
|
||||
result = []
|
||||
for i in range(num_publications):
|
||||
event = test_event.to_model()
|
||||
await event.save()
|
||||
publisher = Publisher(name="telegram")
|
||||
await publisher.save()
|
||||
publication = PublicationModel(
|
||||
id=UUID(int=i + 1),
|
||||
status=PublicationStatus.WAITING,
|
||||
event=event,
|
||||
publisher=publisher,
|
||||
timestamp=None,
|
||||
reason=None,
|
||||
)
|
||||
await publication.save()
|
||||
publication = EventPublication.from_orm(publication, test_event)
|
||||
publication.publisher = mock_publisher_valid
|
||||
publication.formatter = mock_formatter_valid
|
||||
result.append(publication)
|
||||
return result
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_publications", [2])
|
||||
@pytest.mark.asyncio
|
||||
async def test_coordinator_run_success(
|
||||
test_event, mock_publication, mock_publisher_valid
|
||||
):
|
||||
coordinator = PublisherCoordinator(
|
||||
test_event, {UUID(int=1): mock_publication, UUID(int=2): mock_publication}
|
||||
)
|
||||
coordinator.publishers_by_publication_id = {
|
||||
UUID(int=1): mock_publisher_valid,
|
||||
UUID(int=2): mock_publisher_valid,
|
||||
}
|
||||
|
||||
async def test_coordinator_run_success(mock_publications,):
|
||||
coordinator = PublisherCoordinator(publications=mock_publications,)
|
||||
report = coordinator.run()
|
||||
assert len(report.reports) == 2
|
||||
assert report.successful, "\n".join(
|
||||
|
@ -75,14 +78,15 @@ async def test_coordinator_run_success(
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_publications", [1])
|
||||
@pytest.mark.asyncio
|
||||
async def test_coordinator_run_failure(
|
||||
test_event, mock_publication, mock_publisher_invalid
|
||||
mock_publications, mock_publisher_invalid, mock_formatter_invalid
|
||||
):
|
||||
coordinator = PublisherCoordinator(test_event, {UUID(int=1): mock_publication})
|
||||
coordinator.publishers_by_publication_id = {
|
||||
UUID(int=1): mock_publisher_invalid,
|
||||
}
|
||||
for pub in mock_publications:
|
||||
pub.publisher = mock_publisher_invalid
|
||||
pub.formatter = mock_formatter_invalid
|
||||
coordinator = PublisherCoordinator(mock_publications)
|
||||
|
||||
report = coordinator.run()
|
||||
assert len(report.reports) == 1
|
||||
|
@ -93,15 +97,15 @@ async def test_coordinator_run_failure(
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_publications", [1])
|
||||
@pytest.mark.asyncio
|
||||
async def test_coordinator_run_failure_response(
|
||||
test_event, mock_publication, mock_publisher_invalid_response
|
||||
mock_publications, mock_publisher_invalid_response
|
||||
):
|
||||
coordinator = PublisherCoordinator(test_event, {UUID(int=1): mock_publication})
|
||||
coordinator.publishers_by_publication_id = {
|
||||
UUID(int=1): mock_publisher_invalid_response,
|
||||
}
|
||||
|
||||
for pub in mock_publications:
|
||||
pub.publisher = mock_publisher_invalid_response
|
||||
coordinator = PublisherCoordinator(publications=mock_publications)
|
||||
report = coordinator.run()
|
||||
assert len(report.reports) == 1
|
||||
assert not report.successful
|
||||
|
@ -109,39 +113,18 @@ async def test_coordinator_run_failure_response(
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notifier_coordinator_publication_failed(
|
||||
test_event, mock_publisher_valid
|
||||
):
|
||||
async def test_notifier_coordinator_publication_failed(mock_publisher_valid):
|
||||
mock_send = MagicMock()
|
||||
mock_publisher_valid._send = mock_send
|
||||
report = PublisherCoordinatorReport(
|
||||
{UUID(int=1): mock_publisher_valid, UUID(int=2): mock_publisher_valid},
|
||||
{
|
||||
UUID(int=1): PublicationReport(
|
||||
status=PublicationStatus.FAILED,
|
||||
reason="some failure",
|
||||
publication_id=UUID(int=1),
|
||||
),
|
||||
UUID(int=2): PublicationReport(
|
||||
status=PublicationStatus.FAILED,
|
||||
reason="some failure",
|
||||
publication_id=UUID(int=2),
|
||||
),
|
||||
},
|
||||
report = PublicationReport(
|
||||
status=PublicationStatus.FAILED,
|
||||
reason="some failure",
|
||||
publication_id=UUID(int=1),
|
||||
)
|
||||
coordinator = PublicationFailureNotifiersCoordinator(test_event, report)
|
||||
coordinator.notifiers = {
|
||||
UUID(int=1): mock_publisher_valid,
|
||||
UUID(int=2): mock_publisher_valid,
|
||||
}
|
||||
coordinator.notify_failures()
|
||||
coordinator = PublicationFailureNotifiersCoordinator(
|
||||
report, [mock_publisher_valid, mock_publisher_valid]
|
||||
)
|
||||
coordinator.notify_failure()
|
||||
|
||||
# 4 = 2 reports * 2 notifiers
|
||||
assert mock_send.call_count == 4
|
||||
|
||||
|
||||
def test_get_formatted_message(test_event):
|
||||
settings = get_settings()
|
||||
settings.update({"publisher.telegram.msg_template_path": None})
|
||||
message = PublisherCoordinator.get_formatted_message(test_event, "telegram")
|
||||
assert message == TelegramPublisher(test_event).get_message_from_event()
|
||||
assert mock_send.call_count == 2
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
from functools import partial
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
|
||||
from mobilizon_reshare.config.config import get_settings
|
||||
from mobilizon_reshare.models.publication import PublicationStatus
|
||||
from mobilizon_reshare.publishers import get_active_publishers
|
||||
from mobilizon_reshare.publishers.abstract import EventPublication
|
||||
from mobilizon_reshare.publishers.coordinator import PublisherCoordinator
|
||||
from mobilizon_reshare.storage.query import (
|
||||
get_publishers,
|
||||
update_publishers,
|
||||
publications_with_status,
|
||||
get_all_events,
|
||||
)
|
||||
|
||||
|
||||
api_uri = "https://zulip.twc-italia.org/api/v1/"
|
||||
users_me = {
|
||||
"result": "success",
|
||||
|
@ -39,10 +40,7 @@ users_me = {
|
|||
def mocked_valid_response():
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps.add(
|
||||
responses.GET,
|
||||
api_uri + "users/me",
|
||||
json=users_me,
|
||||
status=200,
|
||||
responses.GET, api_uri + "users/me", json=users_me, status=200,
|
||||
)
|
||||
rsps.add(
|
||||
responses.POST,
|
||||
|
@ -69,10 +67,7 @@ def mocked_credential_error_response():
|
|||
def mocked_client_error_response():
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps.add(
|
||||
responses.GET,
|
||||
api_uri + "users/me",
|
||||
json=users_me,
|
||||
status=200,
|
||||
responses.GET, api_uri + "users/me", json=users_me, status=200,
|
||||
)
|
||||
rsps.add(
|
||||
responses.POST,
|
||||
|
@ -105,11 +100,17 @@ async def setup_db(event_model_generator, publication_model_generator):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_zulip_publisher(mocked_valid_response, setup_db):
|
||||
|
||||
async def test_zulip_publisher(mocked_valid_response, setup_db, event):
|
||||
publication_models = await publications_with_status(
|
||||
status=PublicationStatus.WAITING
|
||||
)
|
||||
report = PublisherCoordinator(
|
||||
list(await get_all_events())[0],
|
||||
await publications_with_status(status=PublicationStatus.WAITING),
|
||||
list(
|
||||
map(
|
||||
partial(EventPublication.from_orm, event=event),
|
||||
publication_models.values(),
|
||||
)
|
||||
)
|
||||
).run()
|
||||
|
||||
assert list(report.reports.values())[0].status == PublicationStatus.COMPLETED
|
||||
|
@ -117,11 +118,18 @@ async def test_zulip_publisher(mocked_valid_response, setup_db):
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_zulip_publishr_failure_invalid_credentials(
|
||||
mocked_credential_error_response, setup_db
|
||||
mocked_credential_error_response, setup_db, event
|
||||
):
|
||||
publication_models = await publications_with_status(
|
||||
status=PublicationStatus.WAITING
|
||||
)
|
||||
report = PublisherCoordinator(
|
||||
list(await get_all_events())[0],
|
||||
await publications_with_status(status=PublicationStatus.WAITING),
|
||||
list(
|
||||
map(
|
||||
partial(EventPublication.from_orm, event=event),
|
||||
publication_models.values(),
|
||||
)
|
||||
)
|
||||
).run()
|
||||
assert list(report.reports.values())[0].status == PublicationStatus.FAILED
|
||||
assert list(report.reports.values())[0].reason == "Invalid credentials"
|
||||
|
@ -129,11 +137,18 @@ async def test_zulip_publishr_failure_invalid_credentials(
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_zulip_publishr_failure_client_error(
|
||||
mocked_client_error_response, setup_db
|
||||
mocked_client_error_response, setup_db, event
|
||||
):
|
||||
publication_models = await publications_with_status(
|
||||
status=PublicationStatus.WAITING
|
||||
)
|
||||
report = PublisherCoordinator(
|
||||
list(await get_all_events())[0],
|
||||
await publications_with_status(status=PublicationStatus.WAITING),
|
||||
list(
|
||||
map(
|
||||
partial(EventPublication.from_orm, event=event),
|
||||
publication_models.values(),
|
||||
)
|
||||
)
|
||||
).run()
|
||||
assert list(report.reports.values())[0].status == PublicationStatus.FAILED
|
||||
assert list(report.reports.values())[0].reason == "400 Error - Invalid request"
|
||||
|
|
|
@ -48,10 +48,7 @@ two_publishers_specification = {"publisher": ["telegram", "twitter"]}
|
|||
],
|
||||
)
|
||||
async def test_update_publishers(
|
||||
specification,
|
||||
names,
|
||||
expected_result,
|
||||
generate_models,
|
||||
specification, names, expected_result, generate_models,
|
||||
):
|
||||
await generate_models(specification)
|
||||
await update_publishers(names)
|
||||
|
@ -71,6 +68,7 @@ async def test_update_publishers(
|
|||
[
|
||||
complete_specification,
|
||||
PublisherCoordinatorReport(
|
||||
publications=[],
|
||||
reports={
|
||||
UUID(int=3): PublicationReport(
|
||||
status=PublicationStatus.FAILED,
|
||||
|
@ -83,7 +81,6 @@ async def test_update_publishers(
|
|||
publication_id=UUID(int=4),
|
||||
),
|
||||
},
|
||||
publishers={},
|
||||
),
|
||||
MobilizonEvent(
|
||||
name="event_1",
|
||||
|
@ -110,17 +107,12 @@ async def test_update_publishers(
|
|||
],
|
||||
)
|
||||
async def test_save_publication_report(
|
||||
specification,
|
||||
report,
|
||||
event,
|
||||
expected_result,
|
||||
generate_models,
|
||||
specification, report, event, expected_result, generate_models,
|
||||
):
|
||||
await generate_models(specification)
|
||||
|
||||
publications = await publications_with_status(
|
||||
status=PublicationStatus.WAITING,
|
||||
event_mobilizon_id=event.mobilizon_id,
|
||||
status=PublicationStatus.WAITING, event_mobilizon_id=event.mobilizon_id,
|
||||
)
|
||||
await save_publication_report(report, publications)
|
||||
publication_ids = set(report.reports.keys())
|
||||
|
|
Loading…
Reference in New Issue