diff --git a/tests/adapters/incoming_messages_consumer/test_incoming_message_returns_fallback_message.py b/tests/adapters/incoming_messages_consumer/test_incoming_message_returns_fallback_message.py index 6bf851fcd..b38cf6afc 100644 --- a/tests/adapters/incoming_messages_consumer/test_incoming_message_returns_fallback_message.py +++ b/tests/adapters/incoming_messages_consumer/test_incoming_message_returns_fallback_message.py @@ -1,5 +1,11 @@ +import logging +from typing import Type + import callee import pytest +from pika.adapters.blocking_connection import BlockingChannel +from pytest import LogCaptureFixture +from pytest_mock import MockFixture from tests.adapters.incoming_messages_consumer import helpers from use_case_executor import constants @@ -12,49 +18,28 @@ from use_case_executor.domain.errors import InvalidFlowConfiguration from use_case_executor.domain.errors import InvalidFlowFormat from use_case_executor.domain.errors import InvalidUseCaseConfiguration +from use_case_executor.domain.errors import Misconfiguration from use_case_executor.domain.errors import NLPExtractionError +from use_case_executor.domain.errors import TechnicalException from use_case_executor.domain.errors import UserEventSendError from use_case_executor.domain.message_processing.answer_sender import AnswerSender from use_case_executor.domain.monitoring.monitoring_timer import MonitoringTimer -@pytest.mark.parametrize( - "exception_class, fallback_text", - [ - (ForbiddenUseCase, "Sorry, something looks misconfigured: test exception."), - ( - InvalidUseCaseConfiguration, - "Sorry, something looks misconfigured: test exception.", - ), - ( - InvalidFlowConfiguration, - "Sorry, something looks misconfigured: test exception.", - ), - (InvalidFlowFormat, InvalidFlowFormat().end_user_message), - (InvalidClientMessageFormat, InvalidClientMessageFormat().end_user_message), - (NLPExtractionError, NLPExtractionError().end_user_message), - (UserEventSendError, UserEventSendError().end_user_message), - ( - ImpossibleHandoverAvailabilityCheck, - ImpossibleHandoverAvailabilityCheck().end_user_message, - ), - (ImpossibleTicketCreation, ImpossibleTicketCreation().end_user_message), - (Exception, constants.FALLBACK_MESSAGE), - ], -) -def test_process_incoming_message_raising_exception_sends_a_fallback_message( - mocker, - instance_id, - nlp_language, - content_language, - uce_incoming_messages_channel, - exception_class, - fallback_text, -): +def set_mocks_publish_message_and_check_mock_calls( + mocker: MockFixture, + exception: Exception, + instance_id: int, + nlp_language: str, + content_language: str, + use_latest: bool, + uce_incoming_messages_channel: BlockingChannel, + fallback_text: str, +) -> None: process_incoming_message_mock = mocker.patch.object( facade, "process_incoming_message", - side_effect=exception_class("test exception"), + side_effect=exception, autospec=True, ) send_message_acknowledgement_and_fallback_answer_mock = mocker.patch.object( @@ -63,12 +48,11 @@ def test_process_incoming_message_raising_exception_sends_a_fallback_message( side_effect=None, autospec=True, ) - incoming_message = helpers.make_incoming_message( instance_id=instance_id, nlp_language=nlp_language, content_language=content_language, - use_latest=False, + use_latest=use_latest, ) ( message_properties, @@ -77,7 +61,6 @@ def test_process_incoming_message_raising_exception_sends_a_fallback_message( message_to_send=incoming_message, uce_incoming_messages_channel=uce_incoming_messages_channel, ) - process_incoming_message_mock.assert_called_once_with( message_forward_data=BotMessageForwardData( message_properties=message_properties, message_body=message_body @@ -88,6 +71,134 @@ def test_process_incoming_message_raising_exception_sends_a_fallback_message( ) send_message_acknowledgement_and_fallback_answer_mock.assert_called_once_with( answer_sender=callee.InstanceOf(AnswerSender), - exception_raised=exception_class.__name__, + exception_raised=exception.__class__.__name__, fallback_text=fallback_text, ) + + +@pytest.mark.parametrize("use_latest", [True, False]) +def test_raising_exception_sends_default_fallback_message_and_log_exception_as_error( + mocker: MockFixture, + instance_id: int, + nlp_language: str, + content_language: str, + use_latest: bool, + uce_incoming_messages_channel: BlockingChannel, + caplog: LogCaptureFixture, +) -> None: + exception = Exception("test exception") + set_mocks_publish_message_and_check_mock_calls( + mocker=mocker, + exception=exception, + instance_id=instance_id, + nlp_language=nlp_language, + content_language=content_language, + use_latest=use_latest, + uce_incoming_messages_channel=uce_incoming_messages_channel, + fallback_text=constants.FALLBACK_MESSAGE, + ) + + with caplog.at_level(logging.ERROR): + assert f"{exception.__class__.__name__}: test exception" in caplog.text + + +@pytest.mark.parametrize("use_latest", [True, False]) +@pytest.mark.parametrize( + "exception_class", + [ + InvalidFlowFormat, + InvalidClientMessageFormat, + NLPExtractionError, + UserEventSendError, + ImpossibleHandoverAvailabilityCheck, + ImpossibleTicketCreation, + ], +) +def test_raising_technical_exception_sends_custom_fallback_message_and_log_exception_as_error( + mocker: MockFixture, + instance_id: int, + nlp_language: str, + content_language: str, + use_latest: bool, + uce_incoming_messages_channel: BlockingChannel, + caplog: LogCaptureFixture, + exception_class: Type[TechnicalException], +) -> None: + exception = exception_class("test exception") + set_mocks_publish_message_and_check_mock_calls( + mocker=mocker, + exception=exception, + instance_id=instance_id, + nlp_language=nlp_language, + content_language=content_language, + use_latest=use_latest, + uce_incoming_messages_channel=uce_incoming_messages_channel, + fallback_text=exception.end_user_message, + ) + + with caplog.at_level(logging.ERROR): + assert f"{exception.__class__.__name__}: test exception" in caplog.text + + +@pytest.mark.parametrize( + "exception_class", + [ForbiddenUseCase, InvalidUseCaseConfiguration, InvalidFlowConfiguration], +) +def test_raising_misconfiguration_sends_custom_fallback_message_and_log_exception_as_error_if_not_use_latest( + mocker: MockFixture, + instance_id: int, + nlp_language: str, + content_language: str, + uce_incoming_messages_channel: BlockingChannel, + caplog: LogCaptureFixture, + exception_class: Type[Misconfiguration], +) -> None: + exception = exception_class("test exception") + set_mocks_publish_message_and_check_mock_calls( + mocker=mocker, + exception=exception, + instance_id=instance_id, + nlp_language=nlp_language, + content_language=content_language, + use_latest=False, + uce_incoming_messages_channel=uce_incoming_messages_channel, + fallback_text="Sorry, something looks misconfigured: test exception.", + ) + + with caplog.at_level(logging.ERROR): + assert f"{exception.__class__.__name__}: test exception" in caplog.text + + +@pytest.mark.parametrize( + "exception_class", + [ForbiddenUseCase, InvalidUseCaseConfiguration, InvalidFlowConfiguration], +) +def test_raising_misconfiguration_sends_custom_fallback_message_and_log_exception_as_info_if_use_latest( + mocker: MockFixture, + instance_id: int, + nlp_language: str, + content_language: str, + uce_incoming_messages_channel: BlockingChannel, + caplog: LogCaptureFixture, + exception_class: Type[Misconfiguration], +) -> None: + exception = exception_class("test exception") + set_mocks_publish_message_and_check_mock_calls( + mocker=mocker, + exception=exception, + instance_id=instance_id, + nlp_language=nlp_language, + content_language=content_language, + use_latest=True, + uce_incoming_messages_channel=uce_incoming_messages_channel, + fallback_text="Sorry, something looks misconfigured: test exception.", + ) + + with caplog.at_level(logging.INFO): + assert ( + f"Caught misconfiguration error, ignoring it: {repr(exception)}" + in caplog.text + ) + + with caplog.at_level(logging.ERROR): + assert f"{exception.__class__.__name__}: test exception" not in caplog.text diff --git a/tests/domain/execute_use_case_and_send_answer/test_incoming_message_returns_a_fallback_message.py b/tests/domain/execute_use_case_and_send_answer/test_incoming_message_returns_a_fallback_message.py index a80bf9fe5..23f65d67e 100644 --- a/tests/domain/execute_use_case_and_send_answer/test_incoming_message_returns_a_fallback_message.py +++ b/tests/domain/execute_use_case_and_send_answer/test_incoming_message_returns_a_fallback_message.py @@ -19,7 +19,7 @@ "use_latest, exception_message", [ (True, "use case does not have any flow to execute"), - (False, "This use case is not published"), + (False, "this use case is not published"), ], ) def test_incoming_message_with_matching_use_case_and_no_flow_returns_a_fallback_message( diff --git a/tests/worker/message_handling/test_incoming_message_returns_a_fallback_message.py b/tests/worker/message_handling/test_incoming_message_returns_a_fallback_message.py index d6d7c1263..271f144bc 100644 --- a/tests/worker/message_handling/test_incoming_message_returns_a_fallback_message.py +++ b/tests/worker/message_handling/test_incoming_message_returns_a_fallback_message.py @@ -76,7 +76,7 @@ def test_incoming_message_with_invalid_content_language_returns_a_fallback_messa "use_latest, exception_message", [ (True, "use case does not have any flow to execute"), - (False, "This use case is not published"), + (False, "this use case is not published"), ], ) def test_incoming_message_with_matching_use_case_and_no_flow_returns_a_fallback_message( diff --git a/use_case_executor/adapters/incoming_messages_consumer.py b/use_case_executor/adapters/incoming_messages_consumer.py index c4345775f..cbc6ff8a0 100644 --- a/use_case_executor/adapters/incoming_messages_consumer.py +++ b/use_case_executor/adapters/incoming_messages_consumer.py @@ -10,6 +10,7 @@ from use_case_executor.adapters.bot_message_forwarder import BotMessageForwardData from use_case_executor.domain import facade from use_case_executor.domain.errors import DomainException +from use_case_executor.domain.errors import Misconfiguration from use_case_executor.domain.message_processing.answer_sender import AnswerSender from use_case_executor.domain.monitoring.monitoring_timer import MonitoringTimer from use_case_executor.helpers import rmq_connection @@ -151,4 +152,9 @@ def _process_uce_incoming_message(properties: BasicProperties, body: bytes) -> N fallback_text=fallback_text, ) + # When use_latest is True, we do not send misconfiguration errors to Sentry + if message_json.get("use_latest") and isinstance(exc, Misconfiguration): + log.info("Caught misconfiguration error, ignoring it: %s", repr(exc)) + return + raise diff --git a/use_case_executor/domain/message_processing/message_processing.py b/use_case_executor/domain/message_processing/message_processing.py index 8033cc74f..381490371 100644 --- a/use_case_executor/domain/message_processing/message_processing.py +++ b/use_case_executor/domain/message_processing/message_processing.py @@ -117,7 +117,7 @@ def execute_use_case_and_send_answer( monitoring_timer.step_done("load_use_case_flow") if not flow: if not execution_environment.use_latest: - raise InvalidUseCaseConfiguration("This use case is not published") + raise InvalidUseCaseConfiguration("this use case is not published") raise InvalidUseCaseConfiguration("use case does not have any flow to execute") log.info("Flow loaded")