Skip to content

Commit 32e0f73

Browse files
improve testing and managing email sending
1 parent 1e1bb23 commit 32e0f73

6 files changed

Lines changed: 216 additions & 63 deletions

File tree

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ GALAXY_API_KEY=api-key
1515
# don't process this with pydantic-settings as it needs
1616
# to be used before the FastAPI app loads
1717
CORS_ALLOWED_ORIGINS=http://localhost:8000
18-
# AWS SES configs
18+
# AWS SES configs - required for running tests locally. Ask amanda@biocommons.org.au for credentials values
1919
AWS_ACCESS_KEY_ID=<aws-access-key-id>
2020
AWS_SECRET_ACCESS_KEY=<aws-secret-access-key>

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ dev = [
2121
"polyfactory>=2.21.0",
2222
"pre-commit>=3.7.0",
2323
"freezegun>=1.5.2",
24-
"respx>=0.22.0"
24+
"respx>=0.22.0",
25+
"moto>=5.0.5"
2526
]
2627

2728
[tool.pytest.ini_options]

routers/bpa_register.py

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from datetime import datetime, timezone
22
from typing import Any, Dict
33

4-
from fastapi import APIRouter, Depends, HTTPException
4+
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
55
from httpx import AsyncClient
66

77
from auth.config import Settings, get_settings
@@ -14,6 +14,29 @@
1414
router = APIRouter(prefix="/bpa", tags=["bpa", "registration"])
1515

1616

17+
def send_approver_email(
18+
email_service: EmailService,
19+
approver_email: str,
20+
registration: BPARegistrationRequest,
21+
bpa_resources: list[Dict[str, Any]],
22+
) -> None:
23+
subject = "New BPA User Access Request"
24+
25+
org_list_html = "".join(
26+
f"<li>{res['name']} (ID: {res['id']})</li>" for res in bpa_resources
27+
)
28+
29+
body_html = f"""
30+
<p>A new user has requested access to one or more organizations in the BPA service.</p>
31+
<p><strong>User:</strong> {registration.fullname} ({registration.email})</p>
32+
<p><strong>Requested access to:</strong></p>
33+
<ul>{org_list_html}</ul>
34+
<p>Please log into the AAI Admin Portal to review and approve access.</p>
35+
"""
36+
37+
email_service.send(approver_email, subject, body_html)
38+
39+
1740
@router.post(
1841
"/register",
1942
response_model=Dict[str, Any],
@@ -24,7 +47,9 @@
2447
},
2548
)
2649
async def register_bpa_user(
27-
registration: BPARegistrationRequest, settings: Settings = Depends(get_settings)
50+
registration: BPARegistrationRequest,
51+
background_tasks: BackgroundTasks,
52+
settings: Settings = Depends(get_settings),
2853
) -> Dict[str, Any]:
2954
"""Register a new BPA user with selected organization resources."""
3055
url = f"https://{settings.auth0_domain}/api/v2/users"
@@ -82,25 +107,14 @@ async def register_bpa_user(
82107
if bpa_resources:
83108
email_service = EmailService()
84109
approver_email = "aai-dev@biocommons.org.au" # ideally move to settings
85-
subject = "New BPA User Access Request"
86110

87-
org_list_html = "".join(
88-
f"<li>{res['name']} (ID: {res['id']})</li>" for res in bpa_resources
111+
# Add email sending as a background task
112+
background_tasks.add_task(
113+
send_approver_email, email_service, approver_email, registration, bpa_resources
89114
)
90115

91-
body_html = f"""
92-
<p>A new user has requested access to one or more organizations in the BPA service.</p>
93-
<p><strong>User:</strong> {registration.fullname} ({registration.email})</p>
94-
<p><strong>Requested access to:</strong></p>
95-
<ul>{org_list_html}</ul>
96-
<p>Please log into the AAI Admin Portal to review and approve access.</p>
97-
"""
98-
99-
email_service.send(approver_email, subject, body_html)
100-
101116
return {"message": "User registered successfully", "user": response.json()}
102117

103-
104118
except HTTPException:
105119
raise
106120
except Exception as e:

tests/test_auth_validator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from unittest.mock import patch
22

33
from jose import jwt
4-
from jose.backends.rsa_backend import RSAKey
4+
from jose.backends.cryptography_backend import CryptographyRSAKey
55

66
from auth.config import Settings
77
from auth.validator import get_rsa_key
@@ -26,4 +26,4 @@ def test_get_rsa_key_returns_key(mock_settings: Settings):
2626

2727
key = get_rsa_key(token, settings=mock_settings)
2828
assert key is not None
29-
assert isinstance(key, RSAKey)
29+
assert isinstance(key, CryptographyRSAKey)

tests/test_ses.py

Lines changed: 21 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,35 @@
1-
from unittest.mock import MagicMock, patch
2-
1+
import boto3
32
import pytest
3+
from moto import mock_aws
44

55
from auth.ses import EmailService
66

77

88
@pytest.fixture
9-
def mock_boto_client():
10-
with patch("auth.ses.boto3.client") as mock_client:
11-
yield mock_client
12-
13-
def test_send_email_success(mock_boto_client):
14-
"""Test successful email sending"""
15-
mock_ses = MagicMock()
16-
mock_ses.send_email.return_value = {"MessageId": "test-message-id"}
17-
mock_boto_client.return_value = mock_ses
18-
19-
service = EmailService()
9+
def ses_client():
10+
with mock_aws():
11+
client = boto3.client("ses", region_name="ap-southeast-2")
12+
client.verify_email_identity(EmailAddress="sender@example.com")
13+
yield client
14+
15+
def test_send_email_success_with_moto(ses_client):
16+
service = EmailService(region_name="ap-southeast-2")
2017
service.send(
2118
to_address="recipient@example.com",
22-
subject="Test Subject",
23-
body_html="<p>This is a test</p>",
19+
subject="Test Moto Subject",
20+
body_html="<p>This is a moto test email</p>",
2421
sender="sender@example.com",
2522
)
23+
# If no exception, we assume success with moto
2624

27-
mock_ses.send_email.assert_called_once_with(
28-
Source="sender@example.com",
29-
Destination={"ToAddresses": ["recipient@example.com"]},
30-
Message={
31-
"Subject": {"Data": "Test Subject"},
32-
"Body": {"Html": {"Data": "<p>This is a test</p>"}},
33-
},
34-
)
35-
36-
def test_send_email_failure(mock_boto_client):
37-
"""Test SES failure raises exception and logs"""
38-
from botocore.exceptions import ClientError
39-
40-
mock_ses = MagicMock()
41-
mock_ses.send_email.side_effect = ClientError(
42-
error_response={
43-
"Error": {"Message": "Invalid email", "Code": "MessageRejected"}
44-
},
45-
operation_name="SendEmail",
46-
)
47-
mock_boto_client.return_value = mock_ses
48-
49-
service = EmailService()
25+
def test_send_email_failure_with_moto(ses_client):
26+
service = EmailService(region_name="ap-southeast-2")
5027

51-
with pytest.raises(ClientError):
28+
# Deliberately use an unverified sender to cause failure
29+
with pytest.raises(Exception):
5230
service.send(
53-
to_address="bad@example.com",
54-
subject="Failing",
55-
body_html="<p>Bad request</p>",
56-
sender="no-reply@example.com",
31+
to_address="recipient@example.com",
32+
subject="Should Fail",
33+
body_html="<p>Should not send</p>",
34+
sender="unverified@example.com",
5735
)

0 commit comments

Comments
 (0)