Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .github/actions/build-container/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ inputs:
type_of_deployment:
description: "Type of deployment (app or sandbox)"
required: true
use_mock:
description: "Whether or not the mock for routing requests is on or off"
type: string
required: false

runs:
using: "composite"
Expand All @@ -19,4 +15,3 @@ runs:
env:
PROXYGEN_DOCKER_REGISTRY_URL: example.com/${{ inputs.type_of_deployment }}
CONTAINER_TAG: ${{ github.sha }}
USE_MOCK: ${{ inputs.use_mock }}
3 changes: 2 additions & 1 deletion .github/workflows/cicd-stage-1-deploy-to-internal-qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ jobs:
with:
environment: "internal-qa"
type_of_deployment: "app"
use_mock: "True"
emis_base_url: https://nhs70apptest.emishealth.com
tpp_base_url: https://systmonline2.tpp-uk.com
secrets:
PROXYGEN_CLIENT_ID: ${{ secrets.PROXYGEN_CLIENT_ID }}
PROXYGEN_KEY_ID: ${{ secrets.PROXYGEN_KEY_ID }}
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/cicd-stage-2-deploy-to-int-and-sandbox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ jobs:
with:
environment: "int"
type_of_deployment: "app"
use_mock: "True"
emis_base_url: https://nhs70apptest.emishealth.com
tpp_base_url: https://systmonline2.tpp-uk.com
secrets:
PROXYGEN_CLIENT_ID: ${{ secrets.PROXYGEN_CLIENT_ID }}
PROXYGEN_KEY_ID: ${{ secrets.PROXYGEN_KEY_ID }}
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/pull-request-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ jobs:
environment: "internal-dev"
type_of_deployment: "app"
additional_path: "pr-${{ github.event.number }}"
use_mock: "True"
emis_base_url: https://nhs70apptest.emishealth.com
tpp_base_url: https://systmonline2.tpp-uk.com
secrets:
PROXYGEN_CLIENT_ID: ${{ secrets.PROXYGEN_CLIENT_ID }}
PROXYGEN_KEY_ID: ${{ secrets.PROXYGEN_KEY_ID }}
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/reusable-core-code-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,3 @@ jobs:
uses: ./.github/actions/build-container
with:
type_of_deployment: "app"
use_mock: "True"
11 changes: 8 additions & 3 deletions .github/workflows/reusable-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ on:
description: "Type of deployment, e.g. 'app', 'sandbox'"
required: true
type: string
use_mock:
description: "Whether or not the mock for routing requests is on or off"
emis_base_url:
description: "The base url to connecto to EMIS APIs"
required: false
type: string
tpp_base_url:
description: "The base url to connecto to TPP APIs"
required: false
type: string

outputs:
environment_url:
Expand All @@ -48,7 +52,8 @@ permissions: {}
env:
CONTAINER_TAG: ${{inputs.type_of_deployment}}-${{ github.sha }}
PROXYGEN_DOCKER_REGISTRY_URL: 958002497996.dkr.ecr.eu-west-2.amazonaws.com/im1-pfs-auth
USE_MOCK: ${{ inputs.use_mock }}
EMIS_BASE_URL: ${{ inputs.emis_base_url }}
TPP_BASE_URL: ${{ inputs.tpp_base_url }}

jobs:
validate-inputs:
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ postman-test-pr-environment:
app-build:
cp pyproject.toml app/
cp uv.lock app/
docker buildx build -t "$(PROXYGEN_DOCKER_REGISTRY_URL):$(CONTAINER_TAG)" --build-arg USE_MOCK=$(USE_MOCK) --load app/
docker buildx build -t "$(PROXYGEN_DOCKER_REGISTRY_URL):$(CONTAINER_TAG)" --build-arg EMIS_BASE_URL=$(EMIS_BASE_URL) --build-arg TPP_BASE_URL=$(TPP_BASE_URL) --load app/

app-push:
proxygen docker get-login | bash
Expand Down
11 changes: 8 additions & 3 deletions app/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ RUN find /app -type d -name "test*" -exec rm -rf {} + && \
find /app -type f \( -name "*cache*" -o -name "*.cache*" -o -name ".pytest_cache*" -o -name ".ruff_cache*" \) -exec rm -f {} + && \
find /app -type d \( -name ".pytest_cache" -o -name ".ruff_cache" \) -exec rm -rf {} +

ARG USE_MOCK
ENV USE_MOCK=${USE_MOCK}
ARG EMIS_BASE_URL
ARG TPP_BASE_URL

ENV EMIS_BASE_URL=${EMIS_BASE_URL}
ENV TPP_BASE_URL=${TPP_BASE_URL}

WORKDIR /app
ENV PYTHONPATH="/app:/"

RUN uv sync --project=app/pyproject.toml --only-group=app
RUN rm pyproject.toml uv.lock Dockerfile

