Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
70084e5
[PRMP-1464] introduce model, mock data, start implementing hcw api se…
steph-torres-nhs Feb 19, 2026
52fc5cc
[PRMP-1464] implement get practitioner using hcw api
steph-torres-nhs Feb 20, 2026
fc05a4d
[PRMP-1464] add logging to service
steph-torres-nhs Feb 23, 2026
c7c583a
[PRMP-1464] remove comment
steph-torres-nhs Feb 23, 2026
680929c
[PRMP-1464] return default message from api error
steph-torres-nhs Feb 23, 2026
8473080
Merge branch 'main' into PRMP-1464
steph-torres-nhs Feb 23, 2026
f2e5bf0
[PRMP-1464] add headers
steph-torres-nhs Feb 23, 2026
7985705
[PRMP-1464] remove comment
steph-torres-nhs Feb 23, 2026
dc94136
[PRMP-1464] format
steph-torres-nhs Feb 23, 2026
b9c80f9
Merge branch 'main' into PRMP-1464
steph-torres-nhs Feb 23, 2026
c46ecd7
[PRMP-1486] refactor, start implementation of mock api service
steph-torres-nhs Feb 23, 2026
d56e57b
[PRMP-1464] rename var to not shadow builtin
steph-torres-nhs Feb 23, 2026
79497bd
[PRMP-1485] start mock implementation
steph-torres-nhs Feb 23, 2026
65fe060
[PRMP-1486] format
steph-torres-nhs Feb 23, 2026
f18cf5c
[PRMP-1484] rename function, update mocks returned
steph-torres-nhs Feb 23, 2026
32568ac
[PRMP-1484] add test coverage
steph-torres-nhs Feb 24, 2026
6475e73
[PRMP-1464] remove unused fixture
steph-torres-nhs Feb 24, 2026
3799cfb
Merge branch 'main' into PRMP-1464
steph-torres-nhs Feb 24, 2026
0489f56
[PRMP-1484] move conftest
steph-torres-nhs Feb 24, 2026
9e287b0
Merge branch 'PRMP-1464' into PRMP-1484
steph-torres-nhs Feb 24, 2026
b3982d0
[PRMP-1484] merge in main, resolve conflicts
steph-torres-nhs Feb 25, 2026
b3fd866
[PRMP-1484] refactor, remove magic strings
steph-torres-nhs Feb 25, 2026
855d67d
[PRMP-1484] format
steph-torres-nhs Feb 25, 2026
05674e4
[PRMP-1475] update lambda deployment action, create files
steph-torres-nhs Feb 25, 2026
d5aef1b
[PRMP-1475] implement get_user_information_handler
steph-torres-nhs Feb 25, 2026
924cc41
[PRMP-1475] add env var decorator
steph-torres-nhs Feb 26, 2026
ad87931
[PRMP-1475] adjust workflow
steph-torres-nhs Feb 26, 2026
9505f63
[PRMP-1475] amend api resposne
steph-torres-nhs Feb 26, 2026
8d11076
[PRMP-1475] add route to authoriser
steph-torres-nhs Feb 26, 2026
08ef729
[PRMP-1475] add logging to handler
steph-torres-nhs Feb 26, 2026
d360e3a
Merge branch 'main' into PRMP-1475
steph-torres-nhs Feb 26, 2026
62b020b
[PRMP-1475] handle empty identifier or missing querystring param
steph-torres-nhs Feb 26, 2026
4c5fb9b
[PRMP-1475] amend invalid event handling
steph-torres-nhs Feb 26, 2026
3dac2b3
Merge branch 'main' into PRMP-1475
steph-torres-nhs Feb 26, 2026
ec50fef
Merge branch 'main' into PRMP-1475
steph-torres-nhs Feb 26, 2026
9134ba0
Merge branch 'main' into PRMP-1475
steph-torres-nhs Feb 27, 2026
b5c4f2c
[PRMP-1475] merge main, resolve conflicts
steph-torres-nhs Feb 27, 2026
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
15 changes: 15 additions & 0 deletions .github/workflows/base-lambdas-reusable-deploy-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -880,3 +880,18 @@ jobs:
lambda_layer_names: "core_lambda_layer"
secrets:
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}

