From 1f21cb1eefc90c1e6befc23a5d5c58d6993139de Mon Sep 17 00:00:00 2001 From: dhruvparekh12 Date: Mon, 24 Feb 2025 21:03:40 +0530 Subject: [PATCH] fix: Do not use config file path to override the data directory --- .env.sample | 3 + README.md | 3 + src/base-command.ts | 24 ++- test/unit/base-command.test.ts | 268 +++++++++++++++++++++++++++++++++ 4 files changed, 290 insertions(+), 8 deletions(-) create mode 100644 .env.sample create mode 100644 test/unit/base-command.test.ts diff --git a/.env.sample b/.env.sample new file mode 100644 index 00000000..2d31ab4 --- /dev/null +++ b/.env.sample @@ -0,0 +1,3 @@ +ENVIRONMENT=67b352bf94f6713543168423 +ORG=blt1608fceef3be7de6 +PROJECT=67b352bf94f671354316841c \ No newline at end of file diff --git a/README.md b/README.md index 0047ded..6aa1169 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,9 @@ csdx plugins:link csdx ``` +# How to run tests Locally? +- To run tests locally, create a .env file by cloning the .env.sample file and populate the values with uid of existing Launch org, project, environment and so on. +- Run the command: `npm run test:unit` or `npm run test:unit:report` # Release & SRE Process:- diff --git a/src/base-command.ts b/src/base-command.ts index a36f875..e9102d4 100755 --- a/src/base-command.ts +++ b/src/base-command.ts @@ -1,7 +1,7 @@ import keys from 'lodash/keys'; -import { existsSync } from 'fs'; +import { existsSync, statSync } from 'fs'; import EventEmitter from 'events'; -import { dirname, resolve } from 'path'; +import { resolve } from 'path'; import includes from 'lodash/includes'; import { ApolloClient } from '@apollo/client/core'; import { Command } from '@contentstack/cli-command'; @@ -18,7 +18,8 @@ import { } from '@contentstack/cli-utilities'; import config from './config'; -import { getLaunchHubUrl, GraphqlApiClient, Logger } from './util'; +import { GraphqlApiClient, Logger } from './util'; +import { getLaunchHubUrl } from './util/common-utility'; import { ConfigType, LogFn, Providers } from './types'; export type Flags = Interfaces.InferredFlags<(typeof BaseCommand)['baseFlags'] & T['flags']>; @@ -101,14 +102,21 @@ export abstract class BaseCommand extends Command { * @memberof BaseCommand */ async prepareConfig(): Promise { - let configPath = - this.flags['data-dir'] || this.flags.config - ? this.flags.config || resolve(this.flags['data-dir'], config.configName) - : resolve(process.cwd(), config.configName); + const currentWorkingDirectory = process.cwd(); + + const projectBasePath = this.flags['data-dir'] || currentWorkingDirectory; + if (!existsSync(projectBasePath) || !statSync(projectBasePath).isDirectory()) { + ux.print(`Invalid directory: ${projectBasePath}`, { color: 'red' }); + this.exit(1); + } + + const configPath = this.flags.config || resolve(currentWorkingDirectory, config.configName); + let baseUrl = config.launchBaseUrl || this.launchHubUrl; if (!baseUrl) { baseUrl = getLaunchHubUrl(); } + this.sharedConfig = { ...require('./config').default, currentConfig: {}, @@ -116,7 +124,7 @@ export abstract class BaseCommand extends Command { flags: this.flags, host: this.cmaHost, config: configPath, - projectBasePath: dirname(configPath), + projectBasePath: projectBasePath, authtoken: configHandler.get('authtoken'), authType: configHandler.get('authorisationType'), authorization: configHandler.get('oauthAccessToken'), diff --git a/test/unit/base-command.test.ts b/test/unit/base-command.test.ts new file mode 100644 index 00000000..8520468 --- /dev/null +++ b/test/unit/base-command.test.ts @@ -0,0 +1,268 @@ +//@ts-nocheck +// TODO: Allow ts with any and remove ts-nocheck +import { expect } from 'chai'; +import { stub, createSandbox } from 'sinon'; +import { cliux as ux, configHandler } from '@contentstack/cli-utilities'; +import fs from 'fs'; +import path from 'path'; +import { BaseCommand } from '../../src/base-command'; +import config from '../../src/config'; +import * as commonUtils from '../../src/util/common-utility'; + +describe('BaseCommand', () => { + let sandbox; + let baseCommandInstance; + let flags; + + describe('prepareConfig', () => { + let statSyncResultObj; + let statSyncStub; + let existsSyncStub; + let processCwdStub; + let getLaunchHubUrlStub; + let configHandlerGetStub; + + beforeEach(() => { + sandbox = createSandbox(); + + baseCommandInstance = new (class extends BaseCommand { + async run() {} + })([], {} as any); + + baseCommandInstance.flags = {}; + + sandbox.stub(BaseCommand.prototype, 'exit').callsFake((code) => { + throw new Error(code); + }); + + statSyncResultObj = { + isDirectory: sandbox.stub().returns(true), + }; + statSyncStub = sandbox.stub(fs, 'statSync').returns(statSyncResultObj); + + existsSyncStub = sandbox.stub(fs, 'existsSync').returns(true); + existsSyncStub.withArgs('/root/.cs-launch.json').returns(false); + + processCwdStub = sandbox.stub(process, 'cwd').returns('/root/'); + + getLaunchHubUrlStub = sandbox + .stub(commonUtils, 'getLaunchHubUrl') + .returns('https://dev11-app.csnonprod.com/launch-api'); + + sandbox.stub(BaseCommand.prototype, 'cmaHost').value('host.contentstack.io'); + + configHandlerGetStub = sandbox.stub(configHandler, 'get').returns('testValue'); + configHandlerGetStub.withArgs('authtoken').returns('testauthtoken'); + configHandlerGetStub.withArgs('authorisationType').returns('testauthorisationType'); + configHandlerGetStub.withArgs('oauthAccessToken').returns('testoauthAccessToken'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should initialize sharedConfig with default values if no flags passed', async () => { + await baseCommandInstance.prepareConfig(); + + expect(configHandlerGetStub.args[1][0]).to.equal('authtoken'); + expect(configHandlerGetStub.args[2][0]).to.equal('authorisationType'); + expect(configHandlerGetStub.args[3][0]).to.equal('oauthAccessToken'); + expect(baseCommandInstance.sharedConfig).to.deep.equal({ + ...require('../../src/config').default, + currentConfig: {}, + flags: {}, + host: 'host.contentstack.io', + projectBasePath: '/root/', + authtoken: 'testauthtoken', + authType: 'testauthorisationType', + authorization: 'testoauthAccessToken', + config: '/root/.cs-launch.json', + logsApiBaseUrl: 'https://dev11-app.csnonprod.com/launch-api/logs/graphql', + manageApiBaseUrl: 'https://dev11-app.csnonprod.com/launch-api/manage/graphql', + }); + }); + + it('should successfully initialize sharedConfig.manageApiBaseUrl and sharedConfig.logsApiBaseUrl if config.launchBaseUrl is set', async () => { + sandbox.stub(config, 'launchBaseUrl').value('https://configlaunch-baseurl.csnonprod.com/launch-api'); + + await baseCommandInstance.prepareConfig(); + + expect(existsSyncStub.args[0][0]).to.equal('/root/'); + expect(statSyncStub.args[0][0]).to.equal('/root/'); + expect(statSyncResultObj.isDirectory.calledOnce).to.be.true; + expect(baseCommandInstance.sharedConfig).to.deep.equal({ + ...require('../../src/config').default, + currentConfig: {}, + flags: {}, + host: 'host.contentstack.io', + projectBasePath: '/root/', + authtoken: 'testauthtoken', + authType: 'testauthorisationType', + authorization: 'testoauthAccessToken', + config: '/root/.cs-launch.json', + logsApiBaseUrl: 'https://configlaunch-baseurl.csnonprod.com/launch-api/logs/graphql', + manageApiBaseUrl: 'https://configlaunch-baseurl.csnonprod.com/launch-api/manage/graphql', + }); + }); + + it('should successfully initialize sharedConfig.manageApiBaseUrl and sharedConfig.logsApiBaseUrl if launchHubUrl is set', async () => { + sandbox.stub(BaseCommand.prototype, 'launchHubUrl').value('https://this-launchHubUrl.csnonprod.com/launch-api'); + + await baseCommandInstance.prepareConfig(); + + expect(existsSyncStub.args[0][0]).to.equal('/root/'); + expect(statSyncStub.args[0][0]).to.equal('/root/'); + expect(statSyncResultObj.isDirectory.calledOnce).to.be.true; + expect(baseCommandInstance.sharedConfig).to.deep.equal({ + ...require('../../src/config').default, + currentConfig: {}, + flags: {}, + host: 'host.contentstack.io', + projectBasePath: '/root/', + authtoken: 'testauthtoken', + authType: 'testauthorisationType', + authorization: 'testoauthAccessToken', + config: '/root/.cs-launch.json', + logsApiBaseUrl: 'https://this-launchHubUrl.csnonprod.com/launch-api/logs/graphql', + manageApiBaseUrl: 'https://this-launchHubUrl.csnonprod.com/launch-api/manage/graphql', + }); + }); + + it('should successfully initialize sharedConfig.projectBasePath if "data-dir" flag is passed', async () => { + const flags = { + 'data-dir': '/root/subdirectory/project1', + }; + baseCommandInstance.flags = flags; + + await baseCommandInstance.prepareConfig(); + + expect(existsSyncStub.args[0][0]).to.equal('/root/subdirectory/project1'); + expect(statSyncStub.args[0][0]).to.equal('/root/subdirectory/project1'); + expect(statSyncResultObj.isDirectory.calledOnce).to.be.true; + expect(baseCommandInstance.sharedConfig).to.deep.equal({ + ...require('../../src/config').default, + currentConfig: {}, + flags, + host: 'host.contentstack.io', + 'data-dir': '/root/subdirectory/project1', + projectBasePath: '/root/subdirectory/project1', + config: '/root/.cs-launch.json', + authtoken: 'testauthtoken', + authType: 'testauthorisationType', + authorization: 'testoauthAccessToken', + logsApiBaseUrl: 'https://dev11-app.csnonprod.com/launch-api/logs/graphql', + manageApiBaseUrl: 'https://dev11-app.csnonprod.com/launch-api/manage/graphql', + }); + }); + + it('should initialize sharedConfig.provider if "type" flag is passed', async () => { + const flags = { + 'type': 'FILEUPLOAD', + }; + baseCommandInstance.flags = flags; + + await baseCommandInstance.prepareConfig(); + + expect(configHandlerGetStub.args[1][0]).to.equal('authtoken'); + expect(configHandlerGetStub.args[2][0]).to.equal('authorisationType'); + expect(configHandlerGetStub.args[3][0]).to.equal('oauthAccessToken'); + expect(baseCommandInstance.sharedConfig).to.deep.equal({ + ...require('../../src/config').default, + currentConfig: {}, + flags, + type: 'FILEUPLOAD', + provider: 'FILEUPLOAD', + host: 'host.contentstack.io', + projectBasePath: '/root/', + authtoken: 'testauthtoken', + authType: 'testauthorisationType', + authorization: 'testoauthAccessToken', + config: '/root/.cs-launch.json', + logsApiBaseUrl: 'https://dev11-app.csnonprod.com/launch-api/logs/graphql', + manageApiBaseUrl: 'https://dev11-app.csnonprod.com/launch-api/manage/graphql', + }); + }); + + it('should exit with error if "data-dir" flag specified but path does not exist', async () => { + existsSyncStub.returns(false); + baseCommandInstance.flags = { + 'data-dir': '/root/subdirectory/project1', + }; + let exitStatusCode; + + try { + await baseCommandInstance.prepareConfig(); + } catch (err) { + exitStatusCode = err.message; + } + + expect(existsSyncStub.args[0][0]).to.equal('/root/subdirectory/project1'); + expect(exitStatusCode).to.equal('1'); + }); + + it('should exit with error if "data-dir" flag specified with a non-directory path', async () => { + statSyncResultObj.isDirectory.returns(false); + baseCommandInstance.flags = { + 'data-dir': '/root/subdirectory/project1/file.txt', + }; + let exitStatusCode; + + try { + await baseCommandInstance.prepareConfig(); + } catch (err) { + exitStatusCode = err.message; + } + + expect(existsSyncStub.args[0][0]).to.equal('/root/subdirectory/project1/file.txt'); + expect(statSyncStub.args[0][0]).to.equal('/root/subdirectory/project1/file.txt'); + expect(statSyncResultObj.isDirectory.calledOnce).to.be.true; + expect(exitStatusCode).to.equal('1'); + }); + + it('should initialize sharedConfig.config if "config" flag if passed', async () => { + const flags = { + config: '/root/subdirectory/configs/dev.json', + }; + baseCommandInstance.flags = flags; + existsSyncStub.withArgs('/root/subdirectory/configs/dev.json').returns(false); + + await baseCommandInstance.prepareConfig(); + + expect(baseCommandInstance.sharedConfig).to.deep.equal({ + ...require('../../src/config').default, + currentConfig: {}, + config: '/root/subdirectory/configs/dev.json', + flags, + host: 'host.contentstack.io', + projectBasePath: '/root/', + authtoken: 'testauthtoken', + authType: 'testauthorisationType', + authorization: 'testoauthAccessToken', + config: '/root/subdirectory/configs/dev.json', + logsApiBaseUrl: 'https://dev11-app.csnonprod.com/launch-api/logs/graphql', + manageApiBaseUrl: 'https://dev11-app.csnonprod.com/launch-api/manage/graphql', + }); + }); + + it('should initialize sharedConfig.isExistingProject if config file exists', async () => { + existsSyncStub.withArgs('/root/.cs-launch.json').returns(true); + + await baseCommandInstance.prepareConfig(); + + expect(baseCommandInstance.sharedConfig).to.deep.equal({ + ...require('../../src/config').default, + currentConfig: {}, + flags: {}, + host: 'host.contentstack.io', + projectBasePath: '/root/', + authtoken: 'testauthtoken', + authType: 'testauthorisationType', + authorization: 'testoauthAccessToken', + config: '/root/.cs-launch.json', + isExistingProject: true, + logsApiBaseUrl: 'https://dev11-app.csnonprod.com/launch-api/logs/graphql', + manageApiBaseUrl: 'https://dev11-app.csnonprod.com/launch-api/manage/graphql', + }); + }); + }); +});