diff --git a/packages/sitecore-jss-cli/.nycrc b/packages/sitecore-jss-cli/.nycrc index 5368323135..172dad2e1b 100644 --- a/packages/sitecore-jss-cli/.nycrc +++ b/packages/sitecore-jss-cli/.nycrc @@ -7,7 +7,12 @@ "**/*.test.ts", "src/test-data", "dist", - "src/test.ts" + "src/test.ts", + "**/create.ts", + "**/deploy.package.ts", + "**/index.global.ts", + "**/index.ts", + "**/cli.global.ts" ], "all": true, "reporter": [ diff --git a/packages/sitecore-jss-cli/package.json b/packages/sitecore-jss-cli/package.json index d0e7b2617a..77a8f5ebb4 100644 --- a/packages/sitecore-jss-cli/package.json +++ b/packages/sitecore-jss-cli/package.json @@ -12,8 +12,8 @@ "lint": "eslint ./src/**/*.ts", "prepublishOnly": "npm run build", "jss": "node ./dist/cjs/bin/jss.js", - "test": "mocha --require ts-node/register \"./src/**/*.test.ts\"", - "coverage": "nyc npm test" + "test": "mocha --require ts-node/register/transpile-only \"./src/**/*.test.ts\"", + "coverage": "nyc --require ts-node/register/transpile-only npm test" }, "engines": { "node": ">=12", diff --git a/packages/sitecore-jss-cli/src/cli.test.ts b/packages/sitecore-jss-cli/src/cli.test.ts new file mode 100644 index 0000000000..178b91c209 --- /dev/null +++ b/packages/sitecore-jss-cli/src/cli.test.ts @@ -0,0 +1,73 @@ +/* eslint-disable no-unused-expressions */ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { getPackageScriptCommands, makeCommand } from './cli'; +import * as resolvePkg from './resolve-package'; +import * as packageScript from './run-package-script'; +import { Arguments } from 'yargs'; + +describe('cli', () => { + describe('getPackageScriptCommands', async () => { + afterEach(() => { + sinon.restore(); + }); + const packageJson = { + scripts: { + first: 'do --this', + second: 'do --that', + third: 'do --all', + }, + }; + + it('should read scripts from package.json and return result with handlers', async () => { + sinon.stub(resolvePkg, 'default').resolves(packageJson); + + const result = await getPackageScriptCommands(); + const runScriptStub = sinon.stub(packageScript, 'default'); + const mockArgs: Arguments = { + _: ['arg1', 'arg2'], + $0: '', + }; + + expect(Object.keys(packageJson.scripts)).to.be.deep.equal(Object.keys(result)); + for (const key of Object.keys(result)) { + const expectedCommand = makeCommand(key); + for (const field of Object.keys(expectedCommand)) { + if (typeof expectedCommand[field] === 'function') { + expectedCommand[field](mockArgs); + expect(runScriptStub.called).to.be.true; + } else { + expect(result[key][field]).to.deep.equal(expectedCommand[field]); + } + } + } + }); + + it('should return empty result when package.json contents are empty', async () => { + const emptyPackage = {}; + + sinon.stub(resolvePkg, 'default').resolves(emptyPackage); + + const result = await getPackageScriptCommands(); + + expect(result).to.deep.equal(emptyPackage); + }); + + it('should ignore jss script entry', async () => { + const packageJson = { + scripts: { + jss: 'do --this', + second: 'do --that', + third: 'do --all', + }, + }; + const { jss: _, ...expectedScripts } = packageJson.scripts; + + sinon.stub(resolvePkg, 'default').resolves(packageJson); + + const result = await getPackageScriptCommands(); + + expect(Object.keys(expectedScripts)).to.be.deep.equal(Object.keys(result)); + }); + }); +}); diff --git a/packages/sitecore-jss-cli/src/cli.ts b/packages/sitecore-jss-cli/src/cli.ts index 3fa84dba77..ba6fa91b11 100644 --- a/packages/sitecore-jss-cli/src/cli.ts +++ b/packages/sitecore-jss-cli/src/cli.ts @@ -7,7 +7,7 @@ import * as commands from './scripts'; /** * Get package script commands */ -async function getPackageScriptCommands() { +export async function getPackageScriptCommands() { const packageJson = await resolvePackage(); const result: { [key: string]: CommandModule } = {}; @@ -20,18 +20,7 @@ async function getPackageScriptCommands() { return; } - const command = { - command: script, - describe: 'package.json script', - builder: {}, - disableStrictArgs: true, - handler: (argv: Arguments) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if ((argv as any)._[0]) { - runPackageScript(process.argv.slice(2)); - } - }, - }; + const command = makeCommand(script); result[script] = command; }); @@ -39,6 +28,24 @@ async function getPackageScriptCommands() { return result; } +/** + * @param script + */ +export function makeCommand(script: string) { + return { + command: script, + describe: 'package.json script', + builder: {}, + disableStrictArgs: true, + handler: (argv: Arguments) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((argv as any)._[0]) { + runPackageScript(process.argv.slice(2)); + } + }, + }; +} + /** * implements CLI commands when executed from a local node_modules folder */ diff --git a/packages/sitecore-jss-cli/src/micro-manifest.test.ts b/packages/sitecore-jss-cli/src/micro-manifest.test.ts new file mode 100644 index 0000000000..5237874e85 --- /dev/null +++ b/packages/sitecore-jss-cli/src/micro-manifest.test.ts @@ -0,0 +1,130 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-expressions */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { expect } from 'chai'; +import sinon from 'sinon'; +import fs from 'fs'; +import * as microManifest from './micro-manifest'; +import * as resolvePkg from './resolve-package'; +import * as manifestHandler from './scripts/manifest'; +import * as packageHandler from './scripts/package'; + +import * as packageDeploy from '@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/package-deploy'; +import * as verify from '@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/setup/verify-setup'; +import * as resolveJssConfig from '@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/resolve-scjssconfig'; +import tmp from 'tmp'; +import path from 'path'; + +describe('micro-manifest script', () => { + afterEach(() => { + sinon.restore(); + }); + + const packageJson = { + config: { + appName: 'jss-unit-package', + }, + }; + + const scJssConfig = { + sitecore: { + deploySecret: 'you-are-85%-water', + deployUrl: 'deploy.jss.com', + }, + }; + + const argv = { + appName: 'jss-manifest', + deployUrl: 'customs.jss.com', + deploySecret: 'snape-kills-dumbledore', + debugSecurity: true, + acceptCertificate: 'yes', + }; + + const tmpDirReturnsDefault = { + err: false, + tempDir: 'C:/temp', + cleanupTempDir: sinon.stub(), + }; + + describe('verifyArgs', () => { + it('should use fallaback for appName, deployUrl, deploySecret, if not proided', async () => { + const localArgv = { + ...argv, + appName: undefined, + deployUrl: undefined, + deploySecret: undefined, + }; + + const expectedArgv = { + ...argv, + appName: packageJson.config.appName, + deployUrl: scJssConfig.sitecore.deployUrl, + deploySecret: scJssConfig.sitecore.deploySecret, + }; + + sinon.stub(verify, 'verifySetup'); + sinon.stub(resolveJssConfig, 'resolveScJssConfig').resolves(scJssConfig); + sinon.stub(resolvePkg, 'default').resolves(packageJson); + expect(await microManifest.verifyArgs(localArgv)).to.deep.equal(expectedArgv); + }); + }); + + describe('microManifest', () => { + it('should create temp directory and finalize manifest creation', async () => { + const tmpDirReturns = { + ...tmpDirReturnsDefault, + cleanUpTempDir: sinon.stub(), + }; + + sinon.stub(resolveJssConfig, 'resolveScJssConfig').resolves(scJssConfig); + sinon.stub(resolvePkg, 'default').resolves(packageJson); + const tmpStub = sinon + .stub(tmp, 'dir') + .callsArgWith(1, tmpDirReturns.err, tmpDirReturns.tempDir, tmpDirReturns.cleanupTempDir); + const writeFileStub = sinon.stub(fs, 'writeFileSync'); + sinon.stub(fs, 'existsSync').returns(true); + const manifestStub = sinon.stub(manifestHandler, 'handler'); + const packageStub = sinon.stub(packageHandler, 'handler'); + const deployStub = sinon.stub(packageDeploy, 'packageDeploy'); + + const manifestFolder = path.join(tmpDirReturnsDefault.tempDir, 'manifest'); + const packageDir = path.join(tmpDirReturnsDefault.tempDir, 'package'); + const manifestContents = 'stub'; + + const manifestArgs = { + manifestSourceFiles: [path.join(manifestFolder, 'tempManifestSource.js')], + manifestOutputPath: path.join(manifestFolder, 'tempManifest.json'), + noDictionary: true, + ...argv, + }; + + const packageArgs = { + skipManifest: true, + noFiles: true, + packageOutputPath: path.join(packageDir, 'tempPackage.manifest.zip'), + ...manifestArgs, + }; + + const deployArgs = { + appName: argv.appName, + packagePath: packageArgs.packageOutputPath, + importServiceUrl: argv.deployUrl, + secret: argv.deploySecret, + debugSecurity: argv.debugSecurity, + acceptCertificate: argv.acceptCertificate, + }; + + await microManifest.default(argv, manifestContents); + + expect(tmpStub.called).to.be.true; + expect( + writeFileStub.calledWith(manifestArgs.manifestSourceFiles[0], manifestContents, 'utf8') + ).to.be.true; + expect(manifestStub.calledWith(manifestArgs)).to.be.true; + expect(packageStub.calledWith(packageArgs)).to.be.true; + expect(deployStub.calledWith(deployArgs)).to.be.true; + expect(tmpDirReturns.cleanupTempDir.called).to.be.true; + }); + }); +}); diff --git a/packages/sitecore-jss-cli/src/micro-manifest.ts b/packages/sitecore-jss-cli/src/micro-manifest.ts index fb63046f99..e3e7dad0f5 100644 --- a/packages/sitecore-jss-cli/src/micro-manifest.ts +++ b/packages/sitecore-jss-cli/src/micro-manifest.ts @@ -21,38 +21,7 @@ export default async function microManifest( ) { verifySetup(); - const packageJson = await resolvePackage(); - - if (!argv.appName) { - argv.appName = packageJson.config.appName; - } - if (!argv.appName) { - throw new Error('App Name was not defined as a parameter or in the package.json config'); - } - - const jssConfig = await resolveScJssConfig({ configPath: argv.config as string }); - - if (!argv.deployUrl) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const legacyConfig = jssConfig.sitecore as any; - argv.deployUrl = legacyConfig.shipUrl ? legacyConfig.shipUrl : jssConfig.sitecore.deployUrl; - } - if (!argv.deployUrl) { - throw new Error('deployUrl was not defined as a parameter or in the scjssconfig.json file'); - } - - if (/\/ship\/services\/package/.test(argv.deployUrl)) { - throw new Error( - 'deployUrl appears to be a Sitecore.Ship endpoint. JSS no longer uses Ship. You will need to reconfigure your endpoint to the JSS deploy service and provide an app shared secret to deploy.' - ); - } - - if (!argv.deploySecret) { - argv.deploySecret = jssConfig.sitecore.deploySecret; - } - if (!argv.deploySecret) { - throw new Error('deploySecret was not defined as a parameter or in the scjssconfig.json file'); - } + argv = await verifyArgs(argv); return new Promise((resolve, reject) => { tmp.dir({ unsafeCleanup: true }, async (err, tempDir, cleanupTempDir) => { @@ -110,3 +79,42 @@ export default async function microManifest( }); }); } + +/** + * + */ +export async function verifyArgs(argv: { [key: string]: any }) { + const packageJson = await resolvePackage(); + if (!argv.appName) { + argv.appName = packageJson.config.appName; + } + if (!argv.appName) { + throw new Error('App Name was not defined as a parameter or in the package.json config'); + } + + const jssConfig = await resolveScJssConfig({ configPath: argv.config as string }); + + if (!argv.deployUrl) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const legacyConfig = jssConfig.sitecore as any; + argv.deployUrl = legacyConfig.shipUrl ? legacyConfig.shipUrl : jssConfig.sitecore.deployUrl; + } + if (!argv.deployUrl) { + throw new Error('deployUrl was not defined as a parameter or in the scjssconfig.json file'); + } + + if (/\/ship\/services\/package/.test(argv.deployUrl)) { + throw new Error( + 'deployUrl appears to be a Sitecore.Ship endpoint. JSS no longer uses Ship. You will need to reconfigure your endpoint to the JSS deploy service and provide an app shared secret to deploy.' + ); + } + + if (!argv.deploySecret) { + argv.deploySecret = jssConfig.sitecore.deploySecret; + } + if (!argv.deploySecret) { + throw new Error('deploySecret was not defined as a parameter or in the scjssconfig.json file'); + } + + return argv; +} diff --git a/packages/sitecore-jss-cli/src/run-package-script.test.ts b/packages/sitecore-jss-cli/src/run-package-script.test.ts new file mode 100644 index 0000000000..599555b486 --- /dev/null +++ b/packages/sitecore-jss-cli/src/run-package-script.test.ts @@ -0,0 +1,17 @@ +/* eslint-disable no-unused-expressions */ +import * as spawn from './spawn'; +import sinon from 'sinon'; +import runPackageScript, { transformPackageArgs } from './run-package-script'; +import { expect } from 'chai'; + +describe('run-package-script', () => { + it('runPackageScript should invoke spawn with args', () => { + const spawnMock = sinon.stub(spawn, 'default'); + + const mockArgs = ['arg1', 'arg2']; + + runPackageScript(mockArgs); + + expect(spawnMock.calledWith('npm', transformPackageArgs(mockArgs))).to.be.true; + }); +}); diff --git a/packages/sitecore-jss-cli/src/run-package-script.ts b/packages/sitecore-jss-cli/src/run-package-script.ts index 4659178192..efc00ab59e 100644 --- a/packages/sitecore-jss-cli/src/run-package-script.ts +++ b/packages/sitecore-jss-cli/src/run-package-script.ts @@ -12,7 +12,7 @@ export default function runPackageScript( options?: SpawnSyncOptionsWithStringEncoding ) { // npm needs a -- delimiter before any extra args - const npmArgs = ['run', ...args.slice(0, 1), '--', ...args.slice(1)]; + const npmArgs = transformPackageArgs(args); runPackageManagerCommand(npmArgs, options); } @@ -30,3 +30,10 @@ export function runPackageManagerCommand( console.log(`> npm ${npmArgs.join(' ')}`); spawn('npm', npmArgs, options); } + +/** + * @param args + */ +export function transformPackageArgs(args: string[]) { + return ['run', ...args.slice(0, 1), '--', ...args.slice(1)]; +} diff --git a/packages/sitecore-jss-cli/src/scripts/clean.test.ts b/packages/sitecore-jss-cli/src/scripts/clean.test.ts new file mode 100644 index 0000000000..1e681e82b6 --- /dev/null +++ b/packages/sitecore-jss-cli/src/scripts/clean.test.ts @@ -0,0 +1,44 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-expressions */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable prettier/prettier */ +import { expect } from 'chai'; +import sinon from 'sinon'; +import * as resolvePackage from '../resolve-package'; +import { handler } from './clean'; + +import * as devTools from '@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/clean'; + +describe('clean script', () => { + + afterEach(() => { + sinon.restore(); + }); + + it('clean should be called with path from argv', async () => { + const stub = sinon.stub(devTools, 'clean'); + const argv = { + path: 'C:/The-Curious-Case-of-Benjamin-Button', + }; + + await handler(argv); + expect(stub.calledWith(argv)).to.equal(true); + }); + + it('should exit on missing path', async () => { + const processStub = sinon.stub(process, 'exit'); + const logSpy = sinon.spy(console, 'error'); + const errorMsg = 'Path argument was not specified and no \'buildArtifactsPath\' in package.json.'; + const argv = { path: '' }; + + // ensure clean is not executed - since we stub process.exit - and the script execution will continue + const stub = sinon.stub(devTools, 'clean'); + sinon.stub(resolvePackage, 'default').resolves({ config: { buildArtifactsPath: '' } }); + const cleanImpl = require('./clean'); + + await cleanImpl.handler(argv); + + expect(processStub.calledWith(1)).to.be.true; + expect(logSpy.getCall(0).args[0].toString()).to.contain(errorMsg); + }); +}); diff --git a/packages/sitecore-jss-cli/src/scripts/deploy.app.test.ts b/packages/sitecore-jss-cli/src/scripts/deploy.app.test.ts new file mode 100644 index 0000000000..7044f96358 --- /dev/null +++ b/packages/sitecore-jss-cli/src/scripts/deploy.app.test.ts @@ -0,0 +1,36 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-expressions */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { expect } from 'chai'; +import sinon from 'sinon'; +import * as deployFiles from './deploy.files'; +import * as deployItems from './deploy.items'; +import { handler } from './deploy.app'; + +describe('deploy.app script', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should deploy both items and files', async () => { + const deployFilesStub = sinon.stub(deployFiles, 'handler').resolves(); + const deployItemsStub = sinon.stub(deployItems, 'handler').resolves(); + const argv = {}; + await handler(argv); + + expect(deployItemsStub.calledWith(argv)).to.be.true; + expect(deployFilesStub.calledWith(argv)).to.be.true; + }); + + it('should log error and exit on deployItems error', async () => { + const errorMsg = 'Cant connect to Sitecore if youre a unit test :('; + const deployItemsStub = sinon.stub(deployItems, 'handler').rejects(errorMsg); + const processStub = sinon.stub(process, 'exit'); + const logSpy = sinon.spy(console, 'log'); + await handler({}); + + expect(deployItemsStub.called).to.be.true; + expect(processStub.calledWith(1)).to.be.true; + expect(logSpy.getCall(0).args[0].toString()).to.contain(errorMsg); + }); +}); diff --git a/packages/sitecore-jss-cli/src/scripts/deploy.component.test.ts b/packages/sitecore-jss-cli/src/scripts/deploy.component.test.ts new file mode 100644 index 0000000000..0ebd3361f9 --- /dev/null +++ b/packages/sitecore-jss-cli/src/scripts/deploy.component.test.ts @@ -0,0 +1,59 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-expressions */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { expect } from 'chai'; +import sinon from 'sinon'; +import * as microManifest from '../micro-manifest'; +import { handler } from './deploy.component'; + +describe('deploy.component script', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should parse fields from input', async () => { + const argv = { + fields: ['single', 'multi:multi-line text'], + skipDeploy: true, + }; + const expectedFields = [ + { + name: 'single', + type: 'Single-LineText', + }, + { + name: 'multi', + type: 'multi-linetext', + }, + ]; + const logSpy = sinon.spy(console, 'log'); + + await handler(argv); + + const logOutput = logSpy + .getCall(0) + .args[0].toString() + .replace(/\s/g, ''); + + expect(logOutput).to.contain(JSON.stringify(expectedFields)); + }); + + // the actual work is done in microManifest - so we just test success messages + // and test microManifest separately + it('should log on successful deploy', async () => { + sinon.stub(microManifest, 'default').resolves(); + const logSpy = sinon.spy(console, 'log'); + const argv = { + name: 'unit', + displayName: 'absolute unit', + icon: '', + fields: [], + placeholders: [], + allowedPlaceholders: [], + }; + const successMsg = 'Your component has been created (or updated)!'; + await handler(argv); + + expect(logSpy.getCall(0).args[0].toString()).to.contain(successMsg); + }); +}); diff --git a/packages/sitecore-jss-cli/src/scripts/deploy.component.ts b/packages/sitecore-jss-cli/src/scripts/deploy.component.ts index eab556d1c7..1cd3476829 100644 --- a/packages/sitecore-jss-cli/src/scripts/deploy.component.ts +++ b/packages/sitecore-jss-cli/src/scripts/deploy.component.ts @@ -38,7 +38,7 @@ export function args(yargs: Argv) { * @param {any} argv */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -async function handler(argv: any) { +export async function handler(argv: any) { // create micro-manifest to deploy from const fields: Array<{ name: string; type: string }> = []; diff --git a/packages/sitecore-jss-cli/src/scripts/deploy.config.test.ts b/packages/sitecore-jss-cli/src/scripts/deploy.config.test.ts new file mode 100644 index 0000000000..a52108afe9 --- /dev/null +++ b/packages/sitecore-jss-cli/src/scripts/deploy.config.test.ts @@ -0,0 +1,72 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-expressions */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { handler } from './deploy.config'; +import path from 'path'; +import * as resolvePkg from '../resolve-package'; +import * as deployTools from '@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/deploy'; +import * as verify from '@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/setup/verify-setup'; +import * as scJssConfigTool from '@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/resolve-scjssconfig'; + +describe('deploy.config script', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should call deploy with parsed options', async () => { + const argv = { + destination: 'S:/Santiago', + source: 'F:/Biarritz', + }; + const expectedOptions = { + destinationPath: argv.destination, + sourcePath: argv.source, + clean: false, + }; + const deployStub = sinon.stub(deployTools, 'deploy'); + sinon.stub(verify, 'verifySetup'); + + await handler(argv); + + expect(deployStub.calledWith(expectedOptions)).to.be.true; + }); + + it('should attempt to resolve destination when not provided', async () => { + const argv = { + destination: '', + source: 'F:/Biarritz', + }; + + const scJssConfig = { + sitecore: { + instancePath: 'S:/', + }, + }; + const packageJson = { + config: { + sitecoreConfigPath: 'Santiago', + }, + }; + + const expectedOptions = { + destinationPath: path.join( + scJssConfig.sitecore.instancePath, + packageJson.config.sitecoreConfigPath + ), + sourcePath: argv.source, + clean: false, + }; + + const deployStub = sinon.stub(deployTools, 'deploy'); + sinon.stub(verify, 'verifySetup'); + sinon.stub(scJssConfigTool, 'resolveScJssConfig').resolves(scJssConfig); + + sinon.stub(resolvePkg, 'default').resolves(packageJson); + + await handler(argv); + + expect(deployStub.calledWith(expectedOptions)).to.be.true; + }); +}); diff --git a/packages/sitecore-jss-cli/src/scripts/deploy.files.test.ts b/packages/sitecore-jss-cli/src/scripts/deploy.files.test.ts new file mode 100644 index 0000000000..7b3bd76d41 --- /dev/null +++ b/packages/sitecore-jss-cli/src/scripts/deploy.files.test.ts @@ -0,0 +1,105 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-expressions */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { handler } from './deploy.files'; +import * as resolvePkg from '../resolve-package'; +import path from 'path'; +import * as deployTools from '@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/deploy'; +import * as verify from '@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/setup/verify-setup'; +import * as scJssConfigTool from '@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/resolve-scjssconfig'; + +describe('deploy.files script', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should call deploy with parsed options', async () => { + const argv = { + destination: 'S:/Santiago', + source: 'F:/Biarritz', + exclude: ['this one', 'that one'], + clean: false, + }; + const expectedOptions = { + destinationPath: argv.destination, + sourcePath: argv.source, + excludeFile: argv.exclude, + clean: argv.clean, + }; + + const packageJson = { + config: { + sitecoreConfigPath: 'Santiago', + sitecoreDistPath: 'C:/SanFrancisco', + }, + }; + const deployStub = sinon.stub(deployTools, 'deploy'); + sinon.stub(verify, 'verifySetup'); + sinon.stub(resolvePkg, 'default').resolves(packageJson); + + await handler(argv); + + expect(deployStub.calledWith(expectedOptions)).to.be.true; + }); + + it('should attempt to resolve destination when not provided', async () => { + const argv = { + destination: '', + source: 'F:/Biarritz', + exclude: ['this one', 'that one'], + clean: false, + }; + + const scJssConfig = { + sitecore: { + instancePath: 'S:/', + }, + }; + const packageJson = { + config: { + sitecoreConfigPath: 'Santiago', + sitecoreDistPath: 'SanFrancisco', + }, + }; + + const expectedOptions = { + destinationPath: path.join( + scJssConfig.sitecore.instancePath, + packageJson.config.sitecoreDistPath + ), + sourcePath: argv.source, + excludeFile: argv.exclude, + clean: false, + }; + + const deployStub = sinon.stub(deployTools, 'deploy'); + sinon.stub(resolvePkg, 'default').resolves(packageJson); + sinon.stub(verify, 'verifySetup'); + sinon.stub(scJssConfigTool, 'resolveScJssConfig').resolves(scJssConfig); + + await handler(argv); + + expect(deployStub.calledWith(expectedOptions)).to.be.true; + }); + + it('should abort and log error if sitecore dist path is missing from package.json', async () => { + const packageJson = { + config: { + sitecoreConfigPath: 'Santiago', + }, + }; + const deployStub = sinon.stub(deployTools, 'deploy'); + sinon.stub(verify, 'verifySetup'); + sinon.stub(resolvePkg, 'default').resolves(packageJson); + const logSpy = sinon.spy(console, 'error'); + + await handler({}); + + expect(logSpy.getCall(0).args[0].toString()).to.contain( + 'The current project does not support file deployment into the Sitecore instance. You should use an HTTP POST based integration for Experience Editor support. See SDK documentation for details.' + ); + expect(deployStub.called).to.be.false; + }); +}); diff --git a/packages/sitecore-jss-cli/src/scripts/deploy.items.test.ts b/packages/sitecore-jss-cli/src/scripts/deploy.items.test.ts new file mode 100644 index 0000000000..10a2501d4b --- /dev/null +++ b/packages/sitecore-jss-cli/src/scripts/deploy.items.test.ts @@ -0,0 +1,99 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-expressions */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { handler } from './deploy.items'; +import * as resolvePkg from '../resolve-package'; +import * as deployTools from '@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/package-deploy'; +import * as verify from '@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/setup/verify-setup'; +import * as scJssConfigTool from '@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/resolve-scjssconfig'; + +describe('deploy.items script', () => { + afterEach(() => { + sinon.restore(); + }); + + const expectedDeployArgs = (argv: any) => ({ + appName: argv.appName, + packagePath: argv.packageOutputPath, + importServiceUrl: argv.deployUrl, + secret: argv.deploySecret, + debugSecurity: argv.debugSecurity, + proxy: argv.proxy, + acceptCertificate: argv.acceptCertificate, + }); + + const packageJson = { + config: { + sitecoreConfigPath: 'Santiago', + sitecoreDistPath: 'C:/SanFrancisco', + appName: 'jss-unit-package', + deploySecret: 'you-are-85%-water', + deployUrl: 'deploy.jss.com', + }, + }; + const scJssConfig = { + sitecore: { + instancePath: 'S:/', + deploySecret: 'you-are-85%-water', + deployUrl: 'deploy.jss.com', + }, + }; + + it('should call deployPackage with parsed deployArgs', async () => { + const argv = { + appName: 'jss-unit', + packageOutputPath: 'mock', + deployUrl: 'customs.jss.com', + deploySecret: 'snape-kills-dumbledore', + debugSecurity: true, + proxy: 'localhost:3000', + acceptCertificate: 'yes', + destination: 'S:/Santiago', + source: 'F:/Biarritz', + exclude: ['this one', 'that one'], + clean: false, + skipPackage: true, + }; + + const deployStub = sinon.stub(deployTools, 'packageDeploy').resolves(); + sinon.stub(verify, 'verifySetup'); + sinon.stub(resolvePkg, 'default').resolves(packageJson); + sinon.stub(scJssConfigTool, 'resolveScJssConfig').resolves(scJssConfig); + + await handler(argv); + + expect(deployStub.calledWith(expectedDeployArgs(argv))).to.be.true; + }); + + it('should use fallback appName, deploySecret, deployUrl when missing', async () => { + const argv = { + packageOutputPath: 'mock', + debugSecurity: true, + proxy: 'localhost:3000', + acceptCertificate: 'yes', + destination: 'S:/Santiago', + source: 'F:/Biarritz', + exclude: ['this one', 'that one'], + clean: false, + skipPackage: true, + }; + + const expectedArgs = { + ...expectedDeployArgs(argv), + appName: packageJson.config.appName, + importServiceUrl: scJssConfig.sitecore.deployUrl, + secret: scJssConfig.sitecore.deploySecret, + }; + + const deployStub = sinon.stub(deployTools, 'packageDeploy').resolves(); + sinon.stub(verify, 'verifySetup'); + sinon.stub(resolvePkg, 'default').resolves(packageJson); + sinon.stub(scJssConfigTool, 'resolveScJssConfig').resolves(scJssConfig); + + await handler(argv); + + expect(deployStub.calledWith(expectedArgs)).to.be.true; + }); +}); diff --git a/packages/sitecore-jss-cli/src/scripts/deploy.template.test.ts b/packages/sitecore-jss-cli/src/scripts/deploy.template.test.ts new file mode 100644 index 0000000000..d940d4d3e5 --- /dev/null +++ b/packages/sitecore-jss-cli/src/scripts/deploy.template.test.ts @@ -0,0 +1,59 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-expressions */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { expect } from 'chai'; +import sinon from 'sinon'; +import * as microManifest from '../micro-manifest'; +import { handler } from './deploy.template'; + +describe('deploy.template script', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should parse fields from input', async () => { + const argv = { + fields: ['single', 'multi:multi-line text'], + skipDeploy: true, + }; + const expectedFields = [ + { + name: 'single', + type: 'Single-LineText', + }, + { + name: 'multi', + type: 'multi-linetext', + }, + ]; + const logSpy = sinon.spy(console, 'log'); + + await handler(argv); + + const logOutput = logSpy + .getCall(0) + .args[0].toString() + .replace(/\s/g, ''); + + expect(logOutput).to.contain(JSON.stringify(expectedFields)); + }); + + // the actual work is done in microManifest - so we just test success messages + // and test microManifest separately + it('should log on successful deploy', async () => { + sinon.stub(microManifest, 'default').resolves(); + const logSpy = sinon.spy(console, 'log'); + const argv = { + name: 'unit', + displayName: 'absolute unit', + icon: '', + fields: [], + placeholders: [], + allowedPlaceholders: [], + }; + const successMsg = 'Your template has been created (or updated)!'; + await handler(argv); + + expect(logSpy.getCall(1).args[0].toString()).to.contain(successMsg); + }); +}); diff --git a/packages/sitecore-jss-cli/src/scripts/deploy.template.ts b/packages/sitecore-jss-cli/src/scripts/deploy.template.ts index 76ca001be8..fe84b624ab 100644 --- a/packages/sitecore-jss-cli/src/scripts/deploy.template.ts +++ b/packages/sitecore-jss-cli/src/scripts/deploy.template.ts @@ -76,7 +76,7 @@ export function args(yargs: Argv) { * @param {Argv} argv */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -async function handler(argv: any) { +export async function handler(argv: any) { // create micro-manifest to deploy from const fields: Array<{ name: string; type: string }> = []; diff --git a/packages/sitecore-jss-cli/src/scripts/elephant.test.ts b/packages/sitecore-jss-cli/src/scripts/elephant.test.ts new file mode 100644 index 0000000000..ae8d05204c --- /dev/null +++ b/packages/sitecore-jss-cli/src/scripts/elephant.test.ts @@ -0,0 +1,16 @@ +import { expect } from 'chai'; +import { handler } from './elephant'; + +describe('elephant', () => { + it('should not be horsing around', async () => { + expect(await handler()).to.not.be.equal(` + /\/\ + / \ + ~/(o o) + ~/ ) ( + ~/ ( ) + ~/ ~~ + ~/ | + `); + }); +}); diff --git a/packages/sitecore-jss-cli/src/scripts/environment.test.ts b/packages/sitecore-jss-cli/src/scripts/environment.test.ts new file mode 100644 index 0000000000..295fba0e3d --- /dev/null +++ b/packages/sitecore-jss-cli/src/scripts/environment.test.ts @@ -0,0 +1,16 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { handler } from './environment'; + +describe('environment script', () => { + it('should print env variable', async () => { + process.env.FOO = 'bar'; + const logSpy = sinon.spy(console, 'log'); + const argv = { + name: 'FOO', + }; + handler(argv); + + expect(logSpy.calledWith(`process.env.${argv.name} = ${process.env.FOO}`)); + }); +}); diff --git a/packages/sitecore-jss-cli/src/scripts/manifest.test.ts b/packages/sitecore-jss-cli/src/scripts/manifest.test.ts new file mode 100644 index 0000000000..4710d38662 --- /dev/null +++ b/packages/sitecore-jss-cli/src/scripts/manifest.test.ts @@ -0,0 +1,117 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-expressions */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { expect } from 'chai'; +import sinon from 'sinon'; +import readlineSync from 'readline-sync'; +import chalk from 'chalk'; +import { handler } from './manifest'; +import * as resolvePkg from '../resolve-package'; +import * as generate from '@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/manifest/generator/generate'; +import * as verify from '@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/setup/verify-setup'; +import fs from 'fs'; + +describe('manifest script', () => { + afterEach(() => { + sinon.restore(); + }); + + const packageJson = { + config: { + sitecoreConfigPath: 'Santiago', + sitecoreDistPath: 'C:/SanFrancisco', + appName: 'jss-unit-package', + deploySecret: 'you-are-85%-water', + deployUrl: 'deploy.jss.com', + rootPlaceholders: ['second-best'], + language: 'da-DK', + }, + }; + + const argv = { + manifestSourceFiles: ['one.js', 'another.js'], + require: 'config.js', + appName: 'jss-manifest', + includeContent: false, + includeDictionary: true, + manifestOutputPath: 'C:/JSS', + debug: false, + rootPlaceholders: ['main', 'top'], + pipelinePatchFiles: ['red-tape.txt'], + wipe: false, + allowConflictingPlaceholderNames: false, + language: 'en', + }; + + const defaultExpectedArgs = { + fileGlobs: argv.manifestSourceFiles, + requireArg: argv.require, + appName: argv.appName, + excludeItems: !argv.includeContent, + excludeMedia: !argv.includeContent, + excludeDictionary: !argv.includeDictionary, + outputPath: `${argv.manifestOutputPath}/sitecore-import.json`, + language: argv.language, + pipelinePatchFileGlobs: argv.pipelinePatchFiles, + debug: argv.debug, + rootPlaceholders: argv.rootPlaceholders, + wipe: argv.wipe, + skipPlaceholderBlacklist: argv.allowConflictingPlaceholderNames, + }; + + it('should invoke file generation with parsed args', async () => { + const generateFileStub = sinon.stub(generate, 'generateToFile').resolves(); + sinon.stub(verify, 'verifySetup'); + sinon.stub(resolvePkg, 'default').resolves(packageJson); + + sinon.stub(fs, 'existsSync').returns(false); + + await handler(argv); + + expect(generateFileStub.calledWith(defaultExpectedArgs)).to.be.true; + }); + + it('should use fallaback for appName, language, rootPlaceholders, if not proided', async () => { + const cutArgv = { + ...argv, + appName: undefined, + language: undefined, + rootPlaceholders: undefined, + }; + + const expectedArgs = { + ...defaultExpectedArgs, + appName: packageJson.config.appName, + language: packageJson.config.language, + rootPlaceholders: packageJson.config.rootPlaceholders, + }; + + const generateFileStub = sinon.stub(generate, 'generateToFile').resolves(); + sinon.stub(verify, 'verifySetup'); + sinon.stub(resolvePkg, 'default').resolves(packageJson); + + await handler(cutArgv); + + expect(generateFileStub.calledWith(expectedArgs)).to.be.true; + }); + + it('should clarify when wipe is invoked', async () => { + const cutArgv = { + ...argv, + wipe: true, + unattendedWipe: false, + }; + const keyInStub = sinon.stub(readlineSync, 'keyInYN').returns(false); + sinon.stub(process, 'exit'); + + const generateFileStub = sinon.stub(generate, 'generateToFile').resolves(); + sinon.stub(verify, 'verifySetup'); + sinon.stub(resolvePkg, 'default').resolves(packageJson); + + await handler(cutArgv); + + expect( + keyInStub.calledWith(chalk.yellow('This will delete any content changes made in Sitecore')) + ).to.be.true; + }); +}); diff --git a/packages/sitecore-jss-cli/src/scripts/package.test.ts b/packages/sitecore-jss-cli/src/scripts/package.test.ts new file mode 100644 index 0000000000..4235bb630d --- /dev/null +++ b/packages/sitecore-jss-cli/src/scripts/package.test.ts @@ -0,0 +1,62 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-expressions */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { handler } from './package'; +import * as resolvePkg from '../resolve-package'; +import * as generate from '@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/package-generate'; + +describe('package script', () => { + afterEach(() => { + sinon.restore(); + }); + + const packageJson = { + config: { + appName: 'jss-unit-package', + }, + }; + + const argv = { + appName: 'jss-manifest', + manifestOutputPath: 'C:/JSS', + packageOutputPath: 'C:/packages', + noItems: true, + skipManifest: true, + }; + + const expectedGenerateArgs = { + appName: argv.appName, + manifestPath: argv.manifestOutputPath, + manifestFileName: 'sitecore-import.json', + outputPath: argv.packageOutputPath, + }; + + it('should invoke package generation with parsed args', async () => { + const generatePkgStub = sinon.stub(generate, 'packageGenerate').resolves(); + sinon.stub(resolvePkg, 'default').resolves(packageJson); + + await handler(argv); + + expect(generatePkgStub.calledWith(expectedGenerateArgs)).to.be.true; + }); + + it('should use fallaback for appName if not proided', async () => { + const cutArgv = { + ...argv, + appName: undefined, + }; + + const expectedArgs = { + ...expectedGenerateArgs, + appName: packageJson.config.appName, + }; + const generatePkgStub = sinon.stub(generate, 'packageGenerate').resolves(); + sinon.stub(resolvePkg, 'default').resolves(packageJson); + + await handler(cutArgv); + + expect(generatePkgStub.calledWith(expectedArgs)).to.be.true; + }); +}); diff --git a/packages/sitecore-jss-cli/src/spawn.test.ts b/packages/sitecore-jss-cli/src/spawn.test.ts new file mode 100644 index 0000000000..bcace597a2 --- /dev/null +++ b/packages/sitecore-jss-cli/src/spawn.test.ts @@ -0,0 +1,64 @@ +/* eslint-disable no-unused-expressions */ +import { expect } from 'chai'; +import spawn from 'cross-spawn'; +import sinon from 'sinon'; +import * as scriptSpawn from './spawn'; + +describe('spawn script', () => { + afterEach(() => { + sinon.restore(); + }); + + const defaultSpawnReturn = { + pid: 0, + output: [], + stdout: '', + stderr: '', + status: null, + signal: null, + }; + + it('should log and exit on SIGKILL', () => { + const consoleSpy = sinon.spy(console, 'log'); + const exitStub = sinon.stub(process, 'exit'); + sinon.stub(spawn, 'sync').returns({ + ...defaultSpawnReturn, + signal: 'SIGKILL', + }); + const errorMsg = + 'The operation failed because the process exited too early. ' + + 'This probably means the system ran out of memory or someone called ' + + '`kill -9` on the process.'; + scriptSpawn.default('test', []); + + expect(consoleSpy.calledWith(errorMsg)).to.be.true; + expect(exitStub.calledWith(1)).to.be.true; + }); + + it('should log and exit on SIGTERM', () => { + const consoleSpy = sinon.spy(console, 'log'); + const exitStub = sinon.stub(process, 'exit'); + sinon.stub(spawn, 'sync').returns({ + ...defaultSpawnReturn, + signal: 'SIGTERM', + }); + const errorMsg = + 'The operation failed because the process exited too early. ' + + 'Someone might have called `kill` or `killall`, or the system could ' + + 'be shutting down.'; + scriptSpawn.default('test', []); + expect(consoleSpy.calledWith(errorMsg)).to.be.true; + expect(exitStub.calledWith(1)).to.be.true; + }); + + it('should exit with returned status code, when its not 0', () => { + const exitStatus = 42; + sinon.stub(spawn, 'sync').returns({ + ...defaultSpawnReturn, + status: exitStatus, + }); + const exitStub = sinon.stub(process, 'exit'); + scriptSpawn.default('test', []); + expect(exitStub.calledWith(exitStatus)).to.be.true; + }); +});