From e9339dc378871bb932c29f264c25a1436cf1f959 Mon Sep 17 00:00:00 2001 From: Jeremie Pardou-Piquemal <571533+jrmi@users.noreply.github.com> Date: Sat, 2 May 2020 10:08:00 +0200 Subject: [PATCH] Fix and enhance FileTransfer component --- client/.env.dist | 11 +- .../components/FileTransfer/FileTransfer.js | 123 +++++++++++++ .../FileTransfer/FileTransfer.test.js | 172 ++++++++++-------- .../__snapshots__/FileTransfer.test.js.snap | 2 +- client/src/components/FileTransfer/index.js | 117 +----------- client/src/utils/index.js | 2 +- readme.md | 4 +- 7 files changed, 233 insertions(+), 198 deletions(-) create mode 100644 client/src/components/FileTransfer/FileTransfer.js diff --git a/client/.env.dist b/client/.env.dist index 07e63f9..f577e32 100644 --- a/client/.env.dist +++ b/client/.env.dist @@ -1,5 +1,14 @@ +# Api settings +TZ=UTC REACT_APP_API_HOST=localhost REACT_APP_API_PROTOCOL=http REACT_APP_API_PORT=3001 REACT_APP_COMMIT_SHA=some_sha -TZ=UTC + +# To display darkwire version +REACT_APP_COMMIT_SHA=some_sha + +# Set max transferable file size in MB +REACT_APP_MAX_FILE_SIZE=4 + + diff --git a/client/src/components/FileTransfer/FileTransfer.js b/client/src/components/FileTransfer/FileTransfer.js new file mode 100644 index 0000000..fb093a4 --- /dev/null +++ b/client/src/components/FileTransfer/FileTransfer.js @@ -0,0 +1,123 @@ +import React from 'react' +import PropTypes from 'prop-types' +import uuid from 'uuid' +import { File } from 'react-feather' +import { sanitize } from 'utils' +import { styles } from './styles.module.scss' + +const VALID_FILE_TYPES = ['png', 'jpg', 'jpeg', 'gif', 'zip', 'rar', 'gzip', 'pdf', 'txt', 'json', 'doc', 'docx', 'csv', 'js', 'html', 'css'] + +const MAX_FILE_SIZE = process.env.REACT_APP_MAX_FILE_SIZE || 4 + +/** + * Encode the given file to binary string + * @param {File} file + */ +const encodeFile = (file) => { + return new Promise((resolve, reject) => { + const reader = new window.FileReader() + + if (!file) { + reject() + return + } + + reader.onload = (readerEvent) => { + resolve(window.btoa(readerEvent.target.result)) + } + + reader.readAsBinaryString(file) + }) +} + +export const FileTransfer = ({ sendEncryptedMessage }) => { + const fileInput = React.useRef(null); + + const supported = React.useMemo(() => + Boolean(window.File) && Boolean(window.FileReader) && Boolean(window.FileList) && Boolean(window.Blob) && + Boolean(window.btoa) && Boolean(window.atob) && Boolean(window.URL), + [] + ) + + React.useEffect(() => { + const currentFileInput = fileInput.current + let isMounted = true; + + const handleFileTransfer = async (event) => { + + const file = event.target.files && event.target.files[0] + + if (file) { + const fileType = file.type || 'file' + const fileName = sanitize(file.name) + const fileExtension = file.name.split('.').pop().toLowerCase() + + if (VALID_FILE_TYPES.indexOf(fileExtension) <= -1) { + // eslint-disable-next-line no-alert + alert('File type not supported') + return false + } + + if (file.size > MAX_FILE_SIZE * 1000000) { + // eslint-disable-next-line no-alert + alert(`Max filesize is ${MAX_FILE_SIZE}MB`) + return false + } + + const fileId = uuid.v4() + const fileData = { + id: fileId, + file, + fileName, + fileType, + encodedFile: await encodeFile(file), + } + + // Mounted component guard + if (!isMounted) { + return + } + + fileInput.current.value = '' + + sendEncryptedMessage({ + type: 'SEND_FILE', + payload: { + fileName: fileData.fileName, + encodedFile: fileData.encodedFile, + fileType: fileData.fileType, + timestamp: Date.now(), + }, + }) + } + + return false + } + + currentFileInput.addEventListener('change', handleFileTransfer) + + return () => { + isMounted = false + currentFileInput.removeEventListener('change', handleFileTransfer) + } + }, [sendEncryptedMessage]) + + if (!supported) { + return null + } + + return ( +
+ + +
+ ); +} + +FileTransfer.propTypes = { + sendEncryptedMessage: PropTypes.func.isRequired, +} + +export default FileTransfer \ No newline at end of file diff --git a/client/src/components/FileTransfer/FileTransfer.test.js b/client/src/components/FileTransfer/FileTransfer.test.js index 73ca8d3..052a9bc 100644 --- a/client/src/components/FileTransfer/FileTransfer.test.js +++ b/client/src/components/FileTransfer/FileTransfer.test.js @@ -2,89 +2,103 @@ import React from 'react'; import { render, screen, fireEvent, createEvent } from '@testing-library/react'; import FileTransfer from '.'; -test('FileTransfer component is displaying', async () => { - const { asFragment } = render( {}} />); +// Fake date +jest.spyOn(global.Date, 'now').mockImplementation(() => new Date('2020-03-14T11:01:58.135Z').valueOf()); - expect(asFragment()).toMatchSnapshot(); -}); - -// Skipped as broken in this component version. Should be fixed later. -test.skip('FileTransfer component detect bad browser support', async () => { +describe('FileTransfer tests', () => { const { File } = window; - // Remove one of component dependency - delete window.File; + beforeEach(() => { + // Restore original + window.File = File; + }); + afterEach(() => { + // Restore original + window.File = File; + }); - const { asFragment } = render( {}} />); + it('FileTransfer component is displaying', async () => { + const { asFragment } = render( {}} />); - expect(asFragment()).toMatchSnapshot(); + expect(asFragment()).toMatchSnapshot(); + }); - window.File = File; -}); - -test('Try to send file', async done => { - const sendEncryptedMessage = data => { - try { - expect(data).toMatchObject({ - payload: { encodedFile: 'dGV4dGZpbGU=', fileName: 'filename-png', fileType: 'text/plain' }, - type: 'SEND_FILE', - }); - done(); - } catch (error) { - done(error); - } - }; - - render(); - - const input = screen.getByPlaceholderText('Choose a file...'); - - const testFile = new File(['textfile'], 'filename.png', { type: 'text/plain' }); - - // Fire change event - fireEvent.change(input, { target: { files: [testFile] } }); -}); - -test('Try to send no file', async () => { - render( {}} />); - - const input = screen.getByPlaceholderText('Choose a file...'); - - // Fire change event - fireEvent.change(input, { target: { files: [] } }); -}); - -test('Try to send unsupported file', async () => { - window.alert = jest.fn(); - - render( {}} />); - - const input = screen.getByPlaceholderText('Choose a file...'); - - const testFile = new File(['textfile'], 'filename.fake', { type: 'text/plain' }); - - // Create thange event with fake file - const changeEvent = createEvent.change(input, { target: { files: [testFile] } }); - - // Fire change event - fireEvent(input, changeEvent); - - expect(window.alert).toHaveBeenCalledWith('File type not supported'); -}); - -test('Try to send too big file', async () => { - window.alert = jest.fn(); - - render( {}} />); - - const input = screen.getByPlaceholderText('Choose a file...'); - - var fileContent = new Uint8Array(4000001); - - const testFile = new File([fileContent], 'filename.png', { type: 'text/plain' }); - - // Fire change event - fireEvent.change(input, { target: { files: [testFile] } }); - - expect(window.alert).toHaveBeenCalledWith('Max filesize is 4MB'); + // Skipped as broken in this component version. Should be fixed later. + it.skip('FileTransfer component detect bad browser support', () => { + const { asFragment } = render( {}} />); + + expect(asFragment()).toMatchSnapshot(); + }); + + it('Try to send file', async done => { + const sendEncryptedMessage = data => { + try { + expect(data).toMatchObject({ + payload: { + encodedFile: 'dGV4dGZpbGU=', + fileName: 'filename.png', + fileType: 'text/plain', + timestamp: 1584183718135, + }, + type: 'SEND_FILE', + }); + done(); + } catch (error) { + done(error); + } + }; + + render(); + + const input = screen.getByPlaceholderText('Choose a file...'); + + const testFile = new File(['textfile'], 'filename.png', { type: 'text/plain' }); + + // Fire change event + fireEvent.change(input, { target: { files: [testFile] } }); + }); + + it('Try to send no file', async () => { + render( {}} />); + + const input = screen.getByPlaceholderText('Choose a file...'); + + // Fire change event + fireEvent.change(input, { target: { files: [] } }); + }); + + it('Try to send unsupported file', async () => { + window.alert = jest.fn(); + + render( {}} />); + + const input = screen.getByPlaceholderText('Choose a file...'); + + const testFile = new File(['textfile'], 'filename.fake', { type: 'text/plain' }); + + // Create thange event with fake file + const changeEvent = createEvent.change(input, { target: { files: [testFile] } }); + + // Fire change event + fireEvent(input, changeEvent); + + expect(window.alert).toHaveBeenCalledWith('File type not supported'); + }); + + it('Try to send too big file', async () => { + window.alert = jest.fn(); + + render( {}} />); + + const input = screen.getByPlaceholderText('Choose a file...'); + + var fileContent = new Uint8Array(4000001); + + const testFile = new File([fileContent], 'filename.png', { type: 'text/plain' }); + + // Fire change event + fireEvent.change(input, { target: { files: [testFile] } }); + + expect(window.alert).toHaveBeenCalledWith('Max filesize is 4MB'); + }); }); diff --git a/client/src/components/FileTransfer/__snapshots__/FileTransfer.test.js.snap b/client/src/components/FileTransfer/__snapshots__/FileTransfer.test.js.snap index 221cab1..d66f81f 100644 --- a/client/src/components/FileTransfer/__snapshots__/FileTransfer.test.js.snap +++ b/client/src/components/FileTransfer/__snapshots__/FileTransfer.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`FileTransfer component is displaying 1`] = ` +exports[`FileTransfer tests FileTransfer component is displaying 1`] = `