deploy_get_user_information_lambda:
name: Deploy Concurrency Controller Lambda
uses: ./.github/workflows/base-lambdas-reusable-deploy.yml
with:
environment: ${{ inputs.environment }}
python_version: ${{ inputs.python_version }}
build_branch: ${{ inputs.build_branch }}
sandbox: ${{ inputs.sandbox }}
lambda_handler_name: get_user_information_handler
lambda_aws_name: GetUserInformation
lambda_layer_names: "core_lambda_layer"
lambda_handler_path: user_restrictions
secrets:
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}
13 changes: 13 additions & 0 deletions lambdas/enums/lambda_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,19 @@ def create_error_body(

EdgeNoClient = {"err_code": "CE_4001", "message": "Document not found"}

"""
Errors for User Restrictions feature
"""
UserRestrictionInvalidEvent = {
"err_code": "UR_4001",
"message": "Malformed user restriction event",
}

UserRestrictionModelValidationError = {
"err_code": "UR_5001",
"message": "Malformed user restriction model error",
}

"""
Errors with no exception
"""
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import json

from enums.lambda_error import LambdaError
from services.user_restrictions.utilites import get_healthcare_worker_api_service
from utils.audit_logging_setup import LoggingService
from utils.decorators.ensure_env_var import ensure_environment_variables
from utils.decorators.handle_lambda_exceptions import handle_lambda_exceptions
from utils.exceptions import (
HealthcareWorkerAPIException,
HealthcareWorkerPractitionerModelException,
)
from utils.lambda_response import ApiGatewayResponse

logger = LoggingService(__name__)


@ensure_environment_variables(
names=[
"HEALTHCARE_WORKER_API_URL",
"USE_MOCK_HEALTHCARE_SERVICE",
],
)
@handle_lambda_exceptions
def lambda_handler(event, context):
try:
logger.info("Processing request to retrieve user information.")
identifier = event.get("querystringParameters", {}).get("identifier")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feature flag check?


if not identifier:
logger.error("No identifier provided.")
return ApiGatewayResponse(
400,
LambdaError.UserRestrictionInvalidEvent.create_error_body(),
"GET",
).create_api_gateway_response()

healthcare_worker_api_service = get_healthcare_worker_api_service()

practitioner_information = healthcare_worker_api_service.get_practitioner(
identifier=identifier,
)
logger.info("Returning user information.")
return ApiGatewayResponse(
200,
json.dumps(practitioner_information.model_dump_camel_case()),
"GET",
).create_api_gateway_response()
except HealthcareWorkerAPIException as e:
return ApiGatewayResponse(502, e.message, "GET").create_api_gateway_response()
except HealthcareWorkerPractitionerModelException:
return ApiGatewayResponse(
500,
LambdaError.UserRestrictionModelValidationError.create_error_body(),
"GET",
).create_api_gateway_response()
8 changes: 8 additions & 0 deletions lambdas/models/user_restrictions/practitioner.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
from pydantic import BaseModel
from pydantic.alias_generators import to_camel


class Practitioner(BaseModel):
first_name: str
last_name: str
smartcard_id: str

def model_dump_camel_case(self, *args, **kwargs):
model_dump_results = self.model_dump(*args, **kwargs)
camel_case_model_dump_results = {}
for key in model_dump_results:
camel_case_model_dump_results[to_camel(key)] = model_dump_results[key]
return camel_case_model_dump_results
3 changes: 3 additions & 0 deletions lambdas/services/authoriser_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ def deny_access_policy(self, path, http_verb, user_role, nhs_number: str = None)
not patient_access_is_allowed or is_user_gp_clinical or is_user_pcse
)

case "/UserRestriction/SearchUser":
deny_resource = False

case _:
deny_resource = not patient_access_is_allowed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,20 @@ def get_practitioner(self, identifier: str) -> Practitioner:
body = response.json()
return self.build_practitioner(body)

except requests.exceptions.HTTPError as err:
except requests.exceptions.HTTPError as e:
raise HealthcareWorkerAPIException(
status_code=err.response.status_code,
status_code=e.response.status_code,
)
except (ValidationError, KeyError) as err:
logger.error(err)
except (ValidationError, KeyError) as e:
logger.error(e)
raise HealthcareWorkerPractitionerModelException

def build_practitioner(self, response: dict) -> Practitioner:
if response["total"] > 1:
logger.info("Received more than on entry for practitioner.")
raise HealthcareWorkerPractitionerModelException

logger.info("Getting practitioner model.")
logger.info("Creating practitioner model with user information.")
entry = response["entry"][0]

practitioner_id = entry["resource"]["id"]
Expand Down
1 change: 1 addition & 0 deletions lambdas/tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ def set_env(monkeypatch):
monkeypatch.setenv("EDGE_REFERENCE_TABLE", MOCK_EDGE_REFERENCE_TABLE)
monkeypatch.setenv("REVIEW_SQS_QUEUE_URL", REVIEW_SQS_QUEUE_URL)
monkeypatch.setenv("HEALTHCARE_WORKER_API_URL", HEALTHCARE_WORKER_API_URL)
monkeypatch.setenv("USE_MOCK_HEALTHCARE_SERVICE", "true")


