Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Issue #3540] AWS Pinpoint Mock setup #3652

Merged
merged 4 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions api/src/adapters/aws/aws_session.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
import os

import boto3

from src.util.env_config import PydanticBaseEnvConfig


class BaseAwsConfig(PydanticBaseEnvConfig):
is_local_aws: bool = False


_base_aws_config: BaseAwsConfig | None = None


def get_base_aws_config() -> BaseAwsConfig:
global _base_aws_config
if _base_aws_config is None:
_base_aws_config = BaseAwsConfig()

return _base_aws_config


def is_local_aws() -> bool:
"""Whether we are running against local AWS which affects the credentials we use (forces them to be not real)"""
return get_base_aws_config().is_local_aws


def get_boto_session() -> boto3.Session:
is_local = bool(os.getenv("IS_LOCAL_AWS", False))
if is_local:
return boto3.Session(aws_access_key_id="NO_CREDS", aws_secret_access_key="NO_CREDS")
if is_local_aws():
# Locally, set fake creds in a region we don't actually use so we can't hit actual AWS resources
return boto3.Session(
aws_access_key_id="NO_CREDS", aws_secret_access_key="NO_CREDS", region_name="us-west-2"
)

return boto3.Session()
120 changes: 120 additions & 0 deletions api/src/adapters/aws/pinpoint_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import logging
import uuid

import boto3
import botocore.client
from botocore.exceptions import ClientError
from pydantic import BaseModel, Field

from src.adapters.aws import get_boto_session
from src.adapters.aws.aws_session import is_local_aws

logger = logging.getLogger(__name__)

# An example of what the Pinpoint response looks like:
"""
{
"ResponseMetadata": {
"RequestId": "abcdef11-1111-2222-3333-4444abcabc",
"HTTPStatusCode": 200,
"HTTPHeaders": {
# A bunch of generic HTTP/AWS headers
},
"RetryAttempts": 0
},
"MessageResponse": {
"ApplicationId": "abc123",
"RequestId": "ABCD-ASDASDASDAS",
"Result": {
"person@fake.com": {
"DeliveryStatus": "SUCCESSFUL",
"StatusCode": 200,
"StatusMessage": "abcdef"
}
}
}
}
"""


class PinpointResult(BaseModel):
delivery_status: str = Field(alias="DeliveryStatus")
status_code: int = Field(alias="StatusCode")
status_message: str = Field(alias="StatusMessage")


class PinpointResponse(BaseModel):
results: dict[str, PinpointResult] = Field(alias="Result", default_factory=dict)


def get_pinpoint_client(session: boto3.Session | None = None) -> botocore.client.BaseClient:
if session is None:
session = get_boto_session()

return session.client("pinpoint")


def send_pinpoint_email_raw(
to_address: str,
subject: str,
message: str,
app_id: str,
pinpoint_client: botocore.client.BaseClient | None = None,
) -> PinpointResponse:

if pinpoint_client is None:
pinpoint_client = get_pinpoint_client()

# Based on: https://docs.aws.amazon.com/code-library/latest/ug/python_3_pinpoint_code_examples.html
request = {
"ApplicationId": app_id,
"MessageRequest": {
"Addresses": {to_address: {"ChannelType": "EMAIL"}},
"MessageConfiguration": {
"EmailMessage": {
# TODO - we'll switch this to use templates in the future
# so keeping this simple with html/text the same
"SimpleEmail": {
"Subject": {"Charset": "UTF-8", "Data": subject},
"HtmlPart": {"Charset": "UTF-8", "Data": message},
"TextPart": {"Charset": "UTF-8", "Data": message},
}
}
},
},
}
# If we are running locally (or in unit tests), don't actually query
# AWS - unlike our other AWS integrations, there is no mocking support yet
# for Pinpoint, so we built something ourselves that also works when run locally
if is_local_aws():
return _handle_mock_response(request, to_address)

try:
raw_response = pinpoint_client.send_messages(**request)
except ClientError:
logger.exception("Failed to send email")
raise

return PinpointResponse.model_validate(raw_response["MessageResponse"])


_mock_responses: list[tuple[dict, PinpointResponse]] = []


def _handle_mock_response(request: dict, to_address: str) -> PinpointResponse:
# By default, return a response that roughly looks like a real success
response = PinpointResponse(
Result={
to_address: PinpointResult(
DeliveryStatus="SUCCESSFUL", StatusCode=200, StatusMessage=str(uuid.uuid4())
)
}
)
global _mock_responses
_mock_responses.append((request, response))

return response


def _get_mock_responses() -> list[tuple[dict, PinpointResponse]]:
return _mock_responses
Empty file.
79 changes: 79 additions & 0 deletions api/tests/src/adapters/aws/test_pinpoint_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from src.adapters.aws.pinpoint_adapter import _get_mock_responses, send_pinpoint_email_raw


def test_send_pinpoint_email_raw_with_mock():
"""Just a quick sanity check that the pinpoint mocking has a behavior we can use for other tests"""
resp1 = send_pinpoint_email_raw(
to_address="fake_mail1@fake.com",
subject="email subject",
message="this is an email",
app_id="fake_app_id1",
)
resp2 = send_pinpoint_email_raw(
to_address="fake_mail2@fake.com",
subject="different subject",
message="different email",
app_id="fake_app_id2",
)
resp3 = send_pinpoint_email_raw(
to_address="fake_mail3@fake.com",
subject="another subject",
message="another email",
app_id="fake_app_id2",
)
mock_responses = _get_mock_responses()
assert len(mock_responses) == 3

req_resp1 = mock_responses[0]
assert resp1 == req_resp1[1]
req1 = req_resp1[0]
assert req1["ApplicationId"] == "fake_app_id1"
assert req1["MessageRequest"]["Addresses"] == {"fake_mail1@fake.com": {"ChannelType": "EMAIL"}}
assert (
req1["MessageRequest"]["MessageConfiguration"]["EmailMessage"]["SimpleEmail"]["Subject"][
"Data"
]
== "email subject"
)
assert (
req1["MessageRequest"]["MessageConfiguration"]["EmailMessage"]["SimpleEmail"]["HtmlPart"][
"Data"
]
== "this is an email"
)

req_resp2 = mock_responses[1]
assert resp2 == req_resp2[1]
req2 = req_resp2[0]
assert req2["ApplicationId"] == "fake_app_id2"
assert req2["MessageRequest"]["Addresses"] == {"fake_mail2@fake.com": {"ChannelType": "EMAIL"}}
assert (
req2["MessageRequest"]["MessageConfiguration"]["EmailMessage"]["SimpleEmail"]["Subject"][
"Data"
]
== "different subject"
)
assert (
req2["MessageRequest"]["MessageConfiguration"]["EmailMessage"]["SimpleEmail"]["HtmlPart"][
"Data"
]
== "different email"
)

req_resp3 = mock_responses[2]
assert resp3 == req_resp3[1]
req3 = req_resp3[0]
assert req3["ApplicationId"] == "fake_app_id2"
assert req3["MessageRequest"]["Addresses"] == {"fake_mail3@fake.com": {"ChannelType": "EMAIL"}}
assert (
req3["MessageRequest"]["MessageConfiguration"]["EmailMessage"]["SimpleEmail"]["Subject"][
"Data"
]
== "another subject"
)
assert (
req3["MessageRequest"]["MessageConfiguration"]["EmailMessage"]["SimpleEmail"]["HtmlPart"][
"Data"
]
== "another email"
)