import json import uuid from typing import Any from unittest import mock from fansifter_common.constants import QA_ENVIRONMENT from app.config import Settings from app.connectors.db.models import AccountToFanResponseEmailAddressMapping from app.handlers import SendgridWebhooksReplyHandler from app.models import ReplyRequest TEST_REPLY_SENDER_UUID = uuid.UUID("12345678-1234-5678-1234-567812345678") def build_mapping( vendor_id: int = 7123, email_address: str = "artist@example.com" ) -> AccountToFanResponseEmailAddressMapping: return AccountToFanResponseEmailAddressMapping( vendor_id=vendor_id, subaccount_id=100, email_address=email_address, ) def test_reply_handler_success_with_forward_email( test_reply_handler: SendgridWebhooksReplyHandler, s3_client_mock: mock.MagicMock, kafka_producer_mock: mock.MagicMock, repository_mock: mock.MagicMock, redis_client_mock: mock.MagicMock, sendgrid_client_mock: mock.MagicMock, test_reply_event: dict[str, Any], settings: Settings, ) -> None: repository_mock.get_forward_email_mapping.return_value = build_mapping() request = ReplyRequest.model_validate( test_reply_event, context={"secret_key": "test"}, ) expected_from_email = settings.from_email_template.format( unique_part=TEST_REPLY_SENDER_UUID.hex[:8] ) with mock.patch("app.handlers.uuid.uuid4", return_value=TEST_REPLY_SENDER_UUID): result = test_reply_handler.handle(request, test_reply_event) repository_mock.get_forward_email_mapping.assert_called_once_with( email_id="11111111-1111-1111-1111-111111111111", email_type="CAMPAIGN", ) redis_client_mock.incr.assert_called_once_with( "lambda-audience-sendgrid-webhooks-inbound:reply:" "11111111-1111-1111-1111-111111111111:" f"{request.email_hash}" ) redis_client_mock.expire.assert_called_once_with( "lambda-audience-sendgrid-webhooks-inbound:reply:" "11111111-1111-1111-1111-111111111111:" f"{request.email_hash}", settings.reply_requests_count_reset_seconds, ) sendgrid_client_mock.send_email.assert_called_once_with( to_email="artist@example.com", subject="Re: Hello Inbox", text_content="This is the reply body text.", html_content="", from_email=expected_from_email, from_email_display_name="sender@example.com", reply_to="sender@example.com", categories=["Fan Reply Forwarding"], custom_args={"send_type": "fan_reply_forwarding"}, ) kafka_producer_mock.produce.assert_called_once() kafka_call_args = kafka_producer_mock.produce.call_args kafka_value = json.loads(kafka_call_args.kwargs["value"]) assert kafka_value["email"] == "sender@example.com" assert kafka_value["email_campaign_id"] == "11111111-1111-1111-1111-111111111111" assert kafka_value["email_type"] == "CAMPAIGN" assert kafka_value["email_id"] == "11111111-1111-1111-1111-111111111111" assert kafka_value["automated_email_trigger_id"] is None assert kafka_value["subject"] == "Re: Hello Inbox" assert kafka_value["text"] == "This is the reply body text." assert "timestamp" in kafka_value assert "recipient_email" in kafka_value s3_client_mock.put_object.assert_not_called() assert result == {"statusCode": 200} def test_reply_handler_success_with_default_email( test_reply_handler: SendgridWebhooksReplyHandler, s3_client_mock: mock.MagicMock, kafka_producer_mock: mock.MagicMock, repository_mock: mock.MagicMock, redis_client_mock: mock.MagicMock, sendgrid_client_mock: mock.MagicMock, test_reply_event: dict[str, Any], settings: Settings, ) -> None: repository_mock.get_forward_email_mapping.return_value = None request = ReplyRequest.model_validate( test_reply_event, context={"secret_key": "test"}, ) expected_from_email = settings.from_email_template.format( unique_part=TEST_REPLY_SENDER_UUID.hex[:8] ) with mock.patch("app.handlers.uuid.uuid4", return_value=TEST_REPLY_SENDER_UUID): result = test_reply_handler.handle(request, test_reply_event) repository_mock.get_forward_email_mapping.assert_called_once_with( email_id="11111111-1111-1111-1111-111111111111", email_type="CAMPAIGN", ) sendgrid_client_mock.send_email.assert_called_once_with( to_email=settings.default_forward_email, subject="Re: Hello Inbox", text_content="This is the reply body text.", html_content="", from_email=expected_from_email, from_email_display_name="sender@example.com", reply_to="sender@example.com", categories=["Fan Reply Forwarding"], custom_args={"send_type": "fan_reply_forwarding"}, ) kafka_producer_mock.produce.assert_called_once() redis_client_mock.incr.assert_called_once() s3_client_mock.put_object.assert_not_called() assert result == {"statusCode": 200} def test_reply_handler_repository_fail( test_reply_handler: SendgridWebhooksReplyHandler, s3_client_mock: mock.MagicMock, kafka_producer_mock: mock.MagicMock, repository_mock: mock.MagicMock, redis_client_mock: mock.MagicMock, sendgrid_client_mock: mock.MagicMock, test_reply_event: dict[str, Any], settings: Settings, ) -> None: repository_mock.get_forward_email_mapping.side_effect = Exception() request = ReplyRequest.model_validate( test_reply_event, context={"secret_key": "test"}, ) result = test_reply_handler.handle(request, test_reply_event) repository_mock.get_forward_email_mapping.assert_called_once() redis_client_mock.incr.assert_called_once() sendgrid_client_mock.send_email.assert_not_called() kafka_producer_mock.produce.assert_not_called() s3_client_mock.put_object.assert_called_once() assert result == {"statusCode": 200} def test_reply_handler_sendgrid_fail( test_reply_handler: SendgridWebhooksReplyHandler, s3_client_mock: mock.MagicMock, kafka_producer_mock: mock.MagicMock, repository_mock: mock.MagicMock, redis_client_mock: mock.MagicMock, sendgrid_client_mock: mock.MagicMock, test_reply_event: dict[str, Any], settings: Settings, ) -> None: repository_mock.get_forward_email_mapping.return_value = build_mapping() sendgrid_client_mock.send_email.side_effect = Exception() request = ReplyRequest.model_validate( test_reply_event, context={"secret_key": "test"}, ) result = test_reply_handler.handle(request, test_reply_event) repository_mock.get_forward_email_mapping.assert_called_once() redis_client_mock.incr.assert_called_once() sendgrid_client_mock.send_email.assert_called_once() kafka_producer_mock.produce.assert_not_called() s3_client_mock.put_object.assert_called_once() assert result == {"statusCode": 200} def test_reply_handler_kafka_producer_fail( test_reply_handler: SendgridWebhooksReplyHandler, s3_client_mock: mock.MagicMock, kafka_producer_mock: mock.MagicMock, repository_mock: mock.MagicMock, redis_client_mock: mock.MagicMock, sendgrid_client_mock: mock.MagicMock, test_reply_event: dict[str, Any], settings: Settings, ) -> None: repository_mock.get_forward_email_mapping.return_value = build_mapping() kafka_producer_mock.produce.side_effect = Exception() request = ReplyRequest.model_validate( test_reply_event, context={"secret_key": "test"}, ) result = test_reply_handler.handle(request, test_reply_event) repository_mock.get_forward_email_mapping.assert_called_once() redis_client_mock.incr.assert_called_once() sendgrid_client_mock.send_email.assert_called_once() kafka_producer_mock.produce.assert_called_once() s3_client_mock.put_object.assert_called_once() assert result == {"statusCode": 200} def test_reply_handler_success_with_html_content( test_reply_handler: SendgridWebhooksReplyHandler, s3_client_mock: mock.MagicMock, kafka_producer_mock: mock.MagicMock, repository_mock: mock.MagicMock, redis_client_mock: mock.MagicMock, sendgrid_client_mock: mock.MagicMock, test_reply_event_with_html: dict[str, Any], settings: Settings, ) -> None: repository_mock.get_forward_email_mapping.return_value = build_mapping() request = ReplyRequest.model_validate( test_reply_event_with_html, context={"secret_key": "test"}, ) expected_from_email = settings.from_email_template.format( unique_part=TEST_REPLY_SENDER_UUID.hex[:8] ) with mock.patch("app.handlers.uuid.uuid4", return_value=TEST_REPLY_SENDER_UUID): result = test_reply_handler.handle(request, test_reply_event_with_html) repository_mock.get_forward_email_mapping.assert_called_once_with( email_id="11111111-1111-1111-1111-111111111111", email_type="CAMPAIGN", ) sendgrid_client_mock.send_email.assert_called_once_with( to_email="artist@example.com", subject="Re: Hello Inbox", text_content="This is the reply body text.", html_content="
This is the reply body in HTML.
", from_email=expected_from_email, from_email_display_name="sender@example.com", reply_to="sender@example.com", categories=["Fan Reply Forwarding"], custom_args={"send_type": "fan_reply_forwarding"}, ) kafka_producer_mock.produce.assert_called_once() redis_client_mock.incr.assert_called_once() s3_client_mock.put_object.assert_not_called() assert result == {"statusCode": 200} def test_reply_handler_builds_unique_from_email_for_recipient_display_name( test_reply_handler: SendgridWebhooksReplyHandler, repository_mock: mock.MagicMock, redis_client_mock: mock.MagicMock, sendgrid_client_mock: mock.MagicMock, test_reply_event_recipient_display_name: dict[str, Any], settings: Settings, ) -> None: repository_mock.get_forward_email_mapping.return_value = build_mapping() request = ReplyRequest.model_validate( test_reply_event_recipient_display_name, context={"secret_key": "test"}, ) with mock.patch("app.handlers.uuid.uuid4", return_value=TEST_REPLY_SENDER_UUID): test_reply_handler.handle(request, test_reply_event_recipient_display_name) sendgrid_client_mock.send_email.assert_called_once_with( to_email="artist@example.com", subject="Re: Hello Inbox", text_content="This is the reply body text.", html_content="", from_email=settings.from_email_template.format( unique_part=TEST_REPLY_SENDER_UUID.hex[:8] ), from_email_display_name="sender@example.com", reply_to="sender@example.com", categories=["Fan Reply Forwarding"], custom_args={"send_type": "fan_reply_forwarding"}, ) redis_client_mock.incr.assert_called_once() def test_reply_handler_invalid_signature_puts_event_in_s3( test_reply_handler: SendgridWebhooksReplyHandler, s3_client_mock: mock.MagicMock, kafka_producer_mock: mock.MagicMock, repository_mock: mock.MagicMock, redis_client_mock: mock.MagicMock, sendgrid_client_mock: mock.MagicMock, test_reply_event_invalid_signature: dict[str, Any], ) -> None: request = ReplyRequest.model_validate( test_reply_event_invalid_signature, context={"secret_key": "test"}, ) result = test_reply_handler.handle(request, test_reply_event_invalid_signature) redis_client_mock.incr.assert_not_called() repository_mock.get_forward_email_mapping.assert_not_called() sendgrid_client_mock.send_email.assert_not_called() kafka_producer_mock.produce.assert_not_called() s3_client_mock.put_object.assert_called_once() assert result == {"statusCode": 200} def test_reply_handler_skips_processing_when_request_limit_is_exceeded( test_reply_handler: SendgridWebhooksReplyHandler, s3_client_mock: mock.MagicMock, kafka_producer_mock: mock.MagicMock, repository_mock: mock.MagicMock, redis_client_mock: mock.MagicMock, sendgrid_client_mock: mock.MagicMock, test_reply_event: dict[str, Any], settings: Settings, ) -> None: redis_client_mock.incr.return_value = settings.reply_requests_count_limit + 1 request = ReplyRequest.model_validate( test_reply_event, context={"secret_key": "test"}, ) result = test_reply_handler.handle(request, test_reply_event) redis_client_mock.incr.assert_called_once() redis_client_mock.expire.assert_not_called() repository_mock.get_forward_email_mapping.assert_not_called() sendgrid_client_mock.send_email.assert_not_called() kafka_producer_mock.produce.assert_not_called() s3_client_mock.put_object.assert_not_called() assert result == {"statusCode": 200} def test_reply_handler_skips_sendgrid_forwarding_for_disallowed_qa_account( kafka_producer_mock: mock.MagicMock, s3_client_mock: mock.MagicMock, db_mock: mock.MagicMock, repository_mock: mock.MagicMock, sendgrid_client_mock: mock.MagicMock, redis_client_mock: mock.MagicMock, test_reply_event: dict[str, Any], settings: Settings, ) -> None: repository_mock.get_forward_email_mapping.return_value = build_mapping( vendor_id=9999 ) qa_reply_handler = SendgridWebhooksReplyHandler( kafka_producer=kafka_producer_mock, kafka_sendgrid_reply_topic=settings.kafka_sendgrid_reply_topic, db=db_mock, repository=repository_mock, sendgrid_client=sendgrid_client_mock, redis_client=redis_client_mock, cache_key_template=settings.reply_redis_cache_key_template, requests_count_limit=settings.reply_requests_count_limit, requests_count_reset_seconds=settings.reply_requests_count_reset_seconds, s3_client=s3_client_mock, failed_events_s3_bucket=settings.failed_events_s3_bucket, default_forward_email=settings.default_forward_email, from_email_template=settings.from_email_template, accounts_to_forward_on_qa_env=settings.accounts_to_forward_on_qa_env, env=QA_ENVIRONMENT, ) request = ReplyRequest.model_validate( test_reply_event, context={"secret_key": "test"}, ) result = qa_reply_handler.handle(request, test_reply_event) sendgrid_client_mock.send_email.assert_not_called() kafka_producer_mock.produce.assert_called_once() s3_client_mock.put_object.assert_not_called() assert result == {"statusCode": 200}