Skip to content

Commit 88f201b

Browse files
committed
NRL-1066 Merge remote-tracking branch 'origin/develop' into feature/imaging
2 parents ee231e3 + fa1baef commit 88f201b

File tree

100 files changed

+7671
-1130
lines changed

Some content is hidden

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

100 files changed

+7671
-1130
lines changed

.github/workflows/daily-build.yml

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
name: Build NRL Project on Environment
2+
run-name: Build NRL Project on ${{ inputs.environment || 'dev' }}
3+
permissions:
4+
id-token: write
5+
contents: read
6+
actions: write
7+
8+
on:
9+
schedule:
10+
- cron: "0 1 * * *"
11+
workflow_dispatch:
12+
inputs:
13+
environment:
14+
type: environment
15+
description: "The environment to deploy changes to"
16+
default: "dev"
17+
required: true
18+
19+
jobs:
20+
build:
21+
name: Build - develop
22+
runs-on: [self-hosted, ci]
23+
24+
steps:
25+
- name: Git clone - develop
26+
uses: actions/checkout@v4
27+
with:
28+
ref: develop
29+
30+
- name: Setup asdf cache
31+
uses: actions/cache@v4
32+
with:
33+
path: ~/.asdf
34+
key: ${{ runner.os }}-asdf-${{ hashFiles('**/.tool-versions') }}
35+
restore-keys: |
36+
${{ runner.os }}-asdf-
37+
38+
- name: Install asdf
39+
uses: asdf-vm/actions/[email protected]
40+
with:
41+
asdf_branch: v0.13.1
42+
43+
- name: Install zip
44+
run: sudo apt-get install zip
45+
46+
- name: Setup Python environment
47+
run: |
48+
poetry install --no-root
49+
source $(poetry env info --path)/bin/activate
50+
51+
- name: Run Linting
52+
run: make lint
53+
54+
- name: Run Unit Tests
55+
run: make test
56+
57+
- name: Build Project
58+
run: make build
59+
60+
- name: Configure Management Credentials
61+
uses: aws-actions/configure-aws-credentials@v4
62+
with:
63+
aws-region: eu-west-2
64+
role-to-assume: ${{ secrets.MGMT_ROLE_ARN }}
65+
role-session-name: github-actions-ci-${{ inputs.environment || 'dev' }}-${{ github.run_id }}
66+
67+
- name: Add S3 Permissions to Lambda
68+
run: |
69+
account=$(echo '${{ inputs.environment || 'dev' }}' | cut -d '-' -f1)
70+
inactive_stack=$(poetry run python ./scripts/get_env_config.py inactive-stack ${{ inputs.environment || 'dev' }})
71+
make get-s3-perms ENV=${account} TF_WORKSPACE_NAME=${inactive_stack}
72+
73+
- name: Save Build Artifacts
74+
uses: actions/upload-artifact@v4
75+
with:
76+
name: build-artifacts
77+
path: |
78+
dist/*.zip
79+
!dist/nrlf_permissions.zip
80+
81+
- name: Save NRLF Permissions cache
82+
uses: actions/cache/save@v4
83+
with:
84+
key: ${{ github.run_id }}-nrlf-permissions
85+
path: dist/nrlf_permissions.zip

.github/workflows/pr-env-deploy.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,53 @@ jobs:
264264
- name: Run Integration Tests
265265
run: make test-features-integration TF_WORKSPACE_NAME=${{ needs.set-environment-id.outputs.environment_id }}
266266

267+
smoke-test:
268+
name: Run Smoke Tests
269+
needs: [set-environment-id, integration-test]
270+
environment: pull-request
271+
runs-on: [self-hosted, ci]
272+
steps:
273+
- name: Git Clone - ${{ github.event.pull_request.head.ref }}
274+
uses: actions/checkout@v4
275+
with:
276+
ref: ${{ github.event.pull_request.head.ref }}
277+
278+
- name: Setup asdf cache
279+
uses: actions/cache@v4
280+
with:
281+
path: ~/.asdf
282+
key: ${{ runner.os }}-asdf-${{ hashFiles('**/.tool-versions') }}
283+
restore-keys: |
284+
${{ runner.os }}-asdf-
285+
286+
- name: Install asdf and tools
287+
uses: asdf-vm/actions/[email protected]
288+
with:
289+
asdf_branch: v0.13.1
290+
291+
- name: Setup Python environment
292+
run: |
293+
poetry install --no-root
294+
source $(poetry env info --path)/bin/activate
295+
296+
- name: Configure Management Credentials
297+
uses: aws-actions/configure-aws-credentials@v4
298+
with:
299+
aws-region: eu-west-2
300+
role-to-assume: ${{ secrets.MGMT_ROLE_ARN }}
301+
role-session-name: github-actions-ci-${{ needs.set-environment-id.outputs.environment_id }}
302+
303+
- name: Terraform Init
304+
run: |
305+
terraform -chdir=terraform/infrastructure init
306+
terraform -chdir=terraform/infrastructure workspace new ${{ needs.set-environment-id.outputs.environment_id }} || \
307+
terraform -chdir=terraform/infrastructure workspace select ${{ needs.set-environment-id.outputs.environment_id }}
308+
309+
- name: Smoke Test
310+
run: |
311+
make ENV=dev truststore-pull-client
312+
make ENV=dev TF_WORKSPACE_NAME=${{ needs.set-environment-id.outputs.environment_id }} test-smoke-internal
313+
267314
performance-test:
268315
name: Run Performance Tests
269316
needs: [set-environment-id, integration-test]

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Crown Copyright
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

