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