Skip to content

Commit

Permalink
UIEXPMGR-64 Cannot re-run 1 day+ old holdings exports (#110)
Browse files Browse the repository at this point in the history
  • Loading branch information
NikitaSedyx authored Dec 12, 2022
1 parent a91e175 commit 2d7908d
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 56 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## (IN PROGRESS)

* Cannot re-run 1 day+ old holdings exports. Refs UIEXPMGR-64.

## [2.3.0](https://github.com/folio-org/ui-export-manager/tree/v2.3.0) (2022-10-28)
[Full Changelog](https://github.com/folio-org/ui-export-manager/compare/v2.2.3...v2.3.0)

Expand Down
6 changes: 1 addition & 5 deletions src/ExportEdiJobs/ExportEdiJobDetails/ExportEdiJobDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,7 @@ export const ExportEdiJobDetails = ({ refetchJobs, uuid }) => {
<Row>
<Col xs={3}>
<KeyValue label={<FormattedMessage id="ui-export-manager.exportJob.jobId" />}>
<ExportJobId
jobId={exportJob.name}
files={exportJob.files}
entityType={exportJob.entityType}
/>
<ExportJobId job={exportJob} />
</KeyValue>
</Col>

Expand Down
5 changes: 1 addition & 4 deletions src/ExportEdiJobs/ExportEdiJobsList/ExportEdiJobsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,7 @@ const columnMapping = {

const resultsFormatter = {
jobId: exportJob => (
<ExportJobId
jobId={exportJob.name}
files={exportJob.files}
/>
<ExportJobId job={exportJob} />
),
source: exportJob => (
exportJob.isSystemSource
Expand Down
6 changes: 1 addition & 5 deletions src/ExportJob/ExportJob.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,7 @@ export const ExportJob = ({ uuid }) => {
<Row>
<Col xs={3}>
<KeyValue label={formatMessage({ id: 'ui-export-manager.exportJob.jobId' })}>
<ExportJobId
jobId={exportJob.name}
files={exportJob.files}
entityType={exportJob.entityType}
/>
<ExportJobId job={exportJob} />
</KeyValue>
</Col>

Expand Down
6 changes: 1 addition & 5 deletions src/ExportJobs/ExportJobsList/ExportJobsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,7 @@ const columnMapping = {
};
const resultsFormatter = {
jobId: exportJob => (
<ExportJobId
jobId={exportJob.name}
files={exportJob.files}
entityType={exportJob.entityType}
/>
<ExportJobId job={exportJob} />
),
source: exportJob => (
exportJob.isSystemSource
Expand Down
58 changes: 33 additions & 25 deletions src/common/components/ExportJobId/ExportJobId.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import PropTypes from 'prop-types';
import { useStripes } from '@folio/stripes/core';
import { TextLink } from '@folio/stripes/components';

export const ExportJobId = ({ jobId, files, entityType }) => {
import { useSecureDownload } from '../../hooks';

export const ExportJobId = ({ job }) => {
const { id, name: jobId, files, fileNames, entityType, type: jobType } = job;

const stripes = useStripes();
const { download: downloadSecurely } = useSecureDownload(id);

const hasCsvAnyPerms = stripes.hasPerm('ui-bulk-edit.view') || stripes.hasPerm('ui-bulk-edit.edit');
const hasInAppAnyPerms = stripes.hasPerm('ui-bulk-edit.app-view') || stripes.hasPerm('ui-bulk-edit.app-edit');
Expand All @@ -17,27 +22,32 @@ export const ExportJobId = ({ jobId, files, entityType }) => {

const downloadFiles = (e) => {
e.stopPropagation();
files.forEach((file) => {
if (file) {
const link = document.createElement('a');

link.href = file;
link.download = jobId;
link.target = '_blank';

document.body.appendChild(link);

link.dispatchEvent(
new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window,
}),
);

document.body.removeChild(link);
}
});

if (jobType === 'E_HOLDINGS') {
downloadSecurely(fileNames[0]);
} else {
files.forEach((file) => {
if (file) {
const link = document.createElement('a');

link.href = file;
link.download = jobId;
link.target = '_blank';

document.body.appendChild(link);

link.dispatchEvent(
new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window,
}),
);

document.body.removeChild(link);
}
});
}
};

return (
Expand All @@ -55,7 +65,5 @@ export const ExportJobId = ({ jobId, files, entityType }) => {
};

ExportJobId.propTypes = {
jobId: PropTypes.string.isRequired,
files: PropTypes.arrayOf(PropTypes.string),
entityType: PropTypes.string,
job: PropTypes.object.isRequired,
};
68 changes: 56 additions & 12 deletions src/common/components/ExportJobId/ExportJobId.test.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,75 @@
import React from 'react';
import { render } from '@testing-library/react';
import user from '@testing-library/user-event';

import '@folio/stripes-acq-components/test/jest/__mock__';

import { useSecureDownload } from '../../hooks';
import { ExportJobId } from './ExportJobId';

const renderExportJobId = (jobId, files = []) => render(
<ExportJobId
jobId={jobId}
files={files}
/>,
const renderExportJobId = (job) => render(
<ExportJobId job={job} />,
);

describe('BursarExportsManualRunner', () => {
jest.mock('../../hooks', () => ({
...jest.requireActual('../../hooks'),
useSecureDownload: jest.fn(),
}));

describe('ExportJobId', () => {
const downloadSecurelyMock = jest.fn();

beforeEach(() => {
downloadSecurelyMock.mockClear();

useSecureDownload.mockClear().mockReturnValue({
download: downloadSecurelyMock,
});
});

it('should render job id as a text when no files provided', () => {
const jobId = '1001';
const { getByText, queryByRole } = renderExportJobId(jobId);
const jobName = '1001';
const { getByText, queryByRole } = renderExportJobId({ name: jobName });

expect(getByText(jobId)).toBeDefined();
expect(getByText(jobName)).toBeDefined();
expect(queryByRole('button')).toBeNull();
});

it('should render job id as a button when files are provided', () => {
const jobId = '1001';
const { getByText, getByTestId } = renderExportJobId(jobId, ['/test.png']);
const jobName = '1001';
const { getByText, getByTestId } = renderExportJobId({
name: jobName,
files: ['/test.png'],
});

expect(getByText(jobId)).toBeDefined();
expect(getByText(jobName)).toBeDefined();
expect(getByTestId('text-link')).toBeDefined();
});

it('should use secure download for eholdings exports', () => {
const jobName = '1001';
const { getByTestId } = renderExportJobId({
name: jobName,
files: ['/test.png'],
fileNames: ['/test.png'],
type: 'E_HOLDINGS',
});

user.click(getByTestId('text-link'));

expect(downloadSecurelyMock).toHaveBeenCalled();
});

it('should not use secure download for eholdings exports', () => {
const jobName = '1001';
const { getByTestId } = renderExportJobId({
name: jobName,
files: ['/test.png'],
type: 'CIRCULATION_LOG',
});

user.click(getByTestId('text-link'));

expect(downloadSecurelyMock).not.toHaveBeenCalled();
});
});
1 change: 1 addition & 0 deletions src/common/hooks/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './useExportConfig';
export * from './useExportJobScheduler';
export * from './useSecureDownload';
1 change: 1 addition & 0 deletions src/common/hooks/useSecureDownload/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useSecureDownload';
33 changes: 33 additions & 0 deletions src/common/hooks/useSecureDownload/useSecureDownload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useOkapiKy } from '@folio/stripes/core';

import {
downloadBase64,
useShowCallout,
} from '@folio/stripes-acq-components';

import {
EXPORT_JOBS_API,
} from '../../constants';

export const useSecureDownload = (jobId) => {
const ky = useOkapiKy();
const showCallout = useShowCallout();

const download = async (fileName) => {
return ky.get(`${EXPORT_JOBS_API}/${jobId}/download`, {
headers: { accept: 'application/octet-stream' },
})
.blob()
.then(data => {
downloadBase64(fileName, URL.createObjectURL(data));
})
.catch(() => {
showCallout({
messageId: 'ui-export-manager.exportJob.details.action.download.error',
type: 'error',
});
});
};

return { download };
};
63 changes: 63 additions & 0 deletions src/common/hooks/useSecureDownload/useSecureDownload.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { renderHook } from '@testing-library/react-hooks';

import { useOkapiKy } from '@folio/stripes/core';
import {
downloadBase64,
useShowCallout,
} from '@folio/stripes-acq-components';

import { useSecureDownload } from './useSecureDownload';

jest.mock('@folio/stripes-acq-components', () => {
return {
...jest.requireActual('@folio/stripes-acq-components'),
downloadBase64: jest.fn(),
useShowCallout: jest.fn(() => jest.fn()),
};
});

const jobId = 'uid';
const toastMessage = type => `ui-export-manager.exportJob.details.action.${type}`;

describe('useSecureDownload', () => {
const kyGetMock = jest.fn(() => ({
blob: () => Promise.resolve(),
}));
const showCallout = jest.fn();

beforeEach(() => {
downloadBase64.mockClear();
kyGetMock.mockClear();
showCallout.mockClear();

useShowCallout.mockClear().mockReturnValue(showCallout);

useOkapiKy.mockClear().mockReturnValue({
get: kyGetMock,
});
});

it('should make get request to download file', async () => {
const { result } = renderHook(
() => useSecureDownload(jobId),
);

await result.current.download('fileName.csv');

expect(kyGetMock).toHaveBeenCalled();
});

it('should handle export job download error', async () => {
kyGetMock.mockReturnValue({ blob: () => Promise.reject() });

const { result } = renderHook(
() => useSecureDownload(jobId),
);

await result.current.download('fileName.csv');

expect(showCallout).toHaveBeenCalledWith(expect.objectContaining({
messageId: toastMessage`download.error`,
}));
});
});

0 comments on commit 2d7908d

Please sign in to comment.