Skip to content

Commit 2426db4

Browse files
authored
#116 Notification routes with single handler (#141)
1 parent d34acf6 commit 2426db4

File tree

4 files changed

+163
-8
lines changed

4 files changed

+163
-8
lines changed

app/legacy/v2/notifications/rest.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,45 @@
11
"""All endpoints for the v2/notifications route."""
22

33
import json
4+
from typing import Annotated
5+
from uuid import uuid4
46

5-
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request, status
7+
from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException, Request, status
68
from loguru import logger
9+
from pydantic import HttpUrl
710

811
from app.auth import JWTBearer
12+
from app.constants import USNumberType
913
from app.dao.notifications_dao import dao_create_notification
1014
from app.db.models import Notification, Template
1115
from app.legacy.v2.notifications.route_schema import (
1216
V2PostPushRequestModel,
1317
V2PostPushResponseModel,
18+
V2PostSmsRequestModel,
19+
V2PostSmsResponseModel,
20+
V2SmsContentModel,
21+
V2Template,
1422
)
1523
from app.legacy.v2.notifications.utils import send_push_notification_helper, validate_template
1624
from app.routers import TimedAPIRoute
1725

18-
v2_notification_router = APIRouter(
26+
v2_legacy_notification_router = APIRouter(
1927
dependencies=[Depends(JWTBearer())],
2028
prefix='/legacy/v2/notifications',
2129
route_class=TimedAPIRoute,
30+
tags=['v2 Legacy Notification Endpoints'],
31+
)
32+
33+
34+
v2_notification_router = APIRouter(
35+
dependencies=[Depends(JWTBearer())],
36+
prefix='/v2/notifications',
37+
route_class=TimedAPIRoute,
2238
tags=['v2 Notification Endpoints'],
2339
)
2440

2541

26-
@v2_notification_router.post('/push', status_code=status.HTTP_201_CREATED)
42+
@v2_legacy_notification_router.post('/push', status_code=status.HTTP_201_CREATED)
2743
async def create_push_notification(
2844
request_data: V2PostPushRequestModel,
2945
request: Request,
@@ -72,3 +88,42 @@ async def create_push_notification(
7288
template_id,
7389
)
7490
return V2PostPushResponseModel()
91+
92+
93+
@v2_notification_router.post('/sms', status_code=status.HTTP_201_CREATED)
94+
@v2_legacy_notification_router.post('/sms', status_code=status.HTTP_201_CREATED)
95+
async def create_sms_notification(
96+
request: Annotated[
97+
V2PostSmsRequestModel,
98+
Body(
99+
openapi_examples=V2PostSmsRequestModel.Config.schema_extra['examples'],
100+
),
101+
],
102+
) -> V2PostSmsResponseModel:
103+
"""Create an SMS notification.
104+
105+
Args:
106+
request_data (V2PostSmsRequestModel): The data necessary for the notification.
107+
request (Request): The FastAPI request object.
108+
109+
Returns:
110+
V2PostSmsResponseModel: The notification response data.
111+
112+
"""
113+
logger.debug('Creating SMS notification with request data {}.', request)
114+
return V2PostSmsResponseModel(
115+
id=uuid4(),
116+
billing_code='123456',
117+
callback_url=HttpUrl('https://example.com'),
118+
reference='123456',
119+
template=V2Template(
120+
id=uuid4(),
121+
uri=HttpUrl('https://example.com'),
122+
version=1,
123+
),
124+
uri=HttpUrl('https://example.com'),
125+
content=V2SmsContentModel(
126+
body='example',
127+
from_number=USNumberType('+18005550101'),
128+
),
129+
)

app/legacy/v2/notifications/route_schema.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Request and Response bodies for /v2/notifications."""
22

3-
from typing import Literal
3+
from typing import ClassVar, Literal
44

55
from pydantic import UUID4, AwareDatetime, BaseModel, EmailStr, Field, HttpUrl, model_validator
66
from typing_extensions import Self
@@ -144,6 +144,48 @@ class V2PostSmsRequestModel(V2PostNotificationRequestModel):
144144
phone_number: USNumberType | None = None
145145
sms_sender_id: UUID4
146146

147+
class Config:
148+
"""Configuration for the model. Includes examples for the OpenAPI schema."""
149+
150+
schema_extra: ClassVar = {
151+
'examples': {
152+
'phone number': {
153+
'summary': 'phone number',
154+
'description': 'Send an SMS notification to a phone number.',
155+
'value': {
156+
'billing_code': '12345',
157+
'callback_url': 'https://example.com/',
158+
'personalisation': {
159+
'additionalProp1': 'string',
160+
'additionalProp2': 'string',
161+
'additionalProp3': 'string',
162+
},
163+
'reference': 'an-external-id',
164+
'template_id': 'a71400e3-b2f8-4bd1-91c0-27f9ca7106a1',
165+
'phone_number': '+18005550101',
166+
'sms_sender_id': '4f44ffc8-1ff8-4832-b1af-0b615691b6ea',
167+
},
168+
},
169+
'recipient identifier': {
170+
'summary': 'recipient identifier',
171+
'description': 'Send an SMS notification to a recipient identifier.',
172+
'value': {
173+
'billing_code': 'string',
174+
'callback_url': 'https://example.com/',
175+
'personalisation': {
176+
'additionalProp1': 'string',
177+
'additionalProp2': 'string',
178+
'additionalProp3': 'string',
179+
},
180+
'reference': 'string',
181+
'template_id': 'a71400e3-b2f8-4bd1-91c0-27f9ca7106a1',
182+
'sms_sender_id': '4f44ffc8-1ff8-4832-b1af-0b615691b6ea',
183+
'recipient_identifier': {'id_type': 'ICN', 'id_value': 'not-a-valid-icn'},
184+
},
185+
},
186+
},
187+
}
188+
147189
@model_validator(mode='after')
148190
def phone_number_or_recipient_id(self) -> Self:
149191
"""One, and only one, of "phone_number" or "recipient_identifier" must not be None.

app/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
Notification,
2626
Template,
2727
)
28-
from app.legacy.v2.notifications.rest import v2_notification_router
28+
from app.legacy.v2.notifications.rest import v2_legacy_notification_router, v2_notification_router
2929
from app.logging.logging_config import CustomizeLogger
3030
from app.state import ENPState
3131
from app.v3 import api_router as v3_router
@@ -78,6 +78,7 @@ def create_app() -> CustomFastAPI:
7878
CustomizeLogger.make_logger()
7979
app = CustomFastAPI(lifespan=lifespan)
8080
app.include_router(v3_router)
81+
app.include_router(v2_legacy_notification_router)
8182
app.include_router(v2_notification_router)
8283

8384
# Static site for MkDocs. If unavailable locally, run `mkdocs build` to create the site files

tests/app/legacy/v2/notifications/test_rest.py

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
"""Test module for app/legacy/v2/notifications/rest.py."""
22

33
from unittest.mock import AsyncMock, patch
4+
from uuid import uuid4
45

6+
import pytest
57
from fastapi import BackgroundTasks, status
8+
from fastapi.encoders import jsonable_encoder
69

7-
from app.constants import IdentifierType, MobileAppType
10+
from app.constants import IdentifierType, MobileAppType, USNumberType
811
from app.db.models import Template
912
from app.legacy.v2.notifications.route_schema import (
1013
V2PostPushRequestModel,
1114
V2PostPushResponseModel,
15+
V2PostSmsRequestModel,
1216
)
1317
from tests.conftest import ENPTestClient
1418

@@ -18,8 +22,8 @@
1822
@patch.object(BackgroundTasks, 'add_task')
1923
@patch('app.legacy.v2.notifications.rest.dao_create_notification')
2024
@patch('app.legacy.v2.notifications.rest.validate_template')
21-
class TestRouter:
22-
"""Test the v2 notifications router."""
25+
class TestPushRouter:
26+
"""Test the v2 push notifications router."""
2327

2428
async def test_router_returns_400_with_invalid_request_data(
2529
self,
@@ -155,3 +159,56 @@ async def test_post_push_returns_400_when_unable_to_validate_template(
155159
response = client.post(_push_path, json=request.model_dump())
156160

157161
assert response.status_code == status.HTTP_400_BAD_REQUEST
162+
163+
164+
class TestNotificationRouter:
165+
"""Test the v2 notifications router."""
166+
167+
routes = (
168+
'/legacy/v2/notifications/sms',
169+
'/v2/notifications/sms',
170+
)
171+
172+
@pytest.mark.parametrize('route', routes)
173+
async def test_happy_path(
174+
self,
175+
client: ENPTestClient,
176+
route: str,
177+
) -> None:
178+
"""Test route can return 201.
179+
180+
Args:
181+
client (ENPTestClient): Custom FastAPI client fixture
182+
route (str): Route to test
183+
184+
"""
185+
template_id = uuid4()
186+
sms_sender_id = uuid4()
187+
188+
request = V2PostSmsRequestModel(
189+
reference='test',
190+
template_id=template_id,
191+
phone_number=USNumberType('+18005550101'),
192+
sms_sender_id=sms_sender_id,
193+
)
194+
payload = jsonable_encoder(request)
195+
response = client.post(route, json=payload)
196+
197+
assert response.status_code == status.HTTP_201_CREATED
198+
199+
@pytest.mark.parametrize('route', routes)
200+
async def test_router_returns_400_with_invalid_request_data(
201+
self,
202+
client: ENPTestClient,
203+
route: str,
204+
) -> None:
205+
"""Test route can return 400.
206+
207+
Args:
208+
client (ENPTestClient): Custom FastAPI client fixture
209+
route (str): Route to test
210+
211+
"""
212+
response = client.post(route)
213+
214+
assert response.status_code == status.HTTP_400_BAD_REQUEST

0 commit comments

Comments
 (0)