diff --git a/bin/commands/runs.js b/bin/commands/runs.js index 1eccaee1..a4ee13c4 100644 --- a/bin/commands/runs.js +++ b/bin/commands/runs.js @@ -10,6 +10,7 @@ const archiver = require("../helpers/archiver"), fileHelpers = require("../helpers/fileHelpers"), syncRunner = require("../helpers/syncRunner"), checkUploaded = require("../helpers/checkUploaded"), + packageInstaller = require("../helpers/packageInstaller"), reportGenerator = require('../helpers/reporterHTML').reportGenerator, {initTimeComponents, instrumentEventTime, markBlockStart, markBlockEnd, getTimeComponents} = require('../helpers/timeComponents'), downloadBuildArtifacts = require('../helpers/buildArtifacts').downloadBuildArtifacts, @@ -103,121 +104,151 @@ module.exports = function run(args) { return checkUploaded.checkUploadedMd5(bsConfig, args, {markBlockStart, markBlockEnd}).then(function (md5data) { markBlockEnd('checkAlreadyUploaded'); - // Archive the spec files - markBlockStart('zip'); - markBlockStart('zip.archive'); - return archiver.archive(bsConfig.run_settings, config.fileName, args.exclude, md5data).then(function (data) { - markBlockEnd('zip.archive'); - - // Uploaded zip file - markBlockStart('zip.zipUpload'); - return zipUploader.zipUpload(bsConfig, config.fileName, md5data).then(async function (zip) { - markBlockEnd('zip.zipUpload'); - markBlockEnd('zip'); - // Create build - - //setup Local Testing - markBlockStart('localSetup'); - let bs_local = await utils.setupLocalTesting(bsConfig, args); - markBlockEnd('localSetup'); - markBlockStart('createBuild'); - return build.createBuild(bsConfig, zip).then(function (data) { - markBlockEnd('createBuild'); - markBlockEnd('total'); - utils.setProcessHooks(data.build_id, bsConfig, bs_local, args); - let message = `${data.message}! ${Constants.userMessages.BUILD_CREATED} with build id: ${data.build_id}`; - let dashboardLink = `${Constants.userMessages.VISIT_DASHBOARD} ${data.dashboard_url}`; - utils.exportResults(data.build_id, `${config.dashboardUrl}${data.build_id}`); - if ((utils.isUndefined(bsConfig.run_settings.parallels) && utils.isUndefined(args.parallels)) || (!utils.isUndefined(bsConfig.run_settings.parallels) && bsConfig.run_settings.parallels == Constants.cliMessages.RUN.DEFAULT_PARALLEL_MESSAGE)) { - logger.warn(Constants.userMessages.NO_PARALLELS); - } - if (bsConfig.run_settings.cypress_version && bsConfig.run_settings.cypress_version !== data.cypress_version) { - if (bsConfig.run_settings.cypress_version.toString().match(Constants.LATEST_VERSION_SYNTAX_REGEX)) { - let versionMessage = utils.latestSyntaxToActualVersionMessage(bsConfig.run_settings.cypress_version, data.cypress_version, data.framework_upgrade_message); - logger.info(versionMessage); - } else { - let versionMessage = utils.versionChangedMessage(bsConfig.run_settings.cypress_version, data.cypress_version, data.framework_upgrade_message); - logger.warn(versionMessage); + markBlockStart('packageInstaller'); + return packageInstaller.packageWrapper(bsConfig, config.packageDirName, config.packageFileName, md5data, {markBlockStart, markBlockEnd}).then(function (packageData) { + markBlockEnd('packageInstaller'); + + // Archive the spec files + markBlockStart('zip'); + markBlockStart('zip.archive'); + return archiver.archive(bsConfig.run_settings, config.fileName, args.exclude, md5data).then(function (data) { + markBlockEnd('zip.archive'); + + // Uploaded zip file + markBlockStart('zip.zipUpload'); + return zipUploader.zipUpload(bsConfig, md5data, packageData).then(async function (zip) { + markBlockEnd('zip.zipUpload'); + markBlockEnd('zip'); + + // Create build + //setup Local Testing + markBlockStart('localSetup'); + let bs_local = await utils.setupLocalTesting(bsConfig, args); + markBlockEnd('localSetup'); + markBlockStart('createBuild'); + return build.createBuild(bsConfig, zip).then(function (data) { + markBlockEnd('createBuild'); + markBlockEnd('total'); + utils.setProcessHooks(data.build_id, bsConfig, bs_local, args); + let message = `${data.message}! ${Constants.userMessages.BUILD_CREATED} with build id: ${data.build_id}`; + let dashboardLink = `${Constants.userMessages.VISIT_DASHBOARD} ${data.dashboard_url}`; + utils.exportResults(data.build_id, `${config.dashboardUrl}${data.build_id}`); + if ((utils.isUndefined(bsConfig.run_settings.parallels) && utils.isUndefined(args.parallels)) || (!utils.isUndefined(bsConfig.run_settings.parallels) && bsConfig.run_settings.parallels == Constants.cliMessages.RUN.DEFAULT_PARALLEL_MESSAGE)) { + logger.warn(Constants.userMessages.NO_PARALLELS); } - } - if (!args.disableNpmWarning && bsConfig.run_settings.npm_dependencies && Object.keys(bsConfig.run_settings.npm_dependencies).length <= 0) { - logger.warn(Constants.userMessages.NO_NPM_DEPENDENCIES); - logger.warn(Constants.userMessages.NO_NPM_DEPENDENCIES_READ_MORE); - } + if (bsConfig.run_settings.cypress_version && bsConfig.run_settings.cypress_version !== data.cypress_version) { + if (bsConfig.run_settings.cypress_version.toString().match(Constants.LATEST_VERSION_SYNTAX_REGEX)) { + let versionMessage = utils.latestSyntaxToActualVersionMessage(bsConfig.run_settings.cypress_version, data.cypress_version, data.framework_upgrade_message); + logger.info(versionMessage); + } else { + let versionMessage = utils.versionChangedMessage(bsConfig.run_settings.cypress_version, data.cypress_version, data.framework_upgrade_message); + logger.warn(versionMessage); + } + } - if (args.sync) { - syncRunner.pollBuildStatus(bsConfig, data).then(async (exitCode) => { + if (!args.disableNpmWarning && bsConfig.run_settings.npm_dependencies && Object.keys(bsConfig.run_settings.npm_dependencies).length <= 0) { + logger.warn(Constants.userMessages.NO_NPM_DEPENDENCIES); + logger.warn(Constants.userMessages.NO_NPM_DEPENDENCIES_READ_MORE); + } - // stop the Local instance - await utils.stopLocalBinary(bsConfig, bs_local, args); - // waiting for 5 secs for upload to complete (as a safety measure) - await new Promise(resolve => setTimeout(resolve, 5000)); + if (args.sync) { + syncRunner.pollBuildStatus(bsConfig, data).then(async (exitCode) => { - // download build artifacts - if (utils.nonEmptyArray(bsConfig.run_settings.downloads)) { - await downloadBuildArtifacts(bsConfig, data.build_id, args); - } + // stop the Local instance + await utils.stopLocalBinary(bsConfig, bs_local, args); + + // waiting for 5 secs for upload to complete (as a safety measure) + await new Promise(resolve => setTimeout(resolve, 5000)); + + // download build artifacts + if (utils.nonEmptyArray(bsConfig.run_settings.downloads)) { + await downloadBuildArtifacts(bsConfig, data.build_id, args); + } - // Generate custom report! - reportGenerator(bsConfig, data.build_id, args, function(){ - utils.sendUsageReport(bsConfig, args, `${message}\n${dashboardLink}`, Constants.messageTypes.SUCCESS, null); - utils.handleSyncExit(exitCode, data.dashboard_url); + // Generate custom report! + reportGenerator(bsConfig, data.build_id, args, function(){ + utils.sendUsageReport(bsConfig, args, `${message}\n${dashboardLink}`, Constants.messageTypes.SUCCESS, null); + utils.handleSyncExit(exitCode, data.dashboard_url); + }); }); - }); - } else if (utils.nonEmptyArray(bsConfig.run_settings.downloads)) { - logger.info(Constants.userMessages.ASYNC_DOWNLOADS.replace('', data.build_id)); - } + } else if (utils.nonEmptyArray(bsConfig.run_settings.downloads)) { + logger.info(Constants.userMessages.ASYNC_DOWNLOADS.replace('', data.build_id)); + } - logger.info(message); - logger.info(dashboardLink); - if(!args.sync) logger.info(Constants.userMessages.EXIT_SYNC_CLI_MESSAGE.replace("", data.build_id)); - let dataToSend = { - time_components: getTimeComponents(), - unique_id: utils.generateUniqueHash(), - build_id: data.build_id, - }; - if (bsConfig && bsConfig.connection_settings) { - if (bsConfig.connection_settings.local_mode) { - dataToSend.local_mode = bsConfig.connection_settings.local_mode; + logger.info(message); + logger.info(dashboardLink); + if(!args.sync) logger.info(Constants.userMessages.EXIT_SYNC_CLI_MESSAGE.replace("",data.build_id)); + let dataToSend = { + time_components: getTimeComponents(), + unique_id: utils.generateUniqueHash(), + package_error: utils.checkError(packageData), + checkmd5_error: utils.checkError(md5data), + build_id: data.build_id, + }; + if (bsConfig && bsConfig.connection_settings) { + if (bsConfig.connection_settings.local_mode) { + dataToSend.local_mode = bsConfig.connection_settings.local_mode; + } + if (bsConfig.connection_settings.usedAutoLocal) { + dataToSend.used_auto_local = bsConfig.connection_settings.usedAutoLocal; + } } - if (bsConfig.connection_settings.usedAutoLocal) { - dataToSend.used_auto_local = bsConfig.connection_settings.usedAutoLocal; + utils.sendUsageReport(bsConfig, args, `${message}\n${dashboardLink}`, Constants.messageTypes.SUCCESS, null, dataToSend); + return; + }).catch(async function (err) { + // Build creation failed + logger.error(err); + // stop the Local instance + await utils.stopLocalBinary(bsConfig, bs_local, args); + + utils.sendUsageReport(bsConfig, args, err, Constants.messageTypes.ERROR, 'build_failed'); + process.exitCode = Constants.ERROR_EXIT_CODE; + }); + }).catch(function (err) { + // Zip Upload failed | Local Start failed + logger.error(err); + if(err === Constants.userMessages.LOCAL_START_FAILED){ + utils.sendUsageReport(bsConfig, args, `${err}\n${Constants.userMessages.LOCAL_START_FAILED}`, Constants.messageTypes.ERROR, 'local_start_failed'); + } else { + logger.error(Constants.userMessages.ZIP_UPLOAD_FAILED); + fileHelpers.deleteZip(); + utils.sendUsageReport(bsConfig, args, `${err}\n${Constants.userMessages.ZIP_UPLOAD_FAILED}`, Constants.messageTypes.ERROR, 'zip_upload_failed'); + try { + fileHelpers.deletePackageArchieve(); + } catch (err) { + utils.sendUsageReport(bsConfig, args, Constants.userMessages.NPM_DELETE_FAILED, Constants.messageTypes.ERROR, 'npm_deletion_failed'); } } - utils.sendUsageReport(bsConfig, args, `${message}\n${dashboardLink}`, Constants.messageTypes.SUCCESS, null, dataToSend); - return; - }).catch(async function (err) { - // Build creation failed - logger.error(err); - // stop the Local instance - await utils.stopLocalBinary(bsConfig, bs_local, args); - - utils.sendUsageReport(bsConfig, args, err, Constants.messageTypes.ERROR, 'build_failed'); process.exitCode = Constants.ERROR_EXIT_CODE; }); }).catch(function (err) { - // Zip Upload failed | Local Start failed + // Zipping failed logger.error(err); - if(err === Constants.userMessages.LOCAL_START_FAILED){ - utils.sendUsageReport(bsConfig, args, `${err}\n${Constants.userMessages.LOCAL_START_FAILED}`, Constants.messageTypes.ERROR, 'local_start_failed'); - } else { - logger.error(Constants.userMessages.ZIP_UPLOAD_FAILED); + logger.error(Constants.userMessages.FAILED_TO_ZIP); + utils.sendUsageReport(bsConfig, args, `${err}\n${Constants.userMessages.FAILED_TO_ZIP}`, Constants.messageTypes.ERROR, 'zip_creation_failed'); + try { fileHelpers.deleteZip(); - utils.sendUsageReport(bsConfig, args, `${err}\n${Constants.userMessages.ZIP_UPLOAD_FAILED}`, Constants.messageTypes.ERROR, 'zip_upload_failed'); + } catch (err) { + utils.sendUsageReport(bsConfig, args, Constants.userMessages.ZIP_DELETE_FAILED, Constants.messageTypes.ERROR, 'zip_deletion_failed'); + } + try { + fileHelpers.deletePackageArchieve(); + } catch (err) { + utils.sendUsageReport(bsConfig, args, Constants.userMessages.NPM_DELETE_FAILED, Constants.messageTypes.ERROR, 'npm_deletion_failed'); } process.exitCode = Constants.ERROR_EXIT_CODE; }); }).catch(function (err) { - // Zipping failed + // package installer failed logger.error(err); - logger.error(Constants.userMessages.FAILED_TO_ZIP); - utils.sendUsageReport(bsConfig, args, `${err}\n${Constants.userMessages.FAILED_TO_ZIP}`, Constants.messageTypes.ERROR, 'zip_creation_failed'); + logger.error(Constants.userMessages.FAILED_CREATE_NPM_ARCHIVE); + utils.sendUsageReport(bsConfig, args, Constants.userMessages.FAILED_CREATE_NPM_ARCHIVE, Constants.messageTypes.ERROR, 'npm_package_archive_failed'); try { - fileHelpers.deleteZip(); + fileHelpers.deletePackageArchieve(); } catch (err) { - utils.sendUsageReport(bsConfig, args, Constants.userMessages.ZIP_DELETE_FAILED, Constants.messageTypes.ERROR, 'zip_deletion_failed'); + utils.sendUsageReport(bsConfig, args, Constants.userMessages.NPM_DELETE_FAILED, Constants.messageTypes.ERROR, 'npm_deletion_failed'); } process.exitCode = Constants.ERROR_EXIT_CODE; }); diff --git a/bin/helpers/capabilityHelper.js b/bin/helpers/capabilityHelper.js index b0aac7a2..8c36b8b9 100644 --- a/bin/helpers/capabilityHelper.js +++ b/bin/helpers/capabilityHelper.js @@ -41,6 +41,12 @@ const caps = (bsConfig, zip) => { reject("Test suite is empty"); } + // Npm package + if (zip.npm_package_url && zip.npm_package_url.split("://")[1].length !== 0) { + obj.npm_package_suite = zip.npm_package_url.split("://")[1]; + } + obj.cache_dependencies = bsConfig.run_settings.cache_dependencies; + // Inferred settings if(bsConfig.connection_settings){ if (bsConfig.connection_settings.local_mode_inferred) { diff --git a/bin/helpers/checkUploaded.js b/bin/helpers/checkUploaded.js index 81d3252e..7ff3f68c 100644 --- a/bin/helpers/checkUploaded.js +++ b/bin/helpers/checkUploaded.js @@ -10,10 +10,13 @@ const crypto = require('crypto'), utils = require('./utils'); -const checkSpecsMd5 = (runSettings, excludeFiles, instrumentBlocks) => { +const checkSpecsMd5 = (runSettings, args, instrumentBlocks) => { return new Promise(function (resolve, reject) { + if (args["force-upload"]) { + return resolve("force-upload"); + } let cypressFolderPath = path.dirname(runSettings.cypressConfigFilePath); - let ignoreFiles = utils.getFilesToIgnore(runSettings, excludeFiles, false); + let ignoreFiles = utils.getFilesToIgnore(runSettings, args.exclude, false); let options = { cwd: cypressFolderPath, ignore: ignoreFiles, @@ -45,12 +48,12 @@ const checkPackageMd5 = (runSettings) => { const outputHash = crypto.createHash(Constants.hashingOptions.algo); let packageJSON = {}; if (typeof runSettings.package_config_options === 'object') { - Object.assign(packageJSON, runSettings.package_config_options); + Object.assign(packageJSON, utils.sortJsonKeys(runSettings.package_config_options)); } if (typeof runSettings.npm_dependencies === 'object') { Object.assign(packageJSON, { - devDependencies: runSettings.npm_dependencies, + devDependencies: utils.sortJsonKeys(runSettings.npm_dependencies), }); } @@ -58,6 +61,12 @@ const checkPackageMd5 = (runSettings) => { let packageJSONString = JSON.stringify(packageJSON); outputHash.update(packageJSONString); } + let cypressFolderPath = path.dirname(runSettings.cypressConfigFilePath); + let sourceNpmrc = path.join(cypressFolderPath, ".npmrc"); + if (fs.existsSync(sourceNpmrc)) { + const npmrc = fs.readFileSync(sourceNpmrc, {encoding:'utf8', flag:'r'}); + outputHash.update(npmrc); + } return outputHash.digest(Constants.hashingOptions.encoding) }; @@ -66,18 +75,27 @@ const checkUploadedMd5 = (bsConfig, args, instrumentBlocks) => { return new Promise(function (resolve) { let obj = { zipUrlPresent: false, + packageUrlPresent: false, }; - if (args["force-upload"]) { + if (args["force-upload"] && !utils.isTrueString(bsConfig.run_settings.cache_dependencies)) { return resolve(obj); } + instrumentBlocks.markBlockStart("checkAlreadyUploaded.md5Total"); - checkSpecsMd5(bsConfig.run_settings, args.exclude, instrumentBlocks).then(function (md5data) { - Object.assign(obj, {md5sum: md5data}); + checkSpecsMd5(bsConfig.run_settings, args, instrumentBlocks).then(function (zip_md5sum) { instrumentBlocks.markBlockStart("checkAlreadyUploaded.md5Package"); - let package_md5sum = checkPackageMd5(bsConfig.run_settings); + let npm_package_md5sum = checkPackageMd5(bsConfig.run_settings); instrumentBlocks.markBlockEnd("checkAlreadyUploaded.md5Package"); instrumentBlocks.markBlockEnd("checkAlreadyUploaded.md5Total"); - let data = JSON.stringify({ zip_md5sum: md5data, instrument_package_md5sum: package_md5sum}); + let data = {}; + if (!args["force-upload"]) { + Object.assign(data, { zip_md5sum }); + Object.assign(obj, { zip_md5sum }); + } + if (utils.isTrueString(bsConfig.run_settings.cache_dependencies)) { + Object.assign(data, { npm_package_md5sum }); + Object.assign(obj, { npm_package_md5sum }); + } let options = { url: config.checkMd5sum, @@ -89,7 +107,7 @@ const checkUploadedMd5 = (bsConfig, args, instrumentBlocks) => { 'Content-Type': 'application/json', "User-Agent": utils.getUserAgent(), }, - body: data + body: JSON.stringify(data) }; instrumentBlocks.markBlockStart("checkAlreadyUploaded.railsCheck"); @@ -104,15 +122,30 @@ const checkUploadedMd5 = (bsConfig, args, instrumentBlocks) => { } catch (error) { zipData = {}; } - if (resp.statusCode === 200 && !utils.isUndefined(zipData.zipUrl)) { - Object.assign(obj, zipData, {zipUrlPresent: true}); + if (resp.statusCode === 200) { + if (!utils.isUndefined(zipData.zipUrl)) { + Object.assign(obj, zipData, {zipUrlPresent: true}); + } + if (!utils.isUndefined(zipData.npmPackageUrl)) { + Object.assign(obj, zipData, {packageUrlPresent: true}); + } + } + if (utils.isTrueString(zipData.disableNpmSuiteCache)) { + bsConfig.run_settings.cache_dependencies = false; + Object.assign(obj, {packageUrlPresent: false}); + delete obj.npm_package_md5sum; + } + if (utils.isTrueString(zipData.disableTestSuiteCache)) { + args["force-upload"] = true; + Object.assign(obj, {zipUrlPresent: false}); + delete obj.zip_md5sum; } instrumentBlocks.markBlockEnd("checkAlreadyUploaded.railsCheck"); resolve(obj); } }); - }).catch((error) => { - resolve({zipUrlPresent: false}); + }).catch((err) => { + resolve({zipUrlPresent: false, packageUrlPresent: false, error: err.stack.substring(0,100)}); }); }); }; diff --git a/bin/helpers/config.js b/bin/helpers/config.js index a17f0c43..530ca530 100644 --- a/bin/helpers/config.js +++ b/bin/helpers/config.js @@ -17,6 +17,8 @@ config.buildUrl = `${config.cypress_v1}/builds/`; config.buildStopUrl = `${config.cypress_v1}/builds/stop/`; config.checkMd5sum = `${config.cypress_v1}/md5sumcheck/`; config.fileName = "tests.zip"; +config.packageFileName = "bstackPackages.tar.gz"; +config.packageDirName = "tmpBstackPackages"; config.retries = 5; config.networkErrorExitCode = 2; diff --git a/bin/helpers/constants.js b/bin/helpers/constants.js index 48851393..8d19ae84 100644 --- a/bin/helpers/constants.js +++ b/bin/helpers/constants.js @@ -26,8 +26,11 @@ const userMessages = { MD5_CHECK_FAILED: "There was some issue while checking if zip is already uploaded.", ZIP_DELETE_FAILED: "Could not delete tests.zip successfully.", ZIP_DELETED: "Deleted tests.zip successfully.", + NPM_DELETE_FAILED: "Could not delete the dependency packages.", + NPM_DELETED: "Deleted dependency packages successfully.", API_DEPRECATED: "This version of API is deprecated, please use latest version of API.", FAILED_TO_ZIP: "Failed to zip files.", + FAILED_CREATE_NPM_ARCHIVE: "CLI execution failed due to some issue in npm setup. Please retry.", FAILED_MD5_CHECK: "Something went wrong - you can retry running browserstack-cypress with ‘--force-upload’ parameter, or contact BrowserStack Support.", VISIT_DASHBOARD: "Visit the Automate dashboard for real-time test reporting:", CONFLICTING_INIT_ARGUMENTS: "Conflicting arguments given. You can use --path only with a file name, and not with a file path.", @@ -36,6 +39,9 @@ const userMessages = { NO_NPM_DEPENDENCIES_READ_MORE: "Read more about npm dependencies here: https://www.browserstack.com/docs/automate/cypress/npm-packages. You can suppress this warning by using --disable-npm-warning flag.", VALIDATING_CONFIG: "Validating the config", UPLOADING_TESTS: "Uploading the tests to BrowserStack", + UPLOADING_TESTS_SUCCESS: "Uploaded tests successfully", + UPLOADING_NPM_PACKAGES: "Uploading required node_modules to BrowserStack", + UPLOADING_NPM_PACKAGES_SUCCESS: "Uploaded node_modules successfully", LOCAL_TRUE: "you will now be able to test localhost / private URLs", LOCAL_FALSE: "you won't be able to test localhost / private URLs", EXIT_SYNC_CLI_MESSAGE: "Exiting the CLI, but your build is still running. You can use the --sync option to keep getting test updates. You can also use the build-info command now.", @@ -160,6 +166,8 @@ const filesToIgnoreWhileUploading = [ '.idea/**', '.vscode/**', '.npm/**', + 'bstackPackages.tar.gz', + 'tmpBstackPackages/**', '.yarn/**', 'build_artifacts/**' ]; @@ -179,6 +187,15 @@ const hashingOptions = { encoding: 'hex', }; +const packageInstallerOptions = { + npmLoad: { + loglevel: 'silent', + only: 'dev', + 'save-dev': true, + 'only-dev': true, + } +} + const specFileTypes = ['js', 'ts', 'feature', 'jsx', 'coffee', 'cjsx']; const DEFAULT_CYPRESS_SPEC_PATH = "cypress/integration" @@ -204,6 +221,7 @@ module.exports = Object.freeze({ filesToIgnoreWhileUploading, readDirOptions, hashingOptions, + packageInstallerOptions, specFileTypes, DEFAULT_CYPRESS_SPEC_PATH, SPEC_TOTAL_CHAR_LIMIT, diff --git a/bin/helpers/fileHelpers.js b/bin/helpers/fileHelpers.js index dd8f0383..743ab85c 100644 --- a/bin/helpers/fileHelpers.js +++ b/bin/helpers/fileHelpers.js @@ -4,6 +4,7 @@ const fs = require('fs-extra'), const logger = require('./logger').winstonLogger, Constants = require('../helpers/constants'), + process = require('process'), config = require('../helpers/config'); exports.write = function (f, message, args, cb) { @@ -35,6 +36,19 @@ exports.deleteZip = () => { } }; +exports.deletePackageArchieve = (logging = true) => { + try { + delete process.env.CYPRESS_INSTALL_BINARY; + fs.removeSync(config.packageFileName); + fs.removeSync(config.packageDirName); + if (logging) logger.info(Constants.userMessages.NPM_DELETED); + return 0; + } catch (err) { + if (logging) logger.info(Constants.userMessages.NPM_DELETE_FAILED); + return 1; + } +}; + exports.dirExists = function (filePath, cb) { let exists = false; if (fs.existsSync(path.dirname(filePath), cb)) { diff --git a/bin/helpers/packageInstaller.js b/bin/helpers/packageInstaller.js new file mode 100644 index 00000000..d9746237 --- /dev/null +++ b/bin/helpers/packageInstaller.js @@ -0,0 +1,138 @@ +'use strict'; +const npm = require('npm'), + archiver = require("archiver"), + path = require('path'), + os = require('os'), + fs = require('fs-extra'), + fileHelpers = require('./fileHelpers'), + logger = require("./logger").winstonLogger, + Constants = require('./constants'), + process = require('process'), + utils = require('./utils'); + +const setupPackageFolder = (runSettings, directoryPath) => { + return new Promise(function (resolve, reject) { + fileHelpers.deletePackageArchieve(false); + fs.mkdir(directoryPath, function (err) { + try { + if (err) { + return reject(err); + } + let packageJSON = {}; + if (typeof runSettings.package_config_options === 'object') { + Object.assign(packageJSON, runSettings.package_config_options); + } + + if (typeof runSettings.npm_dependencies === 'object') { + Object.assign(packageJSON, { + devDependencies: runSettings.npm_dependencies, + }); + } + + if (Object.keys(packageJSON).length > 0) { + let packageJSONString = JSON.stringify(packageJSON); + let packagePath = path.join(directoryPath, "package.json"); + fs.writeFileSync(packagePath, packageJSONString); + let cypressFolderPath = path.dirname(runSettings.cypressConfigFilePath); + let sourceNpmrc = path.join(cypressFolderPath, ".npmrc"); + let destNpmrc = path.join(directoryPath, ".npmrc"); + if (fs.existsSync(sourceNpmrc)) { + fs.copyFileSync(sourceNpmrc, destNpmrc); + } + return resolve("package file created"); + } + return reject("Nothing in package file"); + } catch(error) { + return reject(error); + } + }) + }) +}; + +const packageInstall = (packageDir) => { + return new Promise(function (resolve, reject) { + let savedPrefix = null; + let npmLoad = Constants.packageInstallerOptions.npmLoad + npmLoad["cache"] = fs.mkdtempSync(`${os.tmpdir()}${path.sep}`); + const installCallback = (err, result) => { + npm.prefix = savedPrefix; + if (err) { + return reject(err); + } + resolve(result); + }; + const loadCallback = (err) => { + if (err) { + return reject(err); + } + savedPrefix = npm.prefix; + npm.prefix = packageDir; + npm.commands.install(packageDir, [], installCallback); + }; + npm.load(npmLoad, loadCallback); + }); +}; + +const packageArchiver = (packageDir, packageFile) => { + return new Promise(function (resolve, reject) { + let output = fs.createWriteStream(packageFile); + let archive = archiver('tar', { + gzip: true + }); + archive.on('warning', function (err) { + if (err.code === 'ENOENT') { + logger.info(err); + } else { + reject(err); + } + }); + + output.on('close', function () { + resolve('Zipping completed'); + }); + + output.on('end', function () { + logger.info('Data has been drained'); + }); + + archive.on('error', function (err) { + reject(err); + }); + + archive.pipe(output); + archive.directory(packageDir, false); + archive.finalize(); + }) +} + +const packageWrapper = (bsConfig, packageDir, packageFile, md5data, instrumentBlocks) => { + return new Promise(function (resolve) { + let obj = { + packageArchieveCreated: false + }; + if (md5data.packageUrlPresent || !utils.isTrueString(bsConfig.run_settings.cache_dependencies)) { + return resolve(obj); + } + logger.info(`Installing required dependencies and building the package to upload to BrowserStack`); + instrumentBlocks.markBlockStart("packageInstaller.folderSetup"); + return setupPackageFolder(bsConfig.run_settings, packageDir).then((_result) => { + process.env.CYPRESS_INSTALL_BINARY = 0 + instrumentBlocks.markBlockEnd("packageInstaller.folderSetup"); + instrumentBlocks.markBlockStart("packageInstaller.packageInstall"); + return packageInstall(packageDir); + }).then((_result) => { + instrumentBlocks.markBlockEnd("packageInstaller.packageInstall"); + instrumentBlocks.markBlockStart("packageInstaller.packageArchive"); + return packageArchiver(packageDir, packageFile); + }).then((_result) => { + instrumentBlocks.markBlockEnd("packageInstaller.packageArchive"); + Object.assign(obj, { packageArchieveCreated: true }); + return resolve(obj); + }).catch((err) => { + obj.error = err.stack.substring(0,100); + return resolve(obj); + }) + }) +} + +exports.packageWrapper = packageWrapper; diff --git a/bin/helpers/utils.js b/bin/helpers/utils.js index 73f67a9b..b58ffdee 100644 --- a/bin/helpers/utils.js +++ b/bin/helpers/utils.js @@ -13,6 +13,7 @@ const usageReporting = require("./usageReporting"), Constants = require("./constants"), chalk = require('chalk'), syncCliLogger = require("../helpers/logger").syncCliLogger, + fileHelpers = require("./fileHelpers"), config = require("../helpers/config"); const request = require('request'); @@ -191,6 +192,11 @@ exports.setDefaults = (bsConfig, args) => { bsConfig.connection_settings = {}; } + // setting cache_dependencies to true if not present + if (this.isUndefined(bsConfig.run_settings.cache_dependencies)) { + bsConfig.run_settings.cache_dependencies = true; + } + } exports.setUsername = (bsConfig, args) => { @@ -338,6 +344,8 @@ exports.fixCommaSeparatedString = (string) => { exports.isUndefined = value => (value === undefined || value === null); +exports.isTrueString = value => (!this.isUndefined(value) && value.toString().toLowerCase() === 'true'); + exports.isFloat = (value) => Number(value) && Number(value) % 1 !== 0; exports.nonEmptyArray = (value) => { @@ -408,6 +416,82 @@ exports.isCypressProjDirValid = (cypressProjDir, integrationFoldDir) => { return parentTokens.every((t, i) => childTokens[i] === t); }; +exports.generateUploadParams = (bsConfig, filePath, md5data, fileDetails) => { + let options = { + url: config.uploadUrl, + auth: { + user: bsConfig.auth.username, + password: bsConfig.auth.access_key + }, + formData: { + file: fs.createReadStream(filePath), + filetype: fileDetails.filetype, + filename: fileDetails.filename, + zipMd5sum: md5data ? md5data : '', + }, + headers: { + "User-Agent": exports.getUserAgent(), + } + } + return options +} + +exports.sortJsonKeys = (unordered) => { + const ordered = Object.keys(unordered).sort().reduce( + (obj, key) => { + obj[key] = unordered[key]; + return obj; + }, + {} + ); + return ordered +} + +exports.generateUploadOptions = (type, md5data, packageData) => { + let options = {}; + switch (type) { + case 'zip': + options = { + archivePresent: true, + md5ReturnKey: "zip_url", + urlPresent: md5data.zipUrlPresent, + md5Data: md5data.zip_md5sum, + url: md5data.zipUrl, + propogateError: true, + fileDetails: { + filetype: "zip", + filename: "tests" + }, + messages: { + uploading: Constants.userMessages.UPLOADING_TESTS, + uploadingSuccess: Constants.userMessages.UPLOADING_TESTS_SUCCESS + }, + cleanupMethod: fileHelpers.deleteZip, + } + break; + case 'npm': + options = { + archivePresent: packageData.packageArchieveCreated, + md5ReturnKey: "npm_package_url", + urlPresent: md5data.packageUrlPresent, + md5Data: md5data.npm_package_md5sum, + url: md5data.npmPackageUrl, + propogateError: false, + fileDetails: { + filetype: "tar.gz", + filename: "bstackPackages" + }, + messages: { + uploading: Constants.userMessages.UPLOADING_NPM_PACKAGES, + uploadingSuccess: Constants.userMessages.UPLOADING_NPM_PACKAGES_SUCCESS + }, + cleanupMethod: fileHelpers.deletePackageArchieve, + } + break; + } + return options; +}; + exports.getLocalFlag = (connectionSettings) => { return ( !this.isUndefined(connectionSettings) && @@ -752,6 +836,12 @@ exports.latestSyntaxToActualVersionMessage = (latestSyntaxVersion, actualVersion return message } +exports.checkError = (data) => { + if (!this.isUndefined(data.error)) { + return data.error + } +} + exports.isJSONInvalid = (err, args) => { let invalid = true diff --git a/bin/helpers/zipUpload.js b/bin/helpers/zipUpload.js index 1c604206..7ae26da4 100644 --- a/bin/helpers/zipUpload.js +++ b/bin/helpers/zipUpload.js @@ -1,35 +1,22 @@ 'use strict'; const config = require("./config"), request = require("request"), - fs = require("fs"), logger = require("./logger").winstonLogger, Constants = require("./constants"), - utils = require("./utils"), - fileHelpers = require("./fileHelpers"); + utils = require("./utils"); -const uploadCypressZip = (bsConfig, filePath, md5data) => { +const uploadSuits = (bsConfig, filePath, opts) => { return new Promise(function (resolve, reject) { - if (md5data.zipUrlPresent) { - return resolve({ zip_url: md5data.zipUrl }); + if (opts.urlPresent) { + return resolve({ [opts.md5ReturnKey]: opts.url }); } - logger.info(Constants.userMessages.UPLOADING_TESTS); - let options = { - url: config.uploadUrl, - auth: { - user: bsConfig.auth.username, - password: bsConfig.auth.access_key - }, - formData: { - file: fs.createReadStream(filePath), - filetype: 'zip', - filename: 'tests', - zipMd5sum: md5data.md5sum ? md5data.md5sum : '', - }, - headers: { - "User-Agent": utils.getUserAgent(), - } + if (!opts.archivePresent) { + return resolve({}); } + logger.info(opts.messages.uploading); + + let options = utils.generateUploadParams(bsConfig, filePath, opts.md5Data, opts.fileDetails) let responseData = null; request.post(options, function (err, resp, body) { if (err) { @@ -38,23 +25,31 @@ const uploadCypressZip = (bsConfig, filePath, md5data) => { try { responseData = JSON.parse(body); } catch (e) { - responseData = null + responseData = {}; } if (resp.statusCode != 200) { + if (resp.statusCode == 401) { + if (responseData && responseData["error"]) { + return reject(responseData["error"]); + } else { + return reject(Constants.validationMessages.INVALID_DEFAULT_AUTH_PARAMS); + } + } + if (!opts.propogateError){ + return resolve({}); + } if(responseData && responseData["error"]){ reject(responseData["error"]); } else { - if(resp.statusCode == 401){ - reject(Constants.validationMessages.INVALID_DEFAULT_AUTH_PARAMS); - } else if (resp.statusCode == 413) { + if (resp.statusCode == 413) { reject(Constants.userMessages.ZIP_UPLOAD_LIMIT_EXCEEDED); } else { reject(Constants.userMessages.ZIP_UPLOADER_NOT_REACHABLE); } } } else { - logger.info(`Uploaded tests successfully (${responseData.zip_url})`); - fileHelpers.deleteZip(); + logger.info(`${opts.messages.uploadingSuccess} (${responseData[opts.md5ReturnKey]})`); + opts.cleanupMethod(); resolve(responseData); } } @@ -62,4 +57,21 @@ const uploadCypressZip = (bsConfig, filePath, md5data) => { }); } + +const uploadCypressZip = (bsConfig, md5data, packageData) => { + return new Promise(function (resolve, reject) { + let obj = {} + const zipOptions = utils.generateUploadOptions('zip', md5data, packageData); + const npmOptions = utils.generateUploadOptions('npm', md5data, packageData); + let zipUpload = uploadSuits(bsConfig, config.fileName, zipOptions); + let npmPackageUpload = uploadSuits(bsConfig, config.packageFileName, npmOptions); + Promise.all([zipUpload, npmPackageUpload]).then(function (uploads) { + uploads.forEach(upload => Object.assign(obj, upload)) + return resolve(obj); + }).catch((error) => { + return reject(error); + }) + }) +} + exports.zipUpload = uploadCypressZip diff --git a/package.json b/package.json index d8cf00fe..f725a2c5 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "uuid": "^8.3.2", "winston": "^2.3.1", "yargs": "^14.2.3", + "npm": "^6.14.15", "axios": "^0.21.1", "unzipper": "^0.10.11", "update-notifier": "^5.1.0" diff --git a/test/unit/bin/commands/runs.js b/test/unit/bin/commands/runs.js index 039f4c5a..069ebd24 100644 --- a/test/unit/bin/commands/runs.js +++ b/test/unit/bin/commands/runs.js @@ -219,12 +219,14 @@ describe("runs", () => { validateBstackJsonStub = sandbox.stub(); setUsageReportingFlagStub = sandbox.stub().returns(undefined); checkUploadedStub = sandbox.stub(); + packageInstallerStub = sandbox.stub(); sendUsageReportStub = sandbox.stub().callsFake(function () { return "end"; }); capabilityValidatorStub = sandbox.stub(); archiverStub = sandbox.stub(); deleteZipStub = sandbox.stub(); + deletePackageArchieveStub = sandbox.stub(); setLocalStub = sandbox.stub(); setLocalModeStub = sandbox.stub(); setupLocalTestingStub = sandbox.stub(); @@ -289,16 +291,21 @@ describe("runs", () => { }, '../helpers/fileHelpers': { deleteZip: deleteZipStub, + deletePackageArchieve: deletePackageArchieveStub }, '../helpers/checkUploaded': { checkUploadedMd5: checkUploadedStub, }, + '../helpers/packageInstaller': { + packageWrapper: packageInstallerStub, + }, }); validateBstackJsonStub.returns(Promise.resolve(bsConfig)); setupLocalTestingStub.returns(Promise.resolve("nothing")); capabilityValidatorStub.returns(Promise.resolve(Constants.validationMessages.VALIDATED)); checkUploadedStub.returns(Promise.resolve({ zipUrlPresent: false })); + packageInstallerStub.returns(Promise.resolve({ packageArchieveCreated: false })); archiverStub.returns(Promise.reject("random-error")); return runs(args) @@ -361,6 +368,7 @@ describe("runs", () => { getConfigPathStub = sandbox.stub(); setUsageReportingFlagStub = sandbox.stub().returns(undefined); checkUploadedStub = sandbox.stub(); + packageInstallerStub = sandbox.stub(); sendUsageReportStub = sandbox.stub().callsFake(function () { return "end"; }); @@ -368,6 +376,7 @@ describe("runs", () => { archiverStub = sandbox.stub(); zipUploadStub = sandbox.stub(); deleteZipStub = sandbox.stub(); + deletePackageArchieveStub = sandbox.stub(); setLocalStub = sandbox.stub(); setLocalModeStub = sandbox.stub(); setupLocalTestingStub = sandbox.stub(); @@ -432,6 +441,7 @@ describe("runs", () => { }, '../helpers/fileHelpers': { deleteZip: deleteZipStub, + deletePackageArchieve: deletePackageArchieveStub }, '../helpers/zipUpload': { zipUpload: zipUploadStub, @@ -439,12 +449,16 @@ describe("runs", () => { '../helpers/checkUploaded': { checkUploadedMd5: checkUploadedStub, }, + '../helpers/packageInstaller': { + packageWrapper: packageInstallerStub, + }, }); validateBstackJsonStub.returns(Promise.resolve(bsConfig)); capabilityValidatorStub.returns(Promise.resolve(Constants.validationMessages.VALIDATED)); setupLocalTestingStub.returns(Promise.resolve("nothing")); - checkUploadedStub.returns(Promise.resolve({ zipUrlPresent: false })) + checkUploadedStub.returns(Promise.resolve({ zipUrlPresent: false })); + packageInstallerStub.returns(Promise.resolve({ packageArchieveCreated: false })); archiverStub.returns(Promise.resolve("Zipping completed")); zipUploadStub.returns(Promise.reject("random-error")); @@ -507,6 +521,7 @@ describe("runs", () => { getConfigPathStub = sandbox.stub(); setUsageReportingFlagStub = sandbox.stub().returns(undefined); checkUploadedStub = sandbox.stub(); + packageInstallerStub = sandbox.stub(); sendUsageReportStub = sandbox.stub().callsFake(function () { return "end"; }); @@ -515,6 +530,7 @@ describe("runs", () => { zipUploadStub = sandbox.stub(); createBuildStub = sandbox.stub(); deleteZipStub = sandbox.stub(); + deletePackageArchieveStub = sandbox.stub(); setLocalStub = sandbox.stub(); setLocalModeStub = sandbox.stub(); setupLocalTestingStub = sandbox.stub(); @@ -581,6 +597,7 @@ describe("runs", () => { }, '../helpers/fileHelpers': { deleteZip: deleteZipStub, + deletePackageArchieve: deletePackageArchieveStub }, '../helpers/zipUpload': { zipUpload: zipUploadStub, @@ -591,6 +608,9 @@ describe("runs", () => { '../helpers/checkUploaded': { checkUploadedMd5: checkUploadedStub, }, + '../helpers/packageInstaller': { + packageWrapper: packageInstallerStub, + }, }); validateBstackJsonStub.returns(Promise.resolve(bsConfig)); @@ -600,6 +620,7 @@ describe("runs", () => { ); archiverStub.returns(Promise.resolve("Zipping completed")); checkUploadedStub.returns(Promise.resolve({ zipUrlPresent: false })); + packageInstallerStub.returns(Promise.resolve({ packageArchieveCreated: false })); zipUploadStub.returns(Promise.resolve("zip uploaded")); stopLocalBinaryStub.returns(Promise.resolve("nothing")); createBuildStub.returns(Promise.reject("random-error")); @@ -664,9 +685,11 @@ describe("runs", () => { setUserSpecsStub = sandbox.stub(); setTestEnvsStub = sandbox.stub(); setSystemEnvsStub = sandbox.stub(); + checkErrorStub = sandbox.stub(); getConfigPathStub = sandbox.stub(); setUsageReportingFlagStub = sandbox.stub().returns(undefined); checkUploadedStub = sandbox.stub(); + packageInstallerStub = sandbox.stub(); sendUsageReportStub = sandbox.stub().callsFake(function () { return "end"; }); @@ -676,6 +699,7 @@ describe("runs", () => { zipUploadStub = sandbox.stub(); createBuildStub = sandbox.stub(); deleteZipStub = sandbox.stub(); + deletePackageArchieveStub = sandbox.stub(); exportResultsStub = sandbox.stub(); deleteResultsStub = sandbox.stub(); setDefaultsStub = sandbox.stub(); @@ -712,7 +736,7 @@ describe("runs", () => { let errorCode = null; let message = `Success! ${Constants.userMessages.BUILD_CREATED} with build id: random_build_id`; let dashboardLink = `${Constants.userMessages.VISIT_DASHBOARD} ${dashboardUrl}`; - let data = {time_components: {}, unique_id: 'random_hash', build_id: 'random_build_id'} + let data = {time_components: {}, unique_id: 'random_hash', package_error: 'test', checkmd5_error: 'test', build_id: 'random_build_id'} const runs = proxyquire('../../../../bin/commands/runs', { '../helpers/utils': { @@ -747,6 +771,7 @@ describe("runs", () => { setConfig: setConfigStub, stopLocalBinary: stopLocalBinaryStub, nonEmptyArray: nonEmptyArrayStub, + checkError: checkErrorStub, setCLIMode: setCLIModeStub, setProcessHooks: setProcessHooksStub }, @@ -758,6 +783,7 @@ describe("runs", () => { }, '../helpers/fileHelpers': { deleteZip: deleteZipStub, + deletePackageArchieve: deletePackageArchieveStub }, '../helpers/zipUpload': { zipUpload: zipUploadStub, @@ -771,6 +797,9 @@ describe("runs", () => { '../helpers/checkUploaded': { checkUploadedMd5: checkUploadedStub, }, + '../helpers/packageInstaller': { + packageWrapper: packageInstallerStub, + }, '../helpers/timeComponents': { initTimeComponents: initTimeComponentsStub, instrumentEventTime: instrumentEventTimeStub, @@ -786,10 +815,12 @@ describe("runs", () => { Promise.resolve(Constants.validationMessages.VALIDATED) ); archiverStub.returns(Promise.resolve("Zipping completed")); - checkUploadedStub.returns(Promise.resolve({ zipUrlPresent: false })) + checkUploadedStub.returns(Promise.resolve({ zipUrlPresent: false })); + packageInstallerStub.returns(Promise.resolve({ packageArchieveCreated: false })); zipUploadStub.returns(Promise.resolve("zip uploaded")); stopLocalBinaryStub.returns(Promise.resolve("nothing")); nonEmptyArrayStub.returns(false); + checkErrorStub.returns('test'); createBuildStub.returns(Promise.resolve({ message: 'Success', build_id: 'random_build_id', dashboard_url: dashboardUrl })); return runs(args) diff --git a/test/unit/bin/helpers/capabilityHelper.js b/test/unit/bin/helpers/capabilityHelper.js index b640fa5b..c7cdd3aa 100644 --- a/test/unit/bin/helpers/capabilityHelper.js +++ b/test/unit/bin/helpers/capabilityHelper.js @@ -149,6 +149,8 @@ describe("capabilityHelper.js", () => { connection_settings: { local: false, }, + run_settings: { + } }; return capabilityHelper .caps(bsConfig, { zip_url: zip_url }) @@ -178,6 +180,8 @@ describe("capabilityHelper.js", () => { local: true, local_identifier: "abc" }, + run_settings: { + } }; return capabilityHelper .caps(bsConfig, { zip_url: zip_url }) @@ -208,6 +212,8 @@ describe("capabilityHelper.js", () => { connection_settings: { local: true, }, + run_settings: { + } }; return capabilityHelper .caps(bsConfig, { zip_url: zip_url }) @@ -235,6 +241,8 @@ describe("capabilityHelper.js", () => { versions: ["78", "77"], }, ], + run_settings: { + } }; return capabilityHelper .caps(bsConfig, { zip_url: zip_url }) diff --git a/test/unit/bin/helpers/checkUploaded.js b/test/unit/bin/helpers/checkUploaded.js index 891f5c74..f683b7dc 100644 --- a/test/unit/bin/helpers/checkUploaded.js +++ b/test/unit/bin/helpers/checkUploaded.js @@ -27,9 +27,14 @@ describe("checkUploaded", () => { }); context("checkUploadedMd5", () => { - let checkSpecsMd5Stub; + let checkSpecsMd5Stub, checkPackageMd5Stub, instrumentBlocks; beforeEach(() => { checkSpecsMd5Stub = sandbox.stub().returns(Promise.resolve("random_md5sum")); + checkPackageMd5Stub = sandbox.stub().returns("random_md5sum"); + instrumentBlocks = { + markBlockStart: sinon.stub(), + markBlockEnd: sinon.stub() + } }); it("resolves with zipUrlPresent false due to request error", () => { @@ -43,18 +48,64 @@ describe("checkUploaded", () => { checkSpecsMd5: checkSpecsMd5Stub }); let checkUploadedMd5rewire = checkUploaded.__get__('checkUploadedMd5'); - let instrumentBlocks = { - markBlockStart: sinon.stub(), - markBlockEnd: sinon.stub() - } return checkUploadedMd5rewire(bsConfig, {}, instrumentBlocks) - .then(function (data) { - chai.assert.equal(data.md5sum, 'random_md5sum'); + .then((data) => { + chai.assert.equal(data.zip_md5sum, 'random_md5sum'); chai.assert.equal(data.zipUrlPresent, false); + chai.assert.equal(data.packageUrlPresent, false); sinon.assert.calledOnce(requestStub); sinon.assert.calledOnce(checkSpecsMd5Stub); }) - .catch((error) => { + .catch((_error) => { + chai.assert.fail("Promise error"); + }); + }); + + it("resolves with zipUrlPresent false and packageUrlPresent false due to checkSpecsMd5 error", () => { + let requestStub = sandbox + .stub(request, "post") + .yields(new Error("random error"), null, null); + let checkSpecsMd5ErrorStub = sandbox.stub().returns(Promise.reject({message: "test error", stack: "test error stack"})); + + const checkUploaded = rewire("../../../../bin/helpers/checkUploaded"); + checkUploaded.__set__({ + request: { post: requestStub }, + checkSpecsMd5: checkSpecsMd5ErrorStub + }); + let checkUploadedMd5rewire = checkUploaded.__get__('checkUploadedMd5'); + return checkUploadedMd5rewire(bsConfig, {}, instrumentBlocks) + .then((data) => { + chai.assert.equal(data.zipUrlPresent, false); + chai.assert.equal(data.packageUrlPresent, false); + chai.assert.equal(data.error, "test error stack"); + sinon.assert.notCalled(requestStub); + sinon.assert.calledOnce(checkSpecsMd5ErrorStub); + }) + .catch((_error) => { + chai.assert.fail("Promise error"); + }); + }); + + it("resolves with zipUrlPresent false and packageUrlPresent false due to parsing error", () => { + let requestStub = sandbox + .stub(request, "post") + .yields(null, { statusCode: 200 }, '{"zipUrl":"bs://random_hashid}'); + + const checkUploaded = rewire("../../../../bin/helpers/checkUploaded"); + checkUploaded.__set__({ + request: { post: requestStub }, + checkSpecsMd5: checkSpecsMd5Stub + }); + let checkUploadedMd5rewire = checkUploaded.__get__('checkUploadedMd5'); + return checkUploadedMd5rewire(bsConfig, {}, instrumentBlocks) + .then((data) => { + chai.assert.equal(data.zipUrlPresent, false); + chai.assert.equal(data.packageUrlPresent, false); + chai.assert.equal(data.zip_md5sum, "random_md5sum"); + sinon.assert.calledOnce(requestStub); + sinon.assert.calledOnce(checkSpecsMd5Stub); + }) + .catch((_error) => { chai.assert.fail("Promise error"); }); }); @@ -70,17 +121,37 @@ describe("checkUploaded", () => { checkSpecsMd5: checkSpecsMd5Stub }); let checkUploadedMd5rewire = checkUploaded.__get__('checkUploadedMd5'); - let instrumentBlocks = { - markBlockStart: sinon.stub(), - markBlockEnd: sinon.stub() - } return checkUploadedMd5rewire(bsConfig, {}, instrumentBlocks) - .then(function (data) { - chai.assert.deepEqual(data, { md5sum: 'random_md5sum', zipUrlPresent: true, zipUrl: 'bs://random_hashid' }) + .then((data) => { + chai.assert.deepEqual(data, { zip_md5sum: 'random_md5sum', zipUrlPresent: true, packageUrlPresent: false, zipUrl: 'bs://random_hashid' }) sinon.assert.calledOnce(requestStub); sinon.assert.calledOnce(checkSpecsMd5Stub); }) - .catch((error) => { + .catch((_error) => { + chai.assert.fail("Promise error"); + }); + }); + + it("resolves with zipUrlPresent true, packageUrlPresent true, zip url, and packge url", () => { + let requestStub = sandbox + .stub(request, "post") + .yields(null, { statusCode: 200 }, '{"zipUrl":"bs://random_hashid", "npmPackageUrl":"bs://random_hashid2"}'); + + const checkUploaded = rewire("../../../../bin/helpers/checkUploaded"); + checkUploaded.__set__({ + request: { post: requestStub }, + checkSpecsMd5: checkSpecsMd5Stub, + checkPackageMd5: checkPackageMd5Stub + }); + let checkUploadedMd5rewire = checkUploaded.__get__('checkUploadedMd5'); + bsConfig.run_settings.cache_dependencies = true + return checkUploadedMd5rewire(bsConfig, {}, instrumentBlocks) + .then((data) => { + chai.assert.deepEqual(data, { zip_md5sum: 'random_md5sum', npm_package_md5sum: 'random_md5sum', zipUrlPresent: true, packageUrlPresent: true, zipUrl: 'bs://random_hashid', npmPackageUrl: 'bs://random_hashid2' }) + sinon.assert.calledOnce(requestStub); + sinon.assert.calledOnce(checkSpecsMd5Stub); + }) + .catch((_error) => { chai.assert.fail("Promise error"); }); }); @@ -93,25 +164,23 @@ describe("checkUploaded", () => { const checkUploaded = rewire("../../../../bin/helpers/checkUploaded"); checkUploaded.__set__({ request: { post: requestStub }, - checkSpecsMd5: checkSpecsMd5Stub + checkSpecsMd5: checkSpecsMd5Stub, + checkPackageMd5: checkPackageMd5Stub }); let checkUploadedMd5rewire = checkUploaded.__get__('checkUploadedMd5'); - let instrumentBlocks = { - markBlockStart: sinon.stub(), - markBlockEnd: sinon.stub() - } + bsConfig.run_settings.cache_dependencies = false return checkUploadedMd5rewire(bsConfig, {}, instrumentBlocks) - .then(function (data) { - chai.assert.deepEqual(data, { md5sum: 'random_md5sum', zipUrlPresent: false }) + .then((data) => { + chai.assert.deepEqual(data, { zip_md5sum: 'random_md5sum', zipUrlPresent: false, packageUrlPresent: false, }) sinon.assert.calledOnce(requestStub); sinon.assert.calledOnce(checkSpecsMd5Stub); }) - .catch((error) => { + .catch((_error) => { chai.assert.fail("Promise error"); }); }); - it("resolves with zipUrlPresent false if force-upload enabled", () => { + it("resolves with zipUrlPresent and packageUrlPresent false if force-upload enabled and cache_dependencies disabled", () => { let requestStub = sandbox .stub(request, "post") .yields(null, { statusCode: 404 }, '{"message":"zip_url for md5sum random_md5sum not found."}'); @@ -119,25 +188,75 @@ describe("checkUploaded", () => { const checkUploaded = rewire("../../../../bin/helpers/checkUploaded"); checkUploaded.__set__({ request: { post: requestStub }, - checkSpecsMd5: checkSpecsMd5Stub + checkSpecsMd5: checkSpecsMd5Stub, + checkPackageMd5: checkPackageMd5Stub }); let checkUploadedMd5rewire = checkUploaded.__get__('checkUploadedMd5'); - - return checkUploadedMd5rewire(bsConfig, {"force-upload": true}) - .then(function (data) { - chai.assert.deepEqual(data, { zipUrlPresent: false }) + bsConfig.run_settings.cache_dependencies = false + return checkUploadedMd5rewire(bsConfig, {"force-upload": true}, instrumentBlocks) + .then((data) => { + chai.assert.deepEqual(data, { zipUrlPresent: false, packageUrlPresent: false }) sinon.assert.notCalled(requestStub); sinon.assert.notCalled(checkSpecsMd5Stub); }) - .catch((error) => { + .catch((_error) => { + chai.assert.fail("Promise error"); + }); + }); + + it("resolves with zipUrlPresent false and packageUrlPresent true if force-upload enabled and cache_dependencies enabled", () => { + let requestStub = sandbox + .stub(request, "post") + .yields(null, { statusCode: 200 }, '{"npmPackageUrl":"bs://random_hashid2"}'); + + const checkUploaded = rewire("../../../../bin/helpers/checkUploaded"); + checkUploaded.__set__({ + request: { post: requestStub }, + checkSpecsMd5: checkSpecsMd5Stub, + checkPackageMd5: checkPackageMd5Stub + }); + let checkUploadedMd5rewire = checkUploaded.__get__('checkUploadedMd5'); + bsConfig.run_settings.cache_dependencies = true + return checkUploadedMd5rewire(bsConfig, {"force-upload": true}, instrumentBlocks) + .then((data) => { + chai.assert.deepEqual(data, { zipUrlPresent: false, packageUrlPresent: true, npm_package_md5sum: 'random_md5sum', npmPackageUrl: 'bs://random_hashid2' }) + sinon.assert.calledOnce(requestStub); + sinon.assert.calledOnce(checkSpecsMd5Stub); + }) + .catch((_error) => { + chai.assert.fail("Promise error"); + }); + }); + + it("resolves with zipUrlPresent false and packageUrlPresent false if diabled from rails", () => { + let requestStub = sandbox + .stub(request, "post") + .yields(null, { statusCode: 200 }, '{"disableNpmSuiteCache": true, "disableTestSuiteCache": true }'); + + const checkUploaded = rewire("../../../../bin/helpers/checkUploaded"); + checkUploaded.__set__({ + request: { post: requestStub }, + checkSpecsMd5: checkSpecsMd5Stub, + checkPackageMd5: checkPackageMd5Stub + }); + let checkUploadedMd5rewire = checkUploaded.__get__('checkUploadedMd5'); + bsConfig.run_settings.cache_dependencies = true + return checkUploadedMd5rewire(bsConfig, {}, instrumentBlocks) + .then((data) => { + chai.assert.deepEqual(data, { zipUrlPresent: false, packageUrlPresent: false }) + sinon.assert.calledOnce(requestStub); + sinon.assert.calledOnce(checkSpecsMd5Stub); + }) + .catch((_error) => { chai.assert.fail("Promise error"); }); }); }); context("checkSpecsMd5", () => { - let cryptoStub, digestStub, updateStub, pathStub, fsStub; + let cryptoStub, checkPackageMd5Stub, digestStub, updateStub, pathStub, fsStub; beforeEach(() => { + checkPackageMd5Stub = sandbox.stub().returns("random_md5sum") digestStub = sandbox.stub().returns("random_md5sum"); updateStub = sandbox.stub().returns(null); pathStub = { @@ -156,53 +275,151 @@ describe("checkUploaded", () => { }; }); - it("resolves with md5 value without adding config_file and package.json", () => { + it("resolves early due to force upload", () => { let hashElementstub = sandbox.stub().returns(Promise.resolve("random_md5sum")); const checkUploaded = rewire("../../../../bin/helpers/checkUploaded"); checkUploaded.__set__({ hashHelper: { hashWrapper: hashElementstub }, crypto: cryptoStub, - path: pathStub + path: pathStub, + checkPackageMd5: checkPackageMd5Stub }); let checkSpecsMd5Rewire = checkUploaded.__get__('checkSpecsMd5'); + let args = { + exclude: "random_files", + "force-upload": true + } + return checkSpecsMd5Rewire(bsConfig.run_settings, args) + .then((data) => { + chai.assert.equal(data, 'force-upload') + }) + .catch((_error) => { + chai.assert.fail("Promise error"); + }); + }); + + it("reject due to error in hashing", () => { + let hashElementErrorstub = sandbox.stub().returns(Promise.reject({message: "test error", stack: "test error stack"})); + const checkUploaded = rewire("../../../../bin/helpers/checkUploaded"); + checkUploaded.__set__({ + hashHelper: { hashWrapper: hashElementErrorstub }, + crypto: cryptoStub, + path: pathStub, + checkPackageMd5: checkPackageMd5Stub + }); + let checkSpecsMd5Rewire = checkUploaded.__get__('checkSpecsMd5'); + let args = { + exclude: "random_files", + } + return checkSpecsMd5Rewire(bsConfig.run_settings, args) + .then((_data) => { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + chai.assert.equal(error.message, 'test error') + }); + }); - return checkSpecsMd5Rewire(bsConfig.run_settings, "random_files") - .then(function (data) { + it("resolves with md5 value", () => { + let hashElementstub = sandbox.stub().returns(Promise.resolve("random_md5sum")); + const checkUploaded = rewire("../../../../bin/helpers/checkUploaded"); + checkUploaded.__set__({ + hashHelper: { hashWrapper: hashElementstub }, + crypto: cryptoStub, + path: pathStub, + checkPackageMd5: checkPackageMd5Stub, + fs: fsStub + }); + let checkSpecsMd5Rewire = checkUploaded.__get__('checkSpecsMd5'); + let args = { + exclude: "random_files" + } + return checkSpecsMd5Rewire(bsConfig.run_settings, args) + .then((data) => { chai.assert.equal(data, 'random_md5sum') sinon.assert.calledOnce(hashElementstub); - sinon.assert.calledTwice(digestStub); + sinon.assert.calledOnce(digestStub); sinon.assert.calledTwice(updateStub); }) - .catch((error) => { + .catch((_error) => { chai.assert.fail("Promise error"); }); }); - it("resolves with md5 value adding config_file and package.json", () => { + it("resolves with md5 value including config file", () => { let hashElementstub = sandbox.stub().returns(Promise.resolve("random_md5sum")); const checkUploaded = rewire("../../../../bin/helpers/checkUploaded"); checkUploaded.__set__({ hashHelper: { hashWrapper: hashElementstub }, crypto: cryptoStub, path: pathStub, + checkPackageMd5: checkPackageMd5Stub, fs: fsStub }); let checkSpecsMd5Rewire = checkUploaded.__get__('checkSpecsMd5'); - let run_settings = { - package_config_options: {random: "value"}, - cypress_config_file: "random/path" + let args = { + exclude: "random_files" } - - return checkSpecsMd5Rewire(run_settings, "random_files") - .then(function (data) { + bsConfig.run_settings.cypress_config_file = "random/path" + return checkSpecsMd5Rewire(bsConfig.run_settings, args) + .then((data) => { chai.assert.equal(data, 'random_md5sum') sinon.assert.calledOnce(hashElementstub); - sinon.assert.called(digestStub); - sinon.assert.callCount(updateStub, 4); + sinon.assert.calledOnce(digestStub); + sinon.assert.calledThrice(updateStub); }) - .catch((error) => { + .catch((_error) => { chai.assert.fail("Promise error"); }); }); }); + + context("checkPackageMd5", () => { + let cryptoStub, checkPackageMd5Stub, digestStub, updateStub, pathStub, fsStub, utilStub; + beforeEach(() => { + checkPackageMd5Stub = sandbox.stub().returns("random_md5sum") + digestStub = sandbox.stub().returns("random_md5sum"); + updateStub = sandbox.stub().returns(null); + utilStub = { + sortJsonKeys : sandbox.stub().returns('{}') + } + pathStub = { + dirname: sandbox.stub().returns(null), + join: sandbox.stub().returns(null) + }; + fsStub = { + readFileSync: sandbox.stub().returns('{}'), + existsSync: sandbox.stub().returns(true) + } + cryptoStub = { + createHash: () => { + return { + update: updateStub, + digest: digestStub + } + } + }; + }); + + it("resolves early due to force upload", () => { + const checkUploaded = rewire("../../../../bin/helpers/checkUploaded"); + checkUploaded.__set__({ + crypto: cryptoStub, + path: pathStub, + fs: fsStub, + utils: utilStub + }); + let checkPackageMd5Rewire = checkUploaded.__get__('checkPackageMd5'); + let run_settings = { + package_config_options: { + "name": "test" + }, + npm_dependencies: { + "random-package-1": "1.2.3", + "random-package-2": "1.2.4" + } + }; + return chai.assert.equal(checkPackageMd5Rewire(run_settings), 'random_md5sum'); + }); + }); }); diff --git a/test/unit/bin/helpers/packageInstaller.js b/test/unit/bin/helpers/packageInstaller.js new file mode 100644 index 00000000..087eb817 --- /dev/null +++ b/test/unit/bin/helpers/packageInstaller.js @@ -0,0 +1,478 @@ +'use strict'; +const chai = require("chai"), + chaiAsPromised = require("chai-as-promised"), + sinon = require("sinon"), + fs = require('fs-extra'), + path = require('path'), + npm = require('npm'); + +const logger = require("../../../../bin/helpers/logger").winstonLogger, +fileHelpers = require("../../../../bin/helpers/fileHelpers"); + +const rewire = require("rewire"); + +chai.use(chaiAsPromised); +logger.transports["console.info"].silent = true; + + +describe("packageInstaller", () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + sinon.restore(); + }); + + context("setupPackageFolder", () => { + let fileHelpersStub, fsmkdirStub, fsmkdirErrorStub, fswriteFileSyncStub, fsexistsSyncStub, fscopyFileSyncStub, pathdirnameStub, pathjoinStub, pathJoinErrorStub; + const packageInstaller = rewire("../../../../bin/helpers/packageInstaller"); + beforeEach(() => { + fileHelpersStub = sandbox.stub(fileHelpers, "deletePackageArchieve").returns(null); + fsmkdirStub = sandbox.stub(fs, "mkdir").yields(null); + fswriteFileSyncStub = sandbox.stub(fs, "writeFileSync").returns(null); + fsexistsSyncStub = sandbox.stub(fs, "existsSync").returns(true); + fscopyFileSyncStub = sandbox.stub(fs, "copyFileSync").returns(null); + pathdirnameStub = sandbox.stub(path, "dirname").returns(null); + pathjoinStub = sandbox.stub(path, "join").returns(null); + }); + + it("should reject if nothing in package.json", () => { + packageInstaller.__set__({ + fileHelpers: {deletePackageArchieve: fileHelpersStub}, + fs: {mkdir: fsmkdirStub} + }); + let setupPackageFolderrewire = packageInstaller.__get__('setupPackageFolder'); + let runSettings = {}; + let directoryPath = "/random/path"; + return setupPackageFolderrewire(runSettings, directoryPath) + .then((_data) => { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + sinon.assert.calledOnce(fsmkdirStub); + sinon.assert.calledOnce(fileHelpersStub); + chai.assert.equal(error, "Nothing in package file"); + }); + }); + + it("should reject if error in any step", () => { + pathjoinStub.restore(); + let error = new Error("test error"); + pathJoinErrorStub = sandbox.stub(path, "join").throws(error); + packageInstaller.__set__({ + fileHelpers: {deletePackageArchieve: fileHelpersStub}, + fs: {mkdir: fsmkdirStub}, + path: {join: pathJoinErrorStub} + }); + let setupPackageFolderrewire = packageInstaller.__get__('setupPackageFolder'); + let runSettings = { + package_config_options: { + "name": "test" + }, + npm_dependencies: { + "random-package-1": "1.2.3", + "random-package-2": "1.2.4" + } + }; + let directoryPath = "/random/path"; + return setupPackageFolderrewire(runSettings, directoryPath) + .then((_data) => { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + chai.assert.equal(error.message, "test error"); + }); + }); + + it("should reject if directory creation fails", () => { + fsmkdirStub.restore(); + fsmkdirErrorStub = sandbox.stub(fs, "mkdir").yields("test error"); + packageInstaller.__set__({ + fileHelpers: {deletePackageArchieve: fileHelpersStub}, + fs: {mkdir: fsmkdirErrorStub} + }); + let setupPackageFolderrewire = packageInstaller.__get__('setupPackageFolder'); + let runSettings = {}; + let directoryPath = "/random/path"; + return setupPackageFolderrewire(runSettings, directoryPath) + .then((_data) => { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + sinon.assert.calledOnce(fileHelpersStub); + chai.assert.equal(error, "test error"); + }); + }); + + it("should create folder with required files if npmrc exists", () => { + packageInstaller.__set__({ + fileHelpers: {deletePackageArchieve: fileHelpersStub}, + fs: { + mkdir: fsmkdirStub, + writeFileSync: fswriteFileSyncStub, + existsSync: fsexistsSyncStub, + copyFileSync: fscopyFileSyncStub + }, + path: { + dirname: pathdirnameStub, + join: pathjoinStub + } + }); + let setupPackageFolderrewire = packageInstaller.__get__('setupPackageFolder'); + let runSettings = { + package_config_options: { + "name": "test" + }, + npm_dependencies: { + "random-package-1": "1.2.3", + "random-package-2": "1.2.4" + } + }; + let packageCreated = JSON.stringify({ + "name": "test", + "devDependencies": { + "random-package-1": "1.2.3", + "random-package-2": "1.2.4" + } + }) + let directoryPath = "/random/path"; + return setupPackageFolderrewire(runSettings, directoryPath) + .then((data) => { + sinon.assert.calledOnce(fsmkdirStub); + sinon.assert.calledOnce(fileHelpersStub); + sinon.assert.calledOnce(fswriteFileSyncStub); + sinon.assert.calledOnce(fsexistsSyncStub); + sinon.assert.calledOnce(fscopyFileSyncStub); + sinon.assert.calledOnce(pathdirnameStub); + sinon.assert.calledThrice(pathjoinStub); + sinon.assert.calledWith(fswriteFileSyncStub, null, packageCreated); + chai.assert.equal(data, "package file created"); + }) + .catch((_error) => { + console.log(_error) + chai.assert.fail("Promise error"); + }); + }); + + it("should create folder with required files if npmrc doesn't exists", () => { + fsexistsSyncStub.restore(); + let fsexistsSyncFAlseStub = sandbox.stub(fs, "existsSync").returns(false); + packageInstaller.__set__({ + fileHelpers: {deletePackageArchieve: fileHelpersStub}, + fs: { + mkdir: fsmkdirStub, + writeFileSync: fswriteFileSyncStub, + existsSync: fsexistsSyncFAlseStub, + copyFileSync: fscopyFileSyncStub + }, + path: { + dirname: pathdirnameStub, + join: pathjoinStub + } + }); + let setupPackageFolderrewire = packageInstaller.__get__('setupPackageFolder'); + let runSettings = { + package_config_options: { + "name": "test" + }, + npm_dependencies: { + "random-package-1": "1.2.3", + "random-package-2": "1.2.4" + } + }; + let packageCreated = JSON.stringify({ + "name": "test", + "devDependencies": { + "random-package-1": "1.2.3", + "random-package-2": "1.2.4" + } + }) + let directoryPath = "/random/path"; + return setupPackageFolderrewire(runSettings, directoryPath) + .then((data) => { + sinon.assert.calledOnce(fsmkdirStub); + sinon.assert.calledOnce(fileHelpersStub); + sinon.assert.calledOnce(fswriteFileSyncStub); + sinon.assert.calledOnce(fsexistsSyncFAlseStub); + sinon.assert.notCalled(fscopyFileSyncStub); + sinon.assert.calledOnce(pathdirnameStub); + sinon.assert.calledThrice(pathjoinStub); + sinon.assert.calledWith(fswriteFileSyncStub, null, packageCreated); + chai.assert.equal(data, "package file created"); + }) + .catch((_error) => { + console.log(_error) + chai.assert.fail("Promise error"); + }); + }); + }); + + context("packageInstall", () => { + let npmInstallStub; + const packageInstaller = rewire("../../../../bin/helpers/packageInstaller"); + beforeEach(() => { + npmInstallStub = sandbox.stub(npm.commands, "install").returns(null); + }); + + it("should reject if error in npm load", () => { + packageInstaller.__set__({ + npm: { + commands: { + install: npmInstallStub + }, + load: (_npmLoad, loadCallback) => { + loadCallback("test error"); + } + }, + }); + let packageInstallrewire = packageInstaller.__get__('packageInstall'); + let directoryPath = "/random/path"; + return packageInstallrewire(directoryPath) + .then((_data) => { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + chai.assert.equal(error, "test error") + }); + }); + + it("should call npm install on directory", () => { + packageInstaller.__set__({ + npm: { + commands: { + install: (_packageDir, [], installCallback) => { + installCallback(null, "npm install done"); + } + }, + load: (_npmLoad, loadCallback) => { + loadCallback(null); + } + }, + }); + let packageInstallrewire = packageInstaller.__get__('packageInstall'); + let directoryPath = "/random/path"; + return packageInstallrewire(directoryPath) + .then((data) => { + chai.assert.equal(data, "npm install done") + }) + .catch((_error) => { + chai.assert.fail("Promise error"); + }); + }); + + it("should reject if error in npm install", () => { + packageInstaller.__set__({ + npm: { + commands: { + install: (_packageDir, [], installCallback) => { + installCallback("test error", "npm install failed"); + } + }, + load: (_npmLoad, loadCallback) => { + loadCallback(null); + } + }, + }); + let packageInstallrewire = packageInstaller.__get__('packageInstall'); + let directoryPath = "/random/path"; + return packageInstallrewire(directoryPath) + .then((_data) => { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + chai.assert.equal(error, "test error") + }); + }); + }); + + context("packageArchiver", () => { + let fsStub, archiverStub, winstonLoggerInfoStub; + let directoryPath = "/random/path"; + let packageFile = "/random/path/to/file"; + const packageInstaller = rewire("../../../../bin/helpers/packageInstaller"); + beforeEach(() => { + fsStub = { + events: {}, + on: (event, func) => { + fsStub.events[event] = func + }, + createWriteStream: () => { + return fsStub + }, + pipe: () => { + return fsStub + }, + emit: (event, data) => { + fsStub.events[event](data) + } + }; + archiverStub = { + events: {}, + on: (event, func) => { + fsStub.events[event] = func + }, + pipe: () => { + return fsStub + }, + directory: () => { + return fsStub + }, + finalize: () => { + archiverStub.emit("warning", {code: "ENOENT"}); + fsStub.emit("end"); + fsStub.emit("close"); + }, + emit: (event, data) => { + fsStub.events[event](data) + } + }; + winstonLoggerInfoStub = sinon.stub().returns(null); + }); + + it("should retun if zipping complete with ENOENT warning", () => { + packageInstaller.__set__({ + fs: fsStub, + archiver: () => { + return archiverStub; + }, + logger: { + info: winstonLoggerInfoStub + } + }); + let packageArchiverrewire = packageInstaller.__get__('packageArchiver'); + return packageArchiverrewire(directoryPath, packageFile) + .then((data) => { + chai.assert.equal(data, "Zipping completed") + }) + .catch((_error) => { + chai.assert.fail("Promise error"); + }); + }); + + it("should reject if error in archiver", () => { + archiverStub.finalize = () => {archiverStub.emit("error", "test error")} + packageInstaller.__set__({ + fs: fsStub, + archiver: () => { + return archiverStub; + } + }); + let packageArchiverrewire = packageInstaller.__get__('packageArchiver'); + return packageArchiverrewire(directoryPath, packageFile) + .then((_data) => { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + chai.assert.equal(error, "test error") + }); + }); + + it("should reject if archiver warning other then ENOENT", () => { + archiverStub.finalize = () => {archiverStub.emit("warning", {message: "test error"})} + packageInstaller.__set__({ + fs: fsStub, + archiver: () => { + return archiverStub; + } + }); + let packageArchiverrewire = packageInstaller.__get__('packageArchiver'); + return packageArchiverrewire(directoryPath, packageFile) + .then((_data) => { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + chai.assert.equal(error.message, "test error") + }); + }); + }); + + context("packageWrapper", () => { + let setupPackageFolderStub, setupPackageFolderErrorStub, setupPackageInstallStub, setupPackageArchiverStub; + let packageDir = "/random/path"; + let packageFile = "/random/path/to/file"; + const packageInstaller = rewire("../../../../bin/helpers/packageInstaller"); + beforeEach(() => { + setupPackageFolderStub = sandbox.stub().returns(Promise.resolve("random")); + setupPackageInstallStub = sandbox.stub().returns(Promise.resolve("random")); + setupPackageArchiverStub = sandbox.stub().returns(Promise.resolve("random")); + }); + + it("should return if feature not enabled", () => { + let packageWrapperrewire = packageInstaller.__get__('packageWrapper'); + let bsConfig = { + run_settings: { + cache_dependencies: false + } + }; + let md5data = {}; + let instrumentBlocks = { + markBlockStart: sinon.stub(), + markBlockEnd: sinon.stub() + } + return packageWrapperrewire(bsConfig, packageDir, packageFile, md5data, instrumentBlocks) + .then((data) => { + chai.assert.deepEqual(data, {packageArchieveCreated: false}); + }) + .catch((_error) => { + chai.assert.fail("Promise error"); + }); + }); + + it("should resolve with package exist if all step are successful", () => { + packageInstaller.__set__({ + setupPackageFolder: setupPackageFolderStub, + packageInstall:setupPackageInstallStub, + packageArchiver: setupPackageArchiverStub + }); + let packageWrapperrewire = packageInstaller.__get__('packageWrapper'); + let bsConfig = { + run_settings: { + cache_dependencies: true + } + }; + let md5data = {}; + let instrumentBlocks = { + markBlockStart: sinon.stub(), + markBlockEnd: sinon.stub() + } + return packageWrapperrewire(bsConfig, packageDir, packageFile, md5data, instrumentBlocks) + .then((data) => { + chai.assert.deepEqual(data, {packageArchieveCreated: true}); + }) + .catch((_error) => { + chai.assert.fail("Promise error"); + }); + }); + + it("should reject with error if issue in any step", () => { + setupPackageFolderErrorStub = sandbox.stub().returns(Promise.reject({message: "test error", stack: "test error stack"})); + packageInstaller.__set__({ + setupPackageFolder: setupPackageFolderErrorStub, + packageInstall:setupPackageInstallStub, + packageArchiver: setupPackageArchiverStub + }); + let packageWrapperrewire = packageInstaller.__get__('packageWrapper'); + let bsConfig = { + run_settings: { + cache_dependencies: true + } + }; + let md5data = {}; + let instrumentBlocks = { + markBlockStart: sinon.stub(), + markBlockEnd: sinon.stub() + } + return packageWrapperrewire(bsConfig, packageDir, packageFile, md5data, instrumentBlocks) + .then((data) => { + chai.assert.deepEqual(data, { packageArchieveCreated: false, error: 'test error stack' }); + }) + .catch((_error) => { + chai.assert.fail("Promise error"); + }); + }); + }); +}); diff --git a/test/unit/bin/helpers/utils.js b/test/unit/bin/helpers/utils.js index fafd02be..04538724 100644 --- a/test/unit/bin/helpers/utils.js +++ b/test/unit/bin/helpers/utils.js @@ -18,6 +18,8 @@ const usageReporting = require('../../../../bin/helpers/usageReporting'); const utils = require('../../../../bin/helpers/utils'), constant = require('../../../../bin/helpers/constants'), logger = require('../../../../bin/helpers/logger').winstonLogger, + config = require('../../../../bin/helpers/config'), + fileHelpers = require('../../../../bin/helpers/fileHelpers'), testObjects = require('../../support/fixtures/testObjects'), syncLogger = require('../../../../bin/helpers/logger').syncCliLogger; const browserstack = require('browserstack-local'); @@ -237,6 +239,125 @@ describe('utils', () => { }); }); + describe('checkError', () => { + it('should return error if exists', () => { + expect(utils.checkError({error: "test error"})).to.be.eq("test error"); + expect(utils.checkError({})).to.be.eq(undefined); + }) + }) + + describe('isTrueString', () => { + it('should return true if true string', () => { + expect(utils.isTrueString("true")).to.be.eq(true); + expect(utils.isTrueString(true)).to.be.eq(true); + expect(utils.isTrueString(false)).to.be.eq(false); + expect(utils.isTrueString("atrue")).to.be.eq(false); + expect(utils.isTrueString("false")).to.be.eq(false); + }) + }) + + describe('generateUploadParams', () => { + it('should generate upload params based on data', () => { + let bsConfig = { + auth: { + username: "user", + access_key: "key" + } + }; + let filePath = "random/path"; + let md5data = "md5data"; + let fileDetails = { + filetype: "type", + filename: "name" + }; + let options = { + url: config.uploadUrl, + auth: { + user: "user", + password: "key" + }, + formData: { + file: "random_fs", + filetype: "type", + filename: "name", + zipMd5sum: "md5data", + }, + headers: { + "User-Agent": "random_agent", + } + }; + let getUserAgentStub = sinon.stub(utils, 'getUserAgent').returns("random_agent"); + let fsStub = sinon.stub(fs, 'createReadStream').returns("random_fs"); + expect(utils.generateUploadParams(bsConfig, filePath, md5data, fileDetails)).to.deep.equal(options); + getUserAgentStub.restore(); + fsStub.restore(); + }); + }); + + describe('sortJsonKeys', () => { + it('should return josn sorted by keys', () => { + expect(utils.sortJsonKeys({b:1, a:2})).to.deep.equal({a:2, b:1}) + }); + }); + + describe('generateUploadOptions', () => { + it('should generate zip upload options based on data', () => { + let md5data = { + zipUrlPresent: true, + zip_md5sum: "randum_md5", + zipUrl: "bs://random_hash" + }; + let packageData = {}; + let options = { + archivePresent: true, + md5ReturnKey: "zip_url", + urlPresent: true, + md5Data: "randum_md5", + url: "bs://random_hash", + propogateError: true, + fileDetails: { + filetype: "zip", + filename: "tests" + }, + messages: { + uploading: constant.userMessages.UPLOADING_TESTS, + uploadingSuccess: constant.userMessages.UPLOADING_TESTS_SUCCESS + }, + cleanupMethod: fileHelpers.deleteZip, + }; + expect(utils.generateUploadOptions('zip', md5data, packageData)).to.deep.equal(options); + }); + + it('should generate npm upload options based on data', () => { + let md5data = { + packageUrlPresent: true, + npm_package_md5sum: "randum_md5", + npmPackageUrl: "bs://random_hash" + }; + let packageData = { + packageArchieveCreated: true + }; + let options = { + archivePresent: true, + md5ReturnKey: "npm_package_url", + urlPresent: true, + md5Data: "randum_md5", + url: "bs://random_hash", + propogateError: false, + fileDetails: { + filetype: "tar.gz", + filename: "bstackPackages" + }, + messages: { + uploading: constant.userMessages.UPLOADING_NPM_PACKAGES, + uploadingSuccess: constant.userMessages.UPLOADING_NPM_PACKAGES_SUCCESS + }, + cleanupMethod: fileHelpers.deletePackageArchieve, + }; + expect(utils.generateUploadOptions('npm', md5data, packageData)).to.deep.equal(options); + }); + }); + describe('getErrorCodeFromErr', () => { it('should return bstack_json_invalid_unknown if err.Code is not present in the list', () => { expect(utils.getErrorCodeFromErr('random_value')).to.be.eq( diff --git a/test/unit/bin/helpers/zipUpload.js b/test/unit/bin/helpers/zipUpload.js index 3a2de658..b6c75acf 100644 --- a/test/unit/bin/helpers/zipUpload.js +++ b/test/unit/bin/helpers/zipUpload.js @@ -1,28 +1,22 @@ +'use strict'; const chai = require("chai"), chaiAsPromised = require("chai-as-promised"), sinon = require("sinon"), - request = require("request"), - fs = require("fs"); + request = require("request"); -const Constants = require("../../../../bin/helpers/constants"), - logger = require("../../../../bin/helpers/logger").winstonLogger, - testObjects = require("../../support/fixtures/testObjects"); +const logger = require("../../../../bin/helpers/logger").winstonLogger, + constant = require('../../../../bin/helpers/constants'); -const proxyquire = require("proxyquire").noCallThru(); +const rewire = require("rewire"); chai.use(chaiAsPromised); logger.transports["console.info"].silent = true; describe("zipUpload", () => { - let bsConfig = testObjects.sampleBsConfig; - - var sandbox; + let sandbox; beforeEach(() => { sandbox = sinon.createSandbox(); - getUserAgentStub = sandbox.stub().returns("random user-agent"); - createReadStreamStub = sandbox.stub(fs, "createReadStream"); - deleteZipStub = sandbox.stub().returns(true); }); afterEach(() => { @@ -30,206 +24,320 @@ describe("zipUpload", () => { sinon.restore(); }); - it("reject with error", () => { - let requestStub = sandbox - .stub(request, "post") - .yields(new Error("random error"), null, null); - - const zipUploader = proxyquire("../../../../bin/helpers/zipUpload", { - "./utils": { - getUserAgent: getUserAgentStub, - }, - request: { post: requestStub }, + context("uploadSuits", () => { + let utilsStub, loggerStub; + let bsConfig = {} + let filePath = "random/path"; + const zipUploader = rewire("../../../../bin/helpers/zipUpload"); + beforeEach(() => { + utilsStub = { + generateUploadParams: sinon.stub().returns({}) + }; + loggerStub = { + info: sandbox.stub().returns(null) + }; }); - return zipUploader - .zipUpload(bsConfig, "./random_file_path",{}) - .then(function (data) { - chai.assert.fail("Promise error"); - }) - .catch((error) => { - sinon.assert.calledOnce(requestStub); - sinon.assert.calledOnce(getUserAgentStub); - sinon.assert.calledOnce(createReadStreamStub); - chai.assert.equal(error.message, "random error"); - }); - }); + it("reject with error", () => { + let requestStub = sandbox + .stub(request, "post") + .yields(new Error("test error"), null, null); - it("reject with error (if error present in response) if statusCode == 401", () => { - let error = "non 200 code"; + zipUploader.__set__({ + request: { post: requestStub }, + utils: utilsStub, + logger: loggerStub + }); + let uploadSuitsrewire = zipUploader.__get__('uploadSuits'); + let opts = { + archivePresent: true, + messages: {} + } + return uploadSuitsrewire(bsConfig, filePath, opts) + .then((_data) => { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + chai.assert.equal(error.message, "test error"); + }); + }); - let requestStub = sandbox - .stub(request, "post") - .yields(null, { statusCode: 401 }, JSON.stringify({ error: error })); + it("resolve with url if already present", () => { + let uploadSuitsrewire = zipUploader.__get__('uploadSuits'); + let opts = { + urlPresent: true, + md5ReturnKey: 'returnKey', + url: 'bs://random_hash' + } + return uploadSuitsrewire(bsConfig, filePath, opts) + .then((data) => { + chai.assert.deepEqual(data, {returnKey: 'bs://random_hash'}); + }) + .catch((_error) => { + chai.assert.fail("Promise error"); + }); + }); - const zipUploader = proxyquire("../../../../bin/helpers/zipUpload", { - "./utils": { - getUserAgent: getUserAgentStub, - }, - request: { post: requestStub }, + it("resolve with url if archive not present", () => { + let uploadSuitsrewire = zipUploader.__get__('uploadSuits'); + let opts = { + archivePresent: false, + } + return uploadSuitsrewire(bsConfig, filePath, opts) + .then((data) => { + chai.assert.deepEqual(data, {}); + }) + .catch((_error) => { + chai.assert.fail("Promise error"); + }); }); - return zipUploader - .zipUpload(bsConfig, "./random_file_path", {}) - .then(function (data) { - chai.assert.fail("Promise error"); - }) - .catch((error) => { - sinon.assert.calledOnce(requestStub); - sinon.assert.calledOnce(getUserAgentStub); - sinon.assert.calledOnce(createReadStreamStub); - chai.assert.equal(error, "non 200 code"); + it("resolve with nothing if parsing error", () => { + let requestStub = sandbox + .stub(request, "post") + .yields(null, { statusCode: 200 }, '{ random: "test }'); + + zipUploader.__set__({ + request: { post: requestStub }, + utils: utilsStub, + logger: loggerStub }); - }); + let uploadSuitsrewire = zipUploader.__get__('uploadSuits'); + let opts = { + cleanupMethod: sinon.stub().returns(null), + archivePresent: true, + messages: {} + } + return uploadSuitsrewire(bsConfig, filePath, opts) + .then((data) => { + chai.assert.deepEqual(data, {}); + }) + .catch((_error) => { + chai.assert.fail("Promise error"); + }); + }); - it("reject with message if statusCode == 401 and error not in response", () => { - let requestStub = sandbox - .stub(request, "post") - .yields( - null, - { statusCode: 401 }, - JSON.stringify({ message: "random message" }) - ); - - const zipUploader = proxyquire("../../../../bin/helpers/zipUpload", { - "./utils": { - getUserAgent: getUserAgentStub, - }, - request: { post: requestStub }, + it("resolve with message if statusCode = 200", () => { + let requestStub = sandbox + .stub(request, "post") + .yields(null, { statusCode: 200 }, JSON.stringify({ zip_url: "zip_url" })); + + zipUploader.__set__({ + request: { post: requestStub }, + utils: utilsStub, + logger: loggerStub + }); + let uploadSuitsrewire = zipUploader.__get__('uploadSuits'); + let opts = { + cleanupMethod: sinon.stub().returns(null), + archivePresent: true, + messages: {} + } + return uploadSuitsrewire(bsConfig, filePath, opts) + .then((data) => { + chai.assert.deepEqual(data, {zip_url: 'zip_url'}); + }) + .catch((_error) => { + chai.assert.fail("Promise error"); + }); }); - return zipUploader - .zipUpload(bsConfig, "./random_file_path", {}) - .then(function (data) { - chai.assert.fail("Promise error"); - }) - .catch((error) => { - sinon.assert.calledOnce(requestStub); - sinon.assert.calledOnce(getUserAgentStub); - sinon.assert.calledOnce(createReadStreamStub); - chai.assert.equal( - error, - Constants.validationMessages.INVALID_DEFAULT_AUTH_PARAMS - ); + it("reject with returned message if auth failed with message", () => { + let requestStub = sandbox + .stub(request, "post") + .yields(null, { statusCode: 401 }, JSON.stringify({ error: "auth failed" })); + + zipUploader.__set__({ + request: { post: requestStub }, + utils: utilsStub, + logger: loggerStub }); - }); + let uploadSuitsrewire = zipUploader.__get__('uploadSuits'); + let opts = { + archivePresent: true, + messages: {} + } + return uploadSuitsrewire(bsConfig, filePath, opts) + .then((_data) => { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + chai.assert.equal(error, "auth failed"); + }); + }); - it("reject with error (if error present in response) if statusCode != 200 and statusCode != 401", () => { - let error = "non 200 and non 401 code"; + it("reject with predefined message if auth failed without message", () => { + let requestStub = sandbox + .stub(request, "post") + .yields(null, { statusCode: 401 }, JSON.stringify({ })); - let requestStub = sandbox - .stub(request, "post") - .yields(null, { statusCode: 404 }, JSON.stringify({ error: error })); + zipUploader.__set__({ + request: { post: requestStub }, + utils: utilsStub, + logger: loggerStub + }); + let uploadSuitsrewire = zipUploader.__get__('uploadSuits'); + let opts = { + archivePresent: true, + messages: {} + } + return uploadSuitsrewire(bsConfig, filePath, opts) + .then((_data) => { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + chai.assert.equal(error, constant.validationMessages.INVALID_DEFAULT_AUTH_PARAMS); + }); + }); + + it("resolve with nothing if request error but no propogation", () => { + let requestStub = sandbox + .stub(request, "post") + .yields(null, { statusCode: 402 }, JSON.stringify({ })); - const zipUploader = proxyquire("../../../../bin/helpers/zipUpload", { - "./utils": { - getUserAgent: getUserAgentStub, - }, - request: { post: requestStub }, + zipUploader.__set__({ + request: { post: requestStub }, + utils: utilsStub, + logger: loggerStub + }); + let uploadSuitsrewire = zipUploader.__get__('uploadSuits'); + let opts = { + archivePresent: true, + messages: {}, + propogateError: false + } + return uploadSuitsrewire(bsConfig, filePath, opts) + .then((data) => { + chai.assert.deepEqual(data, {}); + }) + .catch((_error) => { + chai.assert.fail("Promise error"); + }); }); - return zipUploader - .zipUpload(bsConfig, "./random_file_path", {}) - .then(function (data) { - chai.assert.fail("Promise error"); - }) - .catch((error) => { - sinon.assert.calledOnce(requestStub); - sinon.assert.calledOnce(getUserAgentStub); - sinon.assert.calledOnce(createReadStreamStub); - chai.assert.equal(error, "non 200 and non 401 code"); + it("reject with error if request error", () => { + let requestStub = sandbox + .stub(request, "post") + .yields(null, { statusCode: 402 }, JSON.stringify({ error: "test error" })); + + zipUploader.__set__({ + request: { post: requestStub }, + utils: utilsStub, + logger: loggerStub }); - }); + let uploadSuitsrewire = zipUploader.__get__('uploadSuits'); + let opts = { + archivePresent: true, + messages: {}, + propogateError: true + } + return uploadSuitsrewire(bsConfig, filePath, opts) + .then((_data) => { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + chai.assert.deepEqual(error, "test error"); + }); + }); - it("reject with message if statusCode != 200 and statusCode != 401 and error not in response", () => { - let requestStub = sandbox - .stub(request, "post") - .yields( - null, - { statusCode: 404 }, - JSON.stringify({ message: "random message" }) - ); - - const zipUploader = proxyquire("../../../../bin/helpers/zipUpload", { - "./utils": { - getUserAgent: getUserAgentStub, - }, - request: { post: requestStub }, + it("reject with limit exceeded error", () => { + let requestStub = sandbox + .stub(request, "post") + .yields(null, { statusCode: 413 }, JSON.stringify({ })); + + zipUploader.__set__({ + request: { post: requestStub }, + utils: utilsStub, + logger: loggerStub + }); + let uploadSuitsrewire = zipUploader.__get__('uploadSuits'); + let opts = { + archivePresent: true, + messages: {}, + propogateError: true + } + return uploadSuitsrewire(bsConfig, filePath, opts) + .then((_data) => { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + chai.assert.deepEqual(error, constant.userMessages.ZIP_UPLOAD_LIMIT_EXCEEDED); + }); }); - return zipUploader - .zipUpload(bsConfig, "./random_file_path", {}) - .then(function (data) { - chai.assert.fail("Promise error"); - }) - .catch((error) => { - sinon.assert.calledOnce(requestStub); - sinon.assert.calledOnce(getUserAgentStub); - sinon.assert.calledOnce(createReadStreamStub); - chai.assert.equal( - error, - Constants.userMessages.ZIP_UPLOADER_NOT_REACHABLE - ); + it("reject with not reachable error", () => { + let requestStub = sandbox + .stub(request, "post") + .yields(null, { statusCode: 414 }, JSON.stringify({ })); + + zipUploader.__set__({ + request: { post: requestStub }, + utils: utilsStub, + logger: loggerStub }); + let uploadSuitsrewire = zipUploader.__get__('uploadSuits'); + let opts = { + archivePresent: true, + messages: {}, + propogateError: true + } + return uploadSuitsrewire(bsConfig, filePath, opts) + .then((_data) => { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + chai.assert.deepEqual(error, constant.userMessages.ZIP_UPLOADER_NOT_REACHABLE); + }); + }); }); - it("resolve with message if statusCode = 200", () => { - let zip_url = "uploaded zip url"; - let requestStub = sandbox - .stub(request, "post") - .yields(null, { statusCode: 200 }, JSON.stringify({ zip_url: zip_url })); - - const zipUploader = proxyquire('../../../../bin/helpers/zipUpload', { - './utils': { - getUserAgent: getUserAgentStub, - }, - request: {post: requestStub}, - './fileHelpers': { - deleteZip: deleteZipStub, - }, + context("uploadCypressZip", () => { + let utilsStub, uploadSuitsStub, uploadSuitsErrorStub; + const zipUploader = rewire("../../../../bin/helpers/zipUpload"); + beforeEach(() => { + utilsStub = { + generateUploadOptions: sinon.stub().returns({}) + }; + uploadSuitsStub = sandbox.stub().returns(Promise.resolve({zip_url: 'zip_url'})); }); - return zipUploader - .zipUpload(bsConfig, "./random_file_path", {}) - .then(function (data) { - sinon.assert.calledOnce(requestStub); - sinon.assert.calledOnce(getUserAgentStub); - sinon.assert.calledOnce(createReadStreamStub); - sinon.assert.calledOnce(deleteZipStub); - chai.assert.equal(data.zip_url, zip_url); - }) - .catch((error) => { - chai.assert.isNotOk(error, "Promise error"); + it("resolve with test suit", () => { + zipUploader.__set__({ + utils: utilsStub, + uploadSuits: uploadSuitsStub + }); + let uploadCypressZiprewire = zipUploader.__get__('uploadCypressZip'); + let bsConfig = {} + let md5data = {}; + let packageData = { + } + return uploadCypressZiprewire(bsConfig, md5data, packageData) + .then((data) => { + chai.assert.deepEqual(data, {zip_url: 'zip_url'}); + }) + .catch((_error) => { + chai.assert.fail("Promise error"); }); - }); - - it("resolve early if zip url already present", () => { - let zip_url = "uploaded zip url"; - let requestStub = sandbox - .stub(request, "post") - .yields(null, { statusCode: 200 }, JSON.stringify({ zip_url: zip_url })); - - const zipUploader = proxyquire('../../../../bin/helpers/zipUpload', { - './utils': { - getUserAgent: getUserAgentStub, - }, - request: {post: requestStub}, - './fileHelpers': { - deleteZip: deleteZipStub, - }, }); - return zipUploader - .zipUpload(bsConfig, "./random_file_path", { zipUrlPresent: true, zipUrl: zip_url }) - .then(function (data) { - sinon.assert.notCalled(requestStub); - sinon.assert.notCalled(getUserAgentStub); - sinon.assert.notCalled(createReadStreamStub); - sinon.assert.notCalled(deleteZipStub); - chai.assert.equal(data.zip_url, zip_url); - }) - .catch((error) => { - chai.assert.isNotOk(error, "Promise error"); + it("reject with error while uploading suit", () => { + let uploadSuitsErrorStub = sandbox.stub().returns(Promise.reject("test error")); + zipUploader.__set__({ + utils: utilsStub, + uploadSuits: uploadSuitsErrorStub + }); + let uploadCypressZiprewire = zipUploader.__get__('uploadCypressZip'); + let bsConfig = {} + let md5data = {}; + let packageData = { + } + return uploadCypressZiprewire(bsConfig, md5data, packageData) + .then((_data) => { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + chai.assert.equal(error, "test error"); }); + }); }); });