Skip to content

Commit ea9db6b

Browse files
AlexandraBensonrobertnovac1
authored andcommitted
Add unittests for cache and logging functions and move cache functions and constants to appropriate files
parent 0719a0d author Akol125 <akinola.olutola1@nhs.net> 1741732167 +0000 committer Robert Novac <robert.novac@airelogic.com> 1742314388 +0000 READ request - drop obfuscation for s-flag patient SEARCH request - removed PDS call from search and used Patient resource from last immunisation Contained resource in the final response
1 parent d00157a commit ea9db6b

File tree

86 files changed

+7848
-909
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+7848
-909
lines changed

.github/workflows/continuous-integration.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ jobs:
88
if: github.ref == 'refs/heads/master'
99
steps:
1010
- name: Checkout
11-
uses: actions/checkout@v2
11+
uses: actions/checkout@v4
1212
with:
1313
fetch-depth: 0 # This causes all history to be fetched, which is required for calculate-version to function
1414

1515
- name: Install Python 3.9
16-
uses: actions/setup-python@v1
16+
uses: actions/setup-python@v5
1717
with:
1818
python-version: 3.9
1919

@@ -40,5 +40,4 @@ jobs:
4040
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4141
with:
4242
tag_name: ${{ env.SPEC_VERSION }}
43-
release_name: ${{ env.SPEC_VERSION }}
44-
43+
release_name: ${{ env.SPEC_VERSION }}

.github/workflows/sonarcloud.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
id: filenameprocessor
3535
continue-on-error: true
3636
run: |
37-
pip install poetry==1.8.4 moto==4.2.11 coverage redis botocore==1.35.49 simplejson pandas freezegun responses structlog fhir.resources jsonpath_ng pydantic==1.10.13 requests aws-lambda-typing cffi pyjwt boto3-stubs-lite[dynamodb]~=1.26.90 python-stdnum==1.20
37+
pip install poetry==1.8.4 moto==4.2.11 coverage redis botocore==1.35.49 simplejson pandas freezegun responses structlog fhir.resources jsonpath_ng pydantic==1.10.13 requests aws-lambda-typing cffi pyjwt boto3-stubs-lite[dynamodb]~=1.26.90 python-stdnum==1.20 fakeredis
3838
poetry run coverage run --source=filenameprocessor -m unittest discover -s filenameprocessor || echo "filenameprocessor tests failed" >> failed_tests.txt
3939
poetry run coverage xml -o sonarcloud-coverage-filenameprocessor-coverage.xml
4040

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ build-proxy:
3838
scripts/build_proxy.sh
3939

4040
#Files to loop over in release
41-
_dist_include="pytest.ini poetry.lock poetry.toml pyproject.toml Makefile build/. e2e specification sandbox terraform scripts backend delta_backend ack_backend filenameprocessor recordprocessor"
41+
_dist_include="pytest.ini poetry.lock poetry.toml pyproject.toml Makefile build/. e2e e2e_batch specification sandbox terraform scripts backend delta_backend ack_backend filenameprocessor recordprocessor"
4242

4343

4444
#Create /dist/ sub-directory and copy files into directory

azure/templates/post-deploy.yml

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ steps:
114114
displayName: Waiting for TF resources to be UP
115115
workingDirectory: "$(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME)"
116116
117+
- bash: |
118+
pyenv install -s 3.10.8
119+
pyenv global 3.10.8
120+
python --version
121+
displayName: Set Python 3.10
122+
117123
- bash: |
118124
set -e
119125
export RELEASE_RELEASEID=$(Build.BuildId)
@@ -166,13 +172,45 @@ steps:
166172
echo "running: $test_cmd -v -c"
167173
$test_cmd -v -c
168174
fi
169-
170175
workingDirectory: "$(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME)/e2e"
171-
displayName: Run full test suite
176+
displayName: Run Full Test Suite
177+
178+
179+
180+
- bash: |
181+
set -e
182+
if ! [[ "$APIGEE_ENVIRONMENT" == "prod" || "$APIGEE_ENVIRONMENT" == "int" || "$APIGEE_ENVIRONMENT" == *"sandbox" ]]; then
183+
echo "Running E2E batch folder test cases"
184+
185+
export AWS_PROFILE="apim-dev"
186+
aws_account_no="$(aws sts get-caller-identity --query Account --output text)"
187+
echo "Using AWS Account: $aws_account_no"
188+
189+
service_name="${FULLY_QUALIFIED_SERVICE_NAME}"
190+
191+
pr_no=$(echo "$service_name" | { grep -oE '[0-9]+$' || true; })
192+
if [ -z "$pr_no" ]; then
193+
workspace="$APIGEE_ENVIRONMENT"
194+
else
195+
workspace="pr-$pr_no"
196+
fi
197+
198+
poetry install --no-root # Install dependencies defined in pyproject.toml
199+
200+
ENV="$workspace" poetry run python -m unittest -v -c
201+
202+
echo "E2E batch folder test cases executed successfully"
203+
else
204+
echo "Skipping E2E batch folder test cases as the environment is prod-int-sandbox"
205+
fi
206+
207+
displayName: Run full batch test suite
208+
workingDirectory: "$(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME)/e2e_batch"
209+
172210