EXPOSE 9000

CMD ["uv", "run", "gunicorn", "api.app:app", "--bind=0.0.0.0:9000"]
CMD ["uv", "run", "gunicorn", "app.api.app:app", "--bind=0.0.0.0:9000"]
8 changes: 4 additions & 4 deletions app/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

from flask import Flask, Response, make_response, request

from .application.forward_request import route_and_forward
from .application.jwt import get_nhs_number_from_jwt_token
from .domain.exception import ApiError, InternalServerError
from .domain.forward_request_model import ForwardRequest
from app.api.application.forward_request import route_and_forward
from app.api.application.jwt import get_nhs_number_from_jwt_token
from app.api.domain.exception import ApiError, InternalServerError
from app.api.domain.forward_request_model import ForwardRequest

app = Flask(__name__)

Expand Down
21 changes: 14 additions & 7 deletions app/api/application/forward_request.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from ..domain.exception import ApiError, DownstreamError
from ..domain.forward_request_model import ForwardRequest
from ..domain.forward_response_model import ForwardResponse
from ..infrastructure.emis.client import EmisClient
from ..infrastructure.tpp.client import TPPClient
from os import environ

client_map = {"https://emis.com": EmisClient, "https://tpp.com": TPPClient}
from app.api.domain.exception import ApiError, DownstreamError, InvalidValueError
from app.api.domain.forward_request_model import ForwardRequest
from app.api.domain.forward_response_model import ForwardResponse
from app.api.infrastructure.emis.client import EmisClient
from app.api.infrastructure.tpp.client import TPPClient

EMIS_BASE_URL = environ.get("EMIS_BASE_URL")
TPP_BASE_URL = environ.get("TPP_BASE_URL")
CLIENT_MAP = {EMIS_BASE_URL: EmisClient, TPP_BASE_URL: TPPClient}