api/consumer/searchDocumentReference/search_document_reference.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from nrlf.core.logger import LogReference, logger
1010
from nrlf.core.model import ConnectionMetadata, ConsumerRequestParams
1111
from nrlf.core.response import Response, SpineErrorResponse
12-
from nrlf.core.validators import validate_category, validate_type_system
12+
from nrlf.core.validators import validate_category, validate_type
1313

1414

1515
@request_handler(params=ConsumerRequestParams)
@@ -46,19 +46,19 @@ def handler(
4646
base_url = f"https://{config.ENVIRONMENT}.api.service.nhs.uk/"
4747
self_link = f"{base_url}record-locator/consumer/FHIR/R4/DocumentReference?subject:identifier=https://fhir.nhs.uk/Id/nhs-number|{params.nhs_number}"
4848

49-
# TODO - Add checks for the type code as well as system
50-
if not validate_type_system(params.type, metadata.pointer_types):
49+
if not validate_type(params.type, metadata.pointer_types):
5150
logger.log(
5251
LogReference.CONSEARCH002,
5352
type=params.type,
5453
pointer_types=metadata.pointer_types,
5554
)
5655
return SpineErrorResponse.INVALID_CODE_SYSTEM(
57-
diagnostics="Invalid query parameter (The provided type system does not match the allowed types for this organisation)",
56+
diagnostics="Invalid query parameter (The provided type does not match the allowed types for this organisation)",
5857
expression="type",
5958
)
6059

61-
if not validate_category(params.category):
60+
categories = params.category.root.split(",") if params.category else []
61+
if not validate_category(categories):
6262
logger.log(
6363
LogReference.CONSEARCH002b,
6464
category=params.category,
@@ -102,7 +102,7 @@ def handler(
102102
nhs_number=params.nhs_number,
103103
custodian=custodian_id,
104104
pointer_types=pointer_types,
105-
categories=[params.category.root] if params.category else [],
105+
categories=categories,
106106
):
107107
try:
108108
document_reference = DocumentReference.model_validate_json(result.document)

api/consumer/searchDocumentReference/tests/test_search_document_reference_consumer.py

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
from moto import mock_aws
44

55
from api.consumer.searchDocumentReference.search_document_reference import handler
6+
from nrlf.core.constants import (
7+
CATEGORY_ATTRIBUTES,
8+
TYPE_ATTRIBUTES,
9+
Categories,
10+
PointerTypes,
11+
)
612
from nrlf.core.dynamodb.repository import DocumentPointer, DocumentPointerRepository
713
from nrlf.tests.data import load_document_reference
814
from nrlf.tests.dynamodb import mock_repository
@@ -53,6 +59,56 @@ def test_search_document_reference_happy_path(repository: DocumentPointerReposit
5359
}
5460

5561

62+
@mock_aws
63+
@mock_repository
64+
def test_search_document_reference_accession_number_in_pointer(
65+
repository: DocumentPointerRepository,
66+
):
67+
doc_ref = load_document_reference("Y05868-736253002-Valid")
68+
doc_ref.identifier = [
69+
{"type": {"text": "Accession-Number"}, "value": "Y05868.123456789"}
70+
]
71+
doc_pointer = DocumentPointer.from_document_reference(doc_ref)
72+
repository.create(doc_pointer)
73+
74+
event = create_test_api_gateway_event(
75+
headers=create_headers(),
76+
query_string_parameters={
77+
"subject:identifier": "https://fhir.nhs.uk/Id/nhs-number|6700028191",
78+
},
79+
)
80+
81+
result = handler(event, create_mock_context())
82+
body = result.pop("body")
83+
84+
assert result == {
85+
"statusCode": "200",
86+
"headers": default_response_headers(),
87+
"isBase64Encoded": False,
88+
}
89+
90+
parsed_body = json.loads(body)
91+
assert parsed_body == {
92+
"resourceType": "Bundle",
93+
"type": "searchset",
94+
"total": 1,
95+
"link": [
96+
{
97+
"relation": "self",
98+
"url": "https://pytest.api.service.nhs.uk/record-locator/consumer/FHIR/R4/DocumentReference?subject:identifier=https://fhir.nhs.uk/Id/nhs-number|6700028191",
99+
}
100+
],
101+
"entry": [{"resource": doc_ref.model_dump(exclude_none=True)}],
102+
}
103+
104+
created_doc_pointer = repository.get_by_id("Y05868-99999-99999-999999")
105+
106+
assert created_doc_pointer is not None
107+
assert json.loads(created_doc_pointer.document)["identifier"] == [
108+
{"type": {"text": "Accession-Number"}, "value": "Y05868.123456789"}
109+
]
110+
111+
56112
@mock_aws
57113
@mock_repository
58114
def test_search_document_reference_happy_path_with_custodian(
@@ -144,6 +200,19 @@ def test_search_document_reference_happy_path_with_category(
144200
doc_pointer = DocumentPointer.from_document_reference(doc_ref)
145201
repository.create(doc_pointer)
146202

203+
# Second pointer different category
204+
doc_ref2 = load_document_reference("Y05868-736253002-Valid")
205+
doc_ref2.id = "Y05868-736253002-Valid2"
206+
doc_ref2.type.coding[0].code = PointerTypes.NEWS2_CHART.coding_value()
207+
doc_ref2.type.coding[0].display = TYPE_ATTRIBUTES.get(
208+
PointerTypes.NEWS2_CHART.value
209+
).get("display")
210+
doc_ref2.category[0].coding[0].code = Categories.OBSERVATIONS.coding_value()
211+
doc_ref2.category[0].coding[0].display = CATEGORY_ATTRIBUTES.get(
212+
Categories.OBSERVATIONS.value
213+
).get("display")
214+
repository.create(DocumentPointer.from_document_reference(doc_ref2))
215+
147216
event = create_test_api_gateway_event(
148217
headers=create_headers(),
149218
query_string_parameters={
@@ -160,7 +229,6 @@ def test_search_document_reference_happy_path_with_category(
160229
"headers": default_response_headers(),
161230
"isBase64Encoded": False,
162231
}
163-
164232
parsed_body = json.loads(body)
165233
assert parsed_body == {
166234
"resourceType": "Bundle",
@@ -176,6 +244,63 @@ def test_search_document_reference_happy_path_with_category(
176244
}
177245

178246

247+
@mock_aws
248+
@mock_repository
249+
def test_search_document_reference_happy_path_with_multiple_categories(
250+
repository: DocumentPointerRepository,
251+
):
252+
doc_ref = load_document_reference("Y05868-736253002-Valid")
253+
doc_pointer = DocumentPointer.from_document_reference(doc_ref)
254+
repository.create(doc_pointer)
255+
256+
# Second pointer different category
257+
doc_ref2 = load_document_reference("Y05868-736253002-Valid")
258+
doc_ref2.id = "Y05868-736253002-Valid2"
259+
doc_ref2.type.coding[0].code = PointerTypes.NEWS2_CHART.coding_value()
260+
doc_ref2.type.coding[0].display = TYPE_ATTRIBUTES.get(
261+
PointerTypes.NEWS2_CHART.value
262+
).get("display")
263+
doc_ref2.category[0].coding[0].code = Categories.OBSERVATIONS.coding_value()
264+
doc_ref2.category[0].coding[0].display = CATEGORY_ATTRIBUTES.get(
265+
Categories.OBSERVATIONS.value
266+
).get("display")
267+
repository.create(DocumentPointer.from_document_reference(doc_ref2))
268+
269+
event = create_test_api_gateway_event(
270+
headers=create_headers(),
271+
query_string_parameters={
272+
"subject:identifier": "https://fhir.nhs.uk/Id/nhs-number|6700028191",
273+
"category": "http://snomed.info/sct|734163000,http://snomed.info/sct|1102421000000108",
274+
},
275+
)
276+
277+
result = handler(event, create_mock_context())
278+
body = result.pop("body")
279+
280+
assert result == {
281+
"statusCode": "200",
282+
"headers": default_response_headers(),
283+
"isBase64Encoded": False,
284+
}
285+
286+
parsed_body = json.loads(body)
287+
assert parsed_body == {
288+
"resourceType": "Bundle",
289+
"type": "searchset",
290+
"link": [
291+
{
292+
"relation": "self",
293+
"url": "https://pytest.api.service.nhs.uk/record-locator/consumer/FHIR/R4/DocumentReference?subject:identifier=https://fhir.nhs.uk/Id/nhs-number|6700028191&category=http://snomed.info/sct|734163000,http://snomed.info/sct|1102421000000108",
294+
}
295+
],
296+
"total": 2,
297+
"entry": [
298+
{"resource": doc_ref2.model_dump(exclude_none=True)},
299+
{"resource": doc_ref.model_dump(exclude_none=True)},
300+
],
301+
}
302+
303+
179304
@mock_aws
180305
@mock_repository
181306
def test_search_document_reference_happy_path_with_nicip_type(
@@ -376,7 +501,7 @@ def test_search_document_reference_invalid_type(repository: DocumentPointerRepos
376501
}
377502
]
378503
},
379-
"diagnostics": "Invalid query parameter (The provided type system does not match the allowed types for this organisation)",
504+
"diagnostics": "Invalid query parameter (The provided type does not match the allowed types for this organisation)",
380505
"expression": ["type"],
381506
}
382507
],

0 commit comments

Comments
 (0)