Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ import {
import { DOCUMENT_TYPE } from '../../../../helpers/utils/documentType';
import { ReviewDetails } from '../../../../types/generic/reviews';
import { routes, routeChildren } from '../../../../types/generic/routes';
import { buildLgFile, buildPatientDetails } from '../../../../helpers/test/testBuilders';
import {
buildLgFile,
buildPatientDetails,
buildUploadSession,
} from '../../../../helpers/test/testBuilders';
import * as uploadDocumentsModule from '../../../../helpers/requests/uploadDocuments';
import * as documentUploadModule from '../../../../helpers/utils/documentUpload';
import * as mergePdfsModule from '../../../../helpers/utils/mergePdfs';
import { JSX } from 'react';

Expand Down Expand Up @@ -42,7 +47,8 @@ vi.mock('../../../../helpers/hooks/useBaseAPIHeaders', () => ({
default: (): typeof mockUseBaseAPIHeaders => mockUseBaseAPIHeaders,
}));

vi.mock('../../../../helpers/utils/urlManipulations', () => ({
vi.mock('../../../../helpers/utils/urlManipulations', async () => ({
...(await vi.importActual('../../../../helpers/utils/urlManipulations')),
useEnhancedNavigate: (): any => {
const fn = mockNavigate;
(fn as any).withParams = mockNavigate;
Expand Down Expand Up @@ -264,7 +270,7 @@ describe('ReviewDetailsDocumentUploadingStage', (): void => {
const error = {
response: { status: 500 },
};
vi.spyOn(uploadDocumentsModule, 'default').mockRejectedValueOnce(error);
vi.spyOn(documentUploadModule, 'getUploadSession').mockRejectedValueOnce(error);

const stitchedReviewData = new ReviewDetails(
'test-review-id',
Expand Down Expand Up @@ -365,6 +371,11 @@ describe('ReviewDetailsDocumentUploadingStage', (): void => {
it('sets up interval timer for document status polling', async (): Promise<void> => {
const setIntervalSpy = vi.spyOn(window, 'setInterval');

vi.spyOn(documentUploadModule, 'getUploadSession').mockResolvedValueOnce(
buildUploadSession(mockDocuments),
);
vi.spyOn(uploadDocumentsModule, 'uploadDocumentToS3').mockResolvedValue();

const stitchedReviewData = new ReviewDetails(
'test-review-id',
'16521000000101' as DOCUMENT_TYPE,
Expand Down Expand Up @@ -437,109 +448,14 @@ describe('ReviewDetailsDocumentUploadingStage', (): void => {
});
});

describe('Session handling', (): void => {
it('navigates to SESSION_EXPIRED on 403 error during upload', async (): Promise<void> => {
const error = {
response: { status: 403 },
};
vi.spyOn(uploadDocumentsModule, 'default').mockRejectedValueOnce(error);

const stitchedReviewData = new ReviewDetails(
'test-review-id',
'16521000000101' as DOCUMENT_TYPE,
'2023-10-01T12:00:00Z',
'Test Uploader',
'2023-10-01T12:00:00Z',
'Test Reason',
'1',
'1234567890',
);

const documentsWithExisting = [
{
...mockDocuments[0],
type: UploadDocumentType.EXISTING,
versionId: 'v1',
},
];

renderComponent(documentsWithExisting, stitchedReviewData);

await waitFor(
(): void => {
expect(screen.queryByText('Preparing documents')).not.toBeInTheDocument();
},
{ timeout: 2000 },
);

await waitFor((): void => {
expect(screen.getByTestId('start-upload-button')).toBeInTheDocument();
});

const startButton = screen.getByTestId('start-upload-button');
await act(async () => {
startButton.click();
});

await waitFor((): void => {
expect(mockNavigate).toHaveBeenCalledWith(routes.SESSION_EXPIRED);
});
});

it('navigates to SESSION_EXPIRED on 403 error during S3 upload', async (): Promise<void> => {
const error = {
response: { status: 403 },
};
vi.spyOn(uploadDocumentsModule, 'uploadDocumentToS3').mockRejectedValueOnce(error);

const stitchedReviewData = new ReviewDetails(
'test-review-id',
'16521000000101' as DOCUMENT_TYPE,
'2023-10-01T12:00:00Z',
'Test Uploader',
'2023-10-01T12:00:00Z',
'Test Reason',
'1',
'1234567890',
);

const documentsWithExisting = [
{
...mockDocuments[0],
type: UploadDocumentType.EXISTING,
versionId: 'v1',
},
];

renderComponent(documentsWithExisting, stitchedReviewData);

await waitFor(
(): void => {
expect(screen.queryByText('Preparing documents')).not.toBeInTheDocument();
},
{ timeout: 2000 },
);

await waitFor((): void => {
expect(screen.getByTestId('start-upload-button')).toBeInTheDocument();
});

const startButton = screen.getByTestId('start-upload-button');
await act(async () => {
startButton.click();
});

await waitFor((): void => {
expect(mockNavigate).toHaveBeenCalledWith(routes.SESSION_EXPIRED);
});
});
});

describe('S3 Upload error handling', (): void => {
it('marks document as failed and navigates to SERVER_ERROR on S3 upload failure', async (): Promise<void> => {
const error = {
response: { status: 500 },
};
vi.spyOn(documentUploadModule, 'getUploadSession').mockResolvedValueOnce(
buildUploadSession(mockDocuments),
);
vi.spyOn(uploadDocumentsModule, 'uploadDocumentToS3').mockRejectedValueOnce(error);

const stitchedReviewData = new ReviewDetails(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ import {
import useBaseAPIHeaders from '../../../../helpers/hooks/useBaseAPIHeaders';
import useBaseAPIUrl from '../../../../helpers/hooks/useBaseAPIUrl';
import usePatient from '../../../../helpers/hooks/usePatient';
import uploadDocuments, {
import {
generateStitchedFileName,
getDocumentStatus,
uploadDocumentToS3,
} from '../../../../helpers/requests/uploadDocuments';
import { DOCUMENT_TYPE, getConfigForDocType } from '../../../../helpers/utils/documentType';
Expand All @@ -24,16 +23,16 @@ import {
import { useEnhancedNavigate } from '../../../../helpers/utils/urlManipulations';
import { ReviewDetails } from '../../../../types/generic/reviews';
import { routeChildren, routes } from '../../../../types/generic/routes';
import { DocumentStatusResult, UploadSession } from '../../../../types/generic/uploadResult';
import { UploadSession } from '../../../../types/generic/uploadResult';
import {
DOCUMENT_STATUS,
DOCUMENT_UPLOAD_STATE,
ReviewUploadDocument,
UploadDocument,
UploadDocumentType,
} from '../../../../types/pages/UploadDocumentsPage/types';
import Spinner from '../../../generic/spinner/Spinner';
import DocumentUploadingStage from '../../_documentUpload/documentUploadingStage/DocumentUploadingStage';
import { getUploadSession, startIntervalTimer } from '../../../../helpers/utils/documentUpload';

type Props = {
reviewData: ReviewDetails | null;
Expand All @@ -55,7 +54,7 @@ const ReviewDetailsDocumentUploadingStage = ({
const navigate = useEnhancedNavigate();
const completeRef = useRef(false);
const virusReference = useRef(false);
const interval = useRef<number>(0);
const [interval, setInterval] = useState<number>(0);
const intervalTimerRef = useRef<number | null>(null);
const documentsRef = useRef<UploadDocument[]>(documents);

Expand Down Expand Up @@ -145,7 +144,7 @@ const ReviewDetailsDocumentUploadingStage = ({
}, [documents]);

useEffect(() => {
if (interval.current * UPDATE_DOCUMENT_STATE_FREQUENCY_MILLISECONDS > MAX_POLLING_TIME) {
if (interval * UPDATE_DOCUMENT_STATE_FREQUENCY_MILLISECONDS > MAX_POLLING_TIME) {
clearIntervalTimer();
navigate(routes.SERVER_ERROR);
return;
Expand Down Expand Up @@ -174,7 +173,7 @@ const ReviewDetailsDocumentUploadingStage = ({
),
);
}
}, [baseHeaders, baseUrl, documents, navigate, nhsNumber, setDocuments]);
}, [baseHeaders, baseUrl, documents, navigate, nhsNumber, setDocuments, interval]);

useEffect(() => {
return (): void => {
Expand Down Expand Up @@ -225,15 +224,15 @@ const ReviewDetailsDocumentUploadingStage = ({

const startUpload = async (): Promise<void> => {
try {
const uploadSession: UploadSession = isLocal
? getMockUploadSession(documents)
: await uploadDocuments({
nhsNumber,
documents: documents,
baseUrl,
baseHeaders,
documentReferenceId: existingId,
});
const uploadSession: UploadSession = await getUploadSession(
patientDetails!,
baseUrl,
baseHeaders,
existingId,
documents,
setDocuments,
);

const uploadingDocuments = markDocumentsAsUploading(documents, uploadSession);
setDocuments(uploadingDocuments);
setIsLoading(false);
Expand All @@ -242,7 +241,16 @@ const ReviewDetailsDocumentUploadingStage = ({
uploadAllDocuments(uploadingDocuments, uploadSession);
}

const updateStateInterval = startIntervalTimer(uploadingDocuments);
const updateStateInterval = startIntervalTimer(
uploadingDocuments,
setInterval,
documents,
setDocuments,
patientDetails!,
baseUrl,
baseHeaders,
UPDATE_DOCUMENT_STATE_FREQUENCY_MILLISECONDS,
);
intervalTimerRef.current = updateStateInterval;
} catch (e) {
setIsLoading(false);
Expand All @@ -269,105 +277,6 @@ const ReviewDetailsDocumentUploadingStage = ({
}
};

const startIntervalTimer = (uploadDocuments: Array<UploadDocument>): number => {
const startIntervalTimerIsLocal = (): void => {
const updatedDocuments = uploadDocuments.map((doc) => {
const min = (doc.progress ?? 0) + 40;
const max = 70;
doc.progress = Math.random() * (min + max - (min + 1)) + min;
doc.progress = doc.progress > 100 ? 100 : doc.progress;
if (doc.progress < 100) {
doc.state = DOCUMENT_UPLOAD_STATE.UPLOADING;
} else if (doc.state !== DOCUMENT_UPLOAD_STATE.SCANNING) {
doc.state = DOCUMENT_UPLOAD_STATE.SCANNING;
} else {
const hasVirusFile = documents.filter(
(d) => d.file.name.toLocaleLowerCase() === 'virus.pdf',
);
const hasFailedFile = documents.filter(
(d) => d.file.name.toLocaleLowerCase() === 'virus-failed.pdf',
);
if (hasVirusFile.length > 0) {
doc.state = DOCUMENT_UPLOAD_STATE.INFECTED;
} else if (hasFailedFile.length > 0) {
doc.state = DOCUMENT_UPLOAD_STATE.FAILED;
} else {
doc.state = DOCUMENT_UPLOAD_STATE.SUCCEEDED;
}
}

return doc;
});
setDocuments(updatedDocuments);
};

return window.setInterval(() => {
interval.current = interval.current + 1;
if (isLocal) {
startIntervalTimerIsLocal();
} else {
getDocumentStatus({
documents: documentsRef.current,
baseUrl,
baseHeaders,
nhsNumber,
})
.then((documentStatusResult) => {
handleDocStatusResult(documentStatusResult);
})
.catch((e) => {
const error = e as AxiosError;
if (error.response?.status === 403) {
navigate(routes.SESSION_EXPIRED);
return;
}
navigate(routes.SERVER_ERROR + errorToParams(error));
});
}
}, UPDATE_DOCUMENT_STATE_FREQUENCY_MILLISECONDS);
};

const handleDocStatusResult = (documentStatusResult: DocumentStatusResult): void => {
setDocuments((previousState) =>
previousState.map((doc) => {
const docStatus = documentStatusResult[doc.ref!];

const updatedDoc = {
...doc,
};

switch (docStatus?.status) {
case DOCUMENT_STATUS.FINAL:
updatedDoc.state = DOCUMENT_UPLOAD_STATE.SUCCEEDED;
break;

case DOCUMENT_STATUS.INFECTED:
updatedDoc.state = DOCUMENT_UPLOAD_STATE.INFECTED;
break;

case DOCUMENT_STATUS.NOT_FOUND:
case DOCUMENT_STATUS.CANCELLED:
updatedDoc.state = DOCUMENT_UPLOAD_STATE.ERROR;
updatedDoc.errorCode = docStatus.error_code;
break;
}

return updatedDoc;
}),
);
};

const getMockUploadSession = (documents: ReviewUploadDocument[]): UploadSession => {
const session: UploadSession = {};
documents.forEach((doc) => {
session[doc.id] = {
url: 'https://example.com/',
} as any;
});

return session;
};

if (!hasNormalisedOnEntry) {
return <Spinner status={'Loading'} />;
}
Expand Down
Loading
Loading