예제 #1
0
def test_processes_secure_events():
    render = Render(MockTemplateStore())
    thread_store = MockThreadStore()
    emails = render.process_event_to_emails(_create_secure_event(1, 2), thread_store)
    assert len(emails) == 1
    email = emails[0]
    assert email.subject == "D1: (secure bug 2)"
    assert email.to == "reviewer@mail"
예제 #2
0
def test_processes_events():
    render = Render(MockTemplateStore())
    thread_store = MockThreadStore()
    emails = render.process_event_to_emails(_create_public_event(), thread_store)
    assert len(emails) == 1
    email = emails[0]
    assert email.subject == "D1: revision"
    assert email.timestamp == 123
    assert email.to == "reviewer@mail"
예제 #3
0
def service(settings: Settings, stats: StatsClient):
    """Runs the service: fetches Phabricator events and sends emails.

    Handles Phabricator communication errors by reporting the failure to
    statsd and retrying after the poll delay.

    Handles event rendering issues (which shouldn't occur in Production) by reporting
    the error to Sentry, informing statsd, and skipping the event.
    """

    source = settings.source
    worker = settings.worker
    logger = settings.logger
    db = settings.db()
    mail = settings.mail()

    if not db.is_initialized():
        raise DBNotInitializedError(
            "Database has not been initialized yet, run "
            "`phabricator-emails prepare` first.")

    raw_css_path = PACKAGE_DIRECTORY / "render/templates/html/style.css"
    css_text = raw_css_path.read_text()
    template_store = TemplateStore(
        settings.phabricator_host,
        css_text,
        # Keep CSS classes when outputting to local files, since that indicates local
        # development/testing
        keep_css_classes=isinstance(mail, FsMail),
    )

    render = Render(template_store)
    pipeline = Pipeline(source, render, mail, logger, stats)
    worker.process(db, pipeline.run)
예제 #4
0
def test_processes_with_minimal_context_if_full_context_error():
    event = {
        "isSecure": True,
        "timestamp": 0,
        "context": {
            "thisContextIsMissingProperties": True,
        },
        "minimalContext": {
            "revision": {
                "revisionId": 1,
                "link": "link",
            },
            "recipients": [{
                "username": "******",
                "email": "2@mail",
                "timezoneOffset": 0,
                "isActor": False,
            }],
        },
    }
    mail = MockMail()
    render = Render(JinjaTemplateStore("", "", False))
    logger = logging.create_dev_logger()
    with spy_on(mail.send) as spy:
        process_event(event, render, MockThreadStore(), logger, 0, Mock(),
                      mail)
        assert len(spy.calls) == 1
        assert spy.calls[0].args[0].template_path == "minimal"
예제 #5
0
def test_unique_number_is_different_for_each_thread_email():
    template_store = MockTemplateStore()
    render = Render(template_store)
    thread_store = MockThreadStore()
    render.process_event_to_emails(_create_public_event(1), thread_store)
    assert template_store.last_template_params["unique_number"] == 1

    render.process_event_to_emails(_create_public_event(1), thread_store)
    assert template_store.last_template_params["unique_number"] == 2

    render.process_event_to_emails(_create_public_event(2), thread_store)
    assert template_store.last_template_params["unique_number"] == 1