def route_and_forward(forward_request: ForwardRequest) -> ForwardResponse:
Expand All @@ -16,9 +20,12 @@ def route_and_forward(forward_request: ForwardRequest) -> ForwardResponse:
ForwardResponse: Transformed response from client
"""
try:
client = client_map[forward_request.forward_to](forward_request)
client = CLIENT_MAP[forward_request.forward_to](forward_request)
response = client.forward_request()
return client.transform_response(response)
except KeyError as exc:
msg = "Invalid URL"
raise InvalidValueError(msg) from exc
except ApiError:
raise
except Exception as exc:
Expand Down
2 changes: 1 addition & 1 deletion app/api/application/jwt.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from jwt import decode

from ..domain.exception import AccessDeniedError
from app.api.domain.exception import AccessDeniedError


def __fetch_nhs_number(decoded_token: dict) -> str:
Expand Down
165 changes: 137 additions & 28 deletions app/api/application/tests/test_forward_request.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,178 @@
from unittest.mock import MagicMock
from importlib import reload
from unittest.mock import patch

import pytest

from app.api.application import forward_request as forward_request_module
from app.api.application.forward_request import route_and_forward
from app.api.domain.exception import DownstreamError, ForbiddenError
from app.api.domain.exception import DownstreamError, ForbiddenError, InvalidValueError
from app.api.domain.forward_request_model import ForwardRequest

FILE_PATH = "app.api.application.forward_request"

def test_route_and_forward() -> None:

def test_route_and_forward_emis() -> None:
"""Tests the route_and_forward function."""
# Arrange
forward_request = ForwardRequest(
application_id="some application",
forward_to="https://example.com",
forward_to="https://emis.com",
patient_nhs_number="1234567890",
patient_ods_code="some ods code",
proxy_nhs_number="0987654321",
use_mock=False,
)

with (
patch.dict("os.environ", {"EMIS_BASE_URL": "https://emis.com"}),
patch("app.api.infrastructure.emis.client.EmisClient") as mock_emis_client,
patch("app.api.infrastructure.tpp.client.TPPClient") as mock_tpp_client,
):
from app.api.application import (
forward_request as forward_request_module,
)

mock_emis_client.return_value.transform_response.return_value = (
"mocked transformed response"
)

reload(forward_request_module)

# Act
actual_result = forward_request_module.route_and_forward(forward_request)

# Assert
assert actual_result == "mocked transformed response"
mock_emis_client.return_value.forward_request.assert_called_once()
mock_emis_client.return_value.transform_response.assert_called_once()
mock_tpp_client.assert_not_called()


def test_route_and_forward_tpp() -> None:
"""Tests the route_and_forward function."""
# Arrange
forward_request = ForwardRequest(
application_id="some application",
forward_to="https://tpp.com",
patient_nhs_number="1234567890",
patient_ods_code="some ods code",
proxy_nhs_number="0987654321",
use_mock=False,
)
mock_client = MagicMock()
mock_client.return_value.transform_response.return_value = (
"mocked transformed response"

with (
patch.dict("os.environ", {"TPP_BASE_URL": "https://tpp.com"}),
patch("app.api.infrastructure.emis.client.EmisClient") as mock_emis_client,
patch("app.api.infrastructure.tpp.client.TPPClient") as mock_tpp_client,
):
from app.api.application import (
forward_request as forward_request_module,
)

mock_tpp_client.return_value.transform_response.return_value = (
"mocked transformed response"
)

reload(forward_request_module)

# Act
actual_result = forward_request_module.route_and_forward(forward_request)

# Assert
assert actual_result == "mocked transformed response"
mock_emis_client.assert_not_called()
mock_tpp_client.return_value.forward_request.assert_called_once()
mock_tpp_client.return_value.transform_response.assert_called_once()


def test_route_and_forward_invalid_url() -> None:
"""Tests the route_and_forward function."""
# Arrange
forward_request = ForwardRequest(
application_id="some application",
forward_to="https://example.com",
patient_nhs_number="1234567890",
patient_ods_code="some ods code",
proxy_nhs_number="0987654321",
use_mock=False,
)
forward_request_module.client_map["https://example.com"] = mock_client
# Act
actual_result = route_and_forward(forward_request)

# Assert
assert actual_result == "mocked transformed response"
with (
patch.dict(
"os.environ",
{"EMIS_BASE_URL": "https://emis.com", "TPP_BASE_URL": "https://tpp.com"},
),
patch("app.api.infrastructure.emis.client.EmisClient") as mock_emis_client,
patch("app.api.infrastructure.tpp.client.TPPClient") as mock_tpp_client,
):
from app.api.application import (
forward_request as forward_request_module,
)

reload(forward_request_module)

# Act & Assert
with pytest.raises(InvalidValueError, match="Invalid URL"):
forward_request_module.route_and_forward(forward_request)
mock_emis_client.assert_not_called()
mock_tpp_client.assert_not_called()


def test_route_and_forward_raises_api_error() -> None:
"""Tests the route_and_forward function raises api error."""
# Arrange
forward_request = ForwardRequest(
application_id="some application",
forward_to="https://example.com",
forward_to="https://emis.com",
patient_nhs_number="1234567890",
patient_ods_code="some ods code",
proxy_nhs_number="0987654321",
use_mock=False,
)
mock_client = MagicMock()
mock_client.return_value.forward_request.side_effect = ForbiddenError("Oops")
forward_request_module.client_map["https://example.com"] = mock_client
# Act & Assert
with pytest.raises(ForbiddenError, match="Oops"):
route_and_forward(forward_request)
with (
patch.dict(
"os.environ",
{"EMIS_BASE_URL": "https://emis.com", "TPP_BASE_URL": "https://tpp.com"},
),
patch("app.api.infrastructure.emis.client.EmisClient") as mock_emis_client,
):
from app.api.application import forward_request as forward_request_module

mock_emis_client.return_value.forward_request.side_effect = ForbiddenError(
"Oops"
)

reload(forward_request_module)

# Act & Assert
with pytest.raises(ForbiddenError, match="Oops"):
forward_request_module.route_and_forward(forward_request)


def test_route_and_forward_raises_downstream_error() -> None:
"""Tests the route_and_forward function raises downstream error."""
# Arrange
forward_request = ForwardRequest(
application_id="some application",
forward_to="https://example.com",
forward_to="https://emis.com",
patient_nhs_number="1234567890",
patient_ods_code="some ods code",
proxy_nhs_number="0987654321",
use_mock=False,
)
mock_client = MagicMock()
mock_client.return_value.forward_request.side_effect = Exception("Oops")
forward_request_module.client_map["https://example.com"] = mock_client
# Act & Assert
with pytest.raises(DownstreamError, match="Error occurred with downstream service"):
route_and_forward(forward_request)
with (
patch.dict(
"os.environ",
{"EMIS_BASE_URL": "https://emis.com", "TPP_BASE_URL": "https://tpp.com"},
),
patch("app.api.infrastructure.emis.client.EmisClient") as mock_emis_client,
):
from app.api.application import forward_request as forward_request_module

mock_emis_client.return_value.forward_request.side_effect = Exception("Oops")

reload(forward_request_module)

# Act & Assert
with pytest.raises(
DownstreamError, match="Error occurred with downstream service"
):
forward_request_module.route_and_forward(forward_request)
4 changes: 2 additions & 2 deletions app/api/domain/base_client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from abc import ABC, abstractmethod

from .forward_request_model import ForwardRequest
from .forward_response_model import ForwardResponse
from app.api.domain.forward_request_model import ForwardRequest
from app.api.domain.forward_response_model import ForwardResponse


class BaseClient(ABC):
Expand Down
Loading