From 9436c32ce99a4d005cc2c5e3519cc11f4389b485 Mon Sep 17 00:00:00 2001 From: Andrey Goncharov Date: Fri, 10 Dec 2021 06:34:37 -0800 Subject: [PATCH] Add unit tests for 'download-file' command Reviewed By: mweststrate Differential Revision: D32926830 fbshipit-source-id: fbd4dcb910ffdcdc365f5f0b4c401423f0256824 --- desktop/flipper-server-core/package.json | 6 +- .../src/FlipperServerImpl.tsx | 1 - .../commands/__tests__/DownloadFile.node.tsx | 220 ++++++++++++++++++ desktop/flipper-server-core/tsconfig.json | 3 +- desktop/yarn.lock | 17 ++ 5 files changed, 244 insertions(+), 3 deletions(-) create mode 100644 desktop/flipper-server-core/src/commands/__tests__/DownloadFile.node.tsx diff --git a/desktop/flipper-server-core/package.json b/desktop/flipper-server-core/package.json index cb5d1c78c..4df4cf248 100644 --- a/desktop/flipper-server-core/package.json +++ b/desktop/flipper-server-core/package.json @@ -41,7 +41,11 @@ "xdg-basedir": "^4.0.0" }, "devDependencies": { - "@types/node": "^15.12.5" + "@types/memorystream": "^0.3.0", + "@types/node": "^15.12.5", + "@types/tmp": "^0.2.2", + "memorystream": "^0.3.1", + "tmp": "^0.2.1" }, "peerDependencies": {}, "scripts": { diff --git a/desktop/flipper-server-core/src/FlipperServerImpl.tsx b/desktop/flipper-server-core/src/FlipperServerImpl.tsx index 8b5cb6ba3..88d423792 100644 --- a/desktop/flipper-server-core/src/FlipperServerImpl.tsx +++ b/desktop/flipper-server-core/src/FlipperServerImpl.tsx @@ -231,7 +231,6 @@ export class FlipperServerImpl implements FlipperServer { 'node-api-fs-unlink': unlink, 'node-api-fs-mkdir': mkdir, 'node-api-fs-copyFile': copyFile, - // TODO: Unit tests // TODO: Do we need API to cancel an active download? 'download-file-start': commandDownloadFileStartFactory( this.emit.bind(this), diff --git a/desktop/flipper-server-core/src/commands/__tests__/DownloadFile.node.tsx b/desktop/flipper-server-core/src/commands/__tests__/DownloadFile.node.tsx new file mode 100644 index 000000000..c1af0b908 --- /dev/null +++ b/desktop/flipper-server-core/src/commands/__tests__/DownloadFile.node.tsx @@ -0,0 +1,220 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +import axios from 'axios'; +import MemoryStream from 'memorystream'; +import {dirSync} from 'tmp'; +import * as uuid from 'uuid'; +import {commandDownloadFileStartFactory} from '../DownloadFile'; + +describe('commands', () => { + describe('DownloadFile', () => { + let commandDownloadFileStart: ReturnType< + typeof commandDownloadFileStartFactory + >; + let emit: jest.Mock; + + beforeEach(() => { + emit = jest.fn(); + commandDownloadFileStart = commandDownloadFileStartFactory(emit); + }); + + test('downloads file and reports the progress', async () => { + const fakeDownloadStream = new MemoryStream(); + const fakeFileSize = 10; + const fakeHeaders = { + 'content-length': fakeFileSize.toString(), + }; + const fakeStatus = 200; + const fakeStatusText = 'Flipper rocks'; + + const requestSpy = jest + .spyOn(axios, 'request') + .mockImplementation(async () => ({ + headers: fakeHeaders, + status: fakeStatus, + statusText: fakeStatusText, + data: fakeDownloadStream, + })); + + const {name: tmpDirName} = dirSync({ + unsafeCleanup: true, + }); + const dest = `${tmpDirName}/flipperTest`; + + expect(requestSpy).toBeCalledTimes(0); + + const fakeDownloadURL = 'https://flipper.rocks'; + const fakeUuid = 'flipper42'; + jest.spyOn(uuid, 'v4').mockImplementation(() => fakeUuid); + + const downloadFileDescriptor = await commandDownloadFileStart( + fakeDownloadURL, + dest, + { + overwrite: true, + }, + ); + + expect(requestSpy).toBeCalledTimes(1); + // Expect first argument of the first fn call to amtch object + expect(requestSpy.mock.calls[0][0]).toMatchObject({ + method: 'GET', + url: fakeDownloadURL, + }); + + expect(downloadFileDescriptor.headers).toBe(fakeHeaders); + expect(downloadFileDescriptor.status).toBe(fakeStatus); + expect(downloadFileDescriptor.statusText).toBe(fakeStatusText); + expect(downloadFileDescriptor.id).toBe(fakeUuid); + + expect(emit).toBeCalledTimes(0); + await new Promise((resolve) => fakeDownloadStream.write('Luke', resolve)); + expect(emit).toBeCalledTimes(1); + expect(emit).toBeCalledWith('download-file-update', { + id: fakeUuid, + downloaded: 4, + totalSize: fakeFileSize, + status: 'downloading', + }); + + await new Promise((resolve) => fakeDownloadStream.write('Obi', resolve)); + expect(emit).toBeCalledTimes(2); + expect(emit).toBeCalledWith('download-file-update', { + id: fakeUuid, + downloaded: 7, + totalSize: fakeFileSize, + status: 'downloading', + }); + + const lastFileUpdateCalled = new Promise((resolve) => + emit.mockImplementationOnce(resolve), + ); + + fakeDownloadStream.end(); + + await lastFileUpdateCalled; + + expect(emit).toBeCalledTimes(3); + expect(emit).toBeCalledWith('download-file-update', { + id: fakeUuid, + downloaded: 7, + totalSize: fakeFileSize, + status: 'success', + }); + }); + + test('rejects "complete" promise if download file readable stream errors', async () => { + const fakeDownloadStream = new MemoryStream(); + const fakeFileSize = 10; + const fakeHeaders = { + 'content-length': fakeFileSize.toString(), + }; + const fakeStatus = 200; + const fakeStatusText = 'Flipper rocks'; + + jest.spyOn(axios, 'request').mockImplementation(async () => ({ + headers: fakeHeaders, + status: fakeStatus, + statusText: fakeStatusText, + data: fakeDownloadStream, + })); + + const {name: tmpDirName} = dirSync({ + unsafeCleanup: true, + }); + const dest = `${tmpDirName}/flipperTest`; + + const fakeDownloadURL = 'https://flipper.rocks'; + const fakeUuid = 'flipper42'; + jest.spyOn(uuid, 'v4').mockImplementation(() => fakeUuid); + + await commandDownloadFileStart(fakeDownloadURL, dest, { + overwrite: true, + }); + + const lastFileUpdateCalled = new Promise((resolve) => + emit.mockImplementationOnce(resolve), + ); + + const fakeError = new Error('Ooops'); + fakeDownloadStream.destroy(fakeError); + + await lastFileUpdateCalled; + + expect(emit).toBeCalledTimes(1); + expect(emit).toBeCalledWith('download-file-update', { + id: fakeUuid, + downloaded: 0, + totalSize: fakeFileSize, + status: 'error', + message: 'Ooops', + stack: expect.anything(), + }); + }); + + test('rejects "complete" promise if writeable stream errors', async () => { + const fakeDownloadStream = new MemoryStream(); + const fakeFileSize = 10; + const fakeHeaders = { + 'content-length': fakeFileSize.toString(), + }; + const fakeStatus = 200; + const fakeStatusText = 'Flipper rocks'; + + jest.spyOn(axios, 'request').mockImplementation(async () => ({ + headers: fakeHeaders, + status: fakeStatus, + statusText: fakeStatusText, + data: fakeDownloadStream, + })); + + const {name: tmpDirName, removeCallback: removeTmpDir} = dirSync({ + unsafeCleanup: true, + }); + const dest = `${tmpDirName}/flipperTest`; + + const fakeDownloadURL = 'https://flipper.rocks'; + const fakeUuid = 'flipper42'; + jest.spyOn(uuid, 'v4').mockImplementation(() => fakeUuid); + + await commandDownloadFileStart(fakeDownloadURL, dest, { + overwrite: true, + }); + + const lastFileUpdateCalled = new Promise((resolve) => + emit.mockImplementation( + (_event, {status}) => status === 'error' && resolve(), + ), + ); + + // We remove the end file to cause an error + removeTmpDir(); + fakeDownloadStream.write('Obi'); + + await lastFileUpdateCalled; + + expect(emit).toBeCalledTimes(2); + expect(emit).toHaveBeenNthCalledWith(1, 'download-file-update', { + id: fakeUuid, + downloaded: 3, + totalSize: fakeFileSize, + status: 'downloading', + }); + expect(emit).toHaveBeenNthCalledWith(2, 'download-file-update', { + id: fakeUuid, + downloaded: 3, + totalSize: fakeFileSize, + status: 'error', + message: expect.stringContaining('ENOENT'), + stack: expect.anything(), + }); + }); + }); +}); diff --git a/desktop/flipper-server-core/tsconfig.json b/desktop/flipper-server-core/tsconfig.json index 015b69582..b50a22606 100644 --- a/desktop/flipper-server-core/tsconfig.json +++ b/desktop/flipper-server-core/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../tsconfig.base.json", "compilerOptions": { "outDir": "lib", - "rootDir": "src" + "rootDir": "src", + "esModuleInterop": true }, "references": [ { diff --git a/desktop/yarn.lock b/desktop/yarn.lock index 17c728f56..4575afeed 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -2795,6 +2795,13 @@ dependencies: "@types/unist" "*" +"@types/memorystream@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@types/memorystream/-/memorystream-0.3.0.tgz#7616df4c42a479805d052a058d990b879d5e368f" + integrity sha512-gzh6mqZcLryYHn4g2MuMWjo9J1+Py/XYwITyZmUxV7ZoBIi7bTbBgSiuC5tcm3UL3gmaiYssQFDlXr/3fK94cw== + dependencies: + "@types/node" "*" + "@types/mime@*": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" @@ -3147,6 +3154,11 @@ resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.1.tgz#83ecf4ec22a8c218c71db25f316619fe5b986011" integrity sha512-7cTXwKP/HLOPVgjg+YhBdQ7bMiobGMuoBmrGmqwIWJv8elC6t1DfVc/mn4fD9UE1IjhwmhaQ5pGVXkmXbH0rhg== +"@types/tmp@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.2.tgz#424537a3b91828cb26aaf697f21ae3cd1b69f7e7" + integrity sha512-MhSa0yylXtVMsyT8qFpHA1DLHj4DvQGH5ntxrhHSh8PxUVNi35Wk+P5hVgqbO2qZqOotqr9jaoPRL+iRjWYm/A== + "@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" @@ -9388,6 +9400,11 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== +memorystream@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" + integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI= + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"