Add unit tests for 'download-file' command

Reviewed By: mweststrate

Differential Revision: D32926830

fbshipit-source-id: fbd4dcb910ffdcdc365f5f0b4c401423f0256824
This commit is contained in:
Andrey Goncharov
2021-12-10 06:34:37 -08:00
committed by Facebook GitHub Bot
parent 6aa0cef927
commit 9436c32ce9
5 changed files with 244 additions and 3 deletions

View File

@@ -41,7 +41,11 @@
"xdg-basedir": "^4.0.0" "xdg-basedir": "^4.0.0"
}, },
"devDependencies": { "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": {}, "peerDependencies": {},
"scripts": { "scripts": {

View File

@@ -231,7 +231,6 @@ export class FlipperServerImpl implements FlipperServer {
'node-api-fs-unlink': unlink, 'node-api-fs-unlink': unlink,
'node-api-fs-mkdir': mkdir, 'node-api-fs-mkdir': mkdir,
'node-api-fs-copyFile': copyFile, 'node-api-fs-copyFile': copyFile,
// TODO: Unit tests
// TODO: Do we need API to cancel an active download? // TODO: Do we need API to cancel an active download?
'download-file-start': commandDownloadFileStartFactory( 'download-file-start': commandDownloadFileStartFactory(
this.emit.bind(this), this.emit.bind(this),

View File

@@ -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<any>;
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<void>((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(),
});
});
});
});

View File

@@ -2,7 +2,8 @@
"extends": "../tsconfig.base.json", "extends": "../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"outDir": "lib", "outDir": "lib",
"rootDir": "src" "rootDir": "src",
"esModuleInterop": true
}, },
"references": [ "references": [
{ {

View File

@@ -2795,6 +2795,13 @@
dependencies: dependencies:
"@types/unist" "*" "@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@*": "@types/mime@*":
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" 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" resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.1.tgz#83ecf4ec22a8c218c71db25f316619fe5b986011"
integrity sha512-7cTXwKP/HLOPVgjg+YhBdQ7bMiobGMuoBmrGmqwIWJv8elC6t1DfVc/mn4fD9UE1IjhwmhaQ5pGVXkmXbH0rhg== 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": "@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3":
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" 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" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== 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: merge-descriptors@1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"