Skip to content

Commit dfbb70a

Browse files
[PRMP-990] Add S3 version id support
1 parent 7f097db commit dfbb70a

File tree

11 files changed

+114
-74
lines changed

11 files changed

+114
-74
lines changed

.github/workflows/full-deploy-to-pre-prod.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
steps:
2828
- name: Display client passed variables
2929
run: |
30-
echo Build Branch: main
30+
echo Build Branch: ${{ github.event.inputs.branch_or_tag }}
3131
echo Sandbox: pre-prod
3232
echo Environment: pre-prod
3333
echo Cypress Base Url: ${{ vars.CYPRESS_BASE_URL }}
@@ -37,27 +37,27 @@ jobs:
3737
uses: ./.github/workflows/base-lambdas-reusable-test.yml
3838
with:
3939
python_version: "3.11"
40-
build_branch: main
40+
build_branch: ${{ github.event.inputs.branch_or_tag }}
4141

4242
react_testing_job:
4343
name: Run UI Unit Tests
4444
uses: ./.github/workflows/base-vitest-test.yml
4545
with:
46-
build_branch: main
46+
build_branch: ${{ github.event.inputs.branch_or_tag }}
4747

4848
cypress_build_job:
4949
name: Build UI version for E2E Tests
5050
uses: ./.github/workflows/base-cypress-build.yml
5151
with:
52-
build_branch: main
52+
build_branch: ${{ github.event.inputs.branch_or_tag }}
5353

5454
cypress_test_job:
5555
name: Run Cypress E2E Tests
5656
needs: [cypress_build_job]
5757
uses: ./.github/workflows/base-cypress-test-all-env.yml
5858
with:
5959
cypress_base_url: ${{ vars.CYPRESS_BASE_URL }}
60-
build_branch: main
60+
build_branch: ${{ github.event.inputs.branch_or_tag }}
6161

6262
tag_and_release:
6363
needs: ["lambda_test_job", "react_testing_job", "cypress_test_job"]

