User email confirmation

Sending emails and testing with Python

Want more?

This lesson for enrolled students only. Join the course to unlock it!

You can see the code changes implemented in this lecture below.

If you have purchased the course in a different platform, you still have access to the code changes per lecture here on Teclado. The lecture video and lecture notes remain locked.
Join course for $30

New files

storeapi/tasks.py
import logging

import httpx

from storeapi.config import config

logger = logging.getLogger(__name__)


class APIResponseError(Exception):
    pass


async def send_simple_message(to: str, subject: str, body: str):
    logger.debug(f"Sending email to '{to[:3]}' with subject '{subject[:20]}'")
    async with httpx.AsyncClient() as client:
        try:
            response = await client.post(
                f"https://api.mailgun.net/v3/{config.MAILGUN_DOMAIN}/messages",
                auth=("api", config.MAILGUN_API_KEY),
                data={
                    "from": f"Jose Salvatierra <mailgun@{config.MAILGUN_DOMAIN}>",
                    "to": [to],
                    "subject": subject,
                    "text": body,
                },
            )
            response.raise_for_status()

            logger.debug(response.content)

            return response
        except httpx.HTTPStatusError as err:
            raise APIResponseError(
                f"API request failed with status code {err.response.status_code}"
            ) from err


async def send_user_registration_email(email: str, confirmation_url: str):
    return await send_simple_message(
        email,
        "Successfully signed up",
        (
            f"Hi {email}! You have successfully signed up to the Stores REST API."
            " Please confirm your email by clicking on the"
            f" following link: {confirmation_url}"
        ),
    )
storeapi/tests/test_tasks.py
import httpx
import pytest
from storeapi.tasks import APIResponseError, send_simple_message


@pytest.mark.anyio
async def test_send_simple_message(mock_httpx_client):
    await send_simple_message("test@example.net", "Test Subject", "Test Body")
    mock_httpx_client.post.assert_called()


@pytest.mark.asyncio
async def test_send_simple_message_api_error(mock_httpx_client):
    mock_httpx_client.post.return_value = httpx.Response(
        status_code=500, content="", request=httpx.Request("POST", "//")
    )

    with pytest.raises(APIResponseError, match="API request failed with status code"):
        await send_simple_message("test@example.com", "Test Subject", "Test Body")

Modified files

requirements.txt
--- 
+++ 
@@ -10,4 +10,5 @@
 logtail-python
 python-jose
 python-multipart
-passlib[bcrypt]+passlib[bcrypt]
+httpx
requirements-dev.txt
--- 
+++ 
@@ -1,6 +1,5 @@
 ruff
 black
 isort
-httpx
 pytest
 pytest-mock
storeapi/tests/conftest.py
--- 
+++ 
@@ -1,9 +1,10 @@
 import os
 from typing import AsyncGenerator, Generator
+from unittest.mock import AsyncMock, Mock

 import pytest
 from fastapi.testclient import TestClient
-from httpx import AsyncClient
+from httpx import AsyncClient, Request, Response

 os.environ["ENV_STATE"] = "test"
 from storeapi.database import database, user_table  # noqa: E402
@@ -58,3 +59,19 @@
 async def logged_in_token(async_client: AsyncClient, confirmed_user: dict) -> str:
     response = await async_client.post("/token", json=confirmed_user)
     return response.json()["access_token"]
+
+
+@pytest.fixture(autouse=True)
+def mock_httpx_client(mocker):
+    """
+    Fixture to mock the HTTPX client so that we never make any
+    real HTTP requests (especially important when registering users).
+    """
+    mocked_client = mocker.patch("storeapi.tasks.httpx.AsyncClient")
+
+    mocked_async_client = Mock()
+    response = Response(status_code=200, content="", request=Request("POST", "//"))
+    mocked_async_client.post = AsyncMock(return_value=response)
+    mocked_client.return_value.__aenter__.return_value = mocked_async_client
+
+    return mocked_async_client