Skip to content

Commit

Permalink
Merge branch 'main' into PRMP-1257
Browse files Browse the repository at this point in the history
  • Loading branch information
steph-torres-nhs authored Jan 13, 2025
2 parents 90758bc + aaec935 commit 9ab8d3a
Show file tree
Hide file tree
Showing 39 changed files with 646 additions and 104 deletions.
1 change: 1 addition & 0 deletions .github/workflows/base-cypress-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
with:
repository: 'nhsconnect/national-document-repository'
ref: ${{ github.event.inputs.build_branch }}

- name: Cypress install
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/base-cypress-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
with:
repository: 'nhsconnect/national-document-repository'
ref: ${{ inputs.build_branch }}

- name: Download the build folder
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/base-lambdas-reusable-deploy-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,20 @@ jobs:
secrets:
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}

deploy_delete_document_object_handler:
name: Deploy delete_document_object_handler
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: delete_document_object_handler
lambda_aws_name: DeleteDocumentObjectS3
lambda_layer_names: 'core_lambda_layer'
secrets:
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}

deploy_document_manifest_job_lambda:
name: Deploy document_manifest_job_lambda
uses: ./.github/workflows/base-lambdas-reusable-deploy.yml
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/lambdas-dev-to-main-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ on:
- main
paths:
- 'lambdas/**'
workflow_call:
secrets:
AWS_ASSUME_ROLE:
required: true

permissions:
pull-requests: write
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/ui-dev-to-main-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ on:
- main
paths:
- 'app/**'
workflow_call:
secrets:
AWS_ASSUME_ROLE:
required: true

permissions:
pull-requests: write
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import authPayload from '../../../fixtures/requests/auth/GET_TokenRequest_GP_ADMIN.json';
import { Roles } from '../../../support/roles';
import dbItem from '../../../fixtures/dynamo-db-items/active-patient.json';
import searchPatientPayload from '../../../fixtures/requests/GET_SearchPatientLGUpload.json';

describe('Authentication & Authorisation', () => {
const baseUrl = Cypress.config('baseUrl');
Expand Down Expand Up @@ -86,4 +88,42 @@ describe('Authentication & Authorisation', () => {
},
);
});