EXPECTED_PARSED_PATIENT_BASE_CASE = PatientDetails(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import json

import pytest

from enums.lambda_error import LambdaError
from handlers.user_restrictions.get_user_information_handler import lambda_handler
from services.mock_data.user_restrictions.build_mock_data import (
build_mock_response_and_practitioner,
)
from tests.unit.services.user_restriction.conftest import MOCK_IDENTIFIER
from utils.exceptions import (
HealthcareWorkerAPIException,
HealthcareWorkerPractitionerModelException,
)
from utils.lambda_response import ApiGatewayResponse


@pytest.fixture
def mock_healthcare_worker_api_service(mocker):
mocked_class = mocker.patch(
"handlers.user_restrictions.get_user_information_handler.get_healthcare_worker_api_service",
)
mocked_instance = mocked_class.return_value
yield mocked_instance


@pytest.fixture
def mock_valid_event(event):
event["querystringParameters"] = {
"identifier": MOCK_IDENTIFIER,
}
yield event


@pytest.fixture
def mock_invalid_event_missing_identifier(event):
event["querystringParameters"] = {
"no_identifier": "abcdef",
}
yield event


@pytest.fixture
def mock_invalid_event_empty_querystring(event):
event["querystringParameters"] = {}
yield event


def test_lambda_handler_happy_path(
mock_healthcare_worker_api_service,
monkeypatch,
context,
mock_valid_event,
set_env,
):
_, _, mock_practitioner = build_mock_response_and_practitioner(MOCK_IDENTIFIER)
mock_healthcare_worker_api_service.get_practitioner.return_value = mock_practitioner

expected = ApiGatewayResponse(
200,
json.dumps(mock_practitioner.model_dump_camel_case()),
"GET",
).create_api_gateway_response()

actual = lambda_handler(mock_valid_event, context)

mock_healthcare_worker_api_service.get_practitioner.assert_called_with(
identifier=MOCK_IDENTIFIER,
)
assert actual == expected


def test_lambda_handler_returns_400_invalid_event(
mock_healthcare_worker_api_service,
context,
event,
mock_invalid_event_missing_identifier,
mock_invalid_event_empty_querystring,
set_env,
):

invalid_events = [
event,
mock_invalid_event_missing_identifier,
mock_invalid_event_empty_querystring,
]
expected_body = LambdaError.UserRestrictionInvalidEvent.create_error_body()

expected = ApiGatewayResponse(
400,
expected_body,
"GET",
).create_api_gateway_response()

for invalid_event in invalid_events:
actual = lambda_handler(invalid_event, context)

assert expected == actual


@pytest.mark.parametrize("status_code", [400, 404, 500, 403, 401])
def test_lambda_handler_handles_non_200_response_from_get_practitioner(
mock_healthcare_worker_api_service,
mock_valid_event,
context,
status_code,
set_env,
):
api_error = HealthcareWorkerAPIException(status_code=status_code)

mock_healthcare_worker_api_service.get_practitioner.side_effect = api_error
expected_body = api_error.message

expected = ApiGatewayResponse(
502,
expected_body,
"GET",
).create_api_gateway_response()

actual = lambda_handler(mock_valid_event, context)

assert actual == expected


def test_lambda_handler_handles_validation_error_from_get_practitioner(
mock_healthcare_worker_api_service,
mock_valid_event,
context,
set_env,
):
mock_healthcare_worker_api_service.get_practitioner.side_effect = (
HealthcareWorkerPractitionerModelException
)

expected = ApiGatewayResponse(
500,
LambdaError.UserRestrictionModelValidationError.create_error_body(),
"GET",
).create_api_gateway_response()

actual = lambda_handler(mock_valid_event, context)

assert actual == expected
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,6 @@ def test_get_practitioner_handles_key_error(mocker, mock_service, mock_get):
mock_service.get_practitioner(MOCK_IDENTIFIER)


def test_service_calls_ssm_for_token_each_call_to_api(mock_service):
pass


def test_build_practitioner_returns_practitioner_model_instance(mock_service):
_, mock_response, expected = build_mock_response_and_practitioner(MOCK_IDENTIFIER)

Expand Down
4 changes: 4 additions & 0 deletions lambdas/utils/lambda_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,7 @@ class UpdateDocumentReviewException(LambdaException):

class ReportDistributionException(LambdaException):
pass


class UserRestrictionsException(LambdaException):
pass
Loading