예제 #6
0
def process_events_minimal(
    timestamp: int,
    minimal_context: dict,
    render: Render,
    thread_store: ThreadStore,
    stats: StatsClient,
    logger: Logger,
    retry_delay_seconds: int,
    filter_recipients: Optional[list[str]],
    mail,
) -> ProcessEventResult:
    """Render and send emails with "minimal context".

    Returns the processing state (success, failed to render, failed to send) with
    contextual information (number of emails sent, recipients who didn't receive
    email, etc).
    """
    try:
        emails = render.process_event_to_emails_with_minimal_context(
            timestamp, minimal_context, thread_store
        )
    except _RENDER_EXCEPTIONS as e:
        _report_render_failure(logger, e)
        return ProcessEventResult(ProcessEventState.FAILED_TO_RENDER, 0)

    if filter_recipients is not None:
        # Don't send to all recipients, just send to the subset of recipients provided
        # who didn't receive a "full context" email.
        emails = [email for email in emails if email.to in filter_recipients]

    permanent_send_failure_recipients = _send_emails(
        mail,
        stats,
        logger,
        emails,
        retry_delay_seconds,
    )
    if permanent_send_failure_recipients:
        return ProcessEventResult(
            ProcessEventState.FAILED_TO_SEND,
            len(emails) - len(permanent_send_failure_recipients),
            permanent_send_failure_recipients,
        )
    return ProcessEventResult(ProcessEventState.SUCCESS, len(emails))
예제 #7
0
def process_emails_full(
    timestamp: int,
    is_secure: bool,
    context: dict,
    render: Render,
    thread_store: ThreadStore,
    stats: StatsClient,
    logger: Logger,
    retry_delay_seconds: int,
    mail,
) -> ProcessEventResult:
    """Render and send emails with "full context".

    Returns the processing state (success, failed to render, failed to send) with
    contextual information (number of emails sent, recipients who didn't receive
    email, etc).
    """
    try:
        emails = render.process_event_to_emails_with_full_context(
            is_secure, timestamp, context, thread_store
        )
    except _RENDER_EXCEPTIONS as e:
        _report_render_failure(logger, e)
        return ProcessEventResult(ProcessEventState.FAILED_TO_RENDER, 0)

    permanent_send_failure_recipients = _send_emails(
        mail,
        stats,
        logger,
        emails,
        retry_delay_seconds,
    )
    if permanent_send_failure_recipients:
        return ProcessEventResult(
            ProcessEventState.FAILED_TO_SEND,
            len(emails) - len(permanent_send_failure_recipients),
            permanent_send_failure_recipients,
        )

    return ProcessEventResult(ProcessEventState.SUCCESS, len(emails))
