diff --git a/src/adapters/file-upload.ts b/src/adapters/file-upload.ts index eb3ccba..7659c45 100755 --- a/src/adapters/file-upload.ts +++ b/src/adapters/file-upload.ts @@ -14,10 +14,10 @@ import { print } from '../util'; import BaseClass from './base-class'; import { getFileList } from '../util/fs'; import { createSignedUploadUrlMutation, importProjectMutation } from '../graphql'; +import { SignedUploadUrlData, FileUploadMethod } from '../types/launch'; +import config from '../config'; export default class FileUpload extends BaseClass { - private signedUploadUrlData!: Record; - /** * @method run * @@ -26,28 +26,9 @@ export default class FileUpload extends BaseClass { */ async run(): Promise { if (this.config.isExistingProject) { - await this.initApolloClient(); - const uploadLastFile = - this.config['redeploy-last-upload'] || - (await cliux.inquire({ - type: 'confirm', - default: false, - name: 'uploadLastFile', - message: 'Redeploy with last file upload?', - })); - if (!uploadLastFile) { - await this.createSignedUploadUrl(); - const { zipName, zipPath } = await this.archive(); - await this.uploadFile(zipName, zipPath); - } - - const { uploadUid } = this.signedUploadUrlData || { - uploadUid: undefined, - }; - await this.createNewDeployment(true, uploadUid); + await this.handleExistingProject(); } else { - await this.prepareForNewProjectCreation(); - await this.createNewProject(); + await this.handleNewProject(); } this.prepareLaunchConfig(); @@ -56,13 +37,79 @@ export default class FileUpload extends BaseClass { this.showSuggestion(); } + private async handleExistingProject(): Promise { + await this.initApolloClient(); + + let redeployLatest = this.config['redeploy-latest']; + let redeployLastUpload = this.config['redeploy-last-upload']; + + if (!redeployLatest && !redeployLastUpload) { + await this.confirmRedeployment(); + const latestRedeploymentConfirmed = await this.confirmLatestRedeployment(); + redeployLatest = latestRedeploymentConfirmed; + redeployLastUpload = !latestRedeploymentConfirmed; + } + + if (redeployLastUpload && redeployLatest) { + this.log('redeploy-last-upload and redeploy-latest flags are not supported together.', 'error'); + this.exit(1); + } + + let uploadUid; + if (redeployLatest) { + const signedUploadUrlData = await this.createSignedUploadUrl(); + uploadUid = signedUploadUrlData.uploadUid; + const { zipName, zipPath } = await this.archive(); + await this.uploadFile(zipName, zipPath, signedUploadUrlData); + } + + await this.createNewDeployment(true, uploadUid); + } + + private async confirmRedeployment(): Promise { + const redeploy = await cliux.inquire({ + type: 'confirm', + name: 'deployLatestCommit', + message: 'Do you want to redeploy this existing Launch project?', + }); + if (!redeploy) { + this.log('Project redeployment aborted.', 'info'); + this.exit(1); + } + } + + private async confirmLatestRedeployment(): Promise { + const choices = [ + ...map(config.supportedFileUploadMethods, (fileUploadMethod) => ({ + value: fileUploadMethod, + name: `Redeploy with ${fileUploadMethod}`, + })) + ]; + + const selectedFileUploadMethod: FileUploadMethod = await cliux.inquire({ + choices: choices, + type: 'search-list', + name: 'fileUploadMethod', + message: 'Choose a redeploy method to proceed', + }); + if (selectedFileUploadMethod === FileUploadMethod.LastFileUpload) { + return false; + } + return true; + } + + private async handleNewProject(): Promise { + const uploadUid = await this.prepareAndUploadNewProjectFile(); + await this.createNewProject(uploadUid); + } + /** * @method createNewProject - Create new launch project * * @return {*} {Promise} * @memberof FileUpload */ - async createNewProject(): Promise { + async createNewProject(uploadUid: string): Promise { const { framework, projectName, buildCommand, outputDirectory, environmentName, serverCommand } = this.config; await this.apolloClient .mutate({ @@ -71,7 +118,7 @@ export default class FileUpload extends BaseClass { project: { projectType: 'FILEUPLOAD', name: projectName, - fileUpload: { uploadUid: this.signedUploadUrlData.uploadUid }, + fileUpload: { uploadUid }, environment: { frameworkPreset: framework, outputDirectory: outputDirectory, @@ -95,7 +142,7 @@ export default class FileUpload extends BaseClass { const canRetry = await this.handleNewProjectCreationError(error); if (canRetry) { - return this.createNewProject(); + return this.createNewProject(uploadUid); } }); } @@ -106,7 +153,7 @@ export default class FileUpload extends BaseClass { * @return {*} {Promise} * @memberof FileUpload */ - async prepareForNewProjectCreation(): Promise { + async prepareAndUploadNewProjectFile(): Promise { const { name, framework, @@ -123,9 +170,9 @@ export default class FileUpload extends BaseClass { this.config.deliveryToken = token; // this.fileValidation(); await this.selectOrg(); - await this.createSignedUploadUrl(); + const signedUploadUrlData = await this.createSignedUploadUrl(); const { zipName, zipPath, projectName } = await this.archive(); - await this.uploadFile(zipName, zipPath); + await this.uploadFile(zipName, zipPath, signedUploadUrlData); this.config.projectName = name || (await cliux.inquire({ @@ -186,6 +233,7 @@ export default class FileUpload extends BaseClass { this.config.variableType = variableType as unknown as string; this.config.envVariables = envVariables; await this.handleEnvImportFlow(); + return signedUploadUrlData.uploadUid; } /** @@ -251,19 +299,23 @@ export default class FileUpload extends BaseClass { /** * @method createSignedUploadUrl - create pre signed url for file upload * - * @return {*} {Promise} + * @return {*} {Promise} * @memberof FileUpload */ - async createSignedUploadUrl(): Promise { - this.signedUploadUrlData = await this.apolloClient - .mutate({ mutation: createSignedUploadUrlMutation }) - .then(({ data: { signedUploadUrl } }) => signedUploadUrl) - .catch((error) => { - this.log('Something went wrong. Please try again.', 'warn'); - this.log(error, 'error'); - this.exit(1); - }); - this.config.uploadUid = this.signedUploadUrlData.uploadUid; + async createSignedUploadUrl(): Promise { + try { + const result = await this.apolloClient.mutate({ mutation: createSignedUploadUrlMutation }); + const signedUploadUrlData = result.data.signedUploadUrl; + this.config.uploadUid = signedUploadUrlData.uploadUid; + return signedUploadUrlData; + } catch (error) { + this.log('Something went wrong. Please try again.', 'warn'); + if (error instanceof Error) { + this.log(error.message, 'error'); + } + this.exit(1); + return {} as SignedUploadUrlData; + } } /** @@ -274,8 +326,8 @@ export default class FileUpload extends BaseClass { * @return {*} {Promise} * @memberof FileUpload */ - async uploadFile(fileName: string, filePath: PathLike): Promise { - const { uploadUrl, fields, headers, method } = this.signedUploadUrlData; + async uploadFile(fileName: string, filePath: PathLike, signedUploadUrlData: SignedUploadUrlData): Promise { + const { uploadUrl, fields, headers, method } = signedUploadUrlData; const formData = new FormData(); if (!isEmpty(fields)) { diff --git a/src/adapters/github.ts b/src/adapters/github.ts index fd44cd6..0a93cf8 100755 --- a/src/adapters/github.ts +++ b/src/adapters/github.ts @@ -21,24 +21,10 @@ export default class GitHub extends BaseClass { * @memberof GitHub */ async run(): Promise { - // NOTE New project creation Flow if (this.config.isExistingProject) { - await this.initApolloClient(); - await this.createNewDeployment(); + await this.handleExistingProject(); } else { - // NOTE Existing project flow - // NOTE Step 1: Check is Github connected - if (await this.checkGitHubConnected()) { - // NOTE Step 2: check is the git remote available in the user's repo list - if (await this.checkGitRemoteAvailableAndValid()) { - if (await this.checkUserGitHubAccess()) { - // NOTE Step 3: check is the user has proper git access - await this.prepareForNewProjectCreation(); - } - } - } - - await this.createNewProject(); + await this.handleNewProject(); } this.prepareLaunchConfig(); @@ -47,6 +33,50 @@ export default class GitHub extends BaseClass { this.showSuggestion(); } + private async handleExistingProject(): Promise { + await this.initApolloClient(); + + const redeployLastUpload = this.config['redeploy-last-upload']; + const redeployLatest = this.config['redeploy-latest']; + + if (redeployLastUpload) { + this.log('redeploy-last-upload flag is not supported for Github Project.', 'error'); + this.exit(1); + } + + if(!redeployLatest && !redeployLastUpload){ + await this.confirmLatestRedeployment(); + } + + await this.createNewDeployment(); + } + + private async confirmLatestRedeployment(): Promise { + const deployLatestCommit = await ux.inquire({ + type: 'confirm', + name: 'deployLatestCommit', + message: 'Redeploy latest commit?', + }); + if (!deployLatestCommit) { + this.log('Cannot create a new project because its an existing project.', 'info'); + this.exit(1); + } + } + + private async handleNewProject(): Promise { + // NOTE Step 1: Check is Github connected + if (await this.checkGitHubConnected()) { + // NOTE Step 2: check is the git remote available in the user's repo list + if (await this.checkGitRemoteAvailableAndValid()) { + if (await this.checkUserGitHubAccess()) { + // NOTE Step 3: check is the user has proper git access + await this.prepareForNewProjectCreation(); + } + } + } + await this.createNewProject(); + } + /** * @method createNewProject - Create new launch project * diff --git a/src/adapters/pre-check.ts b/src/adapters/pre-check.ts index cb08d9a..82e7a1e 100755 --- a/src/adapters/pre-check.ts +++ b/src/adapters/pre-check.ts @@ -47,16 +47,6 @@ export default class PreCheck extends BaseClass { this.log('Existing launch project identified', 'info'); await this.displayPreDeploymentDetails(); - const deployLatestCode = - this.config['redeploy-latest'] || - (await ux.inquire({ - type: 'confirm', - name: 'deployLatestSource', - message: 'Redeploy latest commit/code?', - })); - if (!deployLatestCode) { - this.exit(1); - } } } } diff --git a/src/config/index.ts b/src/config/index.ts index 2bb6438..17617d6 100755 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -40,7 +40,9 @@ const config = { 'Import variables from the local env file', ], variableType: '', - supportedFrameworksForServerCommands: ['ANGULAR', 'OTHER', 'REMIX'] + supportedFrameworksForServerCommands: ['ANGULAR', 'OTHER', 'REMIX'], + supportedFileUploadMethods: ['last file upload', 'new file'] + }; export default config; diff --git a/src/types/launch.ts b/src/types/launch.ts index 3b42cb3..8f1f424 100755 --- a/src/types/launch.ts +++ b/src/types/launch.ts @@ -7,6 +7,11 @@ import { LoggerType } from './utils'; type Providers = 'GitHub' | 'FileUpload'; +enum FileUploadMethod { + LastFileUpload = 'last file upload', + NewFile = 'new file', +} + type LogFn = (message: string | any, logType?: LoggerType | PrintOptions | undefined) => void; type ExitFn = (code?: number | undefined) => void; @@ -38,6 +43,7 @@ type ConfigType = { deployment?: string; environment?: string; provider?: Providers; + fileUploadMethod?: FileUploadMethod; authorization?: string; logsApiBaseUrl: string; projectBasePath: string; @@ -64,6 +70,18 @@ type GraphqlApiClientInput = { headers?: GraphqlHeaders; }; +type FormField = { + formFieldKey: string; + formFieldValue: string; +}; +type SignedUploadUrlData = { + uploadUrl: string; + fields: FormField[]; + headers: { key: string; value: string }[]; + method: string; + uploadUid: string; +}; + export { LogFn, ExitFn, @@ -73,4 +91,6 @@ export { AdapterConstructorInputs, GraphqlHeaders, GraphqlApiClientInput, + SignedUploadUrlData, + FileUploadMethod, }; diff --git a/test/unit/adapters/file-upload.test.ts b/test/unit/adapters/file-upload.test.ts index 6b1aea8..4358fba 100644 --- a/test/unit/adapters/file-upload.test.ts +++ b/test/unit/adapters/file-upload.test.ts @@ -1,17 +1,21 @@ //@ts-nocheck import { expect } from 'chai'; -import { stub, createSandbox } from 'sinon'; +import { stub, createSandbox , sinon } from 'sinon'; import { cliux } from '@contentstack/cli-utilities'; import fs from 'fs'; import { FileUpload, BaseClass } from '../../../src/adapters'; import { BaseCommand } from '../../../src/base-command'; +import { isNull } from 'util'; +import { log } from 'console'; +import { FileUploadMethod } from '../../../src/types/launch'; describe('File Upload', () => { - let inquireStub, prepareApiClientsStub, prepareConfigStub, getConfigStub; + let inquireStub, exitStub, prepareApiClientsStub, prepareConfigStub, getConfigStub; let adapterConstructorInputs; beforeEach(() => { inquireStub = stub(cliux, 'inquire'); + exitStub = stub(BaseCommand.prototype, 'exit'); prepareConfigStub = stub(BaseCommand.prototype, 'prepareConfig').resolves(); prepareApiClientsStub = stub(BaseCommand.prototype, 'prepareApiClients').resolves(); getConfigStub = stub(BaseCommand.prototype, 'getConfig').resolves(); @@ -25,6 +29,7 @@ describe('File Upload', () => { afterEach(() => { inquireStub.restore(); + exitStub.restore(); prepareConfigStub.restore(); getConfigStub.restore(); prepareApiClientsStub.restore(); @@ -36,25 +41,24 @@ describe('File Upload', () => { archiveStub, uploadFileStub, createNewDeploymentStub, - prepareForNewProjectCreationStub, + prepareAndUploadNewProjectFile, createNewProjectStub, prepareLaunchConfigStub, showLogsStub, showDeploymentUrlStub, showSuggestionStub; + const signedUploadUrlData = { uploadUrl: 'http://example.com/upload', uploadUid: '123456789' }; + const zipName = 'test.zip'; + const zipPath = '/path/to/zip'; - let adapterConstructorOptions = { - config: { isExistingProject: true, currentConfig: { uid: '123244', organizationUid: 'bltxxxxxxxx' } }, - }; beforeEach(() => { initApolloClientStub = stub(BaseClass.prototype, 'initApolloClient').resolves(); - createSignedUploadUrlStub = stub(FileUpload.prototype, 'createSignedUploadUrl').resolves(); - archiveStub = stub(FileUpload.prototype, 'archive').resolves({ zipName: 'test.zip', zipPath: '/path/to/zip' }); + createSignedUploadUrlStub = stub(FileUpload.prototype, 'createSignedUploadUrl').resolves(signedUploadUrlData); + archiveStub = stub(FileUpload.prototype, 'archive').resolves({ zipName, zipPath }); uploadFileStub = stub(FileUpload.prototype, 'uploadFile').resolves(); createNewDeploymentStub = stub(FileUpload.prototype, 'createNewDeployment').resolves(); - prepareForNewProjectCreationStub = stub(FileUpload.prototype, 'prepareForNewProjectCreation').resolves(); + prepareAndUploadNewProjectFile = stub(FileUpload.prototype, 'prepareAndUploadNewProjectFile').resolves(); createNewProjectStub = stub(FileUpload.prototype, 'createNewProject').resolves(); - prepareLaunchConfigStub = stub(BaseClass.prototype, 'prepareLaunchConfig').resolves(); showLogsStub = stub(BaseClass.prototype, 'showLogs').resolves(); showDeploymentUrlStub = stub(BaseClass.prototype, 'showDeploymentUrl').resolves(); @@ -67,7 +71,7 @@ describe('File Upload', () => { archiveStub.restore(); uploadFileStub.restore(); createNewDeploymentStub.restore(); - prepareForNewProjectCreationStub.restore(); + prepareAndUploadNewProjectFile.restore(); createNewProjectStub.restore(); prepareLaunchConfigStub.restore(); showLogsStub.restore(); @@ -75,8 +79,206 @@ describe('File Upload', () => { showSuggestionStub.restore(); }); - it('should run github flow', async () => { - new FileUpload(adapterConstructorOptions).run(); + describe('Redeploy existing project', () => { + let sandbox; + let processExitStub; + + beforeEach(() => { + sandbox = createSandbox(); + + processExitStub = sandbox.stub(process, 'exit').callsFake((code) => { + throw new Error(code); + }); + + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should run file upload flow successfully for existing project where flag passed is redeploy-latest', async () => { + let adapterConstructorOptions = { + config: { + isExistingProject: true, + currentConfig: { uid: '123244', organizationUid: 'bltxxxxxxxx' }, + 'redeploy-latest': true, + }, + }; + await new FileUpload(adapterConstructorOptions).run(); + + expect(initApolloClientStub.calledOnce).to.be.true; + expect(createSignedUploadUrlStub.calledOnce).to.be.true; + expect(archiveStub.calledOnce).to.be.true; + expect(uploadFileStub.calledOnce).to.be.true; + expect(uploadFileStub.args[0]).to.deep.equal([zipName, zipPath, signedUploadUrlData]); + expect(createNewDeploymentStub.calledOnce).to.be.true; + expect(createNewDeploymentStub.args[0]).to.deep.equal([true, signedUploadUrlData.uploadUid]); + expect(prepareLaunchConfigStub.calledOnce).to.be.true; + expect(showLogsStub.calledOnce).to.be.true; + expect(showDeploymentUrlStub.calledOnce).to.be.true; + expect(showSuggestionStub.calledOnce).to.be.true; + }); + + it('should run file upload flow successfully for existing project where flag passed is redeploy-last-upload', async () => { + let adapterConstructorOptions = { + config: { + isExistingProject: true, + currentConfig: { uid: '123244', organizationUid: 'bltxxxxxxxx' }, + 'redeploy-last-upload': true, + }, + }; + await new FileUpload(adapterConstructorOptions).run(); + + expect(initApolloClientStub.calledOnce).to.be.true; + expect(createSignedUploadUrlStub.calledOnce).to.be.false; + expect(archiveStub.calledOnce).to.be.false; + expect(uploadFileStub.calledOnce).to.be.false; + expect(createNewDeploymentStub.calledOnce).to.be.true; + expect(createNewDeploymentStub.args[0]).to.deep.equal([true, undefined]); + expect(prepareLaunchConfigStub.calledOnce).to.be.true; + expect(showLogsStub.calledOnce).to.be.true; + expect(showDeploymentUrlStub.calledOnce).to.be.true; + expect(showSuggestionStub.calledOnce).to.be.true; + }); + + it('should exit with an error message when both --redeploy-last-upload and --redeploy-latest flags are passed', async () => { + let adapterConstructorOptions = { + config: { + isExistingProject: true, + currentConfig: { uid: '123244', organizationUid: 'bltxxxxxxxx' }, + 'redeploy-last-upload': true, + 'redeploy-latest': true, + }, + }; + let exitStatusCode; + + try { + await new FileUpload(adapterConstructorOptions).run(); + } catch (err) { + exitStatusCode = err.message; + } + + expect(processExitStub.calledOnceWithExactly(1)).to.be.true; + expect(exitStatusCode).to.equal('1'); + expect(initApolloClientStub.calledOnce).to.be.true; + expect(createSignedUploadUrlStub.calledOnce).to.be.false; + expect(archiveStub.calledOnce).to.be.false; + expect(uploadFileStub.calledOnce).to.be.false; + expect(createNewDeploymentStub.calledOnce).to.be.false; + expect(prepareLaunchConfigStub.calledOnce).to.be.false; + expect(showLogsStub.calledOnce).to.be.false; + expect(showDeploymentUrlStub.calledOnce).to.be.false; + expect(showSuggestionStub.calledOnce).to.be.false; + }); + + it('should show prompt and successfully redeploy with "new file" if the option to redeploy with new file is selected, when --redeploy-latest and --redeploy-last-upload flags are not passed', async () => { + let adapterConstructorOptions = { + config: { + isExistingProject: true, + currentConfig: { uid: '123244', organizationUid: 'bltxxxxxxxx' }, + }, + }; + inquireStub.withArgs({ + type: 'confirm', + name: 'deployLatestCommit', + message: 'Do you want to redeploy this existing Launch project?', + }).resolves(true); + inquireStub.resolves(FileUploadMethod.NewFile); + + await new FileUpload(adapterConstructorOptions).run(); + + expect(initApolloClientStub.calledOnce).to.be.true; + expect(createSignedUploadUrlStub.calledOnce).to.be.true; + expect(archiveStub.calledOnce).to.be.true; + expect(uploadFileStub.calledOnce).to.be.true; + expect(uploadFileStub.args[0]).to.deep.equal([zipName, zipPath, signedUploadUrlData]); + expect(createNewDeploymentStub.calledOnce).to.be.true; + expect(createNewDeploymentStub.args[0]).to.deep.equal([true, signedUploadUrlData.uploadUid]); + expect(prepareLaunchConfigStub.calledOnce).to.be.true; + expect(showLogsStub.calledOnce).to.be.true; + expect(showDeploymentUrlStub.calledOnce).to.be.true; + expect(showSuggestionStub.calledOnce).to.be.true; + }); + + it('should show prompt and successfully redeploy with "last file upload" if the option to redeploy with last file upload is selected, when --redeploy-latest and --redeploy-last-upload flags are not passed', async () => { + let adapterConstructorOptions = { + config: { + isExistingProject: true, + currentConfig: { uid: '123244', organizationUid: 'bltxxxxxxxx' }, + }, + }; + inquireStub.withArgs({ + type: 'confirm', + name: 'deployLatestCommit', + message: 'Do you want to redeploy this existing Launch project?', + }).resolves(true); + inquireStub.resolves(FileUploadMethod.LastFileUpload); + + await new FileUpload(adapterConstructorOptions).run(); + + expect(initApolloClientStub.calledOnce).to.be.true; + expect(createSignedUploadUrlStub.calledOnce).to.be.false; + expect(archiveStub.calledOnce).to.be.false; + expect(uploadFileStub.calledOnce).to.be.false; + expect(createNewDeploymentStub.calledOnce).to.be.true; + expect(createNewDeploymentStub.args[0]).to.deep.equal([true, undefined]); + expect(prepareLaunchConfigStub.calledOnce).to.be.true; + expect(showLogsStub.calledOnce).to.be.true; + expect(showDeploymentUrlStub.calledOnce).to.be.true; + expect(showSuggestionStub.calledOnce).to.be.true; + }); + + it('should exit if "No" is selected for prompt to redeploy, when --redeploy-latest and --redeploy-last-upload flags are not passed', async() => { + let adapterConstructorOptions = { + config: { + isExistingProject: true, + currentConfig: { uid: '123244', organizationUid: 'bltxxxxxxxx' }, + }, + }; + inquireStub.withArgs({ + type: 'confirm', + name: 'deployLatestCommit', + message: 'Do you want to redeploy this existing Launch project?', + }).resolves(false); + let exitStatusCode; + + try { + await new FileUpload(adapterConstructorOptions).run(); + } catch (err) { + exitStatusCode = err.message; + } + + expect(processExitStub.calledOnceWithExactly(1)).to.be.true; + expect(exitStatusCode).to.equal('1'); + expect(initApolloClientStub.calledOnce).to.be.true; + expect(createSignedUploadUrlStub.calledOnce).to.be.false; + expect(archiveStub.calledOnce).to.be.false; + expect(uploadFileStub.calledOnce).to.be.false; + expect(createNewDeploymentStub.calledOnce).to.be.false; + expect(prepareLaunchConfigStub.calledOnce).to.be.false; + expect(showLogsStub.calledOnce).to.be.false; + expect(showDeploymentUrlStub.calledOnce).to.be.false; + expect(showSuggestionStub.calledOnce).to.be.false; + }); + + }); + + describe('Deploy new project', () => { + let adapterConstructorOptions = { + config: { + isExistingProject: false, + }, + }; + it('should run file upload flow for new project', async () => { + await new FileUpload(adapterConstructorOptions).run(); + + expect(prepareAndUploadNewProjectFile.calledOnce).to.be.true; + expect(createNewProjectStub.calledOnce).to.be.true; + expect(prepareLaunchConfigStub.calledOnce).to.be.true; + expect(showLogsStub.calledOnce).to.be.true; + expect(showDeploymentUrlStub.calledOnce).to.be.true; + expect(showSuggestionStub.calledOnce).to.be.true; + }); }); }); @@ -131,7 +333,7 @@ describe('File Upload', () => { }); }); - describe('prepareForNewProjectCreation', () => { + describe('prepareAndUploadNewProjectFile', () => { let createSignedUploadUrlStub, archiveStub, uploadFileStub, @@ -152,13 +354,15 @@ describe('File Upload', () => { { name: 'NextJs', value: 'NEXTJS' }, { name: 'Other', value: 'OTHER' }, ], - outputDirectories:"", - supportedFrameworksForServerCommands: ['ANGULAR', 'OTHER', 'REMIX'] + outputDirectories: '', + supportedFrameworksForServerCommands: ['ANGULAR', 'OTHER', 'REMIX'], }, }; let archiveMockData = { zipName: 'abc.zip', zipPath: 'path/to/zip', projectName: 'test' }; beforeEach(function () { - createSignedUploadUrlStub = stub(FileUpload.prototype, 'createSignedUploadUrl').resolves(); + createSignedUploadUrlStub = stub(FileUpload.prototype, 'createSignedUploadUrl').resolves({ + uploadUid: '123456789', + }); archiveStub = stub(FileUpload.prototype, 'archive').resolves(archiveMockData); uploadFileStub = stub(FileUpload.prototype, 'uploadFile'); uploadFileStub.withArgs(archiveMockData.zipName, archiveMockData.zipPath); @@ -176,7 +380,7 @@ describe('File Upload', () => { }); it('prepare for new project', async function () { - await new FileUpload(adapterConstructorOptions).prepareForNewProjectCreation(); + await new FileUpload(adapterConstructorOptions).prepareAndUploadNewProjectFile(); }); }); @@ -221,36 +425,55 @@ describe('File Upload', () => { }); describe('createSignedUploadUrl', () => { - let sandbox; + let sandbox, logStub, exitStub; beforeEach(() => { sandbox = createSandbox(); + logStub = sandbox.stub(console, 'log'); + exitStub = sandbox.stub(process, 'exit'); }); afterEach(() => { sandbox.restore(); + logStub.restore(); + exitStub.restore(); }); it('should set the signed upload URL and upload UID in the config', async () => { - const signedUploadUrl = 'http://example.com/upload'; + const expectedSignedUploadUrl = { uploadUrl: 'http://example.com/upload', uploadUid: '123456789' }; const apolloClientMock = { - mutate: sandbox.stub().resolves({ data: { signedUploadUrl } }), + mutate: sandbox.stub().resolves({ data: { signedUploadUrl: expectedSignedUploadUrl } }), }; - const logStub = sandbox.stub(console, 'log'); - const exitStub = sandbox.stub(process, 'exit'); const fileUploadInstance = new FileUpload(adapterConstructorInputs); + fileUploadInstance.apolloClient = apolloClientMock; + fileUploadInstance.signedUploadUrlData = expectedSignedUploadUrl.uploadUrl; + fileUploadInstance.config.uploadUid = expectedSignedUploadUrl.uploadUid; + + const signedUploadUrlData = await fileUploadInstance.createSignedUploadUrl(); + + expect(fileUploadInstance.config.uploadUid).to.equal(expectedSignedUploadUrl.uploadUid); + expect(signedUploadUrlData).to.equal(expectedSignedUploadUrl); + }); + + it('should log an error message and exit when the mutation fails', async () => { + const expectedSignedUploadUrl = { uploadUrl: null, uploadUid: null }; + const apolloClientMock = { + mutate: sandbox.stub().rejects(new Error('Mutation failed')), + }; + const fileUploadInstance = new FileUpload(adapterConstructorInputs); + fileUploadInstance.apolloClient = apolloClientMock; fileUploadInstance.log = logStub; fileUploadInstance.exit = exitStub; - fileUploadInstance.signedUploadUrlData = null; - fileUploadInstance.config = { uploadUid: null }; + fileUploadInstance.signedUploadUrlData = expectedSignedUploadUrl.uploadUrl; + fileUploadInstance.config.uploadUid = expectedSignedUploadUrl.uploadUid; await fileUploadInstance.createSignedUploadUrl(); - expect(logStub.called).to.be.false; - expect(exitStub.called).to.be.false; - expect(fileUploadInstance.signedUploadUrlData).to.equal('http://example.com/upload'); + expect(logStub.calledWith('Something went wrong. Please try again.', 'warn')).to.be.true; + expect(logStub.calledWith('Mutation failed', 'error')).to.be.true; + expect(exitStub.calledOnceWithExactly(1)).to.be.true; }); }); }); diff --git a/test/unit/adapters/github.test.ts b/test/unit/adapters/github.test.ts index b2fa6f0..dd53098 100644 --- a/test/unit/adapters/github.test.ts +++ b/test/unit/adapters/github.test.ts @@ -5,6 +5,7 @@ import { cliux } from '@contentstack/cli-utilities'; import { githubAdapterMockData } from '../mock/index'; import { GitHub, BaseClass } from '../../../src/adapters'; import { BaseCommand } from '../../../src/base-command'; +import fs from 'fs'; describe('GitHub', () => { let inquireStub, prepareApiClientsStub, prepareConfigStub, getConfigStub; @@ -41,6 +42,7 @@ describe('GitHub', () => { prepareLaunchConfigStub, showLogsStub, showDeploymentUrlStub, + exitStub, showSuggestionStub; beforeEach(() => { @@ -53,11 +55,11 @@ describe('GitHub', () => { checkUserGitHubAccessStub = stub(GitHub.prototype, 'checkUserGitHubAccess').resolves(true); prepareForNewProjectCreationStub = stub(GitHub.prototype, 'prepareForNewProjectCreation').resolves(); createNewProjectStub = stub(GitHub.prototype, 'createNewProject').resolves(); - prepareLaunchConfigStub = stub(BaseClass.prototype, 'prepareLaunchConfig').resolves(); showLogsStub = stub(BaseClass.prototype, 'showLogs').resolves(); showDeploymentUrlStub = stub(BaseClass.prototype, 'showDeploymentUrl').resolves(); showSuggestionStub = stub(BaseClass.prototype, 'showSuggestion').resolves(); + exitStub = stub(BaseCommand.prototype, 'exit').resolves(); }); afterEach(() => { @@ -72,10 +74,178 @@ describe('GitHub', () => { showLogsStub.restore(); showDeploymentUrlStub.restore(); showSuggestionStub.restore(); + exitStub.restore(); }); - it('should run github flow', async () => { - new GitHub(adapterConstructorInputs).run(); + describe('Redeploy existing project', () => { + let sandbox; + let processExitStub; + + beforeEach(() => { + sandbox = createSandbox(); + + processExitStub = sandbox.stub(process, 'exit').callsFake((code) => { + throw new Error(code); + }); + + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should successfully run github flow for existing project when flag redeploy-latest is passed ', async () => { + let adapterConstructorOptions = { + config: { + isExistingProject: true, + 'redeploy-latest': true, + }, + }; + + await new GitHub(adapterConstructorOptions).run(); + + expect(initApolloClientStub.calledOnce).to.be.true; + expect(createNewDeploymentStub.calledOnce).to.be.true; + expect(prepareLaunchConfigStub.calledOnce).to.be.true; + expect(showLogsStub.calledOnce).to.be.true; + expect(showDeploymentUrlStub.calledOnce).to.be.true; + expect(showSuggestionStub.calledOnce).to.be.true; + }); + + it('should abort github flow for existing project when flag redeploy-last-upload is passed', async () => { + const adapterConstructorOptions = { + config: { + isExistingProject: true, + 'redeploy-last-upload': true, + }, + }; + let exitStatusCode; + + try { + await new GitHub(adapterConstructorOptions).run(); + } catch (err) { + exitStatusCode = err.message; + } + + expect(processExitStub.calledOnceWithExactly(1)).to.be.true; + expect(exitStatusCode).to.equal('1'); + expect(initApolloClientStub.calledOnce).to.be.true; + expect(createNewDeploymentStub.calledOnce).to.be.false; + expect(prepareLaunchConfigStub.calledOnce).to.be.false; + expect(showLogsStub.calledOnce).to.be.false; + expect(showDeploymentUrlStub.calledOnce).to.be.false; + expect(showSuggestionStub.calledOnce).to.be.false; + }); + + it('should show prompt and successfully redeploy with "latest commit" if the option to redeploy is selected, when --redeploy-latest flag is not passed', async() => { + const adapterConstructorOptions = { + config: { + isExistingProject: true + }, + }; + inquireStub.withArgs({ + type: 'confirm', + name: 'deployLatestCommit', + message: 'Redeploy latest commit?', + }).resolves(true); + + await new GitHub(adapterConstructorOptions).run(); + + expect(initApolloClientStub.calledOnce).to.be.true; + expect(createNewDeploymentStub.calledOnce).to.be.true; + expect(prepareLaunchConfigStub.calledOnce).to.be.true; + expect(showLogsStub.calledOnce).to.be.true; + expect(showDeploymentUrlStub.calledOnce).to.be.true; + expect(showSuggestionStub.calledOnce).to.be.true; + }); + + it('should exit if "No" is selected for prompt to redeploy, when --redeploy-latest flag is not passed', async() => { + const adapterConstructorOptions = { + config: { + isExistingProject: true + }, + }; + inquireStub.withArgs({ + type: 'confirm', + name: 'deployLatestCommit', + message: 'Redeploy latest commit?', + }).resolves(false); + let exitStatusCode; + + try { + await new GitHub(adapterConstructorOptions).run(); + } catch (err) { + exitStatusCode = err.message; + } + + expect(processExitStub.calledOnceWithExactly(1)).to.be.true; + expect(exitStatusCode).to.equal('1'); + expect(initApolloClientStub.calledOnce).to.be.true; + expect(createNewDeploymentStub.calledOnce).to.be.false; + expect(prepareLaunchConfigStub.calledOnce).to.be.false; + expect(showLogsStub.calledOnce).to.be.false; + expect(showDeploymentUrlStub.calledOnce).to.be.false; + expect(showSuggestionStub.calledOnce).to.be.false; + }); + + }); + + describe('Deploy new project', () => { + let adapterConstructorOptions = { + config: { + isExistingProject: false, + }, + }; + it('should create new project if GitHub is not connected', async () => { + checkGitHubConnectedStub.resolves(false); + + await new GitHub(adapterConstructorOptions).run(); + + expect(checkGitHubConnectedStub.calledOnce).to.be.true; + expect(checkGitRemoteAvailableAndValidStub.called).to.be.false; + expect(checkUserGitHubAccessStub.called).to.be.false; + expect(prepareForNewProjectCreationStub.called).to.be.false; + expect(createNewProjectStub.calledOnce).to.be.true; + }); + + it('should create new project if git remote is not available', async () => { + checkGitRemoteAvailableAndValidStub.resolves(false); + + await new GitHub(adapterConstructorOptions).run(); + + expect(checkGitHubConnectedStub.calledOnce).to.be.true; + expect(checkGitRemoteAvailableAndValidStub.calledOnce).to.be.true; + expect(checkUserGitHubAccessStub.called).to.be.false; + expect(prepareForNewProjectCreationStub.called).to.be.false; + expect(createNewProjectStub.calledOnce).to.be.true; + }); + it('should not proceed if user does not have GitHub access', async () => { + checkGitHubConnectedStub.resolves(true); + checkGitRemoteAvailableAndValidStub.resolves(true); + checkUserGitHubAccessStub.resolves(false); + + await new GitHub(adapterConstructorOptions).run(); + + expect(checkGitHubConnectedStub.calledOnce).to.be.true; + expect(checkGitRemoteAvailableAndValidStub.calledOnce).to.be.true; + expect(checkUserGitHubAccessStub.calledOnce).to.be.true; + expect(prepareForNewProjectCreationStub.called).to.be.false; + expect(createNewProjectStub.calledOnce).to.be.true; + }); + + it('should proceed to prepare for new project creation if user has GitHub access', async () => { + checkGitHubConnectedStub.resolves(true); + checkGitRemoteAvailableAndValidStub.resolves(true); + checkUserGitHubAccessStub.resolves(true); + + await new GitHub(adapterConstructorOptions).handleNewProject(); + + expect(checkGitHubConnectedStub.calledOnce).to.be.true; + expect(checkGitRemoteAvailableAndValidStub.calledOnce).to.be.true; + expect(checkUserGitHubAccessStub.calledOnce).to.be.true; + expect(prepareForNewProjectCreationStub.calledOnce).to.be.true; + expect(createNewProjectStub.calledOnce).to.be.true; + }); }); }); @@ -163,8 +333,8 @@ describe('GitHub', () => { { name: 'Other', value: 'OTHER' }, ], repository: { fullName: 'Gatsby Starter' }, - outputDirectories:"", - supportedFrameworksForServerCommands: ['ANGULAR', 'OTHER', 'REMIX'] + outputDirectories: '', + supportedFrameworksForServerCommands: ['ANGULAR', 'OTHER', 'REMIX'], }, }; beforeEach(function () {