Search code examples
pythonunit-testingpytestamazon-sesmoto

Cannot pytest AWS SES using mock_ses from moto


I am struggling to write mock test for AWS SES.

Below is the content of aws_ses.py the file that I want to test-

def invoke_ses(send_args,ses_client):
    try:
        response = ses_client.send_templated_email(**send_args)
    except ClientError as e:
        logging.error(e)
        return e.value.response
    else:
        return response

Below is the content of test_aws_ses.py, the test file

from moto import mock_ses
from conftest import TEST_KWARGS
from emails.aws_ses import invoke_ses


def test_invoke_ses(aws_credentials, ses_client):

    # Issue 1
    assert invoke_ses(TEST_KWARGS, ses_client) == ClientError

    verification_response = ses_client.verify_domain_identity(Domain="test.com")
    assert verification_response['VerificationToken'] == "QTKknzFg2J4ygwa+XvHAxUl1hyHoY0gVfZdfjIedHZ0="
    assert verification_response['ResponseMetadata']['HTTPStatusCode'] == 200
    
    # Issue 2
    with pytest.raises(ClientError) as ex:
        invoke_ses(**TEST_KWARGS,ses_client)
    
    assert ex.value.response["Error"]["Code"] == "TemplateDoesNotExist"


The parameters to test_invoke_ses method - aws_credentials and ses_client are python fixtures defined in conftest.py as below -

import os
import boto3
import pytest
from moto import mock_ses


DEFAULT_AWS_REGION = 'us-west-2'
SOURCE = '[email protected]'
TEST_TEMPLATE_NAME = 'TEST_TEMPLATE_1'
TEST_EMAIL_RECIPIENTS = ['[email protected]','[email protected]','[email protected]']
TEST_CC_LIST = ['[email protected]','[email protected]']
TEST_BCC_LIST = ['[email protected]','[email protected]']
TEST_SES_DESTINATION_WITH_LIMITED_EMAIL_RECIPIENTS={
            "ToAddresses": TEST_EMAIL_RECIPIENTS,
            "CcAddresses": TEST_CC_LIST,
            "BccAddresses": TEST_BCC_LIST,
        }
TEST_KWARGS = dict(
        Source=SOURCE,
        Destination=TEST_SES_DESTINATION_WITH_LIMITED_EMAIL_RECIPIENTS,
        Template=TEST_TEMPLATE_NAME,
        TemplateData='{"name": "test"}'
    )

@pytest.fixture
def aws_credentials():
    os.environ["AWS_ACCESS_KEY_ID"] = "testing"
    os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
    os.environ["AWS_SECURITY_TOKEN"] = "testing"
    os.environ["AWS_SESSION_TOKEN"] = "testing"
    os.environ["AWS_DEFAULT_REGION"] = "us-east-1"


@mock_ses
@pytest.fixture
def ses_client():
    ses_client = boto3.client("ses", region_name=DEFAULT_AWS_REGION)
    yield ses_client

As I have commented in test_invoke_ses method there are 2 issues that I am currrently facing -

Issue 1

assert invoke_ses(TEST_KWARGS, ses_client) == ClientError The statement above throws the following error -

>       assert invoke_ses(TEST_KWARGS, ses_client) == ClientError
[CPython37-test] E       AssertionError: assert 'ClientError' == <class 'botoc....ClientError'>
[CPython37-test] E         +'ClientError'
[CPython37-test] E         -<class 'botocore.exceptions.ClientError'>
[CPython37-test] 
[CPython37-test] test/test_aws_ses.py:32: AssertionError

Issue 2

with pytest.raises(ClientError) as ex:
        ses_client.send_templated_email(**TEST_KWARGS)

The statements above throws the following error -

 >           invoke_ses(TEST_KWARGS, ses_client)
[CPython37-test] E           Failed: DID NOT RAISE <class 'botocore.exceptions.ClientError'>
[CPython37-test] 
[CPython37-test] test/test_aws_ses.py:39: Failed

If I am replacing invoke_ses(TEST_KWARGS, ses_client) with ses_client.send_templated_email(**TEST_KWARGS) all the test cases are passing.

I do not understand as to why when calling the same method - ses_client.send_templated_email from a method invoke_ses written in another file aws_ses.py is failing but calling it directly is passing.


Solution

  • The issue was in the response of the method invoke_ses. The response received when the try block is successful is a dictionary. But when the exception block executes, it threw an exception which I was not capturing. hence I modified the response in the Exception block as below -

    Original Method

    def invoke_ses(send_args,ses_client):
        try:
            response = ses_client.send_templated_email(**send_args)
        except ClientError as e:
            logging.error(e)
            return e.value.response
        else:
            return response
    

    New Method

    def invoke_ses(send_args, source, email_recipients, ses_client):
        try:
            response = ses_client.send_templated_email(**send_args)
            httpStatusCode = response['ResponseMetadata']['HTTPStatusCode']
            message_id = response['MessageId']
            logging.info(f"Sent templated mail {message_id} from {source} to Email Recipients SUCCESS")
        except ClientError as e:
            exception_string = str(e)
            logging.error(f"{str(e)}")
            return { 'Exception' : exception_string}
        else:
            return response
    

    The difference is -

    Issue 1 Solution -

    Original Code

    assert invoke_ses(TEST_KWARGS, ses_client) == ClientError
    

    Changed the above code to -

    response = invoke_ses(TEST_KWARGS, SOURCE, TEST_SES_DESTINATION_WITH_LIMITED_EMAIL_RECIPIENTS, ses_client)
        assert response['Exception'] == 'An error occurred (MessageRejected) when calling the SendTemplatedEmail operation: Email address not verified [email protected]'
    

    Issue 2 Solution -

    Original Code

    with pytest.raises(ClientError) as ex:
            invoke_ses(**TEST_KWARGS,ses_client)
        
        assert ex.value.response["Error"]["Code"] == "TemplateDoesNotExist"
    

    Changed the above code to -

    response = invoke_ses(TEST_KWARGS, SOURCE, TEST_SES_DESTINATION_WITH_LIMITED_EMAIL_RECIPIENTS, ses_client)
        assert response['Exception'] == 'An error occurred (TemplateDoesNotExist) when calling the SendTemplatedEmail operation: Template (TEST_TEMPLATE_1) does not exist'