예제 #8
0
def test_integration_pipeline():
    source = MockSource(
        next_result={
            "data": {
                "storyErrors":
                0,
                "events": [
                    {
                        "isSecure": True,
                        "timestamp": 0,
                        "context": {
                            "eventKind": "revision-reclaimed",
                            "actor": {
                                "userName": "******",
                                "realName": "1"
                            },
                            "body": {
                                "reviewers": [
                                    {
                                        "name":
                                        "2",
                                        "isActionable":
                                        False,
                                        "status":
                                        "accepted",
                                        "recipients": [{
                                            "timezoneOffset": 0,
                                            "username": "******",
                                            "email": "2@mail",
                                            "isActor": False,
                                        }],
                                    },
                                    {
                                        "name":
                                        "3",
                                        "isActionable":
                                        True,
                                        "status":
                                        "requested-changes",
                                        "recipients": [{
                                            "timezoneOffset": 0,
                                            "username": "******",
                                            "email": "3@mail",
                                            "isActor": False,
                                        }],
                                    },
                                ],
                                "subscribers": [{
                                    "email": "3@mail",
                                    "username": "******",
                                    "timezoneOffset": 0,
                                    "isActor": False,
                                }],
                                "commentCount":
                                1,
                                "transactionLink":
                                "link",
                            },
                            "revision": {
                                "revisionId": 1,
                                "repositoryName": "repo",
                                "link": "link",
                                "bug": {
                                    "bugId": 1,
                                    "link": "link"
                                },
                            },
                        },
                    },
                    {
                        "isSecure": False,
                        "timestamp": 1,
                        "context": {
                            "eventKind": "revision-abandoned",
                            "actor": {
                                "userName": "******",
                                "realName": "4"
                            },
                            "body": {
                                "reviewers": [{
                                    "username": "******",
                                    "email": "5@mail",
                                    "timezoneOffset": 0,
                                    "isActor": False,
                                }],
                                "subscribers": [],
                                "mainCommentMessage": {
                                    "asText": "Main comment",
                                    "asHtml": "<p>Main comment</p>",
                                },
                                "inlineComments": [{
                                    "contextKind": "code",
                                    "context": {
                                        "diff": [{
                                            "lineNumber": 10,
                                            "type": "added",
                                            "rawContent": "hello world",
                                        }]
                                    },
                                    "fileContext": "/README:20",
                                    "link": "link",
                                    "message": {
                                        "asText": "great content here.",
                                        "asHtml":
                                        "<em>great content here.</em>",
                                    },
                                }],
                                "transactionLink":
                                "link",
                            },
                            "revision": {
                                "revisionId": 2,
                                "name": "name 2",
                                "repositoryName": "repo",
                                "link": "link",
                            },
                        },
                    },
                ],
            },
            "cursor": {
                "after": 20
            },
        })
    mail = MockMail()
    render = Render(JinjaTemplateStore("", "", False))
    logger = logging.create_dev_logger()
    pipeline = Pipeline(source, render, mail, logger, 0, Mock(), False)
    with spy_on(mail.send) as send_spy, spy_on(source.fetch_next) as fetch_spy:
        new_position = pipeline.run(MockThreadStore(), 10)
        assert new_position == 20
        assert fetch_spy.calls[0].args[0] == 10

        emails = []
        for call in send_spy.calls:
            emails.append(call.args[0])

    _assert_mail(
        emails[0],
        "D1: (secure bug 1)",
        "2@mail",
        "1 reclaimed this revision that you've accepted and submitted a comment.",
    )
    _assert_mail(
        emails[1],
        "D1: (secure bug 1)",
        "3@mail",
        "1 reclaimed this revision and submitted a comment.",
    )
    _assert_mail(
        emails[2],
        "D2: name 2",
        "5@mail",
        "4 abandoned this revision and submitted comments.",
    )
예제 #9
0
def test_retries_failed_full_sends_with_minimal_emails(send_emails_fn):
    event = {
        "timestamp": 0,
        "isSecure": True,
        "context": {
            "eventKind": "revision-reclaimed",
            "actor": {
                "userName": "******",
                "realName": "1"
            },
            "body": {
                "reviewers": [{
                    "name":
                    "2",
                    "isActionable":
                    False,
                    "status":
                    "unreviewed",
                    "recipients": [
                        {
                            "username": "******",
                            "email": "2@mail",
                            "timezoneOffset": 0,
                            "isActor": False,
                        },
                        {
                            "username": "******",
                            "email": "3@mail",
                            "timezoneOffset": 0,
                            "isActor": False,
                        },
                    ],
                }],
                "subscribers": [],
                "commentCount":
                1,
                "transactionLink":
                "link",
            },
            "revision": {
                "revisionId": 1,
                "link": "link",
                "bug": {
                    "bugId": 1,
                    "link": "link"
                },
            },
        },
        "minimalContext": {
            "revision": {
                "revisionId": 1,
                "link": "link",
            },
            "recipients": [
                {
                    "username": "******",
                    "email": "2@mail",
                    "timezoneOffset": 0,
                    "isActor": False,
                },
                {
                    "username": "******",
                    "email": "3@mail",
                    "timezoneOffset": 0,
                    "isActor": False,
                },
            ],
        },
    }

    send_emails_fn.side_effect = [["2@mail"], []]
    render = Render(JinjaTemplateStore("", "", False))
    logger = logging.create_dev_logger()
    process_event(event, render, MockThreadStore(), logger, 0, Mock(), None)
    assert len(send_emails_fn.call_args_list) == 2
    assert len(send_emails_fn.call_args_list[1][0][3]) == 1
    _assert_mail(
        send_emails_fn.call_args_list[1][0][3][0],
        "D1",
        "2@mail",
        "An (unknown) action occurred",
    )