context('Page refresh redirection ', () => {
const workspace = Cypress.env('WORKSPACE');
dbItem.FileLocation = dbItem.FileLocation.replace('{env}', workspace);

const lloydGeorgeRecordUrl = '/patient/lloyd-george-record';
const verifyUrl = '/patient/verify';
const patientSearchUrl = '/patient/search';

it(
'Refreshing the browser after searching for a patient will return the user to the patient search page',
{ tags: 'regression ', defaultCommandTimeout: 20000 },
() => {
cy.login(Roles.GP_ADMIN);
cy.visit(patientSearchUrl);

cy.intercept('GET', '/SearchPatient*', {
statusCode: 200,
body: searchPatientPayload,
}).as('search');
cy.intercept('POST', '/LloydGeorgeStitch*', {
statusCode: 404,
}).as('stitch');

cy.getByTestId('nhs-number-input').type(searchPatientPayload.nhsNumber);
cy.getByTestId('search-submit-btn').click();

cy.url().should('contain', verifyUrl);
cy.get('#verify-submit').click();

cy.url().should('contain', lloydGeorgeRecordUrl);

cy.reload();

cy.url().should('contain', patientSearchUrl);
},
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ function LloydGeorgeDownloadStage({
const onPageLoad = async () => {
progressTimer.stop();
window.clearInterval(intervalTimer);
if (!nhsNumber) {
navigate(routes.SEARCH_PATIENT);
return;
}
try {
const preSignedUrl = await getPresignedUrlForZip({
baseUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ function LloydGeorgeSelectDownloadStage({
useEffect(() => {
const onPageLoad = async () => {
setSubmissionSearchState(SEARCH_AND_DOWNLOAD_STATE.SEARCH_PENDING);
if (!nhsNumber) {
navigate(routes.SEARCH_PATIENT);
return;
}
try {
// This check is in place for when we navigate directly to a full download,
// in that instance we do not need to get a list of selectable files as we will download all files
Expand Down
4 changes: 2 additions & 2 deletions app/src/router/guards/patientGuard/PatientGuard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ describe('AuthGuard', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('navigates user to unauthorised when no patient is searched', async () => {
it('navigates user to search patient page when no patient details are stored', async () => {
mockedUsePatient.mockReturnValue(null);

renderGuard();

await waitFor(async () => {
expect(mockedUseNavigate).toHaveBeenCalledWith(routes.UNAUTHORISED);
expect(mockedUseNavigate).toHaveBeenCalledWith(routes.SEARCH_PATIENT);
});
});

Expand Down
2 changes: 1 addition & 1 deletion app/src/router/guards/patientGuard/PatientGuard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function PatientGuard({ children }: Props) {
const navigate = useNavigate();
useEffect(() => {
if (!patient) {
navigate(routes.UNAUTHORISED);
navigate(routes.SEARCH_PATIENT);
}
}, [patient, navigate]);
return <>{children}</>;
Expand Down
6 changes: 6 additions & 0 deletions lambdas/enums/document_retention.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from enum import IntEnum


class DocumentRetentionDays(IntEnum):
SOFT_DELETE = 56
DEATH = 3650
12 changes: 12 additions & 0 deletions lambdas/enums/lambda_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,14 @@ def to_str(self) -> str:
"""
Errors for DocumentDeletionServiceException
"""
DocDelInvalidStreamEvent = {
"err_code": "DDS_4001",
"message": "Failed to delete document object",
}
DocDelObjectFailure = {
"err_code": "DDS_4002",
"message": "Failed to delete document object",
}
DocDelClient = {
"err_code": "DDS_5001",
"message": "Failed to delete documents",
Expand Down Expand Up @@ -470,6 +478,10 @@ def to_str(self) -> str:
"err_code": "LGL_400",
"message": "Incomplete record, Failed to create document manifest",
}
DynamoInvalidStreamEvent = {
"err_code": "DBS_4001",
"message": "Failed to parse DynamoDb event stream",
}

MockError = {
"message": "Client error",
Expand Down
12 changes: 0 additions & 12 deletions lambdas/enums/s3_lifecycle_tags.py

This file was deleted.

59 changes: 59 additions & 0 deletions lambdas/handlers/delete_document_object_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from enums.lambda_error import LambdaError
from enums.logging_app_interaction import LoggingAppInteraction
from models.document_reference import DocumentReference
from pydantic.v1 import ValidationError
from services.document_deletion_service import DocumentDeletionService
from utils.audit_logging_setup import LoggingService
from utils.decorators.handle_lambda_exceptions import handle_lambda_exceptions
from utils.decorators.override_error_check import override_error_check
from utils.decorators.set_audit_arg import set_request_context_for_logging
from utils.decorators.validate_dynamo_stream_event import validate_dynamo_stream
from utils.dynamo_utils import parse_dynamo_record
from utils.lambda_exceptions import DocumentDeletionServiceException
from utils.lambda_response import ApiGatewayResponse
from utils.request_context import request_context

logger = LoggingService(__name__)


@set_request_context_for_logging
@override_error_check
@handle_lambda_exceptions
@validate_dynamo_stream
def lambda_handler(event, context):
request_context.app_interaction = LoggingAppInteraction.DELETE_RECORD.value

logger.info(
"Delete Document Object handler has been triggered by DynamoDb REMOVE event"
)
try:
event_record = event["Records"][0]

event_type = event_record.get("eventName")
deleted_dynamo_reference = event_record.get("dynamodb").get("OldImage", {})

if event_type != "REMOVE" or not deleted_dynamo_reference:
logger.error(
"Failed to extract deleted record from DynamoDb stream",
{"Results": "Failed to delete document"},
)
raise DocumentDeletionServiceException(
400, LambdaError.DynamoInvalidStreamEvent
)
parsed_dynamo_record = parse_dynamo_record(deleted_dynamo_reference)
document = DocumentReference.model_validate(parsed_dynamo_record)

deletion_service = DocumentDeletionService()
deletion_service.handle_object_delete(deleted_reference=document)
except (ValueError, ValidationError) as e:
logger.error(
f"Failed to parse Document Reference from deleted record: {str(e)}",
{"Results": "Failed to delete document"},
)
raise DocumentDeletionServiceException(
400, LambdaError.DynamoInvalidStreamEvent
)

return ApiGatewayResponse(
200, "Successfully deleted Document Reference object", "GET"
).create_api_gateway_response()
2 changes: 1 addition & 1 deletion lambdas/handlers/delete_document_reference_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def lambda_handler(event, context):

deletion_service = DocumentDeletionService()

files_deleted = deletion_service.handle_delete(nhs_number, document_types)
files_deleted = deletion_service.handle_reference_delete(nhs_number, document_types)
if files_deleted:
logger.info(
"Documents were deleted successfully", {"Result": "Successful deletion"}
Expand Down
39 changes: 34 additions & 5 deletions lambdas/services/document_deletion_service.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import os
import uuid
from typing import Literal
from urllib.parse import urlparse

from botocore.exceptions import ClientError
from enums.document_retention import DocumentRetentionDays
from enums.lambda_error import LambdaError
from enums.nrl_sqs_upload import NrlActionTypes
from enums.s3_lifecycle_tags import S3LifecycleTags
from enums.snomed_codes import SnomedCodes
from enums.supported_document_types import SupportedDocumentTypes
from models.document_reference import DocumentReference
Expand All @@ -15,7 +16,7 @@
from services.lloyd_george_stitch_job_service import LloydGeorgeStitchJobService
from utils.audit_logging_setup import LoggingService
from utils.common_query_filters import NotDeleted
from utils.exceptions import DynamoServiceException
from utils.exceptions import DocumentServiceException, DynamoServiceException
from utils.lambda_exceptions import DocumentDeletionServiceException

logger = LoggingService(__name__)
Expand All @@ -27,7 +28,7 @@ def __init__(self):
self.stitch_service = LloydGeorgeStitchJobService()
self.sqs_service = SQSService()

def handle_delete(
def handle_reference_delete(
self, nhs_number: str, doc_types: list[SupportedDocumentTypes]
) -> list[DocumentReference]:
files_deleted = []
Expand All @@ -38,6 +39,34 @@ def handle_delete(
self.send_sqs_message_to_remove_pointer(nhs_number)
return files_deleted

def handle_object_delete(self, deleted_reference: DocumentReference):
try:
s3_uri = deleted_reference.file_location

parsed_uri = urlparse(s3_uri)
bucket_name = parsed_uri.netloc
object_key = parsed_uri.path.lstrip("/")

if not bucket_name or not object_key:
raise DocumentDeletionServiceException(
400, LambdaError.DocDelObjectFailure
)

self.document_service.delete_document_object(
bucket=bucket_name, key=object_key
)

logger.info(
"Successfully deleted Document Reference S3 Object",
{"Result": "Successful deletion"},
)
except DocumentServiceException as e:
logger.error(
str(e),
{"Results": "Failed to delete document"},
)
raise DocumentDeletionServiceException(400, LambdaError.DocDelObjectFailure)

def get_documents_references_in_storage(
self,
nhs_number: str,
Expand Down Expand Up @@ -69,10 +98,10 @@ def delete_specific_doc_type(
try:
results = self.get_documents_references_in_storage(nhs_number, doc_type)
if results:
self.document_service.delete_documents(
self.document_service.delete_document_references(
table_name=doc_type.get_dynamodb_table_name(),
document_references=results,
type_of_delete=str(S3LifecycleTags.SOFT_DELETE.value),
document_ttl_days=DocumentRetentionDays.SOFT_DELETE,
)

logger.info(
Expand Down
Loading

0 comments on commit 9ab8d3a

Please sign in to comment.