lambdas/models/document_reference.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ class DocumentReference(BaseModel):
137137
raw_request: str | None = None
138138
s3_bucket_name: str = Field(exclude=True, default=None)
139139
s3_file_key: str = Field(default=None)
140-
s3_version_id: Optional[str] = Field(default=None, exclude=True)
140+
s3_version_id: Optional[str] = Field(default=None, alias="S3VersionID")
141141
s3_upload_key: str = Field(default=None, exclude=True)
142142
status: Literal["current", "superseded", "entered-in-error"] = Field(
143143
default="current"

lambdas/repositories/bulk_upload/bulk_upload_s3_repository.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ def check_virus_result(
6868
f"Verified that all documents for patient {staging_metadata.nhs_number} are clean."
6969
)
7070

71-
def copy_to_lg_bucket(self, source_file_key: str, dest_file_key: str):
72-
self.s3_repository.copy_across_bucket(
71+
def copy_to_lg_bucket(self, source_file_key: str, dest_file_key: str):
72+
result = self.s3_repository.copy_across_bucket(
7373
source_bucket=self.staging_bucket_name,
7474
source_file_key=source_file_key,
7575
dest_bucket=self.lg_bucket_name,
@@ -78,6 +78,8 @@ def copy_to_lg_bucket(self, source_file_key: str, dest_file_key: str):
7878
self.source_bucket_files_in_transaction.append(source_file_key)
7979
self.dest_bucket_files_in_transaction.append(dest_file_key)
8080

81+
return result
82+
8183
def remove_ingested_file_from_source_bucket(self):
8284
for source_file_key in self.source_bucket_files_in_transaction:
8385
self.s3_repository.delete_object(

lambdas/services/bulk_upload_service.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,11 +363,13 @@ def create_lg_records_and_copy_files(
363363
source_file_key = self.file_path_cache[file_metadata.file_path]
364364
dest_file_key = document_reference.s3_file_key
365365

366-
self.bulk_upload_s3_repository.copy_to_lg_bucket(
366+
copy_result = self.bulk_upload_s3_repository.copy_to_lg_bucket(
367367
source_file_key=source_file_key, dest_file_key=dest_file_key
368368
)
369369
s3_bucket_name = self.bulk_upload_s3_repository.lg_bucket_name
370370

371+
document_reference.s3_version_id = copy_result.get("VersionId")
372+
371373
document_reference.file_size = (
372374
self.bulk_upload_s3_repository.s3_repository.get_file_size(
373375
s3_bucket_name=s3_bucket_name, object_key=dest_file_key

lambdas/services/pdf_stitching_service.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def __init__(self):
4545
self.document_service = DocumentService()
4646
self.sqs_service = SQSService()
4747
self.multipart_references: list[DocumentReference] = []
48-
self.stitched_reference: DocumentReference = None
48+
self.stitched_reference: DocumentReference | None = None
4949

5050
def retrieve_multipart_references(
5151
self, nhs_number: str, doc_type: SupportedDocumentTypes
@@ -101,6 +101,7 @@ def process_message(self, stitching_message: PdfStitchingSqsMessage):
101101
stitch_file_size=sys.getsizeof(stitching_data_stream),
102102
)
103103
self.upload_stitched_file(stitching_data_stream=stitching_data_stream)
104+
self.update_stitched_reference_with_version_id()
104105
self.migrate_multipart_references()
105106
self.write_stitching_reference()
106107
self.publish_nrl_message(
@@ -129,6 +130,11 @@ def create_stitched_reference(
129130
deep=True,
130131
)
131132

133+
def update_stitched_reference_with_version_id(self):
134+
self.stitched_reference.s3_version_id = self.s3_service.get_head_object(
135+
self.target_bucket, self.stitched_reference.s3_file_key
136+
).get("VersionId")
137+
132138
def process_stitching(self, s3_object_keys: list[str]) -> BytesIO:
133139
pdf_writer = PdfWriter()
134140

@@ -370,6 +376,6 @@ def process_manual_trigger(self, ods_code: str, queue_url):
370376
except (InvalidMessageException, Exception) as e:
371377
logger.error(f"Error sending batch to SQS: {str(e)}")
372378
# 1 batch is 10 messages
373-
# we can send up to 300 messages a second
374-
# a 0.1s delay means 10 batches, so 100 messages
379+
# we can send up to 300 messages a second.
380+
# a 0.1 s delay means 10 batches, so 100 messages
375381
time.sleep(0.1)

lambdas/services/upload_document_reference_service.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ def _finalize_and_supersede_with_transaction(self, new_document: DocumentReferen
245245
"file_size",
246246
"uploaded",
247247
"uploading",
248+
"s3_version_id",
248249
},
249250
)
250251

lambdas/tests/e2e/helpers/data_helper.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def create_metadata(self, document_details):
7777
"Uploaded": True,
7878
"Uploading": False,
7979
"Version": "1",
80+
"S3VersionID": "some-version-id",
8081
"VirusScannerResult": "Clean",
8182
}
8283
self.dynamo_service.create_item(self.dynamo_table, dynamo_item)

lambdas/tests/unit/helpers/data/s3_responses.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,27 @@
8282
],
8383
},
8484
]
85+
86+
MOCK_COPY_OBJECT_RESPONSE = {
87+
"CopyObjectResult": {
88+
"ETag": '"d41d8cd98f00b204e9800998ecf8427e"',
89+
"LastModified": "2024-06-01T12:34:56.000Z"
90+
},
91+
"Expiration": "expiry-date=\"2024-12-31T23:59:59Z\", rule-id=\"rule1\"",
92+
"CopySourceVersionId": "source-version-id",
93+
"VersionId": "new-version-id",
94+
"ResponseMetadata": {
95+
"RequestId": "EXAMPLE123456789",
96+
"HostId": "EXAMPLEHostId",
97+
"HTTPStatusCode": 200,
98+
"HTTPHeaders": {
99+
"x-amz-id-2": "EXAMPLEid2",
100+
"x-amz-request-id": "EXAMPLErequestid",
101+
"date": "Mon, 01 Jun 2024 12:34:56 GMT",
102+
"etag": '"d41d8cd98f00b204e9800998ecf8427e"',
103+
"content-length": "123",
104+
"server": "AmazonS3"
105+
},
106+
"RetryAttempts": 0
107+
}
108+
}

lambdas/tests/unit/services/test_bulk_upload_service.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
PDS_PATIENT_DECEASED_INFORMAL,
3939
PDS_PATIENT_RESTRICTED,
4040
)
41+
from tests.unit.helpers.data.s3_responses import MOCK_COPY_OBJECT_RESPONSE
4142
from tests.unit.utils.test_unicode_utils import (
4243
NAME_WITH_ACCENT_NFC_FORM,
4344
NAME_WITH_ACCENT_NFD_FORM,
@@ -417,7 +418,7 @@ def test_handle_sqs_message_calls_report_upload_failure_when_lg_file_name_invali
417418
TEST_STAGING_METADATA_WITH_INVALID_FILENAME,
418419
UploadStatus.FAILED,
419420
str(mocked_error),
420-
"Y12345"
421+
"Y12345",
421422
)
422423
repo_under_test.sqs_repository.send_message_to_pdf_stitching_queue.assert_not_called()
423424

@@ -712,8 +713,8 @@ def test_handle_sqs_message_rollback_transaction_when_validation_pass_but_file_t
712713

713714
# simulate a client error occur when copying the 3rd file
714715
repo_under_test.bulk_upload_s3_repository.copy_to_lg_bucket.side_effect = [
715-
None,
716-
None,
716+
MOCK_COPY_OBJECT_RESPONSE,
717+
MOCK_COPY_OBJECT_RESPONSE,
717718
mock_client_error,
718719
]
719720

@@ -847,6 +848,9 @@ def test_create_lg_records_and_copy_files(set_env, mocker, mock_uuid, repo_under
847848
repo_under_test.convert_to_document_reference = mocker.MagicMock(
848849
return_value=test_document_reference
849850
)
851+
repo_under_test.bulk_upload_s3_repository.copy_to_lg_bucket = mocker.MagicMock(
852+
return_value=MOCK_COPY_OBJECT_RESPONSE
853+
)
850854
TEST_STAGING_METADATA.retries = 0
851855
repo_under_test.resolve_source_file_path(TEST_STAGING_METADATA)
852856

@@ -864,6 +868,10 @@ def test_create_lg_records_and_copy_files(set_env, mocker, mock_uuid, repo_under
864868
dest_file_key=expected_dest_file_key,
865869
)
866870
assert test_document_reference.uploaded.__eq__(True)
871+
assert (
872+
test_document_reference.s3_version_id
873+
== MOCK_COPY_OBJECT_RESPONSE["VersionId"]
874+
)
867875
assert repo_under_test.bulk_upload_s3_repository.copy_to_lg_bucket.call_count == 3
868876
repo_under_test.dynamo_repository.create_record_in_lg_dynamo_table.assert_any_call(
869877
test_document_reference

lambdas/tests/unit/services/test_pdf_stitching_service.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ def mock_upload_stitched_file(mocker, mock_service):
7171
return mocker.patch.object(mock_service, "upload_stitched_file")
7272

7373

74+
@pytest.fixture
75+
def mock_update_stitched_reference_with_version_id(mocker, mock_service):
76+
return mocker.patch.object(
77+
mock_service, "update_stitched_reference_with_version_id"
78+
)
79+
80+
7481
@pytest.fixture
7582
def mock_migrate_multipart_references(mocker, mock_service):
7683
return mocker.patch.object(mock_service, "migrate_multipart_references")
@@ -164,6 +171,7 @@ def test_process_message(
164171
mock_sort_multipart_object_keys,
165172
mock_process_stitching,
166173
mock_upload_stitched_file,
174+
mock_update_stitched_reference_with_version_id,
167175
mock_migrate_multipart_references,
168176
mock_write_stitching_reference,
169177
mock_publish_nrl_message,
@@ -193,6 +201,7 @@ def set_stitched_reference(document_reference, stitch_file_size, *args, **kwargs
193201
mock_sort_multipart_object_keys.assert_called_once_with()
194202
mock_process_stitching.assert_called_once_with(s3_object_keys=test_sorted_keys)
195203
mock_upload_stitched_file.assert_called_once_with(stitching_data_stream=test_stream)
204+
mock_update_stitched_reference_with_version_id.assert_called_once()
196205
mock_migrate_multipart_references.assert_called_once()
197206
mock_write_stitching_reference.assert_called_once()
198207
mock_publish_nrl_message.assert_called_once()
@@ -671,6 +680,24 @@ def test_rollback_reference_migration_handles_exception(mock_service):
671680
mock_service.rollback_reference_migration()
672681

673682

683+
def test_update_stitched_reference_with_version_id(mock_service):
684+
test_version_id = "test-version-id-12345"
685+
mock_service.stitched_reference = TEST_1_OF_1_DOCUMENT_REFERENCE
686+
687+
mock_service.s3_service.get_head_object.return_value = {
688+
"VersionId": test_version_id,
689+
"ContentType": "application/pdf",
690+
"ContentLength": 1234,
691+
}
692+
693+
mock_service.update_stitched_reference_with_version_id()
694+
695+
mock_service.s3_service.get_head_object.assert_called_once_with(
696+
MOCK_LG_BUCKET, TEST_1_OF_1_DOCUMENT_REFERENCE.s3_file_key
697+
)
698+
assert mock_service.stitched_reference.s3_version_id == test_version_id
699+
700+
674701
def test_process_manual_trigger_calls_process_message_for_each_nhs_number(
675702
mocker, mock_service
676703
):

0 commit comments

Comments
 (0)