173211
- task: PublishTestResults@2
174212
displayName: 'Publish test results'
175213
condition: always()
176214
inputs:
177215
testResultsFiles: '$(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME)/tests/test-report.xml'
178-
failTaskOnFailedTests: true
216+
failTaskOnFailedTests: true

backend/.envrc.default

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
layout pyenv 3.10.12
1+
layout pyenv 3.10.16
22

3-
dotenv
3+
dotenv

backend/.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
build
2-
.venv
1+
build
2+
.venv

backend/src/fhir_service.py

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from models.field_names import FieldNames
2020
from models.errors import InvalidPatientId, CustomValidationError, UnhandledResponseError
2121
from models.fhir_immunization import ImmunizationValidator
22-
from models.utils.generic_utils import nhs_number_mod11_check, get_occurrence_datetime, create_diagnostics, form_json
22+
from models.utils.generic_utils import nhs_number_mod11_check, get_occurrence_datetime, create_diagnostics, form_json, get_contained_patient
2323
from models.constants import Constants
2424
from models.errors import MandatoryError
2525
from pds_service import PdsService
@@ -76,7 +76,7 @@ def get_immunization_by_identifier(
7676
base_url = f"{get_service_url()}/Immunization"
7777
response = form_json(imms_resp, element, identifier, base_url)
7878
return response
79-
79+
8080
def get_immunization_by_id(self, imms_id: str, imms_vax_type_perms: str) -> Optional[dict]:
8181
"""
8282
Get an Immunization by its ID. Return None if not found. If the patient doesn't have an NHS number,
@@ -85,26 +85,19 @@ def get_immunization_by_id(self, imms_id: str, imms_vax_type_perms: str) -> Opti
8585
if not (imms_resp := self.immunization_repo.get_immunization_by_id(imms_id, imms_vax_type_perms)):
8686
return None
8787

88-
# Remove fields rom the imms resource which are not to be returned for read
89-
imms_filtered_for_read = Filter.read(imms_resp.get("Resource", {}))
88+
# Returns the Immunisation full resource with no obfuscation
89+
resource = imms_resp.get("Resource", {})
90+
imms_filtered_for_read = Filter.read(resource) if resource else {}
9091

91-
# Handle s-flag filtering, where applicable
92-
if not (nhs_number := obtain_field_value(imms_filtered_for_read, FieldNames.patient_identifier_value)):
93-
imms_filtered_for_read_and_s_flag = imms_filtered_for_read
94-
else:
95-
if patient := self.pds_service.get_patient_details(nhs_number):
96-
imms_filtered_for_read_and_s_flag = handle_s_flag(imms_filtered_for_read, patient)
97-
else:
98-
raise UnhandledResponseError("unable to validate NHS number with downstream service")
9992

10093
return {
10194
"Version": imms_resp.get("Version", ""),
102-
"Resource": Immunization.parse_obj(imms_filtered_for_read_and_s_flag),
95+
"Resource": Immunization.parse_obj(imms_filtered_for_read),
10396
}
10497

10598
def get_immunization_by_id_all(self, imms_id: str, imms: dict) -> Optional[dict]:
10699
"""
107-
Get an Immunization by its ID. Return None if not found. If the patient doesn't have an NHS number,
100+
Get an Immunization by its ID. Return None if it is not found. If the patient doesn't have an NHS number,
108101
return the Immunization without calling PDS or checking S flag.
109102
"""
110103
imms["id"] = imms_id
@@ -323,25 +316,22 @@ def search_immunizations(
323316
if self.is_valid_date_from(r, date_from) and self.is_valid_date_to(r, date_to)
324317
]
325318

326-
# Check whether the Superseded NHS number present in PDS
327-
if pds_patient := self.pds_service.get_patient_details(nhs_number):
328-
if pds_patient["identifier"][0]["value"] != nhs_number:
329-
return create_diagnostics()
330-
331319
# Create the patient URN for the fullUrl field.
332320
# NOTE: This UUID is assigned when a SEARCH request is received and used only for referencing the patient
333321
# resource from immunisation resources within the bundle. The fullUrl value we are using is a urn (hence the
334322
# FHIR key name of "fullUrl" is somewhat misleading) which cannot be used to locate any externally stored
335323
# patient resource. This is as agreed with VDS team for backwards compatibility with Immunisation History API.
336324
patient_full_url = f"urn:uuid:{str(uuid4())}"
337-
338-
# Filter and amend the immunization resources for the SEARCH response
339-
resources_filtered_for_search = [Filter.search(imms, patient_full_url, pds_patient) for imms in resources]
325+
326+
imms_patient_record = get_contained_patient(resources[-1]) if resources else None
327+
328+
# Filter and amend the immunization resources for the SEARCH response
329+
resources_filtered_for_search = [Filter.search(imms, patient_full_url, []) for imms in resources]
340330

341331
# Add bundle entries for each of the immunization resources
342332
entries = [
343333
BundleEntry(
344-
resource=Immunization.parse_obj(handle_s_flag(imms, pds_patient)),
334+
resource=Immunization.parse_obj(imms),
345335
search=BundleEntrySearch(mode="match"),
346336
fullUrl=f"https://api.service.nhs.uk/immunisation-fhir-api/Immunization/{imms['id']}",
347337
)
@@ -352,7 +342,7 @@ def search_immunizations(
352342
if len(resources) > 0:
353343
entries.append(
354344
BundleEntry(
355-
resource=self.process_patient_for_bundle(pds_patient),
345+
resource=self.process_patient_for_bundle(imms_patient_record),
356346
search=BundleEntrySearch(mode="include"),
357347
fullUrl=patient_full_url,
358348
)

backend/src/filter.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Functions for filtering a FHIR Immunization Resource"""
22

3-
from models.utils.generic_utils import is_actor_referencing_contained_resource, get_contained_practitioner
3+
from models.utils.generic_utils import is_actor_referencing_contained_resource, get_contained_practitioner, get_contained_patient
44
from constants import Urls
55

66

@@ -47,7 +47,7 @@ def replace_address_postal_codes(imms: dict) -> dict:
4747
# Remove all other keys in the address dictionary
4848
keys_to_remove = [key for key in address.keys() if key != "postalCode"]
4949
for key in keys_to_remove:
50-
del address[key]
50+
del address[key]
5151

5252
return imms
5353

@@ -59,23 +59,23 @@ def replace_organization_values(imms: dict) -> dict:
5959
"""
6060
for performer in imms.get("performer", [{}]):
6161
if performer.get("actor", {}).get("type") == "Organization":
62-
62+
6363
# Obfuscate or set the identifier value and system.
6464
identifier = performer["actor"].get("identifier", {})
6565
if identifier.get("value") is not None:
6666
identifier["value"] = "N2N9I"
6767
identifier["system"] = Urls.ods_organization_code
6868
if identifier.get("system") is not None:
6969
identifier["system"] = Urls.ods_organization_code
70-
70+
7171
# Ensure only 'system' and 'value' remain in identifier
7272
keys = {"system", "value"}
7373
keys_to_remove = [key for key in identifier.keys() if key not in keys]
7474
for key in keys_to_remove:
7575
del identifier[key]
76-
76+
7777
# Remove all other fields except 'identifier' in actor
78-
keys_to_remove = [key for key in performer["actor"].keys() if key not in ("identifier","type")]
78+
keys_to_remove = [key for key in performer["actor"].keys() if key not in ("identifier", "type")]
7979
for key in keys_to_remove:
8080
del performer["actor"][key]
8181

@@ -94,7 +94,6 @@ def add_use_to_identifier(imms: dict) -> dict:
9494

9595
class Filter:
9696
"""Functions for filtering a FHIR Immunization Resource"""
97-
9897
@staticmethod
9998
def read(imms: dict) -> dict:
10099
"""Apply filtering for READ request"""
@@ -105,12 +104,10 @@ def read(imms: dict) -> dict:
105104
def search(imms: dict, patient_full_url: str, bundle_patient: dict = None) -> dict:
106105
"""Apply filtering for an individual FHIR Immunization Resource as part of SEARCH request"""
107106
imms = remove_reference_to_contained_practitioner(imms)
107+
imms_patient_data = get_contained_patient(imms)
108108
imms.pop("contained")
109-
imms["patient"] = create_reference_to_patient_resource(patient_full_url, bundle_patient)
109+
imms["patient"] = create_reference_to_patient_resource(patient_full_url, imms_patient_data)
110110
imms = add_use_to_identifier(imms)
111-
# Location identifier system and value are to be overwritten
112-
# (for backwards compatibility with Immunisation History API, as agreed with VDS team)
113-
imms["location"] = {"identifier": {"system": "urn:iso:std:iso:3166", "value": "GB"}}
114111
return imms
115112

116113
@staticmethod

backend/src/get_imms_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def get_imms_handler(event, context):
1616

1717
def get_immunization_by_id(event, controller: FhirController):
1818
try:
19-
return controller.get_immunization_by_id(event)
19+
return controller.get_immunization_by_id(event)
2020
except Exception: # pylint: disable = broad-exception-caught
2121
exp_error = create_operation_outcome(
2222
resource_id=str(uuid.uuid4()),

backend/tests/sample_data/completed_covid19_immunization_event_filtered_for_search_using_bundle_patient_resource.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@
4646
"display": "AstraZeneca Ltd"
4747
},
4848
"location": {
49+
"type": "Location",
4950
"identifier": {
50-
"value": "GB",
51-
"system": "urn:iso:std:iso:3166"
51+
"value": "X99999",
52+
"system": "https://fhir.nhs.uk/Id/ods-organization-code"
5253
}
5354
},
5455
"lotNumber": "4120Z001",

0 commit comments

Comments
 (0)