diff --git a/packages/fx-core/package.json b/packages/fx-core/package.json index a6c78931f1..b6f8810543 100644 --- a/packages/fx-core/package.json +++ b/packages/fx-core/package.json @@ -65,6 +65,7 @@ "test:projcheck": "nyc mocha \"tests/common/projectTypeChecker.test.ts\"", "test:telemetry": "nyc mocha \"tests/common/telemetry.test.ts\"", "test:stringUtils": "nyc mocha \"tests/common/stringUtils.test.ts\"", + "test:generatorUtils": "nyc mocha \"tests/component/generatorUtils.test.ts\"", "clean": "rm -rf build", "prebuild": "npm run gen:cli", "build": "rimraf build && npx tsc -p ./", diff --git a/packages/fx-core/src/component/generator/officeAddin/generator.ts b/packages/fx-core/src/component/generator/officeAddin/generator.ts index 8009097395..8ee82b40dc 100644 --- a/packages/fx-core/src/component/generator/officeAddin/generator.ts +++ b/packages/fx-core/src/component/generator/officeAddin/generator.ts @@ -39,6 +39,7 @@ import { toLower } from "lodash"; import { convertToLangKey } from "../utils"; import { DefaultTemplateGenerator } from "../templates/templateGenerator"; import { TemplateInfo } from "../templates/templateInfo"; +import { fetchAndUnzip } from "../../utils"; const componentName = "office-addin"; const telemetryEvent = "generate"; @@ -141,7 +142,10 @@ export class OfficeAddinGenerator { // Copy project template files from project repository if (projectLink) { - await HelperMethods.downloadProjectTemplateZipFile(addinRoot, projectLink); + const fetchRes = await fetchAndUnzip("office-addin-generator", projectLink, addinRoot); + if (fetchRes.isErr()) { + return err(fetchRes.error); + } let cmdLine = ""; // Call 'convert-to-single-host' npm script in generated project, passing in host parameter if (inputs[QuestionNames.ProjectType] === ProjectTypeOptions.officeAddin().id) { cmdLine = `npm run convert-to-single-host --if-present -- ${host} json`; diff --git a/packages/fx-core/src/component/generator/officeAddin/helperMethods.ts b/packages/fx-core/src/component/generator/officeAddin/helperMethods.ts index d75fd4e18e..a1b827b8f8 100644 --- a/packages/fx-core/src/component/generator/officeAddin/helperMethods.ts +++ b/packages/fx-core/src/component/generator/officeAddin/helperMethods.ts @@ -16,82 +16,6 @@ import { AccessGithubError, ReadFileError } from "../../../error/common"; const zipFile = "project.zip"; export class HelperMethods { - static async downloadProjectTemplateZipFile( - projectFolder: string, - projectRepo: string - ): Promise { - const projectTemplateZipFile = projectRepo; - let response: any; - try { - response = await fetch(projectTemplateZipFile, { method: "GET" }); - } catch (e: any) { - throw new AccessGithubError(projectTemplateZipFile, "OfficeAddinGenerator", e); - } - - return new Promise((resolve, reject) => { - if (response.body) { - response.body - .pipe(fs.createWriteStream(path.resolve(projectFolder, zipFile))) - .on("error", (err: Error) => { - reject(new AccessGithubError(projectTemplateZipFile, "OfficeAddinGenerator", err)); - }) - .on("close", () => { - HelperMethods.unzipProjectTemplate(projectFolder) - .then(() => { - resolve(); - }) - .catch((err) => { - reject(err); - }); - }); - } else { - reject( - new AccessGithubError( - projectTemplateZipFile, - "OfficeAddinGenerator", - new Error(`Response body of GET "${projectTemplateZipFile}" is null.`) - ) - ); - } - }); - } - - static async unzipProjectTemplate(projectFolder: string): Promise { - return new Promise((resolve, reject) => { - // TODO: Verify file exists - const readStream = fs.createReadStream(path.resolve(`${projectFolder}/${zipFile}`)); - readStream - .pipe(unzip.Extract({ path: projectFolder })) - .on("error", function (err: Error) { - unzipErrorHandler(projectFolder, reject, err); - }) - .on("close", () => { - HelperMethods.moveUnzippedFiles(projectFolder); - resolve(); - }); - }); - } - - static moveUnzippedFiles(projectFolder: string): void { - // delete original zip file - const zipFilePath = path.resolve(`${projectFolder}/${zipFile}`); - if (fs.existsSync(zipFilePath)) { - fs.unlinkSync(zipFilePath); - } - - // get path to unzipped folder - const unzippedFolder = fs.readdirSync(projectFolder).filter(function (file) { - return fs.statSync(`${projectFolder}/${file}`).isDirectory(); - }); - - // construct paths to move files out of unzipped folder into project root folder - const fromFolder = path.resolve(`${projectFolder}/${unzippedFolder[0]}`); - HelperMethods.copyAddinFiles(fromFolder, projectFolder); - - // delete project zipped folder - fs.rmSync(fromFolder, { recursive: true, force: true }); - } - static copyAddinFiles(fromFolder: string, toFolder: string): void { fse.copySync(fromFolder, toFolder, { filter: (path) => !path.includes("node_modules"), diff --git a/packages/fx-core/src/component/generator/officeXMLAddin/generator.ts b/packages/fx-core/src/component/generator/officeXMLAddin/generator.ts index 027511b8af..fe38772437 100644 --- a/packages/fx-core/src/component/generator/officeXMLAddin/generator.ts +++ b/packages/fx-core/src/component/generator/officeXMLAddin/generator.ts @@ -20,6 +20,7 @@ import { Generator } from "../generator"; import { HelperMethods } from "../officeAddin/helperMethods"; import { getOfficeAddinTemplateConfig } from "./projectConfig"; import { convertToLangKey } from "../utils"; +import { fetchAndUnzip } from "../../utils"; const COMPONENT_NAME = "office-xml-addin"; const TELEMETRY_EVENT = "generate"; @@ -85,8 +86,14 @@ export class OfficeXMLAddinGenerator { // [Condition]: Project have remote repo (not manifest-only proj) // -> Step: Download the project from GitHub - await HelperMethods.downloadProjectTemplateZipFile(destinationPath, projectLink); - + const fetchRes = await fetchAndUnzip( + "office-xml-addin-generator", + projectLink, + destinationPath + ); + if (fetchRes.isErr()) { + return err(fetchRes.error); + } // -> Step: Convert to single Host await OfficeXMLAddinGenerator.childProcessExec( `npm run convert-to-single-host --if-present -- ${_.toLower(host)}` diff --git a/packages/fx-core/src/component/utils.ts b/packages/fx-core/src/component/utils.ts index 89bfd5be14..8fcab6c116 100644 --- a/packages/fx-core/src/component/utils.ts +++ b/packages/fx-core/src/component/utils.ts @@ -2,9 +2,22 @@ // Licensed under the MIT license. "use strict"; -import { Context, FxError, Inputs, TelemetryReporter, UserError } from "@microsoft/teamsfx-api"; +import { + Context, + FxError, + Inputs, + Result, + TelemetryReporter, + UserError, + err, + ok, +} from "@microsoft/teamsfx-api"; +import AdmZip from "adm-zip"; +import fs from "fs-extra"; import { cloneDeep } from "lodash"; +import path from "path"; import { TOOLS } from "../core/globalVars"; +import { AccessGithubError, WriteFileError } from "../error/common"; import { ComponentNames, Scenarios, @@ -12,6 +25,7 @@ import { SolutionTelemetryProperty, } from "./constants"; import { DriverContext } from "./driver/interface/commonArgs"; +import { fetchZipFromUrl } from "./generator/utils"; import { getComponent, getComponentByScenario } from "./workflow"; export function createContextV3(): Context { @@ -111,3 +125,48 @@ export function sendErrorTelemetryThenReturnError( reporter?.sendTelemetryErrorEvent(eventName, properties, measurements, errorProps); return error; } + +export async function fetchAndUnzip( + component: string, + zipUrl: string, + targetDir: string, + skipRootFolder = true +): Promise> { + let zip: AdmZip; + try { + zip = await fetchZipFromUrl(zipUrl); + } catch (e: any) { + return err(new AccessGithubError(zipUrl, component, e)); + } + if (!zip) { + return err( + new AccessGithubError( + zipUrl, + component, + new Error(`Failed to fetch zip from url: ${zipUrl}, result is undefined.`) + ) + ); + } + const entries = zip.getEntries(); + let rootFolderName = ""; + for (const entry of entries) { + const entryName: string = entry.entryName; + if (skipRootFolder && !rootFolderName) { + rootFolderName = entryName; + continue; + } + const rawEntryData: Buffer = entry.getData(); + const entryData: string | Buffer = rawEntryData; + const targetPath = path.join(targetDir, entryName.replace(rootFolderName, "")); + try { + if (entry.isDirectory) { + await fs.ensureDir(targetPath); + } else { + await fs.writeFile(targetPath, entryData); + } + } catch (error: any) { + return err(new WriteFileError(error, component)); + } + } + return ok(undefined); +} diff --git a/packages/fx-core/tests/component/generator/officeAddinGenerator.test.ts b/packages/fx-core/tests/component/generator/officeAddinGenerator.test.ts index a23e50aa76..8c61dca8e4 100644 --- a/packages/fx-core/tests/component/generator/officeAddinGenerator.test.ts +++ b/packages/fx-core/tests/component/generator/officeAddinGenerator.test.ts @@ -1,4 +1,3 @@ -import { Capability } from "./../../../../tests/src/utils/constants"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. @@ -18,34 +17,29 @@ import { } from "@microsoft/teamsfx-api"; import * as chai from "chai"; import * as childProcess from "child_process"; -import EventEmitter from "events"; import fs from "fs"; import fse from "fs-extra"; import "mocha"; import mockfs from "mock-fs"; import mockedEnv, { RestoreFn } from "mocked-env"; -import * as fetch from "node-fetch"; import { OfficeAddinManifest } from "office-addin-manifest"; import * as path from "path"; import proxyquire from "proxyquire"; import * as sinon from "sinon"; -import * as unzip from "unzipper"; import * as uuid from "uuid"; import { cpUtils } from "../../../src/common/deps-checker"; import { manifestUtils } from "../../../src/component/driver/teamsApp/utils/ManifestUtils"; import { Generator } from "../../../src/component/generator/generator"; import { + getHost, OfficeAddinGenerator, OfficeAddinGeneratorNew, - getHost, } from "../../../src/component/generator/officeAddin/generator"; -import { - HelperMethods, - unzipErrorHandler, -} from "../../../src/component/generator/officeAddin/helperMethods"; +import { HelperMethods } from "../../../src/component/generator/officeAddin/helperMethods"; +import * as componentUtils from "../../../src/component/utils"; import { createContextV3 } from "../../../src/component/utils"; import { setTools } from "../../../src/core/globalVars"; -import { AccessGithubError, UserCancelError } from "../../../src/error"; +import { UserCancelError } from "../../../src/error"; import { CapabilityOptions, OfficeAddinHostOptions, @@ -195,7 +189,7 @@ describe("OfficeAddinGenerator for Outlook Addin", function () { inputs[QuestionNames.ProgrammingLanguage] = "typescript"; sinon.stub(OfficeAddinGenerator, "childProcessExec").resolves(); - sinon.stub(HelperMethods, "downloadProjectTemplateZipFile").resolves(undefined); + sinon.stub(componentUtils, "fetchAndUnzip").resolves(ok(undefined)); sinon.stub(OfficeAddinManifest, "modifyManifestFile").resolves({}); const result = await OfficeAddinGenerator.doScaffolding(context, inputs, testFolder); @@ -215,7 +209,7 @@ describe("OfficeAddinGenerator for Outlook Addin", function () { inputs[QuestionNames.ProgrammingLanguage] = "typescript"; sinon.stub(OfficeAddinGenerator, "childProcessExec").resolves(); - sinon.stub(HelperMethods, "downloadProjectTemplateZipFile").resolves(undefined); + sinon.stub(componentUtils, "fetchAndUnzip").resolves(ok(undefined)); sinon.stub(OfficeAddinManifest, "modifyManifestFile").resolves({}); const result = await OfficeAddinGenerator.doScaffolding(context, inputs, testFolder); @@ -234,7 +228,7 @@ describe("OfficeAddinGenerator for Outlook Addin", function () { inputs[QuestionNames.ProgrammingLanguage] = "typescript"; sinon.stub(OfficeAddinGenerator, "childProcessExec").resolves(); - sinon.stub(HelperMethods, "downloadProjectTemplateZipFile").rejects(new UserCancelError()); + sinon.stub(componentUtils, "fetchAndUnzip").rejects(new UserCancelError()); sinon.stub(OfficeAddinManifest, "modifyManifestFile").resolves({}); const result = await OfficeAddinGenerator.doScaffolding(context, inputs, testFolder); @@ -416,7 +410,7 @@ describe("OfficeAddinGenerator for Outlook Addin", function () { }); }); -describe("helperMethods", async () => { +describe("HelperMethods", async () => { describe("updateManifest", () => { const sandbox = sinon.createSandbox(); const manifestPath = "manifestPath"; @@ -474,172 +468,6 @@ describe("helperMethods", async () => { }); }); - describe("downloadProjectTemplateZipFile", async () => { - const sandbox = sinon.createSandbox(); - class ResponseData extends EventEmitter { - pipe(ws: fs.WriteStream) { - return this; - } - } - - class MockedWriteStream { - on(event: string, cb: () => void) { - return this; - } - } - - afterEach(() => { - sandbox.restore(); - }); - it("should fetch fail", async () => { - const resp = new ResponseData(); - sandbox.stub(fetch, "default").rejects(new Error()); - const mockedStream = new MockedWriteStream(); - const unzipStub = sandbox.stub(HelperMethods, "unzipProjectTemplate").resolves(); - sandbox.stub(fs, "createWriteStream").returns(mockedStream); - try { - await HelperMethods.downloadProjectTemplateZipFile("", ""); - chai.assert.fail("should not reach here"); - } catch (e) { - chai.assert.isTrue(e instanceof AccessGithubError); - } - }); - it("should download project template zip file", async () => { - const resp = new ResponseData(); - sandbox.stub(fetch, "default").resolves({ body: resp } as any); - const mockedStream = new MockedWriteStream(); - const unzipStub = sandbox.stub(HelperMethods, "unzipProjectTemplate").resolves(); - sandbox.stub(fs, "createWriteStream").returns(mockedStream); - const promise = HelperMethods.downloadProjectTemplateZipFile("", ""); - // manully wait for the close event to be registered - await new Promise((resolve) => setTimeout(resolve, 100)); - resp.emit("close"); - await promise; - chai.assert.isTrue(unzipStub.calledOnce); - }); - - it("unzipProjectTemplate error", async () => { - const resp = new ResponseData(); - sandbox.stub(fetch, "default").resolves({ body: resp } as any); - const mockedStream = new MockedWriteStream(); - sandbox.stub(HelperMethods, "unzipProjectTemplate").rejects(new Error()); - sandbox.stub(fs, "createWriteStream").returns(mockedStream); - const promise = HelperMethods.downloadProjectTemplateZipFile("", ""); - // manully wait for the close event to be registered - await new Promise((resolve) => setTimeout(resolve, 100)); - resp.emit("close"); - try { - await promise; - chai.assert.fail("should throw error"); - } catch (e) {} - }); - - it("download error", async () => { - const resp = new ResponseData(); - sandbox.stub(fetch, "default").resolves({ body: resp } as any); - const mockedStream = new MockedWriteStream(); - const unzipStub = sandbox.stub(HelperMethods, "unzipProjectTemplate").resolves(); - sandbox.stub(fs, "createWriteStream").returns(mockedStream); - const promise = HelperMethods.downloadProjectTemplateZipFile("", ""); - // manully wait for the close event to be registered - await new Promise((resolve) => setTimeout(resolve, 100)); - resp.emit("error", new Error()); - try { - await promise; - chai.assert.fail("should throw error"); - } catch (e) {} - chai.assert.isTrue(unzipStub.notCalled); - }); - - it("Response body is null.", async () => { - sandbox.stub(fetch, "default").resolves({ body: null } as any); - const promise = HelperMethods.downloadProjectTemplateZipFile("", ""); - try { - await promise; - chai.assert.fail("should throw error"); - } catch (e) { - chai.assert.isTrue(e instanceof AccessGithubError); - } - }); - }); - - describe("unzipProjectTemplate", () => { - const sandbox = sinon.createSandbox(); - - class MockedReadStream { - on(event: string, cb: () => void) { - return this; - } - - pipe(ws: fs.WriteStream) { - return this; - } - } - - afterEach(() => { - sandbox.restore(); - }); - - it("work as expected", async () => { - sandbox.stub(fs, "createReadStream").returns(new MockedReadStream()); - sandbox.stub(unzip, "Extract").returns({}); - try { - HelperMethods.unzipProjectTemplate(""); - } catch (err) { - chai.assert.fail(err); - } finally { - sandbox.restore(); - } - }); - - it("unzipErrorHandler", async () => { - let i = 0; - const reject = () => { - i++; - }; - unzipErrorHandler("", reject, new Error()); - chai.assert.equal(i, 1); - }); - it("unzipErrorHandler 2", async () => { - let i = 0; - const reject = () => { - i++; - }; - unzipErrorHandler("", reject, new Error("test")); - chai.assert.equal(i, 1); - }); - }); - - describe("moveUnzippedFiles", () => { - const projectRoot = "/home/user/teamsapp"; - - beforeEach(() => { - mockfs({ - "/home/user/teamsapp/project.zip": "xxx", - "/home/user/teamsapp/project": { - file1: "xxx", - file2: "yyy", - }, - }); - }); - - afterEach(() => { - mockfs.restore(); - }); - - it("should remove zip file and unzipped folder and copy files", async () => { - try { - HelperMethods.moveUnzippedFiles(projectRoot); - chai.assert.equal(fs.existsSync("/home/user/teamsapp/project.zip"), false); - chai.assert.equal(fs.existsSync("/home/user/teamsapp/project"), false); - chai.assert.equal(fs.existsSync("/home/user/teamsapp/file1"), true); - chai.assert.equal(fs.existsSync("/home/user/teamsapp/file2"), true); - } catch (err) { - chai.assert.fail(err); - } - }); - }); - describe("copyAddinFiles", () => { const projectRoot = "/home/user/teamsapp"; @@ -855,7 +683,7 @@ describe("OfficeAddinGenerator for Office Addin", function () { inputs[QuestionNames.ProgrammingLanguage] = "typescript"; sinon.stub(OfficeAddinGenerator, "childProcessExec").resolves(); - sinon.stub(HelperMethods, "downloadProjectTemplateZipFile").resolves(undefined); + sinon.stub(componentUtils, "fetchAndUnzip").resolves(ok(undefined)); sinon.stub(OfficeAddinManifest, "modifyManifestFile").resolves({}); const result = await OfficeAddinGenerator.doScaffolding(context, inputs, testFolder); @@ -874,7 +702,7 @@ describe("OfficeAddinGenerator for Office Addin", function () { inputs[QuestionNames.ProgrammingLanguage] = "typescript"; sinon.stub(OfficeAddinGenerator, "childProcessExec").resolves(); - sinon.stub(HelperMethods, "downloadProjectTemplateZipFile").resolves(undefined); + sinon.stub(componentUtils, "fetchAndUnzip").resolves(ok(undefined)); sinon.stub(OfficeAddinManifest, "modifyManifestFile").resolves({}); const result = await OfficeAddinGenerator.doScaffolding(context, inputs, testFolder); @@ -894,7 +722,7 @@ describe("OfficeAddinGenerator for Office Addin", function () { inputs[QuestionNames.OfficeAddinFramework] = "default"; sinon.stub(OfficeAddinGenerator, "childProcessExec").resolves(); - sinon.stub(HelperMethods, "downloadProjectTemplateZipFile").rejects(new UserCancelError()); + sinon.stub(componentUtils, "fetchAndUnzip").rejects(new UserCancelError()); sinon.stub(OfficeAddinManifest, "modifyManifestFile").resolves({}); const result = await OfficeAddinGenerator.doScaffolding(context, inputs, testFolder); @@ -1144,29 +972,11 @@ describe("OfficeAddinGenerator for Office Addin", function () { result.isOk() && stub.calledWith(context, testFolder, "office-json-addin", "js") ); }); - - // it("should scaffold taskpane successfully on happy path if capability is office-content-addin", async () => { - // const inputs: Inputs = { - // platform: Platform.CLI, - // projectPath: testFolder, - // "project-type": ProjectTypeOptions.officeAddin().id, - // "app-name": "office-addin-test", - // "office-addin-framework-type": "default", - // }; - // inputs[QuestionNames.Capabilities] = CapabilityOptions.officeContentAddin().id; - // inputs[QuestionNames.OfficeAddinFolder] = undefined; - // inputs[QuestionNames.ProgrammingLanguage] = "typescript"; - - // sinon.stub(OfficeAddinGenerator, "childProcessExec").resolves(); - // sinon.stub(HelperMethods, "downloadProjectTemplateZipFile").resolves(undefined); - // sinon.stub(OfficeAddinManifest, "modifyManifestFile").resolves({}); - // const result = await OfficeAddinGenerator.doScaffolding(context, inputs, testFolder); - - // chai.expect(result.isOk()).to.eq(true); - // }); }); describe("OfficeAddinGeneratorNew", () => { + const gtools = new MockTools(); + setTools(gtools); const generator = new OfficeAddinGeneratorNew(); const context = createContextV3(); describe("active()", () => { diff --git a/packages/fx-core/tests/component/generator/officeXMLAddinGenerator.test.ts b/packages/fx-core/tests/component/generator/officeXMLAddinGenerator.test.ts index 1c3875ab33..dd26c3b875 100644 --- a/packages/fx-core/tests/component/generator/officeXMLAddinGenerator.test.ts +++ b/packages/fx-core/tests/component/generator/officeXMLAddinGenerator.test.ts @@ -5,7 +5,7 @@ * @author zyun@microsoft.com */ -import { Context, Inputs, ok, Platform, err, SystemError } from "@microsoft/teamsfx-api"; +import { Context, Inputs, Platform, SystemError, err, ok } from "@microsoft/teamsfx-api"; import * as chai from "chai"; import * as childProcess from "child_process"; import fs from "fs"; @@ -16,16 +16,17 @@ import { OfficeAddinManifest } from "office-addin-manifest"; import * as path from "path"; import * as sinon from "sinon"; import * as uuid from "uuid"; +import { FeatureFlagName } from "../../../src/common/constants"; import { cpUtils } from "../../../src/common/deps-checker"; import { Generator } from "../../../src/component/generator/generator"; -import { OfficeXMLAddinGenerator } from "../../../src/component/generator/officeXMLAddin/generator"; import { HelperMethods } from "../../../src/component/generator/officeAddin/helperMethods"; +import { OfficeXMLAddinGenerator } from "../../../src/component/generator/officeXMLAddin/generator"; +import { getOfficeAddinTemplateConfig } from "../../../src/component/generator/officeXMLAddin/projectConfig"; +import * as componentUtils from "../../../src/component/utils"; import { createContextV3 } from "../../../src/component/utils"; import { setTools } from "../../../src/core/globalVars"; import { OfficeAddinHostOptions, ProjectTypeOptions, QuestionNames } from "../../../src/question"; import { MockTools } from "../../core/utils"; -import { FeatureFlagName } from "../../../src/common/constants"; -import { getOfficeAddinTemplateConfig } from "../../../src/component/generator/officeXMLAddin/projectConfig"; describe("OfficeXMLAddinGenerator", function () { const testFolder = path.resolve("./tmp"); @@ -89,7 +90,7 @@ describe("OfficeXMLAddinGenerator", function () { [QuestionNames.ProgrammingLanguage]: "typescript", }; - sinon.stub(HelperMethods, "downloadProjectTemplateZipFile").resolves(undefined); + sinon.stub(componentUtils, "fetchAndUnzip").resolves(ok(undefined)); sinon.stub(OfficeXMLAddinGenerator, "childProcessExec").resolves(); sinon.stub(OfficeAddinManifest, "modifyManifestFile").resolves({}); sinon.stub(Generator, "generateTemplate").resolves(ok(undefined)); @@ -129,7 +130,7 @@ describe("OfficeXMLAddinGenerator", function () { [QuestionNames.ProgrammingLanguage]: "typescript", }; - sinon.stub(HelperMethods, "downloadProjectTemplateZipFile").rejects(undefined); + sinon.stub(componentUtils, "fetchAndUnzip").resolves(ok(undefined)); sinon.stub(OfficeAddinManifest, "modifyManifestFile").resolves({}); const result = await OfficeXMLAddinGenerator.generate(context, inputs, testFolder); diff --git a/packages/fx-core/tests/component/generatorUtils.test.ts b/packages/fx-core/tests/component/generatorUtils.test.ts new file mode 100644 index 0000000000..5a954945dd --- /dev/null +++ b/packages/fx-core/tests/component/generatorUtils.test.ts @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * @author huajiezhang@microsoft.com + */ + +import * as chai from "chai"; +import fse from "fs-extra"; +import "mocha"; +import * as sinon from "sinon"; +import * as generatorUtils from "../../src/component/generator/utils"; +import { fetchAndUnzip } from "../../src/component/utils"; + +describe("Generator related Utils", function () { + describe("fetchAndUnzip", async () => { + const sandbox = sinon.createSandbox(); + class ZipEntry { + isDirectory: boolean; + entryName: string; + getData() { + return undefined; + } + constructor(isDir: boolean, entryName: string) { + this.isDirectory = isDir; + this.entryName = entryName; + } + } + + class MockAdmZip { + getEntries() { + return [ + new ZipEntry(true, "dir/"), + new ZipEntry(true, "dir/subdir/"), + new ZipEntry(false, "dir/subdir/file"), + ]; + } + } + + afterEach(() => { + sandbox.restore(); + }); + + it("happy path", async () => { + sandbox.stub(generatorUtils, "fetchZipFromUrl").resolves(new MockAdmZip() as any); + const stub1 = sandbox.stub(fse, "ensureDir").resolves(); + const stub2 = sandbox.stub(fse, "writeFile").resolves(); + const res = await fetchAndUnzip("test", "url", "dest"); + chai.assert.isTrue(res.isOk()); + chai.assert.isTrue(stub1.calledOnce); + chai.assert.isTrue(stub2.calledOnce); + }); + + it("fail case: fetch zip throw error", async () => { + sandbox.stub(generatorUtils, "fetchZipFromUrl").rejects(new Error()); + const res = await fetchAndUnzip("test", "url", "dest"); + chai.assert.isTrue(res.isErr()); + }); + + it("fail case: fetch zip returns undefined", async () => { + sandbox.stub(generatorUtils, "fetchZipFromUrl").resolves(undefined); + const res = await fetchAndUnzip("test", "url", "dest"); + chai.assert.isTrue(res.isErr()); + }); + + it("fail case: ensureDir throws error", async () => { + sandbox.stub(generatorUtils, "fetchZipFromUrl").resolves(new MockAdmZip() as any); + sandbox.stub(fse, "ensureDir").rejects(new Error()); + const res = await fetchAndUnzip("test", "url", "dest"); + chai.assert.isTrue(res.isErr()); + }); + }); +});