From 0872ecd65baf39641e3d0a35e7db647a3ac797dc Mon Sep 17 00:00:00 2001 From: Emmanuel Date: Fri, 12 Jul 2024 09:42:46 +0200 Subject: [PATCH] feat: improve error message when downloading message --- package.json | 1 + src/js/new-architecture/utils/files.js | 19 -------- src/js/new-architecture/utils/files.spec.ts | 49 +++++++++++++++++++++ src/js/new-architecture/utils/files.ts | 26 +++++++++++ yarn.lock | 5 +++ 5 files changed, 81 insertions(+), 19 deletions(-) delete mode 100644 src/js/new-architecture/utils/files.js create mode 100644 src/js/new-architecture/utils/files.spec.ts create mode 100644 src/js/new-architecture/utils/files.ts diff --git a/package.json b/package.json index 1496b2f66..7de36df5f 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", + "@types/file-saver": "^2.0.7", "@types/jest": "^29.5.12", "@typescript-eslint/parser": "^7.15.0", "babel-jest": "^29.7.0", diff --git a/src/js/new-architecture/utils/files.js b/src/js/new-architecture/utils/files.js deleted file mode 100644 index a37ab80f8..000000000 --- a/src/js/new-architecture/utils/files.js +++ /dev/null @@ -1,19 +0,0 @@ -import FileSaver from 'file-saver'; - -const CONTENT_DISPOSITION_FILENAME_REGEXP = - /filename[^;\n=]*="((['"]).*?\2|[^;\n]*)"/; - -export const getContentDisposition = (disposition) => { - return disposition.match(CONTENT_DISPOSITION_FILENAME_REGEXP)[1]; -}; - -export const saveFileFromHttpResponse = (response) => { - const fileName = getContentDisposition( - response.headers.get('Content-Disposition') - ); - - return response.blob().then((blob) => saveFileToDisk(blob, fileName)); -}; - -export const saveFileToDisk = (blob, fileName) => - FileSaver.saveAs(blob, fileName); diff --git a/src/js/new-architecture/utils/files.spec.ts b/src/js/new-architecture/utils/files.spec.ts new file mode 100644 index 000000000..8a3d56345 --- /dev/null +++ b/src/js/new-architecture/utils/files.spec.ts @@ -0,0 +1,49 @@ +import { saveFileFromHttpResponse } from './files'; +import FileSaver from 'file-saver'; + +jest.mock('file-saver', () => ({ + saveAs: jest.fn(), +})); + +describe('saveFileFromHttpResponse', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + it('should reject if Content-Disposition header is missing', async () => { + const response = new Response(null, { + headers: new Headers({}), + }); + + await expect(saveFileFromHttpResponse(response)).rejects.toBeUndefined(); + expect(console.error).toHaveBeenCalledWith( + 'Unable to download the File due to a missing Content-Disposition header' + ); + }); + + it('should reject if Content-Disposition header is invalid', async () => { + const response = new Response(null, { + headers: new Headers({ + 'Content-Disposition': 'invalid-header', + }), + }); + + await expect(saveFileFromHttpResponse(response)).rejects.toBeUndefined(); + expect(console.error).toHaveBeenCalledWith( + 'Unable to parse the Content-Disposition header' + ); + }); + + it('should save file with correct file name from Content-Disposition header', async () => { + const blob = new Blob(['test content'], { type: 'text/plain' }); + const response = new Response(blob, { + headers: new Headers({ + 'Content-Disposition': 'attachment; filename="testfile.txt"', + }), + }); + + await saveFileFromHttpResponse(response); + + expect(FileSaver.saveAs).toHaveBeenCalledWith(blob, 'testfile.txt'); + }); +}); diff --git a/src/js/new-architecture/utils/files.ts b/src/js/new-architecture/utils/files.ts new file mode 100644 index 000000000..56c42ff7b --- /dev/null +++ b/src/js/new-architecture/utils/files.ts @@ -0,0 +1,26 @@ +import FileSaver from 'file-saver'; + +const CONTENT_DISPOSITION_FILENAME_REGEXP = + /filename[^;\n=]*="((['"]).*?\2|[^;\n]*)"/; + +export const saveFileFromHttpResponse = (response: Response) => { + const contentDisposition = response.headers.get('Content-Disposition'); + + if (contentDisposition === null) { + console.error( + 'Unable to download the File due to a missing Content-Disposition header' + ); + return Promise.reject(); + } + + const matches = contentDisposition.match(CONTENT_DISPOSITION_FILENAME_REGEXP); + + if (matches === null) { + console.error('Unable to parse the Content-Disposition header'); + return Promise.reject(); + } + + const fileName = matches[1]; + + return response.blob().then((blob) => FileSaver.saveAs(blob, fileName)); +}; diff --git a/yarn.lock b/yarn.lock index 9ffb09f23..f98139afe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2363,6 +2363,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== +"@types/file-saver@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.7.tgz#8dbb2f24bdc7486c54aa854eb414940bbd056f7d" + integrity sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A== + "@types/glob@^7.1.1": version "7.2.0" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"