From 27a8356f671c5edf6d1029f10d07da0742b45f71 Mon Sep 17 00:00:00 2001 From: Ellie Bound <175816742+ellie-bound1-NHSD@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:13:56 +0000 Subject: [PATCH 1/9] NPA-6139: Up Rate Limit, Update App Imports Plus Check for Allowed List of URLs --- app/api/app.py | 8 +++--- app/api/application/forward_request.py | 10 +++---- app/api/application/jwt.py | 2 +- .../application/tests/test_forward_request.py | 12 ++++----- app/api/domain/base_client.py | 4 +-- app/api/domain/forward_request_model.py | 10 ++++--- .../tests/test_forward_request_model.py | 26 ++++++++++++------- app/api/infrastructure/emis/client.py | 8 +++--- app/api/infrastructure/emis/models.py | 2 +- .../infrastructure/emis/tests/test_client.py | 2 +- app/api/infrastructure/tpp/client.py | 8 +++--- app/api/infrastructure/tpp/models.py | 2 +- .../infrastructure/tpp/tests/test_client.py | 2 +- .../parameters/NHSE-Forward-To.yaml | 6 +++-- .../x-nhsd-apim/x-nhsd-apim-int.yaml | 4 +-- .../x-nhsd-apim/x-nhsd-apim-internal-dev.yaml | 4 +-- .../x-nhsd-apim/x-nhsd-apim-internal-qa.yaml | 4 +-- .../end_to_end/authenticate/POST/test_400.py | 4 ++- 18 files changed, 66 insertions(+), 52 deletions(-) diff --git a/app/api/app.py b/app/api/app.py index 78588a44..69a69b5d 100644 --- a/app/api/app.py +++ b/app/api/app.py @@ -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__) diff --git a/app/api/application/forward_request.py b/app/api/application/forward_request.py index 71754c04..9c07db68 100644 --- a/app/api/application/forward_request.py +++ b/app/api/application/forward_request.py @@ -1,8 +1,8 @@ -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 app.api.domain.exception import ApiError, DownstreamError +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 client_map = {"https://emis.com": EmisClient, "https://tpp.com": TPPClient} diff --git a/app/api/application/jwt.py b/app/api/application/jwt.py index 1f1acf7d..52317516 100644 --- a/app/api/application/jwt.py +++ b/app/api/application/jwt.py @@ -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: diff --git a/app/api/application/tests/test_forward_request.py b/app/api/application/tests/test_forward_request.py index 0ec0ff66..a7f2d84e 100644 --- a/app/api/application/tests/test_forward_request.py +++ b/app/api/application/tests/test_forward_request.py @@ -13,7 +13,7 @@ def test_route_and_forward() -> None: # 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", @@ -23,7 +23,7 @@ def test_route_and_forward() -> None: mock_client.return_value.transform_response.return_value = ( "mocked transformed response" ) - forward_request_module.client_map["https://example.com"] = mock_client + forward_request_module.client_map["https://emis.com"] = mock_client # Act actual_result = route_and_forward(forward_request) @@ -36,7 +36,7 @@ def test_route_and_forward_raises_api_error() -> None: # 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", @@ -44,7 +44,7 @@ def test_route_and_forward_raises_api_error() -> None: ) mock_client = MagicMock() mock_client.return_value.forward_request.side_effect = ForbiddenError("Oops") - forward_request_module.client_map["https://example.com"] = mock_client + forward_request_module.client_map["https://emis.com"] = mock_client # Act & Assert with pytest.raises(ForbiddenError, match="Oops"): route_and_forward(forward_request) @@ -55,7 +55,7 @@ def test_route_and_forward_raises_downstream_error() -> None: # 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", @@ -63,7 +63,7 @@ def test_route_and_forward_raises_downstream_error() -> None: ) mock_client = MagicMock() mock_client.return_value.forward_request.side_effect = Exception("Oops") - forward_request_module.client_map["https://example.com"] = mock_client + forward_request_module.client_map["https://emis.com"] = mock_client # Act & Assert with pytest.raises(DownstreamError, match="Error occurred with downstream service"): route_and_forward(forward_request) diff --git a/app/api/domain/base_client.py b/app/api/domain/base_client.py index 536a1745..f5cb22e9 100644 --- a/app/api/domain/base_client.py +++ b/app/api/domain/base_client.py @@ -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): diff --git a/app/api/domain/forward_request_model.py b/app/api/domain/forward_request_model.py index f553c74b..b6f0c2d1 100644 --- a/app/api/domain/forward_request_model.py +++ b/app/api/domain/forward_request_model.py @@ -1,6 +1,6 @@ from pydantic import BaseModel, field_validator, model_validator -from .exception import ( +from app.api.domain.exception import ( AccessDeniedError, InvalidValueError, MissingValueError, @@ -18,6 +18,7 @@ class ForwardRequest(BaseModel): use_mock: bool @model_validator(mode="before") + @classmethod def validate_required_value(cls, values: list) -> list: """Validates if required value is present. @@ -31,6 +32,7 @@ def validate_required_value(cls, values: list) -> list: return values @model_validator(mode="before") + @classmethod def validate_string(cls, values: list) -> list: """Validates if value is a string. @@ -44,6 +46,7 @@ def validate_string(cls, values: list) -> list: return values @model_validator(mode="before") + @classmethod def validate_nhs_number(cls, values: list) -> list: """Validates nhs number. @@ -66,12 +69,13 @@ def validate_nhs_number(cls, values: list) -> list: return values @field_validator("forward_to") + @classmethod def validate_url(cls, value: str) -> str: - """Validates url. + """Validates url is one of the allowed urls. If unsuccessful will raise An InvalidValueError Exception """ - if not value.startswith("https:"): + if value not in ["https://emis.com", "https://tpp.com"]: msg = "Invalid url" raise InvalidValueError(msg) diff --git a/app/api/domain/tests/test_forward_request_model.py b/app/api/domain/tests/test_forward_request_model.py index f3ad4d1e..640caa87 100644 --- a/app/api/domain/tests/test_forward_request_model.py +++ b/app/api/domain/tests/test_forward_request_model.py @@ -8,12 +8,16 @@ from app.api.domain.forward_request_model import ForwardRequest -def test_forward_request() -> None: +@pytest.mark.parametrize( + "forward_to", + ["https://emis.com", "https://tpp.com"], +) +def test_forward_request(forward_to: str) -> None: """Tests the ForwardRequest model.""" # Act & Assert ForwardRequest( application_id="some application", - forward_to="https://example.com", + forward_to=forward_to, patient_nhs_number="1234567890", patient_ods_code="some ods code", proxy_nhs_number="0987654321", @@ -24,10 +28,10 @@ def test_forward_request() -> None: @pytest.mark.parametrize( ("application_id", "ods_code", "forward_to"), [ - (None, "ods code", "https://example.com"), - ("", "ods code", "https://example.com"), - ("application id", None, "https://example.com"), - ("application id", "", "https://example.com"), + (None, "ods code", "https://emis.com"), + ("", "ods code", "https://emis.com"), + ("application id", None, "https://emis.com"), + ("application id", "", "https://emis.com"), ("application id", "ods code", None), ("application id", "ods code", ""), ], @@ -67,7 +71,7 @@ def test_forward_request_validates_nhs_numbers( with pytest.raises(AccessDeniedError, match="Failed to retrieve NHS Number"): ForwardRequest( application_id="some application", - forward_to="https://example.com", + forward_to="https://emis.com", patient_nhs_number=patient_nhs_number, patient_ods_code="some ods code", proxy_nhs_number=proxy_nhs_number, @@ -75,10 +79,12 @@ def test_forward_request_validates_nhs_numbers( ) -def test_forward_request_validates_forward_to() -> None: +@pytest.mark.parametrize( + "forward_to", + ["some random value", "https://invalid.com", "www.google.com"], +) +def test_forward_request_validates_forward_to(forward_to: str) -> None: """Tests the ForwardRequest model validates forward to is a url.""" - # Arrange - forward_to = "some random value" # Act & Assert with pytest.raises(InvalidValueError, match="Invalid url"): ForwardRequest( diff --git a/app/api/infrastructure/emis/client.py b/app/api/infrastructure/emis/client.py index 56988e82..9ff6a604 100644 --- a/app/api/infrastructure/emis/client.py +++ b/app/api/infrastructure/emis/client.py @@ -3,15 +3,15 @@ import requests -from ...domain.base_client import BaseClient -from ...domain.exception import ( +from app.api.domain.base_client import BaseClient +from app.api.domain.exception import ( DownstreamError, ForbiddenError, InvalidValueError, NotFoundError, ) -from ...domain.forward_response_model import Demographics -from .models import ( +from app.api.domain.forward_response_model import Demographics +from app.api.infrastructure.emis.models import ( Identifier, MedicalRecordPermissions, Patient, diff --git a/app/api/infrastructure/emis/models.py b/app/api/infrastructure/emis/models.py index 4574de56..a6a746d0 100644 --- a/app/api/infrastructure/emis/models.py +++ b/app/api/infrastructure/emis/models.py @@ -1,7 +1,7 @@ from pydantic import BaseModel, ConfigDict from pydantic.alias_generators import to_camel -from ...domain.forward_response_model import Demographics, ForwardResponse +from app.api.domain.forward_response_model import Demographics, ForwardResponse class Identifier(BaseModel): diff --git a/app/api/infrastructure/emis/tests/test_client.py b/app/api/infrastructure/emis/tests/test_client.py index 5137f870..926df2a2 100644 --- a/app/api/infrastructure/emis/tests/test_client.py +++ b/app/api/infrastructure/emis/tests/test_client.py @@ -28,7 +28,7 @@ def setup_client() -> EmisClient: request = ForwardRequest( application_id="some application id", - forward_to="https://somewhere", + forward_to="https://emis.com", patient_nhs_number="1234567890", patient_ods_code="some patient ods code", proxy_nhs_number="0987654321", diff --git a/app/api/infrastructure/tpp/client.py b/app/api/infrastructure/tpp/client.py index 5bdc5f88..6869e028 100644 --- a/app/api/infrastructure/tpp/client.py +++ b/app/api/infrastructure/tpp/client.py @@ -3,15 +3,15 @@ import requests import xmltodict -from ...domain.base_client import BaseClient -from ...domain.exception import ( +from app.api.domain.base_client import BaseClient +from app.api.domain.exception import ( DownstreamError, ForbiddenError, InvalidValueError, NotFoundError, ) -from ...domain.forward_response_model import Demographics, ForwardResponse -from .models import ( +from app.api.domain.forward_response_model import Demographics, ForwardResponse +from app.api.infrastructure.tpp.models import ( Application, Identifier, Patient, diff --git a/app/api/infrastructure/tpp/models.py b/app/api/infrastructure/tpp/models.py index 264e6fc0..87271150 100644 --- a/app/api/infrastructure/tpp/models.py +++ b/app/api/infrastructure/tpp/models.py @@ -4,7 +4,7 @@ from pydantic import BaseModel, ConfigDict from pydantic.alias_generators import to_camel -from ...domain.forward_response_model import Demographics, ForwardResponse +from app.api.domain.forward_response_model import Demographics, ForwardResponse class Application(BaseModel): diff --git a/app/api/infrastructure/tpp/tests/test_client.py b/app/api/infrastructure/tpp/tests/test_client.py index 1289bf47..0a23032e 100644 --- a/app/api/infrastructure/tpp/tests/test_client.py +++ b/app/api/infrastructure/tpp/tests/test_client.py @@ -35,7 +35,7 @@ def setup_client() -> TPPClient: request = ForwardRequest( application_id="some application id", - forward_to="https://somewhere", + forward_to="https://tpp.com", patient_nhs_number="1234567890", patient_ods_code="some patient ods code", proxy_nhs_number="0987654321", diff --git a/specification/components/parameters/NHSE-Forward-To.yaml b/specification/components/parameters/NHSE-Forward-To.yaml index 0e30740e..cb56c4de 100644 --- a/specification/components/parameters/NHSE-Forward-To.yaml +++ b/specification/components/parameters/NHSE-Forward-To.yaml @@ -1,7 +1,9 @@ name: NHSE-Forward-To in: header -description: The base URL to forward requests to. +description: The GPIT Supplier base URL to forward requests to. required: true schema: type: string - example: https://example.com + enum: + - https://tpp.com + - https://enum.com diff --git a/specification/x-nhsd-apim/x-nhsd-apim-int.yaml b/specification/x-nhsd-apim/x-nhsd-apim-int.yaml index ef567954..f8268a54 100644 --- a/specification/x-nhsd-apim/x-nhsd-apim-int.yaml +++ b/specification/x-nhsd-apim/x-nhsd-apim-int.yaml @@ -8,10 +8,10 @@ access: visible: false ratelimiting: proxy: - limit: 2 + limit: 10 timeunit: second app-default: - limit: 2 + limit: 10 timeunit: second target: type: hosted diff --git a/specification/x-nhsd-apim/x-nhsd-apim-internal-dev.yaml b/specification/x-nhsd-apim/x-nhsd-apim-internal-dev.yaml index c08f4414..c75187db 100644 --- a/specification/x-nhsd-apim/x-nhsd-apim-internal-dev.yaml +++ b/specification/x-nhsd-apim/x-nhsd-apim-internal-dev.yaml @@ -8,10 +8,10 @@ access: visible: false ratelimiting: proxy: - limit: 2 + limit: 10 timeunit: second app-default: - limit: 2 + limit: 10 timeunit: second target: type: hosted diff --git a/specification/x-nhsd-apim/x-nhsd-apim-internal-qa.yaml b/specification/x-nhsd-apim/x-nhsd-apim-internal-qa.yaml index a3a8ffb1..eeb70029 100644 --- a/specification/x-nhsd-apim/x-nhsd-apim-internal-qa.yaml +++ b/specification/x-nhsd-apim/x-nhsd-apim-internal-qa.yaml @@ -8,10 +8,10 @@ access: visible: false ratelimiting: proxy: - limit: 2 + limit: 10 timeunit: second app-default: - limit: 2 + limit: 10 timeunit: second target: type: hosted diff --git a/tests/end_to_end/authenticate/POST/test_400.py b/tests/end_to_end/authenticate/POST/test_400.py index e76937a8..f53795e5 100644 --- a/tests/end_to_end/authenticate/POST/test_400.py +++ b/tests/end_to_end/authenticate/POST/test_400.py @@ -101,7 +101,9 @@ def test_missing_forward_to_header( @pytest.mark.unhappy -@pytest.mark.parametrize("forward_to_url", ["something random", "http://example.com"]) +@pytest.mark.parametrize( + "forward_to_url", ["something random", "www.google.com", "https://google.com"] +) def test_invalid_forward_to_header( request: pytest.FixtureRequest, api_url: str, forward_to_url: str ) -> None: From db947a3b8c06e9b070560b3c4e528bbaa3561a85 Mon Sep 17 00:00:00 2001 From: Ellie Bound <175816742+ellie-bound1-NHSD@users.noreply.github.com> Date: Fri, 30 Jan 2026 08:54:27 +0000 Subject: [PATCH 2/9] NPA-6139: Remove USE MOCK --- .github/actions/build-container/action.yaml | 5 ----- .github/workflows/cicd-stage-1-deploy-to-internal-qa.yml | 1 - .github/workflows/cicd-stage-2-deploy-to-int-and-sandbox.yml | 1 - .github/workflows/pull-request-checks.yml | 1 - .github/workflows/reusable-core-code-checks.yml | 1 - .github/workflows/reusable-deploy.yml | 5 ----- Makefile | 2 +- app/Dockerfile | 2 -- app/api/infrastructure/emis/tests/test_client.py | 2 -- app/api/infrastructure/tpp/tests/test_client.py | 4 ---- docs/user-guides/Deploy_To_Apigee.md | 2 +- 11 files changed, 2 insertions(+), 24 deletions(-) diff --git a/.github/actions/build-container/action.yaml b/.github/actions/build-container/action.yaml index 1ebd4391..9b1bc44e 100644 --- a/.github/actions/build-container/action.yaml +++ b/.github/actions/build-container/action.yaml @@ -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" @@ -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 }} diff --git a/.github/workflows/cicd-stage-1-deploy-to-internal-qa.yml b/.github/workflows/cicd-stage-1-deploy-to-internal-qa.yml index 1bfd4d3e..610587a7 100644 --- a/.github/workflows/cicd-stage-1-deploy-to-internal-qa.yml +++ b/.github/workflows/cicd-stage-1-deploy-to-internal-qa.yml @@ -13,7 +13,6 @@ jobs: with: environment: "internal-qa" type_of_deployment: "app" - use_mock: "True" secrets: PROXYGEN_CLIENT_ID: ${{ secrets.PROXYGEN_CLIENT_ID }} PROXYGEN_KEY_ID: ${{ secrets.PROXYGEN_KEY_ID }} diff --git a/.github/workflows/cicd-stage-2-deploy-to-int-and-sandbox.yml b/.github/workflows/cicd-stage-2-deploy-to-int-and-sandbox.yml index f5e31c6a..d1c08838 100644 --- a/.github/workflows/cicd-stage-2-deploy-to-int-and-sandbox.yml +++ b/.github/workflows/cicd-stage-2-deploy-to-int-and-sandbox.yml @@ -12,7 +12,6 @@ jobs: with: environment: "int" type_of_deployment: "app" - use_mock: "True" secrets: PROXYGEN_CLIENT_ID: ${{ secrets.PROXYGEN_CLIENT_ID }} PROXYGEN_KEY_ID: ${{ secrets.PROXYGEN_KEY_ID }} diff --git a/.github/workflows/pull-request-checks.yml b/.github/workflows/pull-request-checks.yml index 5746ebbc..3c093c05 100644 --- a/.github/workflows/pull-request-checks.yml +++ b/.github/workflows/pull-request-checks.yml @@ -32,7 +32,6 @@ jobs: environment: "internal-dev" type_of_deployment: "app" additional_path: "pr-${{ github.event.number }}" - use_mock: "True" secrets: PROXYGEN_CLIENT_ID: ${{ secrets.PROXYGEN_CLIENT_ID }} PROXYGEN_KEY_ID: ${{ secrets.PROXYGEN_KEY_ID }} diff --git a/.github/workflows/reusable-core-code-checks.yml b/.github/workflows/reusable-core-code-checks.yml index da04fb2b..ece853bf 100644 --- a/.github/workflows/reusable-core-code-checks.yml +++ b/.github/workflows/reusable-core-code-checks.yml @@ -114,4 +114,3 @@ jobs: uses: ./.github/actions/build-container with: type_of_deployment: "app" - use_mock: "True" diff --git a/.github/workflows/reusable-deploy.yml b/.github/workflows/reusable-deploy.yml index 32ba7dbe..5c803da9 100644 --- a/.github/workflows/reusable-deploy.yml +++ b/.github/workflows/reusable-deploy.yml @@ -19,10 +19,6 @@ 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" - type: string - required: false outputs: environment_url: @@ -48,7 +44,6 @@ 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 }} jobs: validate-inputs: diff --git a/Makefile b/Makefile index 994015c3..17258a18 100644 --- a/Makefile +++ b/Makefile @@ -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)" --load app/ app-push: proxygen docker get-login | bash diff --git a/app/Dockerfile b/app/Dockerfile index 6c79b51a..0bef6330 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -8,8 +8,6 @@ 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} WORKDIR /app RUN uv sync --project=app/pyproject.toml --only-group=app diff --git a/app/api/infrastructure/emis/tests/test_client.py b/app/api/infrastructure/emis/tests/test_client.py index 926df2a2..2ff7e519 100644 --- a/app/api/infrastructure/emis/tests/test_client.py +++ b/app/api/infrastructure/emis/tests/test_client.py @@ -1,5 +1,4 @@ from json import load -from os import environ from pathlib import Path from unittest.mock import MagicMock, patch @@ -81,7 +80,6 @@ def test_emis_forward_request_use_mock_on(client: EmisClient) -> None: assert actual_result == expected_response -@patch.dict(environ, {"USE_MOCK": "False"}) @patch("app.api.infrastructure.emis.client.requests") def test_emis_forward_request_use_mock_off( mock_request: MagicMock, client: EmisClient diff --git a/app/api/infrastructure/tpp/tests/test_client.py b/app/api/infrastructure/tpp/tests/test_client.py index 0a23032e..d91dbead 100644 --- a/app/api/infrastructure/tpp/tests/test_client.py +++ b/app/api/infrastructure/tpp/tests/test_client.py @@ -1,4 +1,3 @@ -from os import environ from pathlib import Path from unittest.mock import MagicMock, patch @@ -79,7 +78,6 @@ def test_tpp_client_get_data(_: MagicMock, client: TPPClient) -> None: } -@patch.dict(environ, {"USE_MOCK": "True"}) def test_tpp_forward_request_use_mock_on(client: TPPClient) -> None: """Test the TPPClient forward_request function when mock is turned on.""" # Assert @@ -91,7 +89,6 @@ def test_tpp_forward_request_use_mock_on(client: TPPClient) -> None: assert actual_result == xmltodict.parse(MOCKED_RESPONSE) -@patch.dict(environ, {"USE_MOCK": "False"}) @patch("app.api.infrastructure.tpp.client.requests") def test_tpp_forward_request_use_mock_off( mock_request: MagicMock, client: TPPClient @@ -118,7 +115,6 @@ def test_tpp_forward_request_use_mock_off( (500, "", DownstreamError), ], ) -@patch.dict(environ, {"USE_MOCK": "False"}) @patch("app.api.infrastructure.tpp.client.requests") def test_tpp_forward_request_use_mock_off_exception( mock_request: MagicMock, diff --git a/docs/user-guides/Deploy_To_Apigee.md b/docs/user-guides/Deploy_To_Apigee.md index bdd07e42..2a735b6f 100644 --- a/docs/user-guides/Deploy_To_Apigee.md +++ b/docs/user-guides/Deploy_To_Apigee.md @@ -15,7 +15,7 @@ Build application container image: ```shell - make app-build PROXYGEN_DOCKER_REGISTRY_URL="958002497996.dkr.ecr.eu-west-2.amazonaws.com/im1-pfs-auth" CONTAINER_TAG= USE_MOCK= + make app-build PROXYGEN_DOCKER_REGISTRY_URL="958002497996.dkr.ecr.eu-west-2.amazonaws.com/im1-pfs-auth" CONTAINER_TAG= ``` Build sandbox container image: From bc585bb9923ab3d4761d6250feca491f3dbe4e46 Mon Sep 17 00:00:00 2001 From: Ellie Bound <175816742+ellie-bound1-NHSD@users.noreply.github.com> Date: Fri, 30 Jan 2026 09:06:21 +0000 Subject: [PATCH 3/9] NPA-6139: Attempt to ensure docker app uses same directory structure as repo --- app/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Dockerfile b/app/Dockerfile index 0bef6330..41cef902 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -9,10 +9,11 @@ RUN find /app -type d -name "test*" -exec rm -rf {} + && \ find /app -type d \( -name ".pytest_cache" -o -name ".ruff_cache" \) -exec rm -rf {} + 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"] From 04b447786c71a6e10e1f50de744f4ed11caeb387 Mon Sep 17 00:00:00 2001 From: Ellie Bound <175816742+ellie-bound1-NHSD@users.noreply.github.com> Date: Fri, 30 Jan 2026 13:58:26 +0000 Subject: [PATCH 4/9] NPA-6139: Add proper GPIT system URLS with env check in application layer --- app/api/application/forward_request.py | 21 ++- .../application/tests/test_forward_request.py | 176 +++++++++++++++--- app/api/domain/forward_request_model.py | 2 +- .../tests/test_forward_request_model.py | 20 +- .../parameters/NHSE-Forward-To.yaml | 18 +- 5 files changed, 189 insertions(+), 48 deletions(-) diff --git a/app/api/application/forward_request.py b/app/api/application/forward_request.py index 9c07db68..f3b28c92 100644 --- a/app/api/application/forward_request.py +++ b/app/api/application/forward_request.py @@ -1,10 +1,23 @@ -from app.api.domain.exception import ApiError, DownstreamError +from os import environ + +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 -client_map = {"https://emis.com": EmisClient, "https://tpp.com": TPPClient} +ENVIRONMENT = environ.get("ENVIRONMENT") +EMIS_URL = ( + "https://api.pfs.emis-x.uk" + if ENVIRONMENT == "prod" + else "https://nhs70apptest.emishealth.com" +) +TPP_URL = ( + "https://systmonline.tpp-uk.com" + if ENVIRONMENT == "prod" + else "https://systmonline2.tpp-uk.com" +) +CLIENT_MAP = {EMIS_URL: EmisClient, TPP_URL: TPPClient} def route_and_forward(forward_request: ForwardRequest) -> ForwardResponse: @@ -16,9 +29,11 @@ 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: + raise InvalidValueError(f"Invalid URL: {exc}") except ApiError: raise except Exception as exc: diff --git a/app/api/application/tests/test_forward_request.py b/app/api/application/tests/test_forward_request.py index a7f2d84e..98bddc47 100644 --- a/app/api/application/tests/test_forward_request.py +++ b/app/api/application/tests/test_forward_request.py @@ -1,34 +1,138 @@ -from unittest.mock import MagicMock +from importlib import reload +from os import environ +from unittest.mock import patch, MagicMock 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 -def test_route_and_forward() -> None: +FILE_PATH = "app.api.application.forward_request" + + +@pytest.mark.parametrize( + "forward_to, environment", + [ + ("https://nhs70apptest.emishealth.com", "pre-prod"), + ("https://api.pfs.emis-x.uk", "prod"), + ], +) +def test_route_and_forward_emis(forward_to: str, environment: str) -> None: """Tests the route_and_forward function.""" # Arrange forward_request = ForwardRequest( application_id="some application", - forward_to="https://emis.com", + forward_to=forward_to, 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", {"ENVIRONMENT": environment}): + with patch("app.api.infrastructure.emis.client.EmisClient") as mock_emis_client: + with 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() + + +@pytest.mark.parametrize( + "forward_to, environment", + [ + ("https://systmonline2.tpp-uk.com", "pre-prod"), + ("https://systmonline.tpp-uk.com", "prod"), + ], +) +def test_route_and_forward_tpp(forward_to: str, environment: str) -> None: + """Tests the route_and_forward function.""" + # Arrange + forward_request = ForwardRequest( + application_id="some application", + forward_to=forward_to, + patient_nhs_number="1234567890", + patient_ods_code="some ods code", + proxy_nhs_number="0987654321", + use_mock=False, ) - forward_request_module.client_map["https://emis.com"] = mock_client - # Act - actual_result = route_and_forward(forward_request) - # Assert - assert actual_result == "mocked transformed response" + with patch.dict("os.environ", {"ENVIRONMENT": environment}): + with patch("app.api.infrastructure.emis.client.EmisClient") as mock_emis_client: + with 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://google.com", + patient_nhs_number="1234567890", + patient_ods_code="some ods code", + proxy_nhs_number="0987654321", + use_mock=False, + ) + + with patch.dict("os.environ", {"ENVIRONMENT": "pre-prod"}): + with patch("app.api.infrastructure.emis.client.EmisClient") as mock_emis_client: + with 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: 'https://google.com'" + ): + 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: @@ -36,18 +140,25 @@ def test_route_and_forward_raises_api_error() -> None: # Arrange forward_request = ForwardRequest( application_id="some application", - forward_to="https://emis.com", + forward_to="https://nhs70apptest.emishealth.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://emis.com"] = mock_client - # Act & Assert - with pytest.raises(ForbiddenError, match="Oops"): - route_and_forward(forward_request) + with patch.dict("os.environ", {"ENVIRONMENT": "pre-prod"}): + with 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: @@ -55,15 +166,24 @@ def test_route_and_forward_raises_downstream_error() -> None: # Arrange forward_request = ForwardRequest( application_id="some application", - forward_to="https://emis.com", + forward_to="https://nhs70apptest.emishealth.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://emis.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", {"ENVIRONMENT": "pre-prod"}): + with 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) diff --git a/app/api/domain/forward_request_model.py b/app/api/domain/forward_request_model.py index b6f0c2d1..88c22fc3 100644 --- a/app/api/domain/forward_request_model.py +++ b/app/api/domain/forward_request_model.py @@ -75,7 +75,7 @@ def validate_url(cls, value: str) -> str: If unsuccessful will raise An InvalidValueError Exception """ - if value not in ["https://emis.com", "https://tpp.com"]: + if not value.startswith("https:"): msg = "Invalid url" raise InvalidValueError(msg) diff --git a/app/api/domain/tests/test_forward_request_model.py b/app/api/domain/tests/test_forward_request_model.py index 640caa87..bac63b19 100644 --- a/app/api/domain/tests/test_forward_request_model.py +++ b/app/api/domain/tests/test_forward_request_model.py @@ -8,16 +8,12 @@ from app.api.domain.forward_request_model import ForwardRequest -@pytest.mark.parametrize( - "forward_to", - ["https://emis.com", "https://tpp.com"], -) -def test_forward_request(forward_to: str) -> None: +def test_forward_request() -> None: """Tests the ForwardRequest model.""" # Act & Assert ForwardRequest( application_id="some application", - forward_to=forward_to, + forward_to="https://google.com", patient_nhs_number="1234567890", patient_ods_code="some ods code", proxy_nhs_number="0987654321", @@ -28,10 +24,10 @@ def test_forward_request(forward_to: str) -> None: @pytest.mark.parametrize( ("application_id", "ods_code", "forward_to"), [ - (None, "ods code", "https://emis.com"), - ("", "ods code", "https://emis.com"), - ("application id", None, "https://emis.com"), - ("application id", "", "https://emis.com"), + (None, "ods code", "https://google.com"), + ("", "ods code", "https://google.com"), + ("application id", None, "https://google.com"), + ("application id", "", "https://google.com"), ("application id", "ods code", None), ("application id", "ods code", ""), ], @@ -71,7 +67,7 @@ def test_forward_request_validates_nhs_numbers( with pytest.raises(AccessDeniedError, match="Failed to retrieve NHS Number"): ForwardRequest( application_id="some application", - forward_to="https://emis.com", + forward_to="https://google.com", patient_nhs_number=patient_nhs_number, patient_ods_code="some ods code", proxy_nhs_number=proxy_nhs_number, @@ -81,7 +77,7 @@ def test_forward_request_validates_nhs_numbers( @pytest.mark.parametrize( "forward_to", - ["some random value", "https://invalid.com", "www.google.com"], + ["some random value", "invalid.com", "www.google.com"], ) def test_forward_request_validates_forward_to(forward_to: str) -> None: """Tests the ForwardRequest model validates forward to is a url.""" diff --git a/specification/components/parameters/NHSE-Forward-To.yaml b/specification/components/parameters/NHSE-Forward-To.yaml index cb56c4de..5c6650f5 100644 --- a/specification/components/parameters/NHSE-Forward-To.yaml +++ b/specification/components/parameters/NHSE-Forward-To.yaml @@ -1,9 +1,19 @@ name: NHSE-Forward-To in: header -description: The GPIT Supplier base URL to forward requests to. +description: | + The GPIT Supplier base URL to forward requests to. + + TPP: + | Environment | Allowed URLs | + |-------------|---------------------------------| + | Integration | https://systmonline2.tpp-uk.com | + | Production | https://systmonline.tpp-uk.com | + + EMIS: + | Environment | Allowed URLs | + |-------------|-------------------------------------| + | Integration | https://nhs70apptest.emishealth.com | + | Production | https://api.pfs.emis-x.uk | required: true schema: type: string - enum: - - https://tpp.com - - https://enum.com From c081dfd256f17dbf712120428706c737c87e2392 Mon Sep 17 00:00:00 2001 From: Ellie Bound <175816742+ellie-bound1-NHSD@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:21:36 +0000 Subject: [PATCH 5/9] NPA-6139: Defulat value for ENVIRONMENT --- app/api/application/forward_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/application/forward_request.py b/app/api/application/forward_request.py index f3b28c92..98e2e5cb 100644 --- a/app/api/application/forward_request.py +++ b/app/api/application/forward_request.py @@ -6,7 +6,7 @@ from app.api.infrastructure.emis.client import EmisClient from app.api.infrastructure.tpp.client import TPPClient -ENVIRONMENT = environ.get("ENVIRONMENT") +ENVIRONMENT = environ.get("ENVIRONMENT", "") EMIS_URL = ( "https://api.pfs.emis-x.uk" if ENVIRONMENT == "prod" From 64b5a891c3c836221598bb84a2eda5b8310d1158 Mon Sep 17 00:00:00 2001 From: Ellie Bound <175816742+ellie-bound1-NHSD@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:35:24 +0000 Subject: [PATCH 6/9] NPA-6139: Add Environment to Docker Container --- .github/workflows/reusable-deploy.yml | 1 + Makefile | 2 +- app/Dockerfile | 3 +++ app/api/application/tests/test_forward_request.py | 4 +--- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/reusable-deploy.yml b/.github/workflows/reusable-deploy.yml index 5c803da9..95d56e54 100644 --- a/.github/workflows/reusable-deploy.yml +++ b/.github/workflows/reusable-deploy.yml @@ -42,6 +42,7 @@ on: permissions: {} env: + ENVIRONMENT: ${{ inputs.environment }} CONTAINER_TAG: ${{inputs.type_of_deployment}}-${{ github.sha }} PROXYGEN_DOCKER_REGISTRY_URL: 958002497996.dkr.ecr.eu-west-2.amazonaws.com/im1-pfs-auth diff --git a/Makefile b/Makefile index 17258a18..106d5898 100644 --- a/Makefile +++ b/Makefile @@ -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)" --load app/ + docker buildx build -t "$(PROXYGEN_DOCKER_REGISTRY_URL):$(CONTAINER_TAG)" --build-arg ENVIRONMENT=$(ENVIRONMENT) --load app/ app-push: proxygen docker get-login | bash diff --git a/app/Dockerfile b/app/Dockerfile index 41cef902..f80b4d68 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -8,6 +8,9 @@ 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 ENVIRONMENT +ENV ENVIRONMENT=${ENVIRONMENT} + WORKDIR /app ENV PYTHONPATH="/app:/" diff --git a/app/api/application/tests/test_forward_request.py b/app/api/application/tests/test_forward_request.py index 98bddc47..63ba2738 100644 --- a/app/api/application/tests/test_forward_request.py +++ b/app/api/application/tests/test_forward_request.py @@ -1,13 +1,11 @@ from importlib import reload -from os import environ -from unittest.mock import patch, MagicMock +from unittest.mock import patch import pytest 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" From 0ec8a7ccacb862e97e61a994f677985af0ce2a57 Mon Sep 17 00:00:00 2001 From: Ellie Bound <175816742+ellie-bound1-NHSD@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:44:33 +0000 Subject: [PATCH 7/9] NPA-6139: Add proper URLS to E2E tests --- tests/end_to_end/authenticate/POST/test_201.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/end_to_end/authenticate/POST/test_201.py b/tests/end_to_end/authenticate/POST/test_201.py index d9a3b84b..506ef4a7 100644 --- a/tests/end_to_end/authenticate/POST/test_201.py +++ b/tests/end_to_end/authenticate/POST/test_201.py @@ -16,7 +16,7 @@ ("forward_to_url", "expected_response"), [ ( - "https://emis.com", + "https://nhs70apptest.emishealth.com", { "patients": [ { @@ -105,7 +105,7 @@ }, ), ( - "https://tpp.com", + "https://systmonline2.tpp-uk.com", { "patients": [ { From b999d7043c7c9a5fe731035cd9dec6c63d9a1779 Mon Sep 17 00:00:00 2001 From: Ellie Bound <175816742+ellie-bound1-NHSD@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:30:49 +0000 Subject: [PATCH 8/9] NPA-6139: Linting fixes --- app/api/application/forward_request.py | 3 +- .../application/tests/test_forward_request.py | 160 +++++++++--------- pyproject.toml | 2 +- 3 files changed, 81 insertions(+), 84 deletions(-) diff --git a/app/api/application/forward_request.py b/app/api/application/forward_request.py index 98e2e5cb..50e1f9e0 100644 --- a/app/api/application/forward_request.py +++ b/app/api/application/forward_request.py @@ -33,7 +33,8 @@ def route_and_forward(forward_request: ForwardRequest) -> ForwardResponse: response = client.forward_request() return client.transform_response(response) except KeyError as exc: - raise InvalidValueError(f"Invalid URL: {exc}") + msg = "Invalid URL" + raise InvalidValueError(msg) from exc except ApiError: raise except Exception as exc: diff --git a/app/api/application/tests/test_forward_request.py b/app/api/application/tests/test_forward_request.py index 63ba2738..7849b72a 100644 --- a/app/api/application/tests/test_forward_request.py +++ b/app/api/application/tests/test_forward_request.py @@ -10,7 +10,7 @@ @pytest.mark.parametrize( - "forward_to, environment", + ("forward_to", "environment"), [ ("https://nhs70apptest.emishealth.com", "pre-prod"), ("https://api.pfs.emis-x.uk", "prod"), @@ -28,35 +28,33 @@ def test_route_and_forward_emis(forward_to: str, environment: str) -> None: use_mock=False, ) - with patch.dict("os.environ", {"ENVIRONMENT": environment}): - with patch("app.api.infrastructure.emis.client.EmisClient") as mock_emis_client: - with patch( - "app.api.infrastructure.tpp.client.TPPClient" - ) as mock_tpp_client: - from app.api.application import ( - forward_request as forward_request_module, - ) + with ( + patch.dict("os.environ", {"ENVIRONMENT": environment}), + 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" - ) + mock_emis_client.return_value.transform_response.return_value = ( + "mocked transformed response" + ) - reload(forward_request_module) + reload(forward_request_module) - # Act - actual_result = forward_request_module.route_and_forward( - forward_request - ) + # 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() + # 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() @pytest.mark.parametrize( - "forward_to, environment", + ("forward_to", "environment"), [ ("https://systmonline2.tpp-uk.com", "pre-prod"), ("https://systmonline.tpp-uk.com", "prod"), @@ -74,31 +72,29 @@ def test_route_and_forward_tpp(forward_to: str, environment: str) -> None: use_mock=False, ) - with patch.dict("os.environ", {"ENVIRONMENT": environment}): - with patch("app.api.infrastructure.emis.client.EmisClient") as mock_emis_client: - with patch( - "app.api.infrastructure.tpp.client.TPPClient" - ) as mock_tpp_client: - from app.api.application import ( - forward_request as forward_request_module, - ) + with ( + patch.dict("os.environ", {"ENVIRONMENT": environment}), + 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" - ) + mock_tpp_client.return_value.transform_response.return_value = ( + "mocked transformed response" + ) - reload(forward_request_module) + reload(forward_request_module) - # Act - actual_result = forward_request_module.route_and_forward( - forward_request - ) + # 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() + # 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: @@ -113,24 +109,22 @@ def test_route_and_forward_invalid_url() -> None: use_mock=False, ) - with patch.dict("os.environ", {"ENVIRONMENT": "pre-prod"}): - with patch("app.api.infrastructure.emis.client.EmisClient") as mock_emis_client: - with patch( - "app.api.infrastructure.tpp.client.TPPClient" - ) as mock_tpp_client: - from app.api.application import ( - forward_request as forward_request_module, - ) + with ( + patch.dict("os.environ", {"ENVIRONMENT": "pre-prod"}), + 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) + reload(forward_request_module) - # Act & Assert - with pytest.raises( - InvalidValueError, match="Invalid URL: 'https://google.com'" - ): - forward_request_module.route_and_forward(forward_request) - mock_emis_client.assert_not_called() - mock_tpp_client.assert_not_called() + # 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: @@ -144,19 +138,21 @@ def test_route_and_forward_raises_api_error() -> None: proxy_nhs_number="0987654321", use_mock=False, ) - with patch.dict("os.environ", {"ENVIRONMENT": "pre-prod"}): - with patch("app.api.infrastructure.emis.client.EmisClient") as mock_emis_client: - from app.api.application import forward_request as forward_request_module + with ( + patch.dict("os.environ", {"ENVIRONMENT": "pre-prod"}), + 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" - ) + mock_emis_client.return_value.forward_request.side_effect = ForbiddenError( + "Oops" + ) - reload(forward_request_module) + reload(forward_request_module) - # Act & Assert - with pytest.raises(ForbiddenError, match="Oops"): - forward_request_module.route_and_forward(forward_request) + # 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: @@ -170,18 +166,18 @@ def test_route_and_forward_raises_downstream_error() -> None: proxy_nhs_number="0987654321", use_mock=False, ) - with patch.dict("os.environ", {"ENVIRONMENT": "pre-prod"}): - with patch("app.api.infrastructure.emis.client.EmisClient") as mock_emis_client: - from app.api.application import forward_request as forward_request_module + with ( + patch.dict("os.environ", {"ENVIRONMENT": "pre-prod"}), + 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" - ) + mock_emis_client.return_value.forward_request.side_effect = Exception("Oops") - reload(forward_request_module) + reload(forward_request_module) - # Act & Assert - with pytest.raises( - DownstreamError, match="Error occurred with downstream service" - ): - forward_request_module.route_and_forward(forward_request) + # Act & Assert + with pytest.raises( + DownstreamError, match="Error occurred with downstream service" + ): + forward_request_module.route_and_forward(forward_request) diff --git a/pyproject.toml b/pyproject.toml index a9522f69..3452eedc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ ignore = [ "app/api/domain/forward_request_model.py" = [ "N805", ] # Pydantic basemodel needs cls instead of self -"**test_*.py" = ["S101", "D102", "D103", "SLF001", "PT019", "PLR0913", "G004"] +"**test_*.py" = ["S101", "D102", "D103", "SLF001", "PT019", "PLR0913", "PLC0415", "G004"] [tool.ruff.lint.pydocstyle] convention = "google" From df7fac94336a14965ada42c1623d1e002e4658ab Mon Sep 17 00:00:00 2001 From: Ellie Bound <175816742+ellie-bound1-NHSD@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:24:00 +0000 Subject: [PATCH 9/9] NPA-6139: Use Env vars to hold URLs rather than base on environment --- .../cicd-stage-1-deploy-to-internal-qa.yml | 2 + ...cicd-stage-2-deploy-to-int-and-sandbox.yml | 2 + .github/workflows/pull-request-checks.yml | 2 + .github/workflows/reusable-deploy.yml | 11 ++++- Makefile | 2 +- app/Dockerfile | 7 ++- app/api/application/forward_request.py | 15 ++---- .../application/tests/test_forward_request.py | 47 +++++++++---------- app/api/domain/forward_request_model.py | 2 +- .../tests/test_forward_request_model.py | 14 +++--- 10 files changed, 54 insertions(+), 50 deletions(-) diff --git a/.github/workflows/cicd-stage-1-deploy-to-internal-qa.yml b/.github/workflows/cicd-stage-1-deploy-to-internal-qa.yml index 610587a7..b66e10e8 100644 --- a/.github/workflows/cicd-stage-1-deploy-to-internal-qa.yml +++ b/.github/workflows/cicd-stage-1-deploy-to-internal-qa.yml @@ -13,6 +13,8 @@ jobs: with: environment: "internal-qa" type_of_deployment: "app" + 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 }} diff --git a/.github/workflows/cicd-stage-2-deploy-to-int-and-sandbox.yml b/.github/workflows/cicd-stage-2-deploy-to-int-and-sandbox.yml index d1c08838..cd313d62 100644 --- a/.github/workflows/cicd-stage-2-deploy-to-int-and-sandbox.yml +++ b/.github/workflows/cicd-stage-2-deploy-to-int-and-sandbox.yml @@ -12,6 +12,8 @@ jobs: with: environment: "int" type_of_deployment: "app" + 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 }} diff --git a/.github/workflows/pull-request-checks.yml b/.github/workflows/pull-request-checks.yml index 3c093c05..a78348d7 100644 --- a/.github/workflows/pull-request-checks.yml +++ b/.github/workflows/pull-request-checks.yml @@ -32,6 +32,8 @@ jobs: environment: "internal-dev" type_of_deployment: "app" additional_path: "pr-${{ github.event.number }}" + 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 }} diff --git a/.github/workflows/reusable-deploy.yml b/.github/workflows/reusable-deploy.yml index 95d56e54..48bbe1e9 100644 --- a/.github/workflows/reusable-deploy.yml +++ b/.github/workflows/reusable-deploy.yml @@ -19,6 +19,14 @@ on: description: "Type of deployment, e.g. 'app', 'sandbox'" required: true type: string + 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: @@ -42,9 +50,10 @@ on: permissions: {} env: - ENVIRONMENT: ${{ inputs.environment }} CONTAINER_TAG: ${{inputs.type_of_deployment}}-${{ github.sha }} PROXYGEN_DOCKER_REGISTRY_URL: 958002497996.dkr.ecr.eu-west-2.amazonaws.com/im1-pfs-auth + EMIS_BASE_URL: ${{ inputs.emis_base_url }} + TPP_BASE_URL: ${{ inputs.tpp_base_url }} jobs: validate-inputs: diff --git a/Makefile b/Makefile index 106d5898..166b65b7 100644 --- a/Makefile +++ b/Makefile @@ -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 ENVIRONMENT=$(ENVIRONMENT) --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 diff --git a/app/Dockerfile b/app/Dockerfile index f80b4d68..b523b348 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -8,8 +8,11 @@ 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 ENVIRONMENT -ENV ENVIRONMENT=${ENVIRONMENT} +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:/" diff --git a/app/api/application/forward_request.py b/app/api/application/forward_request.py index 50e1f9e0..bc1115ff 100644 --- a/app/api/application/forward_request.py +++ b/app/api/application/forward_request.py @@ -6,18 +6,9 @@ from app.api.infrastructure.emis.client import EmisClient from app.api.infrastructure.tpp.client import TPPClient -ENVIRONMENT = environ.get("ENVIRONMENT", "") -EMIS_URL = ( - "https://api.pfs.emis-x.uk" - if ENVIRONMENT == "prod" - else "https://nhs70apptest.emishealth.com" -) -TPP_URL = ( - "https://systmonline.tpp-uk.com" - if ENVIRONMENT == "prod" - else "https://systmonline2.tpp-uk.com" -) -CLIENT_MAP = {EMIS_URL: EmisClient, TPP_URL: 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: diff --git a/app/api/application/tests/test_forward_request.py b/app/api/application/tests/test_forward_request.py index 7849b72a..2cde7b21 100644 --- a/app/api/application/tests/test_forward_request.py +++ b/app/api/application/tests/test_forward_request.py @@ -9,19 +9,12 @@ FILE_PATH = "app.api.application.forward_request" -@pytest.mark.parametrize( - ("forward_to", "environment"), - [ - ("https://nhs70apptest.emishealth.com", "pre-prod"), - ("https://api.pfs.emis-x.uk", "prod"), - ], -) -def test_route_and_forward_emis(forward_to: str, environment: str) -> None: +def test_route_and_forward_emis() -> None: """Tests the route_and_forward function.""" # Arrange forward_request = ForwardRequest( application_id="some application", - forward_to=forward_to, + forward_to="https://emis.com", patient_nhs_number="1234567890", patient_ods_code="some ods code", proxy_nhs_number="0987654321", @@ -29,7 +22,7 @@ def test_route_and_forward_emis(forward_to: str, environment: str) -> None: ) with ( - patch.dict("os.environ", {"ENVIRONMENT": environment}), + 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, ): @@ -53,19 +46,12 @@ def test_route_and_forward_emis(forward_to: str, environment: str) -> None: mock_tpp_client.assert_not_called() -@pytest.mark.parametrize( - ("forward_to", "environment"), - [ - ("https://systmonline2.tpp-uk.com", "pre-prod"), - ("https://systmonline.tpp-uk.com", "prod"), - ], -) -def test_route_and_forward_tpp(forward_to: str, environment: str) -> None: +def test_route_and_forward_tpp() -> None: """Tests the route_and_forward function.""" # Arrange forward_request = ForwardRequest( application_id="some application", - forward_to=forward_to, + forward_to="https://tpp.com", patient_nhs_number="1234567890", patient_ods_code="some ods code", proxy_nhs_number="0987654321", @@ -73,7 +59,7 @@ def test_route_and_forward_tpp(forward_to: str, environment: str) -> None: ) with ( - patch.dict("os.environ", {"ENVIRONMENT": environment}), + 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, ): @@ -102,7 +88,7 @@ def test_route_and_forward_invalid_url() -> None: # Arrange forward_request = ForwardRequest( application_id="some application", - forward_to="https://google.com", + forward_to="https://example.com", patient_nhs_number="1234567890", patient_ods_code="some ods code", proxy_nhs_number="0987654321", @@ -110,7 +96,10 @@ def test_route_and_forward_invalid_url() -> None: ) with ( - patch.dict("os.environ", {"ENVIRONMENT": "pre-prod"}), + 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, ): @@ -132,14 +121,17 @@ def test_route_and_forward_raises_api_error() -> None: # Arrange forward_request = ForwardRequest( application_id="some application", - forward_to="https://nhs70apptest.emishealth.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", {"ENVIRONMENT": "pre-prod"}), + 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 @@ -160,14 +152,17 @@ def test_route_and_forward_raises_downstream_error() -> None: # Arrange forward_request = ForwardRequest( application_id="some application", - forward_to="https://nhs70apptest.emishealth.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", {"ENVIRONMENT": "pre-prod"}), + 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 diff --git a/app/api/domain/forward_request_model.py b/app/api/domain/forward_request_model.py index 88c22fc3..a624e7e7 100644 --- a/app/api/domain/forward_request_model.py +++ b/app/api/domain/forward_request_model.py @@ -71,7 +71,7 @@ def validate_nhs_number(cls, values: list) -> list: @field_validator("forward_to") @classmethod def validate_url(cls, value: str) -> str: - """Validates url is one of the allowed urls. + """Validates url. If unsuccessful will raise An InvalidValueError Exception """ diff --git a/app/api/domain/tests/test_forward_request_model.py b/app/api/domain/tests/test_forward_request_model.py index bac63b19..3012a57f 100644 --- a/app/api/domain/tests/test_forward_request_model.py +++ b/app/api/domain/tests/test_forward_request_model.py @@ -13,7 +13,7 @@ def test_forward_request() -> None: # Act & Assert ForwardRequest( application_id="some application", - forward_to="https://google.com", + forward_to="https://example.com", patient_nhs_number="1234567890", patient_ods_code="some ods code", proxy_nhs_number="0987654321", @@ -24,10 +24,10 @@ def test_forward_request() -> None: @pytest.mark.parametrize( ("application_id", "ods_code", "forward_to"), [ - (None, "ods code", "https://google.com"), - ("", "ods code", "https://google.com"), - ("application id", None, "https://google.com"), - ("application id", "", "https://google.com"), + (None, "ods code", "https://example.com"), + ("", "ods code", "https://example.com"), + ("application id", None, "https://example.com"), + ("application id", "", "https://example.com"), ("application id", "ods code", None), ("application id", "ods code", ""), ], @@ -67,7 +67,7 @@ def test_forward_request_validates_nhs_numbers( with pytest.raises(AccessDeniedError, match="Failed to retrieve NHS Number"): ForwardRequest( application_id="some application", - forward_to="https://google.com", + forward_to="https://example.com", patient_nhs_number=patient_nhs_number, patient_ods_code="some ods code", proxy_nhs_number=proxy_nhs_number, @@ -77,7 +77,7 @@ def test_forward_request_validates_nhs_numbers( @pytest.mark.parametrize( "forward_to", - ["some random value", "invalid.com", "www.google.com"], + ["some random value", "invalid.com", "www.example.com"], ) def test_forward_request_validates_forward_to(forward_to: str) -> None: """Tests the ForwardRequest model validates forward to is a url."""