diff --git a/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/download_lloyd_george_workflow.cy.js b/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/download_lloyd_george_workflow.cy.js index 8aa60367b..051463d05 100644 --- a/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/download_lloyd_george_workflow.cy.js +++ b/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/download_lloyd_george_workflow.cy.js @@ -19,18 +19,21 @@ const testFiles = [ created: '2024-05-07T14:52:00.827602Z', virusScannerResult: 'Clean', id: 'test-id', + fileSize: 200, }, { fileName: '2of2_testy_test.pdf', created: '2024-05-07T14:52:00.827602Z', virusScannerResult: 'Clean', id: 'test-id-2', + fileSize: 200, }, { fileName: '1of1_lone_test_file.pdf', created: '2024-01-01T14:52:00.827602Z', virusScannerResult: 'Clean', id: 'test-id-3', + fileSize: 200, }, ]; @@ -40,6 +43,7 @@ const singleTestFile = [ created: '2024-01-01T14:52:00.827602Z', virusScannerResult: 'Clean', id: 'test-id-3', + fileSize: 200, }, ]; diff --git a/app/src/components/blocks/_lloydGeorge/lloydGeorgeSelectSearchResults/LloydGeorgeSelectSearchResults.test.tsx b/app/src/components/blocks/_lloydGeorge/lloydGeorgeSelectSearchResults/LloydGeorgeSelectSearchResults.test.tsx index 877a70902..048ae2d04 100644 --- a/app/src/components/blocks/_lloydGeorge/lloydGeorgeSelectSearchResults/LloydGeorgeSelectSearchResults.test.tsx +++ b/app/src/components/blocks/_lloydGeorge/lloydGeorgeSelectSearchResults/LloydGeorgeSelectSearchResults.test.tsx @@ -5,7 +5,6 @@ import usePatient from '../../../../helpers/hooks/usePatient'; import { LinkProps } from 'react-router-dom'; import LloydGeorgeSelectSearchResults, { Props } from './LloydGeorgeSelectSearchResults'; import userEvent from '@testing-library/user-event'; -import { routes } from '../../../../types/generic/routes'; import { SEARCH_AND_DOWNLOAD_STATE } from '../../../../types/pages/documentSearchResultsPage/types'; import { runAxeTest } from '../../../../helpers/test/axeTestHelper'; @@ -57,6 +56,20 @@ describe('LloydGeorgeSelectSearchResults', () => { expect(screen.getByTestId('toggle-selection-btn')).toBeInTheDocument(); }); + it('renders the correct table headers', () => { + renderComponent({ selectedDocuments: mockSelectedDocuments }); + + const headers = screen.getAllByRole('columnheader'); + const expectedHeaders = ['Selected', 'Filename', 'Upload date', 'File size']; + + expectedHeaders.forEach((headerText, index) => { + expect(headers[index]).toHaveTextContent(headerText); + }); + + const filesTable = screen.getByTestId('available-files-table-title'); + expect(filesTable).toHaveTextContent(/bytes|KB|MB|GB/); + }); + it('shows error box when download selected files button is clicked but no files selected', async () => { renderComponent({ selectedDocuments: [] }); diff --git a/app/src/components/blocks/_lloydGeorge/lloydGeorgeSelectSearchResults/LloydGeorgeSelectSearchResults.tsx b/app/src/components/blocks/_lloydGeorge/lloydGeorgeSelectSearchResults/LloydGeorgeSelectSearchResults.tsx index 8a7da7ecc..8a009b5c8 100644 --- a/app/src/components/blocks/_lloydGeorge/lloydGeorgeSelectSearchResults/LloydGeorgeSelectSearchResults.tsx +++ b/app/src/components/blocks/_lloydGeorge/lloydGeorgeSelectSearchResults/LloydGeorgeSelectSearchResults.tsx @@ -8,6 +8,7 @@ import { SEARCH_AND_DOWNLOAD_STATE } from '../../../../types/pages/documentSearc import ErrorBox from '../../../layout/errorBox/ErrorBox'; import PatientSummary from '../../../generic/patientSummary/PatientSummary'; import BackButton from '../../../generic/backButton/BackButton'; +import formatFileSize from '../../../../helpers/utils/formatFileSize'; export type Props = { searchResults: Array; @@ -108,6 +109,7 @@ const AvailableFilesTable = ({ )} Filename Upload date + File size @@ -147,6 +149,12 @@ const AvailableFilesTable = ({ > {getFormattedDatetime(new Date(result.created))} + + {formatFileSize(result.fileSize)} + ))} diff --git a/app/src/helpers/test/testBuilders.ts b/app/src/helpers/test/testBuilders.ts index b73100c67..53523f93c 100644 --- a/app/src/helpers/test/testBuilders.ts +++ b/app/src/helpers/test/testBuilders.ts @@ -123,6 +123,7 @@ const buildSearchResult = (searchResultOverride?: Partial) => { created: moment().format(), virusScannerResult: 'Clean', ID: '1234qwer-241ewewr', + fileSize: 224, ...searchResultOverride, }; return result; diff --git a/app/src/helpers/utils/formatFileSize.test.tsx b/app/src/helpers/utils/formatFileSize.test.tsx new file mode 100644 index 000000000..748d1e0a0 --- /dev/null +++ b/app/src/helpers/utils/formatFileSize.test.tsx @@ -0,0 +1,41 @@ +import formatFileSize from "./formatFileSize"; + +describe('formatFileSize', () => { + + it('returns rounded file size formats for valid inputs', () => { + + expect(formatFileSize(0)).toBe('0 bytes'); + expect(formatFileSize(-0)).toBe('0 bytes'); + expect(formatFileSize(1)).toBe('1 bytes'); + expect(formatFileSize(1.5)).toBe('2 bytes'); + + expect(formatFileSize(1023)).toBe('1023 bytes'); + expect(formatFileSize(1024)).toBe('1 KB'); + expect(formatFileSize(1025)).toBe('1 KB'); + + expect(formatFileSize(1535)).toBe('1 KB'); + expect(formatFileSize(1536)).toBe('2 KB'); + expect(formatFileSize(2048)).toBe('2 KB'); + + expect(formatFileSize(Math.pow(2, 20) - 1)).toBe('1024 KB'); + expect(formatFileSize(Math.pow(2, 20))).toBe('1 MB'); + expect(formatFileSize(Math.pow(2, 20) + 1)).toBe('1 MB'); + + expect(formatFileSize(Math.pow(2, 30) - 1)).toBe('1024 MB'); + expect(formatFileSize(Math.pow(2, 30))).toBe('1 GB'); + expect(formatFileSize(Math.pow(2, 30) + 1)).toBe('1 GB'); + + }); + + it('throws "Invalid file size" exception for invalid inputs', () => { + + expect(() => formatFileSize(Number.MIN_SAFE_INTEGER)).toThrow('Invalid file size'); + expect(() => formatFileSize(-1)).toThrow('Invalid file size'); + expect(() => formatFileSize(NaN)).toThrow('Invalid file size'); + expect(() => formatFileSize(undefined as unknown as number)).toThrow('Invalid file size'); + expect(() => formatFileSize(Math.pow(2, 40))).toThrow('Invalid file size'); // 1TB + expect(() => formatFileSize(Number.MAX_SAFE_INTEGER)).toThrow('Invalid file size'); + + }); + +}); \ No newline at end of file diff --git a/app/src/types/generic/searchResult.ts b/app/src/types/generic/searchResult.ts index 769d93871..9225ba18e 100644 --- a/app/src/types/generic/searchResult.ts +++ b/app/src/types/generic/searchResult.ts @@ -3,4 +3,5 @@ export type SearchResult = { created: string; virusScannerResult: string; ID: string; + fileSize: number; }; diff --git a/lambdas/services/document_reference_search_service.py b/lambdas/services/document_reference_search_service.py index 8f1f4c2bd..d3d2fb3d9 100644 --- a/lambdas/services/document_reference_search_service.py +++ b/lambdas/services/document_reference_search_service.py @@ -49,10 +49,21 @@ def get_document_references(self, nhs_number: str): 423, LambdaError.UploadInProgressError ) results.extend( - document.model_dump( - include={"file_name", "created", "virus_scanner_result", "id"}, - by_alias=True, - ) + { + **document.model_dump( + include={ + "file_name", + "created", + "virus_scanner_result", + "id", + }, + by_alias=True, + ), + "fileSize": self.s3_service.get_file_size( + s3_bucket_name=document.get_file_bucket(), + object_key=document.get_file_key(), + ), + } for document in documents ) return results diff --git a/lambdas/tests/unit/services/test_document_reference_search_service.py b/lambdas/tests/unit/services/test_document_reference_search_service.py index 667fd3c9e..10ff8fdae 100644 --- a/lambdas/tests/unit/services/test_document_reference_search_service.py +++ b/lambdas/tests/unit/services/test_document_reference_search_service.py @@ -12,18 +12,22 @@ DocumentReference.model_validate(MOCK_SEARCH_RESPONSE["Items"][0]) ] +MOCK_FILE_SIZE = 24000 + EXPECTED_RESPONSE = { "created": "2024-01-01T12:00:00.000Z", "fileName": "document.csv", "virusScannerResult": "Clean", "ID": "3d8683b9-1665-40d2-8499-6e8302d507ff", + "fileSize": MOCK_FILE_SIZE, } @pytest.fixture def patched_service(mocker, set_env): service = DocumentReferenceSearchService() - mocker.patch.object(service, "s3_service") + mock_s3_service = mocker.patch.object(service, "s3_service") + mocker.patch.object(mock_s3_service, "get_file_size", return_value=MOCK_FILE_SIZE) mocker.patch.object(service, "dynamo_service") mocker.patch.object(service, "fetch_documents_from_table_with_filter") mocker.patch.object(service, "is_upload_in_process", return_value=False) @@ -83,10 +87,10 @@ def test_get_document_references_dynamo_return_successful_response_single_table( patched_service, monkeypatch ): monkeypatch.setenv("DYNAMODB_TABLE_LIST", json.dumps(["test_table"])) - patched_service.fetch_documents_from_table_with_filter.return_value = ( MOCK_DOCUMENT_REFERENCE ) + expected_results = [EXPECTED_RESPONSE] actual = patched_service.get_document_references("1111111111")