Skip to content

Commit

Permalink
#116 Notification routes with single handler (#141)
Browse files Browse the repository at this point in the history
  • Loading branch information
mchlwellman authored Jan 14, 2025
1 parent d34acf6 commit 2426db4
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 8 deletions.
61 changes: 58 additions & 3 deletions app/legacy/v2/notifications/rest.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,45 @@
"""All endpoints for the v2/notifications route."""

import json
from typing import Annotated
from uuid import uuid4

from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request, status
from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException, Request, status
from loguru import logger
from pydantic import HttpUrl

from app.auth import JWTBearer
from app.constants import USNumberType
from app.dao.notifications_dao import dao_create_notification
from app.db.models import Notification, Template
from app.legacy.v2.notifications.route_schema import (
V2PostPushRequestModel,
V2PostPushResponseModel,
V2PostSmsRequestModel,
V2PostSmsResponseModel,
V2SmsContentModel,
V2Template,
)
from app.legacy.v2.notifications.utils import send_push_notification_helper, validate_template
from app.routers import TimedAPIRoute

v2_notification_router = APIRouter(
v2_legacy_notification_router = APIRouter(
dependencies=[Depends(JWTBearer())],
prefix='/legacy/v2/notifications',
route_class=TimedAPIRoute,
tags=['v2 Legacy Notification Endpoints'],
)


v2_notification_router = APIRouter(
dependencies=[Depends(JWTBearer())],
prefix='/v2/notifications',
route_class=TimedAPIRoute,
tags=['v2 Notification Endpoints'],
)


@v2_notification_router.post('/push', status_code=status.HTTP_201_CREATED)
@v2_legacy_notification_router.post('/push', status_code=status.HTTP_201_CREATED)
async def create_push_notification(
request_data: V2PostPushRequestModel,
request: Request,
Expand Down Expand Up @@ -72,3 +88,42 @@ async def create_push_notification(
template_id,
)
return V2PostPushResponseModel()


@v2_notification_router.post('/sms', status_code=status.HTTP_201_CREATED)
@v2_legacy_notification_router.post('/sms', status_code=status.HTTP_201_CREATED)
async def create_sms_notification(
request: Annotated[
V2PostSmsRequestModel,
Body(
openapi_examples=V2PostSmsRequestModel.Config.schema_extra['examples'],
),
],
) -> V2PostSmsResponseModel:
"""Create an SMS notification.
Args:
request_data (V2PostSmsRequestModel): The data necessary for the notification.
request (Request): The FastAPI request object.
Returns:
V2PostSmsResponseModel: The notification response data.
"""
logger.debug('Creating SMS notification with request data {}.', request)
return V2PostSmsResponseModel(
id=uuid4(),
billing_code='123456',
callback_url=HttpUrl('https://example.com'),
reference='123456',
template=V2Template(
id=uuid4(),
uri=HttpUrl('https://example.com'),
version=1,
),
uri=HttpUrl('https://example.com'),
content=V2SmsContentModel(
body='example',
from_number=USNumberType('+18005550101'),
),
)
44 changes: 43 additions & 1 deletion app/legacy/v2/notifications/route_schema.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Request and Response bodies for /v2/notifications."""

from typing import Literal
from typing import ClassVar, Literal

from pydantic import UUID4, AwareDatetime, BaseModel, EmailStr, Field, HttpUrl, model_validator
from typing_extensions import Self
Expand Down Expand Up @@ -144,6 +144,48 @@ class V2PostSmsRequestModel(V2PostNotificationRequestModel):
phone_number: USNumberType | None = None
sms_sender_id: UUID4

class Config:
"""Configuration for the model. Includes examples for the OpenAPI schema."""

schema_extra: ClassVar = {
'examples': {
'phone number': {
'summary': 'phone number',
'description': 'Send an SMS notification to a phone number.',
'value': {
'billing_code': '12345',
'callback_url': 'https://example.com/',
'personalisation': {
'additionalProp1': 'string',
'additionalProp2': 'string',
'additionalProp3': 'string',
},
'reference': 'an-external-id',
'template_id': 'a71400e3-b2f8-4bd1-91c0-27f9ca7106a1',
'phone_number': '+18005550101',
'sms_sender_id': '4f44ffc8-1ff8-4832-b1af-0b615691b6ea',
},
},
'recipient identifier': {
'summary': 'recipient identifier',
'description': 'Send an SMS notification to a recipient identifier.',
'value': {
'billing_code': 'string',
'callback_url': 'https://example.com/',
'personalisation': {
'additionalProp1': 'string',
'additionalProp2': 'string',
'additionalProp3': 'string',
},
'reference': 'string',
'template_id': 'a71400e3-b2f8-4bd1-91c0-27f9ca7106a1',
'sms_sender_id': '4f44ffc8-1ff8-4832-b1af-0b615691b6ea',
'recipient_identifier': {'id_type': 'ICN', 'id_value': 'not-a-valid-icn'},
},
},
},
}

@model_validator(mode='after')
def phone_number_or_recipient_id(self) -> Self:
"""One, and only one, of "phone_number" or "recipient_identifier" must not be None.
Expand Down
3 changes: 2 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
Notification,
Template,
)
from app.legacy.v2.notifications.rest import v2_notification_router
from app.legacy.v2.notifications.rest import v2_legacy_notification_router, v2_notification_router
from app.logging.logging_config import CustomizeLogger
from app.state import ENPState
from app.v3 import api_router as v3_router
Expand Down Expand Up @@ -78,6 +78,7 @@ def create_app() -> CustomFastAPI:
CustomizeLogger.make_logger()
app = CustomFastAPI(lifespan=lifespan)
app.include_router(v3_router)
app.include_router(v2_legacy_notification_router)
app.include_router(v2_notification_router)

# Static site for MkDocs. If unavailable locally, run `mkdocs build` to create the site files
Expand Down
63 changes: 60 additions & 3 deletions tests/app/legacy/v2/notifications/test_rest.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
"""Test module for app/legacy/v2/notifications/rest.py."""

from unittest.mock import AsyncMock, patch
from uuid import uuid4

import pytest
from fastapi import BackgroundTasks, status
from fastapi.encoders import jsonable_encoder

from app.constants import IdentifierType, MobileAppType
from app.constants import IdentifierType, MobileAppType, USNumberType
from app.db.models import Template
from app.legacy.v2.notifications.route_schema import (
V2PostPushRequestModel,
V2PostPushResponseModel,
V2PostSmsRequestModel,
)
from tests.conftest import ENPTestClient

Expand All @@ -18,8 +22,8 @@
@patch.object(BackgroundTasks, 'add_task')
@patch('app.legacy.v2.notifications.rest.dao_create_notification')
@patch('app.legacy.v2.notifications.rest.validate_template')
class TestRouter:
"""Test the v2 notifications router."""
class TestPushRouter:
"""Test the v2 push notifications router."""

async def test_router_returns_400_with_invalid_request_data(
self,
Expand Down Expand Up @@ -155,3 +159,56 @@ async def test_post_push_returns_400_when_unable_to_validate_template(
response = client.post(_push_path, json=request.model_dump())

assert response.status_code == status.HTTP_400_BAD_REQUEST


class TestNotificationRouter:
"""Test the v2 notifications router."""

routes = (
'/legacy/v2/notifications/sms',
'/v2/notifications/sms',
)

@pytest.mark.parametrize('route', routes)
async def test_happy_path(
self,
client: ENPTestClient,
route: str,
) -> None:
"""Test route can return 201.
Args:
client (ENPTestClient): Custom FastAPI client fixture
route (str): Route to test
"""
template_id = uuid4()
sms_sender_id = uuid4()

request = V2PostSmsRequestModel(
reference='test',
template_id=template_id,
phone_number=USNumberType('+18005550101'),
sms_sender_id=sms_sender_id,
)
payload = jsonable_encoder(request)
response = client.post(route, json=payload)

assert response.status_code == status.HTTP_201_CREATED

@pytest.mark.parametrize('route', routes)
async def test_router_returns_400_with_invalid_request_data(
self,
client: ENPTestClient,
route: str,
) -> None:
"""Test route can return 400.
Args:
client (ENPTestClient): Custom FastAPI client fixture
route (str): Route to test
"""
response = client.post(route)

assert response.status_code == status.HTTP_400_BAD_REQUEST

0 comments on commit 2426db4

Please sign in to comment.