diff --git a/.nycrc.js b/.nycrc.js index 414da9e57..62ba72e2e 100644 --- a/.nycrc.js +++ b/.nycrc.js @@ -2,7 +2,17 @@ const TEST_TYPE = ((argv) => { let match = argv[argv.length - 1].match(/npm\/test-(\w+).js/); return match && match[1] || ''; -})(process.argv); +})(process.argv), + RUN_TEST_FILES = [ + 'lib/commands/run/*.js', + 'lib/reporter/*.js', + 'lib/config/*.js', + 'lib/print/*.js', + 'lib/*.js', + 'bin/**/*.js' + ]; + + function configOverrides (testType) { switch (testType) { @@ -12,20 +22,22 @@ function configOverrides (testType) { branches: 65, functions: 85, lines: 80 - }; + }; case 'integration': return { statements: 40, branches: 20, functions: 40, - lines: 40 + lines: 40, + include: RUN_TEST_FILES }; case 'library': return { statements: 55, branches: 40, functions: 55, - lines: 55 + lines: 55, + include: RUN_TEST_FILES }; case 'unit': return { diff --git a/DIR_COMMANDS.md b/DIR_COMMANDS.md new file mode 100644 index 000000000..4fde71a74 --- /dev/null +++ b/DIR_COMMANDS.md @@ -0,0 +1,211 @@ +# Splitting Postman collection into directories - a better storage format + +Postman collections are created using the Postman UI application. These +collections are represented as a [JSON +file](https://learning.postman.com/collection-format/getting-started/structure-of-a-collection/) +by the Postman UI application. It is a single file that contains all the +requests/tests/variables etc of the collection. It is designed to be edited +within the Postman UI. The `newman` tool allows teams to run Postman collections +as part of their CI pipelines by supplying the Postman collection file as an +argument. Typically, teams that rely on Postman for their API testing in CI +pipelines maintain a copy of the collection file in their source repository. +When teams maintain the Postman collection in their repository, they will have +the following workflow to make changes to it and push is upstream: + +* Import the collection json file from the repo into Postman UI +* Make changes to the collection in Postman UI +* Export the collection to a file using Postman UI +* Commit and push the changes + +There are a few challenges with the above workflow: + +1. Collection file diffs are very hard to review in pull requests + * Old format collection diff: https://github.com/juspay/hyperswitch/commit/7e29adb5c9dee8b03ef58ccbd85b07b106459380 + ![Screenshot 2023-10-06 at 5 24 35 PM](https://github.com/knutties/newman/assets/77204/d4c67bb2-59fa-4938-be4a-68f8159584a1) + * New format collection diff: https://github.com/juspay/hyperswitch/pull/2117/files + ![Screenshot 2023-10-06 at 5 25 47 PM](https://github.com/knutties/newman/assets/77204/1c3798c6-84c0-4589-9e5e-ad067d62bb9f) +1. Developers cannot use their favourite editor to make changes, add/remove new tests + +As a consequence of the above, collection maintenance suffers and often become stale. + +## Solution to the above challenges + +The core reason for challenges presented above is the file format in which the +Postman requests are maintained. A single JSON file that encapsulates the entire +request code and sequencing does not lend itself for easy reviewing and be an +all-editor friendly format. + +This fork of newman attempts to address the challenge by representing the +Postman collection as a set of directories/files. It allows developers to +create Postman collection using the directory representation and also run them. +It achieves this by re-constructing the collection json from the directory/file +structure and leveraging the existing newman `run` implementation. + +For example, this is the [directory equivalent +representation](examples/Sample%20Postman%20Collection) of a [sample +collection](examples/sample-collection.json) in the examples directory as +generated by the `dir-export` command. + +### Directory representation of collection + +The following is a tree structure of the directory that represents the Postman +collection with annotations of what each file corresponds to in a Postman +collection. +``` +examples/Sample Postman Collection +├── .info.json # contains the info element from the top of collection json +├── .meta.json # contains the ordering of the request folders +├── .auth.json # contains the auth element from the top of the collection json +├── .variable.json # contains the variable element from the top of the collection json +├── .event.json # contains the event element from the top of the collection json +└── A simple GET request +    ├── .event.meta.json # contains the ordering of the scripts - prerequest / test +    ├── event.test.js # the test script that runs post the request +    ├── event.prerequest.js # the test script that runs before the request +    ├── request.json # the request json from the Postman collection +    └── response.json # the response json +``` + +### Next steps +This concept of representing Postman collections as a directory opens up programmatic +pre-processing of request data before running the requests. This could include things like the following: + +* Re-using same data across requests +* Using other javascript libraries in testing code + +The instructions for using the tool are given below. Please give it spin and let me know it you if it is useful. + +## Usage + +### Installing newman from this fork + +One can install the newman executable in this fork using the command: + +``` +npm install -g 'git+ssh://git@github.com:knutties/newman.git#feature/newman-dir' +``` + +The following no-arg run shows the new commands added to newman to help manage Postman +requests maintained as directories/files instead of a single json file. + +``` +$ newman + +Usage: newman [options] [command] + +Options: + -v, --version output the version number + -h, --help display help for command + +Commands: + dir-add-folder [options] Add a folder to directory based Postman collection in the given path + dir-add-request [options] Add a request to directory based Postman collection in the given path + dir-collection-create [options] Create a directory based Postman collection in the given path + dir-export [options] Convert a Postman collection file into its directory representation + dir-export-import-check [options] Check if an export followed by import results in same collection + dir-import [options] Convert a Postman directory representation into a postman collection + dir-remove-folder Remove folder at given path from directory based Postman collection + dir-remove-request Remove request at given path from directory based Postman collection + dir-run [options] Runs the requests in collection-dir, with all the provided options + run [options] Initiate a Postman Collection run from a given URL or path + + To get available options for a command: + newman -h +``` + +The following sections show the invocation of the different commands added in +this fork of newman. + +### Create a new directory based Postman collection +``` +newman dir-collection-create new-dir-collection +``` + +### Generating directory representation for an existing collection +``` +newman dir-export examples/sample-collection.json +``` + +### Convert a directory representation back to a Postman collection json file +``` +newman dir-import examples/Sample\ Postman\ Collection/ -o examples/sample-collection.json +``` + +### Diff the collection generated by export followed by its import +This command is typically used to test the tool itself to ensure it can handle +all collection json use-cases. +``` +newman dir-export-import-check examples/sample-collection.json +``` + +### Execute a Postman collection stored in the directory format +``` +newman dir-run examples/Sample\ Postman\ Collection/ +``` +The `dir-run` command supports all the options that the stock `run` command of +newman supports. This is achieved by re-using the same set of command options +for both the commands. + +Sample output of `dir-run` shown below +``` +newman + +Sample Postman Collection + +→ A simple GET request + GET https://postman-echo.com/get?source=newman-sample-github-collection [200 OK, 847B, 1054ms] + ✓ expect response be 200 + ✓ expect response json contain args + +→ A simple POST request + POST https://postman-echo.com/post [200 OK, 1.06kB, 317ms] + +→ A simple POST request with JSON body + POST https://postman-echo.com/post [200 OK, 1.17kB, 233ms] + +┌─────────────────────────┬─────────────────────┬─────────────────────┐ +│ │ executed │ failed │ +├─────────────────────────┼─────────────────────┼─────────────────────┤ +│ iterations │ 1 │ 0 │ +├─────────────────────────┼─────────────────────┼─────────────────────┤ +│ requests │ 3 │ 0 │ +├─────────────────────────┼─────────────────────┼─────────────────────┤ +│ test-scripts │ 1 │ 0 │ +├─────────────────────────┼─────────────────────┼─────────────────────┤ +│ prerequest-scripts │ 0 │ 0 │ +├─────────────────────────┼─────────────────────┼─────────────────────┤ +│ assertions │ 2 │ 0 │ +├─────────────────────────┴─────────────────────┴─────────────────────┤ +│ total run duration: 1647ms │ +├─────────────────────────────────────────────────────────────────────┤ +│ total data received: 2.13kB (approx) │ +├─────────────────────────────────────────────────────────────────────┤ +│ average response time: 534ms [min: 233ms, max: 1054ms, s.d.: 368ms] │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Add a folder under a existing directory +``` +newman dir-add-folder examples/Sample\ Postman\ Collection/folder1 +``` + + +### Add a request under a directory +``` +newman dir-add-request examples/Sample\ Postman\ Collection/test4 +``` + +This command adds the request to the end of the requests already present in the +folder. The order of the requests is stored in a separate file called +[.meta.json](examples/Sample%20Postman%20Collection/.meta.json). The order of requests +can be changed by re-ordering the requests in this file. + +### Remove a folder +``` +newman dir-remove-folder examples/Sample\ Postman\ Collection/folder1 +``` + +### Remove a request +``` +newman dir-remove-request examples/Sample\ Postman\ Collection/test4 +``` diff --git a/bin/newman.js b/bin/newman.js index 234052c6c..f26ab417b 100755 --- a/bin/newman.js +++ b/bin/newman.js @@ -2,107 +2,62 @@ require('../lib/node-version-check'); // @note that this should not respect CLI --silent -const _ = require('lodash'), - waterfall = require('async/waterfall'), - { Command } = require('commander'), - program = new Command(), +const waterfall = require('async/waterfall'), version = require('../package.json').version, - newman = require('../'), + { Command } = require('commander'), + commands = require('../'), util = require('./util'); -program - .name('newman') - .addHelpCommand(false) - .version(version, '-v, --version'); - -// The `run` command allows you to specify a collection to be run with the provided options. -program - .command('run ') - .description('Initiate a Postman Collection run from a given URL or path') - .usage(' [options]') - .option('-e, --environment ', 'Specify a URL or path to a Postman Environment') - .option('-g, --globals ', 'Specify a URL or path to a file containing Postman Globals') - .option('-r, --reporters [reporters]', 'Specify the reporters to use for this run', util.cast.csvParse, ['cli']) - .option('-n, --iteration-count ', 'Define the number of iterations to run', util.cast.integer) - .option('-d, --iteration-data ', 'Specify a data file to use for iterations (either JSON or CSV)') - .option('--folder ', - 'Specify the folder to run from a collection. Can be specified multiple times to run multiple folders', - util.cast.memoize, []) - .option('--global-var ', - 'Allows the specification of global variables via the command line, in a key=value format', - util.cast.memoizeKeyVal, []) - .option('--env-var ', - 'Allows the specification of environment variables via the command line, in a key=value format', - util.cast.memoizeKeyVal, []) - .option('--export-environment ', 'Exports the final environment to a file after completing the run') - .option('--export-globals ', 'Exports the final globals to a file after completing the run') - .option('--export-collection ', 'Exports the executed collection to a file after completing the run') - .option('--postman-api-key ', 'API Key used to load the resources from the Postman API') - .option('--bail [modifiers]', - 'Specify whether or not to gracefully stop a collection run on encountering an error' + - ' and whether to end the run with an error based on the optional modifier', util.cast.csvParse) - .option('--ignore-redirects', 'Prevents Newman from automatically following 3XX redirect responses') - .option('-x , --suppress-exit-code', 'Specify whether or not to override the default exit code for the current run') - .option('--silent', 'Prevents Newman from showing output to CLI') - .option('--disable-unicode', 'Forces Unicode compliant symbols to be replaced by their plain text equivalents') - .option('--color ', 'Enable/Disable colored output (auto|on|off)', util.cast.colorOptions, 'auto') - .option('--delay-request [n]', 'Specify the extent of delay between requests (milliseconds)', util.cast.integer, 0) - .option('--timeout [n]', 'Specify a timeout for collection run (milliseconds)', util.cast.integer, 0) - .option('--timeout-request [n]', 'Specify a timeout for requests (milliseconds)', util.cast.integer, 0) - .option('--timeout-script [n]', 'Specify a timeout for scripts (milliseconds)', util.cast.integer, 0) - .option('--working-dir ', 'Specify the path to the working directory') - .option('--no-insecure-file-read', 'Prevents reading the files situated outside of the working directory') - .option('-k, --insecure', 'Disables SSL validations') - .option('--ssl-client-cert-list ', 'Specify the path to a client certificates configurations (JSON)') - .option('--ssl-client-cert ', 'Specify the path to a client certificate (PEM)') - .option('--ssl-client-key ', 'Specify the path to a client certificate private key') - .option('--ssl-client-passphrase ', 'Specify the client certificate passphrase (for protected key)') - .option('--ssl-extra-ca-certs ', 'Specify additionally trusted CA certificates (PEM)') - .option('--cookie-jar ', 'Specify the path to a custom cookie jar (serialized tough-cookie JSON) ') - .option('--export-cookie-jar ', 'Exports the cookie jar to a file after completing the run') - .option('--verbose', 'Show detailed information of collection run and each request sent') - .action((collection, command) => { - let options = util.commanderToObject(command), - - // parse custom reporter options - reporterOptions = util.parseNestedOptions(program._originalArgs, '--reporter-', options.reporters); - - // Inject additional properties into the options object - options.collection = collection; - options.reporterOptions = reporterOptions._generic; - options.reporter = _.transform(_.omit(reporterOptions, '_generic'), (acc, value, key) => { - acc[key] = _.assignIn(value, reporterOptions._generic); // overrides reporter options with _generic - }, {}); - - newman.run(options, function (err, summary) { - const runError = err || summary.run.error || summary.run.failures.length; - - if (err) { - console.error(`error: ${err.message || err}\n`); - err.friendly && console.error(` ${err.friendly}\n`); - } - runError && !_.get(options, 'suppressExitCode') && (process.exitCode = 1); + +/** + * Creates a new command instance - useful for testing + * + * @param {Command} Command - Command type from commander library + * @param {Object} commands - list of supported commands - each defining cliSetup and action + */ +function getProgram (Command, commands) { + const program = new Command(); + + program + .name('newman') + .addHelpCommand(false) + .version(version, '-v, --version'); + + Object.keys(commands).forEach((commandSetupFunction) => { + let cliSetup = commands[commandSetupFunction].cliSetup, + action = commands[commandSetupFunction].action; + + cliSetup(program).action((args, command) => { + action(args, command, program); }); }); -program.addHelpText('after', ` -To get available options for a command: - newman -h`); + program.addHelpText('after', ` + To get available options for a command: + newman -h`); -// Warn on invalid command and then exits. -program.on('command:*', (command) => { - console.error(`error: invalid command \`${command}\`\n`); - program.help(); -}); + // Warn on invalid command and then exits. + program.on('command:*', (command) => { + console.error(`error: invalid command \`${command}\`\n`); + program.help(); + }); + + return program; +} /** * Starts the script execution. * callback is required when this is required as a module in tests. * * @param {String[]} argv - Argument vector. + * @param {Command} program - Commander command instance to run * @param {?Function} callback - The callback function invoked on the completion of execution. */ -function run (argv, callback) { +function run (argv, program, callback) { + if (!program) { + program = getProgram(Command, commands); + } + waterfall([ (next) => { // cache original argv, required to parse nested options later. @@ -141,4 +96,7 @@ function run (argv, callback) { !module.parent && run(process.argv); // Export to allow debugging and testing. -module.exports = run; +module.exports = { + run, + getProgram +}; diff --git a/examples/Sample Postman Collection/.info.json b/examples/Sample Postman Collection/.info.json new file mode 100644 index 000000000..8bedbe2fd --- /dev/null +++ b/examples/Sample Postman Collection/.info.json @@ -0,0 +1,7 @@ +{ + "info": { + "name": "Sample Postman Collection", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", + "description": "A sample collection to demonstrate collections as a set of related requests" + } +} diff --git a/examples/Sample Postman Collection/.meta.json b/examples/Sample Postman Collection/.meta.json new file mode 100644 index 000000000..6984bf8cf --- /dev/null +++ b/examples/Sample Postman Collection/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "A simple GET request", + "A simple POST request", + "A simple POST request with JSON body" + ] +} diff --git a/examples/Sample Postman Collection/A simple GET request/.event.meta.json b/examples/Sample Postman Collection/A simple GET request/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/examples/Sample Postman Collection/A simple GET request/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/examples/Sample Postman Collection/A simple GET request/event.test.js b/examples/Sample Postman Collection/A simple GET request/event.test.js new file mode 100644 index 000000000..a2d0fcb3f --- /dev/null +++ b/examples/Sample Postman Collection/A simple GET request/event.test.js @@ -0,0 +1,7 @@ +pm.test('expect response be 200', function () { + pm.response.to.be.ok +}) +pm.test('expect response json contain args', function () { + pm.expect(pm.response.json().args).to.have.property('source') + .and.equal('newman-sample-github-collection') +}) \ No newline at end of file diff --git a/examples/Sample Postman Collection/A simple GET request/request.json b/examples/Sample Postman Collection/A simple GET request/request.json new file mode 100644 index 000000000..90660d2d0 --- /dev/null +++ b/examples/Sample Postman Collection/A simple GET request/request.json @@ -0,0 +1,4 @@ +{ + "url": "https://postman-echo.com/get?source=newman-sample-github-collection", + "method": "GET" +} diff --git a/examples/Sample Postman Collection/A simple POST request with JSON body/request.json b/examples/Sample Postman Collection/A simple POST request with JSON body/request.json new file mode 100644 index 000000000..5b87e25f9 --- /dev/null +++ b/examples/Sample Postman Collection/A simple POST request with JSON body/request.json @@ -0,0 +1,16 @@ +{ + "url": "https://postman-echo.com/post", + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw_json_formatted": { + "text": "Duis posuere augue vel cursus pharetra. In luctus a ex nec pretium..." + } + } +} diff --git a/examples/Sample Postman Collection/A simple POST request/request.json b/examples/Sample Postman Collection/A simple POST request/request.json new file mode 100644 index 000000000..07e823215 --- /dev/null +++ b/examples/Sample Postman Collection/A simple POST request/request.json @@ -0,0 +1,14 @@ +{ + "url": "https://postman-echo.com/post", + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "text/plain" + } + ], + "body": { + "mode": "raw", + "raw": "Duis posuere augue vel cursus pharetra. In luctus a ex nec pretium..." + } +} diff --git a/lib/commands/dir-add-folder/index.js b/lib/commands/dir-add-folder/index.js new file mode 100644 index 000000000..c5ce96065 --- /dev/null +++ b/lib/commands/dir-add-folder/index.js @@ -0,0 +1,29 @@ +const commandUtil = require('../../../bin/util'), + dirUtils = require('../dir-utils'); + +/* + @param {Command} - An commander Command instance to which this command is added +*/ +function cliSetup (program) { + return program + .command('dir-add-folder ') + .description('Add a folder to directory based Postman collection in the given path') + .usage(' [options]') + .option('-f, --force-overwrite', 'overwrite if test already exists', false); +} + +function action (folderPath, command) { + const options = commandUtil.commanderToObject(command); + + // check if test with same name exists when forceOverwrite is false + if (!options.forceOverwrite) { + dirUtils.assertDirectoryAbsence(folderPath); + } + + dirUtils.createPostmanFolder(folderPath); +} + +module.exports = { + cliSetup, + action +}; diff --git a/lib/commands/dir-add-request/index.js b/lib/commands/dir-add-request/index.js new file mode 100644 index 000000000..044f00422 --- /dev/null +++ b/lib/commands/dir-add-request/index.js @@ -0,0 +1,76 @@ +const commandUtil = require('../../../bin/util'), + path = require('path'), + fs = require('fs'), + dirUtils = require('../dir-utils'), + { Option } = require('commander'); + +/* + @param {Command} - An commander Command instance to which this command is added +*/ +function cliSetup (program) { + return program + .command('dir-add-request ') + .description('Add a request to directory based Postman collection in the given path') + .usage(' [options]') + .addOption(new Option('-t, --type ', 'HTTP Method template').choices(['GET', 'POST']).default('GET')) + .option('-f, --force-overwrite', 'overwrite if request already exists', false); +} + +function action (requestPath, command) { + const options = commandUtil.commanderToObject(command), + methodTemplateMap = { + GET: path.join(__dirname, '/templates/GET template'), + POST: path.join(__dirname, '/templates/POST body template') + }, + parentDir = path.dirname(requestPath), + trimmedTestPath = requestPath.replace(/\/+$/, ''), + requestPathBaseName = path.basename(requestPath), + metaFilePath = path.join(parentDir, '.meta.json'); + + // check if request with same name exists when forceOverwrite is false + if (!options.forceOverwrite) { + dirUtils.assertDirectoryAbsence(requestPath); + } + + // check if requestPath's parent is already a collection folder + dirUtils.assertCollectionDir(parentDir); + + // copy request, response, event files + try { + fs.cpSync(methodTemplateMap[options.type], + trimmedTestPath, + { recursive: true }); + } + catch (e) { + console.error(`Could not copy from template at ${methodTemplateMap[options.type]}`); + fs.rmSync(`${requestPath}`, { recursive: true, force: true }); + process.exit(-1); + } + + // add new request to parent's .meta.json + try { + fs.accessSync(metaFilePath, fs.constants.R_OK); + let meta = JSON.parse(fs.readFileSync(metaFilePath)), + childrenOrder = meta.childrenOrder; + + if (!childrenOrder.includes(requestPathBaseName)) { + childrenOrder.push(requestPathBaseName); + } + + meta = { + childrenOrder + }; + + dirUtils.createFile(metaFilePath, JSON.stringify(meta, null, 2)); + } + catch (e) { + console.error(`Could not update ${metaFilePath} with new request ${requestPath}: ${e}`); + fs.rmSync(`${requestPath}`, { recursive: true, force: true }); + process.exit(-1); + } +} + +module.exports = { + cliSetup, + action +}; diff --git a/lib/commands/dir-add-request/templates/GET template/.event.meta.json b/lib/commands/dir-add-request/templates/GET template/.event.meta.json new file mode 100644 index 000000000..2df9d47d9 --- /dev/null +++ b/lib/commands/dir-add-request/templates/GET template/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] +} diff --git a/lib/commands/dir-add-request/templates/GET template/event.prerequest.js b/lib/commands/dir-add-request/templates/GET template/event.prerequest.js new file mode 100644 index 000000000..9944ca812 --- /dev/null +++ b/lib/commands/dir-add-request/templates/GET template/event.prerequest.js @@ -0,0 +1 @@ +// any prerequest js code goes here diff --git a/lib/commands/dir-add-request/templates/GET template/event.test.js b/lib/commands/dir-add-request/templates/GET template/event.test.js new file mode 100644 index 000000000..2f88e5882 --- /dev/null +++ b/lib/commands/dir-add-request/templates/GET template/event.test.js @@ -0,0 +1,5 @@ +/* global pm */ + +pm.test('expect response be 200', function () { + pm.response.to.be.ok; +}); diff --git a/lib/commands/dir-add-request/templates/GET template/request.json b/lib/commands/dir-add-request/templates/GET template/request.json new file mode 100644 index 000000000..90660d2d0 --- /dev/null +++ b/lib/commands/dir-add-request/templates/GET template/request.json @@ -0,0 +1,4 @@ +{ + "url": "https://postman-echo.com/get?source=newman-sample-github-collection", + "method": "GET" +} diff --git a/lib/commands/dir-add-request/templates/POST body template/.event.meta.json b/lib/commands/dir-add-request/templates/POST body template/.event.meta.json new file mode 100644 index 000000000..2df9d47d9 --- /dev/null +++ b/lib/commands/dir-add-request/templates/POST body template/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] +} diff --git a/lib/commands/dir-add-request/templates/POST body template/event.prerequest.js b/lib/commands/dir-add-request/templates/POST body template/event.prerequest.js new file mode 100644 index 000000000..9944ca812 --- /dev/null +++ b/lib/commands/dir-add-request/templates/POST body template/event.prerequest.js @@ -0,0 +1 @@ +// any prerequest js code goes here diff --git a/lib/commands/dir-add-request/templates/POST body template/event.test.js b/lib/commands/dir-add-request/templates/POST body template/event.test.js new file mode 100644 index 000000000..2f88e5882 --- /dev/null +++ b/lib/commands/dir-add-request/templates/POST body template/event.test.js @@ -0,0 +1,5 @@ +/* global pm */ + +pm.test('expect response be 200', function () { + pm.response.to.be.ok; +}); diff --git a/lib/commands/dir-add-request/templates/POST body template/request.json b/lib/commands/dir-add-request/templates/POST body template/request.json new file mode 100644 index 000000000..5b87e25f9 --- /dev/null +++ b/lib/commands/dir-add-request/templates/POST body template/request.json @@ -0,0 +1,16 @@ +{ + "url": "https://postman-echo.com/post", + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw_json_formatted": { + "text": "Duis posuere augue vel cursus pharetra. In luctus a ex nec pretium..." + } + } +} diff --git a/lib/commands/dir-collection-create/index.js b/lib/commands/dir-collection-create/index.js new file mode 100644 index 000000000..a8858e438 --- /dev/null +++ b/lib/commands/dir-collection-create/index.js @@ -0,0 +1,41 @@ +const commandUtil = require('../../../bin/util'), + fs = require('fs'), + path = require('path'), + dirUtils = require('../dir-utils'); + +/* + @param {Command} - An commander Command instance to which this command is added +*/ +function cliSetup (program) { + return program + .command('dir-collection-create ') + .description('Create a directory based Postman collection in the given path') + .usage(' [options]') + .option('-f, --force-overwrite', 'overwrite if collection directory already exists', false); +} + +function action (collectionPath, command) { + const options = commandUtil.commanderToObject(command), + trimmedCollectionPath = collectionPath.replace(/\/+$/, ''), + collectionTemplate = path.join(__dirname, '/templates/Sample Postman Collection'); + + // check if test with same name exists when forceOverwrite is false + if (!options.forceOverwrite) { + dirUtils.assertDirectoryAbsence(collectionPath); + } + + // copy from template + try { + fs.cpSync(collectionTemplate, trimmedCollectionPath, { recursive: true }); + } + catch (e) { + console.error(`Could not copy from template at ${collectionTemplate}`); + fs.rmSync(`${collectionPath}`, { recursive: true, force: true }); + process.exit(-1); + } +} + +module.exports = { + cliSetup, + action +}; diff --git a/lib/commands/dir-collection-create/templates/Sample Postman Collection/.info.json b/lib/commands/dir-collection-create/templates/Sample Postman Collection/.info.json new file mode 100644 index 000000000..8bedbe2fd --- /dev/null +++ b/lib/commands/dir-collection-create/templates/Sample Postman Collection/.info.json @@ -0,0 +1,7 @@ +{ + "info": { + "name": "Sample Postman Collection", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", + "description": "A sample collection to demonstrate collections as a set of related requests" + } +} diff --git a/lib/commands/dir-collection-create/templates/Sample Postman Collection/.meta.json b/lib/commands/dir-collection-create/templates/Sample Postman Collection/.meta.json new file mode 100644 index 000000000..6984bf8cf --- /dev/null +++ b/lib/commands/dir-collection-create/templates/Sample Postman Collection/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "A simple GET request", + "A simple POST request", + "A simple POST request with JSON body" + ] +} diff --git a/lib/commands/dir-collection-create/templates/Sample Postman Collection/A simple GET request/.event.meta.json b/lib/commands/dir-collection-create/templates/Sample Postman Collection/A simple GET request/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/lib/commands/dir-collection-create/templates/Sample Postman Collection/A simple GET request/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/lib/commands/dir-collection-create/templates/Sample Postman Collection/A simple GET request/event.test.js b/lib/commands/dir-collection-create/templates/Sample Postman Collection/A simple GET request/event.test.js new file mode 100644 index 000000000..9c3bee193 --- /dev/null +++ b/lib/commands/dir-collection-create/templates/Sample Postman Collection/A simple GET request/event.test.js @@ -0,0 +1,10 @@ +/* global pm */ + +pm.test('expect response be 200', function () { + pm.response.to.be.ok; +}); + +pm.test('expect response json contain args', function () { + pm.expect(pm.response.json().args).to.have.property('source') + .and.equal('newman-sample-github-collection'); +}); diff --git a/lib/commands/dir-collection-create/templates/Sample Postman Collection/A simple GET request/request.json b/lib/commands/dir-collection-create/templates/Sample Postman Collection/A simple GET request/request.json new file mode 100644 index 000000000..90660d2d0 --- /dev/null +++ b/lib/commands/dir-collection-create/templates/Sample Postman Collection/A simple GET request/request.json @@ -0,0 +1,4 @@ +{ + "url": "https://postman-echo.com/get?source=newman-sample-github-collection", + "method": "GET" +} diff --git a/lib/commands/dir-collection-create/templates/Sample Postman Collection/A simple POST request with JSON body/request.json b/lib/commands/dir-collection-create/templates/Sample Postman Collection/A simple POST request with JSON body/request.json new file mode 100644 index 000000000..5b87e25f9 --- /dev/null +++ b/lib/commands/dir-collection-create/templates/Sample Postman Collection/A simple POST request with JSON body/request.json @@ -0,0 +1,16 @@ +{ + "url": "https://postman-echo.com/post", + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw_json_formatted": { + "text": "Duis posuere augue vel cursus pharetra. In luctus a ex nec pretium..." + } + } +} diff --git a/lib/commands/dir-collection-create/templates/Sample Postman Collection/A simple POST request/request.json b/lib/commands/dir-collection-create/templates/Sample Postman Collection/A simple POST request/request.json new file mode 100644 index 000000000..07e823215 --- /dev/null +++ b/lib/commands/dir-collection-create/templates/Sample Postman Collection/A simple POST request/request.json @@ -0,0 +1,14 @@ +{ + "url": "https://postman-echo.com/post", + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "text/plain" + } + ], + "body": { + "mode": "raw", + "raw": "Duis posuere augue vel cursus pharetra. In luctus a ex nec pretium..." + } +} diff --git a/lib/commands/dir-export-import-check/index.js b/lib/commands/dir-export-import-check/index.js new file mode 100644 index 000000000..cf6eb42be --- /dev/null +++ b/lib/commands/dir-export-import-check/index.js @@ -0,0 +1,63 @@ +const path = require('path'); +const dirUtils = require('../dir-utils'); +const assert = require('assert'); +const fs = require('fs'); +const commandUtil = require('../../../bin/util'); + +/* + @param {Command} - An commander Command instance to which this command is added +*/ +function cliSetup (program) { + return program + .command('dir-export-import-check ') + .description('Check if an export followed by import results in same collection') + .usage('') + .option('-s, --substitute-slashes', 'if slashes are found in name field - substitute them'); +} + +function action (collectionFile, command) { + const collectionFilePath = collectionFile.startsWith('/') ? + collectionFile : path.join(process.cwd(), collectionFile), + options = commandUtil.commanderToObject(command); + + let inputCollection = require(collectionFilePath), + inputCollectionCloned, + outputCollection = {}, + tempDir = dirUtils.createTempDir(), + collectionDir; + + dirUtils.assertFileExistence(collectionFilePath); + + // handle different format postman collection file + if (inputCollection.collection) { + inputCollection = inputCollection.collection; + } + inputCollectionCloned = JSON.parse(JSON.stringify(inputCollection)); + + collectionDir = inputCollection.info.name; + + try { + // create export directory under a temporary directory + process.chdir(tempDir); + dirUtils.traverse(inputCollection, [], options); + } + catch (e) { + console.error(e); + fs.rmSync(tempDir, { recursive: true, force: true }); + process.exit(1); + } + + dirUtils.assertDirectoryExistence(collectionDir); + outputCollection = dirUtils.dirTreeToCollectionJson(collectionDir); + // clean-up temp directory + fs.rmSync(`./${collectionDir}`, { recursive: true, force: true }); + // console.log(`${JSON.stringify(inputCollection, null, 2)}`); + // console.log(`${JSON.stringify(outputCollection, null, 2)}`); + + assert.deepStrictEqual(inputCollectionCloned, outputCollection); +} + +module.exports = { + cliSetup, + action +}; diff --git a/lib/commands/dir-export/index.js b/lib/commands/dir-export/index.js new file mode 100644 index 000000000..928c2b2e0 --- /dev/null +++ b/lib/commands/dir-export/index.js @@ -0,0 +1,50 @@ +const path = require('path'); +const dirUtils = require('../dir-utils'); +const commandUtil = require('../../../bin/util'); +const fs = require('fs'); + +/* + @param {Command} - An commander Command instance to which this command is added +*/ +function cliSetup (program) { + return program + .command('dir-export ') + .description('Convert a Postman collection file into its directory representation') + .usage(' [options]') + .option('-s, --substitute-slashes', 'if slashes are found in name field - substitute them') + .option('-f, --force-overwrite', 'overwrite if directory already exists'); +} + +function action (collectionFile, command) { + let collectionFilePath = path.isAbsolute(collectionFile) ? + collectionFile : path.join(process.cwd(), collectionFile), + options = commandUtil.commanderToObject(command); + + dirUtils.assertFileExistence(collectionFilePath); + + let collection = require(collectionFilePath); + + // handle different format postman collection file + if (collection.collection) { + collection = collection.collection; + } + + if (options.forceOverwrite) { + fs.rmSync(`./${collection.info.name}`, { recursive: true, force: true }); + } + + try { + dirUtils.traverse(collection, [], options); + } + catch (e) { + console.error(e); + process.exit(1); + } + + process.exit(0); +} + +module.exports = { + cliSetup, + action +}; diff --git a/lib/commands/dir-import/index.js b/lib/commands/dir-import/index.js new file mode 100644 index 000000000..7e3d42928 --- /dev/null +++ b/lib/commands/dir-import/index.js @@ -0,0 +1,37 @@ +const dirUtils = require('../dir-utils'); +const commandUtil = require('../../../bin/util'); + +/* + @param {Command} - An commander Command instance to which this command is added +*/ +function cliSetup (program) { + return program + .command('dir-import ') + .description('Convert a Postman directory representation into a postman collection') + .usage(' [options]') + .option('-o, --output-file ', 'output collection file, default is collection.json'); +} + +function action (collectionDir, command) { + dirUtils.assertDirectoryExistence(collectionDir); + + let collectionJson = {}, + content, + options = commandUtil.commanderToObject(command), + outputFile = 'collection.json'; + + if (typeof (options.outputFile) === 'string') { + outputFile = options.outputFile; + } + collectionJson = dirUtils.dirTreeToCollectionJson(collectionDir); + + content = JSON.stringify(collectionJson, null, 2); + + dirUtils.createFile(outputFile, content); + process.exit(0); +} + +module.exports = { + cliSetup, + action +}; diff --git a/lib/commands/dir-remove-folder/index.js b/lib/commands/dir-remove-folder/index.js new file mode 100644 index 000000000..60edadc58 --- /dev/null +++ b/lib/commands/dir-remove-folder/index.js @@ -0,0 +1,20 @@ +const dirUtils = require('../dir-utils'); + +/* + @param {Command} - An commander Command instance to which this command is added +*/ +function cliSetup (program) { + return program + .command('dir-remove-folder ') + .description('Remove folder at given path from directory based Postman collection') + .usage(''); +} + +function action (folderPath) { + dirUtils.removePostmanFolder(folderPath); +} + +module.exports = { + cliSetup, + action +}; diff --git a/lib/commands/dir-remove-request/index.js b/lib/commands/dir-remove-request/index.js new file mode 100644 index 000000000..7b505e2e3 --- /dev/null +++ b/lib/commands/dir-remove-request/index.js @@ -0,0 +1,56 @@ +const path = require('path'), + fs = require('fs'), + dirUtils = require('../dir-utils'); + +/* + @param {Command} - An commander Command instance to which this command is added +*/ +function cliSetup (program) { + return program + .command('dir-remove-request ') + .description('Remove request at given path from directory based Postman collection') + .usage(' [options]'); +} + +function action (requestPath) { + const parentDir = path.dirname(requestPath), + requestPathBaseName = path.basename(requestPath), + metaFilePath = path.join(parentDir, '.meta.json'); + + // check if requestPath's parent is already a collection folder + dirUtils.assertCollectionDir(parentDir); + + // remove directory + try { + fs.rmSync(requestPath, { recursive: true, force: true }); + } + catch (e) { + console.error(`Could not delete request at ${requestPath}, please check permissions`); + process.exit(-1); + } + + // remove request from parent's .meta.json + try { + fs.accessSync(metaFilePath, fs.constants.R_OK); + let meta = JSON.parse(fs.readFileSync(metaFilePath)), + childrenOrder = meta.childrenOrder; + + childrenOrder = childrenOrder.filter((item) => { return item !== requestPathBaseName; }); + + meta = { + childrenOrder + }; + + dirUtils.createFile(metaFilePath, JSON.stringify(meta, null, 2)); + } + catch (e) { + console.error(`Could not update ${metaFilePath} with ${requestPath} removed: ${e}`); + fs.rmSync(requestPath, { recursive: true, force: true }); + process.exit(-1); + } +} + +module.exports = { + cliSetup, + action +}; diff --git a/lib/commands/dir-run/index.js b/lib/commands/dir-run/index.js new file mode 100644 index 000000000..4e3ac1ff1 --- /dev/null +++ b/lib/commands/dir-run/index.js @@ -0,0 +1,55 @@ +const _ = require('lodash'), + fs = require('fs'), + dirUtils = require('../dir-utils'), + { addRunOptions } = require('../run/program-options'), + { optionCollector, run } = require('../run/collection-runner'); + + +/* + @param {Command} - An commander Command instance to which this command is added +*/ +function cliSetup (program) { + let prg = program + .command('dir-run ') + .description('Runs the tests in collection-dir, with all the provided options') + .usage(' [options]'); + + prg = addRunOptions(prg); + + return prg; +} + +function action (collectionDir, command, program) { + let tempDir = dirUtils.createTempDir(), + collectionFile = `${tempDir}/collection.json`, + collectionJson; + + try { + collectionJson = dirUtils.dirTreeToCollectionJson(collectionDir); + } + catch (e) { + console.error(`error: Unable to convert directory to collection: ${e.message}`); + fs.rmSync(tempDir, { recursive: true }); + process.exit(-1); + } + + dirUtils.createFile(collectionFile, JSON.stringify(collectionJson, null, 2)); + + const options = optionCollector(program, collectionFile, command); + + run(options, function (err, summary) { + const runError = err || summary.run.error || summary.run.failures.length; + + if (err) { + console.error(`error: ${err.message || err}\n`); + err.friendly && console.error(` ${err.friendly}\n`); + } + runError && !_.get(options, 'suppressExitCode') && (process.exitCode = 1); + fs.rmSync(tempDir, { recursive: true }); + }); +} + +module.exports = { + cliSetup, + action +}; diff --git a/lib/commands/dir-utils.js b/lib/commands/dir-utils.js new file mode 100644 index 000000000..68bceb2a8 --- /dev/null +++ b/lib/commands/dir-utils.js @@ -0,0 +1,500 @@ +const fs = require('fs'); +const pathLib = require('path'); +const dirTree = require('directory-tree'); +const os = require('os'), + + assertDirectoryExistence = (dir) => { + try { + fs.readdirSync(dir); + } + catch (e) { + console.error(`Unable to open directory ${dir} for implode:\n${e}\n`); + process.exit(-1); + } + }, + + assertDirectoryAbsence = (dir) => { + try { + fs.readdirSync(dir); + console.error(`Directory ${dir} already exists, exiting`); + process.exit(-1); + } + catch (e) { + // do nothing, directory is absent + + } + }, + + assertFileExistence = (file) => { + try { + fs.accessSync(file, fs.constants.R_OK); + } + catch (e) { + console.error(`Unable to open file ${file}\n`); + process.exit(-1); + } + }, + + assertCollectionDir = (dir) => { + try { + assertDirectoryExistence(dir); + } + catch (e) { + console.error(`Unable to open directory ${dir} for creating test:\n${e}\n`); + process.exit(-1); + } + try { + assertFileExistence(`${dir}/.meta.json`); + } + catch (e) { + console.error(`${dir} is not a collection dir. .meta.json file not-readable :\n${e}\n`); + process.exit(-1); + } + }, + + // create directory + createDir = (dir) => { + if (fs.existsSync(dir)) { + throw new + Error(`directory ${dir} already exists, use unique names for your requests files within a directory`); + } + else { + fs.mkdirSync(dir, { recursive: true }); + } + }, + + // create file + createFile = (fileName, content, opts) => { + // fs.writeFileSync(fileName, content + '\n'); + fs.writeFileSync(fileName, content + ((typeof (opts) === 'object' && opts.skipTrailingNewLine) ? '' : '\n')); + }, + + // remove forward slashes in name + sanitizePathName = (name) => { + return name.replace(/\//g, '_slash_'); + }, + + // break down event attribute into individual JS code files + createEventFiles = (itemDir, event) => { + let eventOrder = [], + eventMeta; + + event.forEach((element) => { + let eventFileName = `event.${element.listen}.js`, + eventFilePath = `${itemDir}/${eventFileName}`; + + eventOrder.push(eventFileName); + createFile(eventFilePath, element.script.exec.join('\n'), { skipTrailingNewLine: true }); + }); + + eventMeta = { + eventOrder + }; + + createFile(`${itemDir}/.event.meta.json`, JSON.stringify(eventMeta, null, 2)); + }, + + checkAndCreateEventEntry = (name, path) => { + const eventMatches = name.match(/^event\.([^.]+)\.js$/); + let result = {}, + fileContent; + + if (eventMatches) { + fileContent = fs.readFileSync(path).toString(); + result = { + listen: eventMatches[1], + script: { + exec: fileContent.split('\n'), + type: 'text/javascript' + } + }; + } + + return result; + }, + + topLevelFileKeys = { + variable: 1, + info: 1, + auth: 1 + }, + + traverse = (thing, ancestors, options) => { + let parent = './' + ancestors.join('/'), + elementOrder = [], + elementMap = {}, + itemDir, + meta, + name = '', + newParent; + + if (ancestors.length === 0) { + // this is top level set directory name from info + name = thing.info.name; + } + else { + // set directory name to thing['name'] + name = thing.name; + } + + if (name.includes('/') && !options.substituteSlashes) { + throw new + Error(`collection with names containing slash ${name} cannot be processed - consider renaming them`, + 'slash'); + } + + name = sanitizePathName(name); + + itemDir = `${parent}/${name}`; + + if (thing.item) { + // TODO - handle directory creation error + createDir(itemDir); + + // top-level files to be created after top-level directory is created + if (ancestors.length === 0) { + Object.keys(topLevelFileKeys).forEach((element) => { + if (thing[element]) { + let obj = {}; + + obj[element] = thing[element]; + // TODO - handle file creation error + createFile(`${parent}/${name}/.${element}.json`, JSON.stringify(obj, null, 2)); + } + }); + + // create top level event javascript files + if (thing.event) { + createEventFiles(itemDir, thing.event); + } + } + + // walk-through items + newParent = ancestors.map((x) => { return x; }); + newParent.push(name); + + + thing.item.forEach((element) => { + // dis-ambiguate duplicate folder names + if (elementMap[element.name]) { + element.name += '-copy'; + } + elementMap[element.name] = true; + + elementOrder.push(sanitizePathName(element.name)); + traverse(element, newParent, options); + }); + + meta = { + childrenOrder: elementOrder + }; + + if (thing.description) { + meta.description = thing.description; + } + + // TODO save order of folders as meta + createFile(`${parent}/${name}/.meta.json`, JSON.stringify(meta, null, 2)); + } + + if (thing.request) { + /* + - save request under ancestors dir + - save tests that is part of event + */ + createDir(itemDir); + let requestFileName = `${itemDir}/request.json`, + contentTypeJsonHeaderIndex = thing.request.header ? thing.request.header.findIndex((element) => { + return (element.key === 'Content-Type' && element.value === 'application/json'); + }) : -1, + isBodyLangJson = (thing.request.body && thing.request.body.options && + thing.request.body.options.raw && thing.request.body.options.raw.language === 'json'); + + // we have a body in the request + if ((contentTypeJsonHeaderIndex > -1 || isBodyLangJson) && (thing.request.body && thing.request.body.raw)) { + // add raw post body as formatted JSON, will be removed in the import + try { + thing.request.body.raw_json_formatted = JSON.parse(thing.request.body.raw); + delete thing.request.body.raw; + } + catch (e) { + console.warn(`Unable to parse raw body for ${name}`); + } + } + + createFile(requestFileName, JSON.stringify(thing.request, null, 2)); + + if (thing.event) { + createEventFiles(itemDir, thing.event); + } + + if (thing.response) { + let responseFileName = `${itemDir}/response.json`; + + createFile(responseFileName, JSON.stringify(thing.response, null, 2)); + } + } + }, + + walkDirTree = (dirTreeJson, level) => { + // console.log(JSON.stringify(dirTreeJson, null, 2)); + const { path, name, children, type } = dirTreeJson; + let items, + others, + result = {}; + + // console.log(`@@@@ Walking directory ${name}:${level}:${type}`); + + if (level === 1) { + // collect following top level keys + // info + // auth + // event + // variable + let matches = name.match(/\.([^./]*)\.json$/); + + if (matches && topLevelFileKeys[matches[1]]) { + let item = matches[1]; + + result[item] = JSON.parse(fs.readFileSync(path))[item]; + + return result; + } + } + // console.log(`processing name:${name} with type:${type} in path:${path}`); + + switch (type) { + case 'file': + // console.log(`@@@@ Leaf node ${name}:${level}:${type}`); + switch (name) { + case '.meta.json': + break; + case 'request.json': + result = { + request: JSON.parse(fs.readFileSync(path)) + }; + if (result.request.body && result.request.body.raw_json_formatted) { + result.request.body.raw = JSON.stringify(result.request.body.raw_json_formatted); + delete result.request.body.raw_json_formatted; + } + break; + case 'response.json': + result = { + response: JSON.parse(fs.readFileSync(path)) + }; + break; + default: + result = checkAndCreateEventEntry(name, path); + break; + } + break; + case 'directory': + items = []; + others = {}; + + // console.log(`@@@@ Getting into directory ${name}:${level}:${type}`); + // top level name is part of info key + if (level !== 0) { + result.name = name; + } + if (children instanceof Array && children.length > 0) { + let metaFilePath = pathLib.join(path, '.meta.json'); + + try { + fs.accessSync(metaFilePath, fs.constants.R_OK); + let meta = JSON.parse(fs.readFileSync(metaFilePath)), + childrenOrder = meta.childrenOrder; + + // console.log(`childrenOrder: ${JSON.stringify(childrenOrder)}`); + // console.log(`children filtered: ${JSON.stringify(children)}`); + children.sort((a, b) => { + // console.log(`a.name: :${a.name}: b.name: :${b.name}:`); + let aIndex = childrenOrder.findIndex((e) => { return (e === a.name); }), + bIndex = childrenOrder.findIndex((e) => { return (e === b.name); }); + + // console.log(`aIndex: ${aIndex} bIndex: ${bIndex}`); + return (aIndex < bIndex) ? -1 : 1; + }); + // console.log(`childrenOrder after sort: ${JSON.stringify(children)}`); + // add description from meta + if (meta.description) { + result.description = meta.description; + } + } + catch (e) { + // ignore if .meta.json does not exist + } + children.forEach((child) => { + let output = walkDirTree(child, level + 1); + + if (child.type === 'file') { + if (child.name.match(/event/)) { + if (!result.event) { + result.event = []; + } + result.event.push(output); + } + else { + Object.assign(others, output); + } + } + else { + items.push(output); + } + }); + // sort event entries using event meta + if (result.event) { + let eventMetaFilePath = pathLib.join(path, '.event.meta.json'), + eventOrder, + eventMeta; + + fs.accessSync(eventMetaFilePath, fs.constants.R_OK); + eventMeta = JSON.parse(fs.readFileSync(eventMetaFilePath)); + eventOrder = eventMeta.eventOrder; + + result.event.sort((a, b) => { + // console.log(`a.name: :${a.name}: b.name: :${b.name}:`); + let aIndex = eventOrder.findIndex((e) => { return (e === `event.${a.listen}.js`); }), + bIndex = eventOrder.findIndex((e) => { return (e === `event.${b.listen}.js`); }); + + // console.log(`aIndex: ${aIndex} bIndex: ${bIndex}`); + return (aIndex < bIndex) ? -1 : 1; + }); + } + + if (items.length > 0) { + result.item = items; + } + } + Object.assign(result, others); + break; + default: + break; + } + + return result; + }, + + dirTreeToCollectionJson = function (collectionDir) { + const tree = dirTree(collectionDir, + { attributes: ['type'], exclude: /\.meta\.json/ }), + collectionJson = walkDirTree(tree, 0); + + return collectionJson; + }, + + createPostmanFolder = function (folderPath) { + const parentDir = pathLib.dirname(folderPath), + trimmedFolderPath = folderPath.replace(/\/+$/, ''), + folderPathBaseName = pathLib.basename(folderPath), + parentMetaFilePath = pathLib.join(parentDir, '.meta.json'), + metaFilePath = pathLib.join(folderPath, '.meta.json'); + + // clean-up if directory already exists + fs.rmSync(`${folderPath}`, { recursive: true, force: true }); + + // check if folderPath's parent is already a collection folder + assertCollectionDir(parentDir); + + try { + createDir(trimmedFolderPath); + } + catch (e) { + console.error(`Could not create folder at ${folderPath}`); + process.exit(-1); + } + + // add new folder to parent's .meta.json + try { + fs.accessSync(parentMetaFilePath, fs.constants.R_OK); + let meta = JSON.parse(fs.readFileSync(parentMetaFilePath)), + childrenOrder = meta.childrenOrder; + + if (!childrenOrder.includes(folderPathBaseName)) { + childrenOrder.push(folderPathBaseName); + } + + meta = { + childrenOrder + }; + + createFile(parentMetaFilePath, JSON.stringify(meta, null, 2)); + } + catch (e) { + console.error(`Could not update ${parentMetaFilePath} with new request ${folderPath}: ${e}`); + fs.rmSync(`${folderPath}`, { recursive: true, force: true }); + process.exit(-1); + } + + // add .meta.json to new folder + try { + let meta = { + childrenOrder: [] + }; + + createFile(metaFilePath, JSON.stringify(meta, null, 2)); + } + catch (e) { + console.error(`Could not create ${metaFilePath} under new folder ${folderPath}: ${e}`); + fs.rmSync(`${folderPath}`, { recursive: true, force: true }); + process.exit(-1); + } + }, + + removePostmanFolder = function (folderPath) { + const parentDir = pathLib.dirname(folderPath), + folderPathBaseName = pathLib.basename(folderPath), + metaFilePath = pathLib.join(parentDir, '.meta.json'); + + // check if folderPath's parent is already a collection folder + assertCollectionDir(parentDir); + + // remove directory + try { + fs.rmSync(folderPath, { recursive: true, force: true }); + } + catch (e) { + console.error(`Could not delete folder at ${folderPath}, please check permissions`); + process.exit(-1); + } + + // remove folder from parent's .meta.json + try { + fs.accessSync(metaFilePath, fs.constants.R_OK); + let meta = JSON.parse(fs.readFileSync(metaFilePath)), + childrenOrder = meta.childrenOrder; + + childrenOrder = childrenOrder.filter((item) => { return item !== folderPathBaseName; }); + + meta = { + childrenOrder + }; + + createFile(metaFilePath, JSON.stringify(meta, null, 2)); + } + catch (e) { + console.error(`Could not update ${metaFilePath} with ${folderPath} removed: ${e}`); + fs.rmSync(folderPath, { recursive: true, force: true }); + process.exit(-1); + } + }, + + createTempDir = function () { + return fs.mkdtempSync(pathLib.join(os.tmpdir(), 'newman-')); + }; + +module.exports = { + assertCollectionDir, + assertDirectoryAbsence, + assertDirectoryExistence, + assertFileExistence, + createFile, + createDir, + createPostmanFolder, + createTempDir, + dirTreeToCollectionJson, + removePostmanFolder, + sanitizePathName, + traverse, + walkDirTree +}; diff --git a/lib/commands/index.js b/lib/commands/index.js new file mode 100644 index 000000000..c584c7bc0 --- /dev/null +++ b/lib/commands/index.js @@ -0,0 +1,16 @@ +const commands = [ + 'dir-add-folder', + 'dir-add-request', + 'dir-collection-create', + 'dir-export', + 'dir-export-import-check', + 'dir-import', + 'dir-remove-folder', + 'dir-remove-request', + 'dir-run', + 'run' +]; + +commands.forEach(function (value) { + module.exports[value] = require('./' + value); +}); diff --git a/lib/run/index.js b/lib/commands/run/collection-runner.js similarity index 94% rename from lib/run/index.js rename to lib/commands/run/collection-runner.js index b63409691..e9663112f 100644 --- a/lib/run/index.js +++ b/lib/commands/run/collection-runner.js @@ -8,7 +8,8 @@ var _ = require('lodash'), RunSummary = require('./summary'), getOptions = require('./options'), exportFile = require('./export-file'), - util = require('../util'), + util = require('../../util'), + commandUtil = require('../../../bin/util'), /** * This object describes the various events raised by Newman, and what each event argument contains. @@ -38,11 +39,11 @@ var _ = require('lodash'), * @type {Object} */ defaultReporters = { - cli: require('../reporters/cli'), - json: require('../reporters/json'), - junit: require('../reporters/junit'), - progress: require('../reporters/progress'), - emojitrain: require('../reporters/emojitrain') + cli: require('../../reporters/cli'), + json: require('../../reporters/json'), + junit: require('../../reporters/junit'), + progress: require('../../reporters/progress'), + emojitrain: require('../../reporters/emojitrain') }, /** @@ -84,7 +85,7 @@ var _ = require('lodash'), * @param {Function} callback - The callback function invoked to mark the end of the collection run. * @returns {EventEmitter} - An EventEmitter instance with done and error event attachments. */ -module.exports = function (options, callback) { +function run (options, callback) { // validate all options. it is to be noted that `options` parameter is option and is polymorphic (!callback && _.isFunction(options)) && ( (callback = options), @@ -438,4 +439,23 @@ module.exports = function (options, callback) { }); return emitter; +} + +module.exports = { + optionCollector (program, collection, command) { + let options = commandUtil.commanderToObject(command), + + // parse custom reporter options + reporterOptions = commandUtil.parseNestedOptions(program._originalArgs, '--reporter-', options.reporters); + + // Inject additional properties into the options object + options.collection = collection; + options.reporterOptions = reporterOptions._generic; + options.reporter = _.transform(_.omit(reporterOptions, '_generic'), (acc, value, key) => { + acc[key] = _.assignIn(value, reporterOptions._generic); // overrides reporter options with _generic + }, {}); + + return options; + }, + run }; diff --git a/lib/run/export-file.js b/lib/commands/run/export-file.js similarity index 100% rename from lib/run/export-file.js rename to lib/commands/run/export-file.js diff --git a/lib/commands/run/index.js b/lib/commands/run/index.js new file mode 100644 index 000000000..a8d9464ce --- /dev/null +++ b/lib/commands/run/index.js @@ -0,0 +1,40 @@ +const _ = require('lodash'), + { optionCollector, run } = require('./collection-runner'), + { addRunOptions } = require('../run/program-options'); + +function cliSetup (program) { + // The `run` command allows you to specify a collection to be run with the provided options. + let prg = program + .command('run ') + .description('Initiate a Postman Collection run from a given URL or path') + .usage(' [options]'); + + /* + cliOptions.forEach(function (value) { + console.log(`@@@@ creation option in cliSetup: ${value}`); + prg = prg.option(...value); + }); + */ + prg = addRunOptions(prg); + + return prg; +} + +function action (collection, command, program) { + const options = optionCollector(program, collection, command); + + run(options, function (err, summary) { + const runError = err || summary.run.error || summary.run.failures.length; + + if (err) { + console.error(`error: ${err.message || err}\n`); + err.friendly && console.error(` ${err.friendly}\n`); + } + runError && !_.get(options, 'suppressExitCode') && (process.exitCode = 1); + }); +} + +module.exports = { + cliSetup, + action +}; diff --git a/lib/run/options.js b/lib/commands/run/options.js similarity index 99% rename from lib/run/options.js rename to lib/commands/run/options.js index 19c876ce2..c2fd1c1e2 100644 --- a/lib/run/options.js +++ b/lib/commands/run/options.js @@ -7,8 +7,8 @@ var _ = require('lodash'), transformer = require('postman-collection-transformer'), liquidJSON = require('liquid-json'), parseCsv = require('csv-parse'), - util = require('../util'), - config = require('../config'), + util = require('../../util'), + config = require('../../config'), /** * The message displayed when the specified collection file can't be loaded. diff --git a/lib/commands/run/program-options.js b/lib/commands/run/program-options.js new file mode 100644 index 000000000..b72cce5d6 --- /dev/null +++ b/lib/commands/run/program-options.js @@ -0,0 +1,57 @@ +const commandUtil = require('../../../bin/util'); + +/* + @param {Command} - An commander Command instance to which options are added +*/ +function addRunOptions (program) { + program.option('-e, --environment ', 'Specify a URL or path to a Postman Environment') + .option('-g, --globals ', 'Specify a URL or path to a file containing Postman Globals') + .option('-r, --reporters [reporters]', 'Specify the reporters to use for this run', + commandUtil.cast.csvParse, ['cli']) + .option('-n, --iteration-count ', 'Define the number of iterations to run', commandUtil.cast.integer) + .option('-d, --iteration-data ', 'Specify a data file to use for iterations (either JSON or CSV)') + .option('--folder ', + 'Specify the folder to run from a collection. Can be specified multiple times to run multiple folders', + commandUtil.cast.memoize, []) + .option('--global-var ', + 'Allows the specification of global variables via the command line, in a key=value format', + commandUtil.cast.memoizeKeyVal, []) + .option('--env-var ', + 'Allows the specification of environment variables via the command line, in a key=value format', + commandUtil.cast.memoizeKeyVal, []) + .option('--export-environment ', 'Exports the final environment to a file after completing the run') + .option('--export-globals ', 'Exports the final globals to a file after completing the run') + .option('--export-collection ', 'Exports the executed collection to a file after completing the run') + .option('--postman-api-key ', 'API Key used to load the resources from the Postman API') + .option('--bail [modifiers]', + 'Specify whether or not to gracefully stop a collection run on encountering an error' + + ' and whether to end the run with an error based on the optional modifier', commandUtil.cast.csvParse) + .option('--ignore-redirects', 'Prevents Newman from automatically following 3XX redirect responses') + .option('-x , --suppress-exit-code', + 'Specify whether or not to override the default exit code for the current run') + .option('--silent', 'Prevents Newman from showing output to CLI') + .option('--disable-unicode', 'Forces Unicode compliant symbols to be replaced by their plain text equivalents') + .option('--color ', 'Enable/Disable colored output (auto|on|off)', commandUtil.cast.colorOptions, 'auto') + .option('--delay-request [n]', 'Specify the extent of delay between requests (milliseconds)', + commandUtil.cast.integer, 0) + .option('--timeout [n]', 'Specify a timeout for collection run (milliseconds)', commandUtil.cast.integer, 0) + .option('--timeout-request [n]', 'Specify a timeout for requests (milliseconds)', commandUtil.cast.integer, 0) + .option('--timeout-script [n]', 'Specify a timeout for scripts (milliseconds)', commandUtil.cast.integer, 0) + .option('--working-dir ', 'Specify the path to the working directory') + .option('--no-insecure-file-read', 'Prevents reading the files situated outside of the working directory') + .option('-k, --insecure', 'Disables SSL validations') + .option('--ssl-client-cert-list ', 'Specify the path to a client certificates configurations (JSON)') + .option('--ssl-client-cert ', 'Specify the path to a client certificate (PEM)') + .option('--ssl-client-key ', 'Specify the path to a client certificate private key') + .option('--ssl-client-passphrase ', 'Specify the client certificate passphrase (for protected key)') + .option('--ssl-extra-ca-certs ', 'Specify additionally trusted CA certificates (PEM)') + .option('--cookie-jar ', 'Specify the path to a custom cookie jar (serialized tough-cookie JSON) ') + .option('--export-cookie-jar ', 'Exports the cookie jar to a file after completing the run') + .option('--verbose', 'Show detailed information of collection run and each request sent'); + + return program; +} + +module.exports = { + addRunOptions +}; diff --git a/lib/run/secure-fs.js b/lib/commands/run/secure-fs.js similarity index 100% rename from lib/run/secure-fs.js rename to lib/commands/run/secure-fs.js diff --git a/lib/run/summary.js b/lib/commands/run/summary.js similarity index 100% rename from lib/run/summary.js rename to lib/commands/run/summary.js diff --git a/lib/index.js b/lib/index.js index abb378da1..d7a8eba14 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,3 +1 @@ -module.exports = { - run: require('./run') -}; +module.exports = require('./commands'); diff --git a/npm/test-integration.js b/npm/test-integration.js index ccc1caa17..0b559fecf 100755 --- a/npm/test-integration.js +++ b/npm/test-integration.js @@ -10,7 +10,7 @@ const fs = require('fs'), async = require('async'), colors = require('colors/safe'), recursive = require('recursive-readdir'), - newman = require(path.join(__dirname, '..', 'index')), + newman = require('../lib/commands/run/collection-runner'), echoServer = require('./server').createRawEchoServer(), redirectServer = require('./server').createRedirectServer(), diff --git a/package-lock.json b/package-lock.json index da6a03924..03cbfb9a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,8 @@ "colors": "1.4.0", "commander": "11.0.0", "csv-parse": "4.16.3", + "directory-tree": "3.5.1", + "eventemitter3": "4.0.7", "filesize": "10.0.12", "liquid-json": "0.3.1", "lodash": "4.17.21", @@ -982,7 +984,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -1039,6 +1040,14 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", @@ -1403,7 +1412,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -1526,7 +1534,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -1534,8 +1541,7 @@ "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "node_modules/colors": { "version": "1.4.0", @@ -1556,6 +1562,50 @@ "node": ">= 0.8" } }, + "node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "dependencies": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-usage": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", + "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", + "dependencies": { + "array-back": "^4.0.2", + "chalk": "^2.4.2", + "table-layout": "^1.0.2", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/command-line-usage/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/command-line-usage/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "engines": { + "node": ">=8" + } + }, "node_modules/commander": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", @@ -1727,6 +1777,14 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1880,6 +1938,21 @@ "node": ">=8" } }, + "node_modules/directory-tree": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/directory-tree/-/directory-tree-3.5.1.tgz", + "integrity": "sha512-HqjZ49fDzUnKYUhHxVw9eKBqbQ+lL0v4kSBInlDlaktmLtGoV9tC54a6A0ZfYeIrkMHWTE6MwwmUXP477+UEKQ==", + "dependencies": { + "command-line-args": "^5.2.0", + "command-line-usage": "^6.1.1" + }, + "bin": { + "directory-tree": "bin/index.js" + }, + "engines": { + "node": ">=10.0" + } + }, "node_modules/docker-modem": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.9.tgz", @@ -2165,7 +2238,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true, "engines": { "node": ">=0.8.0" } @@ -2566,6 +2638,11 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -2686,6 +2763,17 @@ "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3088,7 +3176,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, "engines": { "node": ">=4" } @@ -4084,6 +4171,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, "node_modules/lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", @@ -5552,6 +5644,14 @@ "node": ">=6.0.0" } }, + "node_modules/reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", + "engines": { + "node": ">=6" + } + }, "node_modules/regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -6143,7 +6243,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -6151,6 +6250,36 @@ "node": ">=4" } }, + "node_modules/table-layout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", + "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", + "dependencies": { + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/table-layout/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/table-layout/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "engines": { + "node": ">=8" + } + }, "node_modules/tar-fs": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", @@ -6406,6 +6535,14 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "engines": { + "node": ">=8" + } + }, "node_modules/uglify-js": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", @@ -6632,6 +6769,26 @@ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" }, + "node_modules/wordwrapjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", + "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", + "dependencies": { + "reduce-flatten": "^2.0.0", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/wordwrapjs/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "engines": { + "node": ">=8" + } + }, "node_modules/workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", @@ -7657,7 +7814,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -7702,6 +7858,11 @@ "sprintf-js": "~1.0.2" } }, + "array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==" + }, "array-buffer-byte-length": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", @@ -7998,7 +8159,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -8087,7 +8247,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -8095,8 +8254,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "colors": { "version": "1.4.0", @@ -8111,6 +8269,40 @@ "delayed-stream": "~1.0.0" } }, + "command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "requires": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + } + }, + "command-line-usage": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", + "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", + "requires": { + "array-back": "^4.0.2", + "chalk": "^2.4.2", + "table-layout": "^1.0.2", + "typical": "^5.2.0" + }, + "dependencies": { + "array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==" + }, + "typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==" + } + } + }, "commander": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", @@ -8260,6 +8452,11 @@ "type-detect": "^4.0.0" } }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -8371,6 +8568,15 @@ "path-type": "^4.0.0" } }, + "directory-tree": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/directory-tree/-/directory-tree-3.5.1.tgz", + "integrity": "sha512-HqjZ49fDzUnKYUhHxVw9eKBqbQ+lL0v4kSBInlDlaktmLtGoV9tC54a6A0ZfYeIrkMHWTE6MwwmUXP477+UEKQ==", + "requires": { + "command-line-args": "^5.2.0", + "command-line-usage": "^6.1.1" + } + }, "docker-modem": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.9.tgz", @@ -8609,8 +8815,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { "version": "8.49.0", @@ -8882,6 +9087,11 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -8975,6 +9185,14 @@ "pkg-dir": "^4.1.0" } }, + "find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "requires": { + "array-back": "^3.0.1" + } + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -9269,8 +9487,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-property-descriptors": { "version": "1.0.0", @@ -9996,6 +10213,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, "lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", @@ -11131,6 +11353,11 @@ "minimatch": "^3.0.5" } }, + "reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==" + }, "regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -11586,11 +11813,33 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } }, + "table-layout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", + "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", + "requires": { + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" + }, + "dependencies": { + "array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==" + }, + "typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==" + } + } + }, "tar-fs": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", @@ -11808,6 +12057,11 @@ "is-typedarray": "^1.0.0" } }, + "typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==" + }, "uglify-js": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", @@ -11989,6 +12243,22 @@ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" }, + "wordwrapjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", + "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", + "requires": { + "reduce-flatten": "^2.0.0", + "typical": "^5.2.0" + }, + "dependencies": { + "typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==" + } + } + }, "workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", diff --git a/package.json b/package.json index 102dfd66c..6a5bc0983 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,8 @@ "colors": "1.4.0", "commander": "11.0.0", "csv-parse": "4.16.3", + "directory-tree": "3.5.1", + "eventemitter3": "4.0.7", "filesize": "10.0.12", "liquid-json": "0.3.1", "lodash": "4.17.21", diff --git a/test/cli/dir-commands.test.js b/test/cli/dir-commands.test.js new file mode 100644 index 000000000..48c08821b --- /dev/null +++ b/test/cli/dir-commands.test.js @@ -0,0 +1,57 @@ +const fs = require('fs'), + expect = require('chai').expect; + +/* + statusCodeCheck = function (code) { + expect(code).to.equal(0); + }*/ + +describe('CLI dir command options', function () { + it('should export a collection file to directory', function (done) { + exec('node ./bin/newman.js dir-export examples/sample-collection.json', function (code) { + expect(code).to.equal(0); + fs.rmSync('./Sample Postman Collection', { recursive: true, force: true }); + done(); + }); + }); + + it('should import a directory into a collection file', function (done) { + exec('node ./bin/newman.js dir-import "examples/Sample Postman Collection" -o dir-import-test-collection.json', + function (code) { + expect(code).to.equal(0); + fs.rmSync('./dir-import-test-collection.json', { force: true }); + done(); + }); + }); + + it('should be able to run dir-export-import-check', function (done) { + exec('node ./bin/newman.js dir-export-import-check examples/sample-collection.json', function (code) { + expect(code).to.equal(0); + done(); + }); + }); + + it('should be able to run directory based collection', function (done) { + exec('node ./bin/newman.js dir-run "examples/Sample Postman Collection"', function (code) { + expect(code).to.equal(0); + done(); + }); + }); + + it('should be able to create new directory based collection', function (done) { + exec('node ./bin/newman.js dir-collection-create create-collection-test', + function (code) { + expect(code).to.equal(0); + fs.rmSync('./create-collection-test', { recursive: true, force: true }); + done(); + }); + }); + + it('should be able to add a new folder and a request under it and remove them', function (done) { + exec('node ./bin/newman.js dir-add-folder "examples/Sample Postman Collection/foo"'); + exec('node ./bin/newman.js dir-add-request "examples/Sample Postman Collection/foo/test"'); + exec('node ./bin/newman.js dir-remove-request "examples/Sample Postman Collection/foo/test"'); + exec('node ./bin/newman.js dir-remove-folder "examples/Sample Postman Collection/foo"'); + done(); + }); +}); diff --git a/test/library/cookie-jar.test.js b/test/library/cookie-jar.test.js index 2aede9bb5..16b7acd71 100644 --- a/test/library/cookie-jar.test.js +++ b/test/library/cookie-jar.test.js @@ -4,7 +4,7 @@ const path = require('path'), expect = require('chai').expect, CookieJar = require('@postman/tough-cookie').CookieJar, - newman = require('../../'); + newman = require('../../lib/commands/run/collection-runner'); describe('newman.run cookieJar', function () { var cookieJar = new CookieJar(), diff --git a/test/library/export-environment.test.js b/test/library/export-environment.test.js index 1f8c27359..4b3ba49a9 100644 --- a/test/library/export-environment.test.js +++ b/test/library/export-environment.test.js @@ -4,7 +4,7 @@ const fs = require('fs'), sh = require('shelljs'), expect = require('chai').expect, - newman = require('../../'); + newman = require('../../lib/commands/run/collection-runner'); describe('newman.run exportEnvironment', function () { var outDir = 'out', diff --git a/test/library/export-globals.test.js b/test/library/export-globals.test.js index f531c87d0..63091c468 100644 --- a/test/library/export-globals.test.js +++ b/test/library/export-globals.test.js @@ -4,7 +4,7 @@ const fs = require('fs'), sh = require('shelljs'), expect = require('chai').expect, - newman = require('../../'); + newman = require('../../lib/commands/run/collection-runner'); describe('newman.run exportGlobals', function () { var outDir = 'out', diff --git a/test/library/folder-variants.test.js b/test/library/folder-variants.test.js index d36e6c3fe..8ee85738a 100644 --- a/test/library/folder-variants.test.js +++ b/test/library/folder-variants.test.js @@ -1,6 +1,6 @@ const expect = require('chai').expect, - newman = require('../../'); + newman = require('../../lib/commands/run/collection-runner'); describe('folder variants', function () { var collection = { diff --git a/test/library/iteration-count.test.js b/test/library/iteration-count.test.js index d21753c93..622937a09 100644 --- a/test/library/iteration-count.test.js +++ b/test/library/iteration-count.test.js @@ -4,7 +4,7 @@ var fs = require('fs'), _ = require('lodash'), expect = require('chai').expect, - newman = require('../../'); + newman = require('../../lib/commands/run/collection-runner'); describe('iterationCount vs iterationData.length conflicts', function () { var iterationProperty = 'run.stats.iterations.total', diff --git a/test/library/postman-api-key.test.js b/test/library/postman-api-key.test.js index 214390420..329043f1e 100644 --- a/test/library/postman-api-key.test.js +++ b/test/library/postman-api-key.test.js @@ -4,7 +4,7 @@ const fs = require('fs'), request = require('postman-request'), expect = require('chai').expect, - newman = require('../../'), + newman = require('../../lib/commands/run/collection-runner'), COLLECTION = { id: 'C1', diff --git a/test/library/requestAgents.test.js b/test/library/requestAgents.test.js index c067ed06c..69b8fc85a 100644 --- a/test/library/requestAgents.test.js +++ b/test/library/requestAgents.test.js @@ -3,7 +3,7 @@ const sinon = require('sinon'), https = require('https'), expect = require('chai').expect, - newman = require('../../'); + newman = require('../../lib/commands/run/collection-runner'); describe('newman.run requestAgents', function () { let httpAgent = new http.Agent(), diff --git a/test/library/run-options.test.js b/test/library/run-options.test.js index 0fa90e4ad..05f68e32c 100644 --- a/test/library/run-options.test.js +++ b/test/library/run-options.test.js @@ -1,7 +1,7 @@ const _ = require('lodash'), expect = require('chai').expect, - newman = require('../../'), + newman = require('../../lib/commands/run/collection-runner'), runtimeVersion = require('../../package.json').dependencies['postman-runtime']; describe('Newman run options', function () { diff --git a/test/library/shallow-junit-reporter.test.js b/test/library/shallow-junit-reporter.test.js index e209bc670..c62b5e497 100644 --- a/test/library/shallow-junit-reporter.test.js +++ b/test/library/shallow-junit-reporter.test.js @@ -5,7 +5,7 @@ const fs = require('fs'), parseXml = require('xml2js').parseString, expect = require('chai').expect, - newman = require('../../'); + newman = require('../../lib/commands/run/collection-runner'); describe('JUnit reporter', function () { var outDir = 'out', diff --git a/test/library/ssl-client-cert.test.js b/test/library/ssl-client-cert.test.js index 36ee4a855..6686e228e 100644 --- a/test/library/ssl-client-cert.test.js +++ b/test/library/ssl-client-cert.test.js @@ -3,7 +3,7 @@ const fs = require('fs'), expect = require('chai').expect, https = require('https'), - newman = require('../../'); + newman = require('../../lib/commands/run/collection-runner'); describe('SSL Client certificates', function () { var server1, server2, server3; diff --git a/test/library/suppress-exit-code.test.js b/test/library/suppress-exit-code.test.js index 5e0c961fc..c6c82e806 100644 --- a/test/library/suppress-exit-code.test.js +++ b/test/library/suppress-exit-code.test.js @@ -1,5 +1,5 @@ const expect = require('chai').expect, - newman = require('../../'); + newman = require('../../lib/commands/run/collection-runner'); describe('newman.run suppressExitCode', function () { it('should accept the suppressExitCode parameter', function (done) { diff --git a/test/library/working-directory.test.js b/test/library/working-directory.test.js index b09c6ce5f..6811f51f3 100644 --- a/test/library/working-directory.test.js +++ b/test/library/working-directory.test.js @@ -1,7 +1,7 @@ const path = require('path'), expect = require('chai').expect, - newman = require('../../'), + newman = require('../../lib/commands/run/collection-runner'), workingDir = path.resolve(__dirname, '../fixtures/files/work-dir'); describe('newman.run workingDir', function () { diff --git a/test/system/npm-publish.test.js b/test/system/npm-publish.test.js index b0b3c2c84..8c4662ad0 100644 --- a/test/system/npm-publish.test.js +++ b/test/system/npm-publish.test.js @@ -5,7 +5,9 @@ const expect = require('chai').expect, describe('npm publish', function () { const stdout = exec('npm pack --dry-run --json').toString(), packageInfo = JSON.parse(stdout.substring(stdout.indexOf('[')))[0], - packagedFiles = packageInfo.files.map(function (file) { return file.path; }); + packagedFiles = packageInfo.files + .map(function (file) { return file.path; }) + .filter(function (item) { return (item !== 'DIR_COMMANDS.md'); }); it('should have a valid package name', function () { expect(packageInfo.name).to.equal('newman'); diff --git a/test/unit/cli.test.js b/test/unit/cli.test.js index e445d05fd..c9c7dafee 100644 --- a/test/unit/cli.test.js +++ b/test/unit/cli.test.js @@ -1,11 +1,12 @@ -const _ = require('lodash'), - sinon = require('sinon'), - expect = require('chai').expect, - - newman = require('../../'); +const expect = require('chai').expect; describe('cli parser', function () { - let newmanCLI, + var _ = require('lodash'), + sinon = require('sinon'), + { Command } = require('commander'), + newman = require('../../lib/commands/run/collection-runner'), + newmanCLI, + commands = require('../../lib/commands'), /** * Wrap newmanCLI callback to extract options passed to sinon `newman` stub. @@ -15,8 +16,10 @@ describe('cli parser', function () { * @param {Function} callback - The callback function invoked on the completion of commander parsing. */ cli = (argv, command, callback) => { - newmanCLI(argv, (err) => { - callback(err, _.get(newman, [command, 'lastCall', 'returnValue'])); + let program = newmanCLI.getProgram(Command, commands); + + newmanCLI.run(argv, program, (err) => { + callback(err, _.get(commands, [command, 'action', 'lastCall', 'returnValue'])); }); }; @@ -27,16 +30,18 @@ describe('cli parser', function () { }); describe('Run Command', function () { - // stub `newman.run`, directly return options passed to `newman.run` in newmanCLI. + // stub `run.action`, directly return options passed to `run.action` in newmanCLI. before(function () { - sinon.stub(newman, 'run').callsFake((options) => { - return options; + sinon.stub(commands.run, 'action').callsFake(function (collection, command, program) { + let opts = newman.optionCollector(program, collection, command); + + return opts; }); }); // restore original `newman.run` function. after(function () { - newman.run.restore(); + commands.run.action.restore(); }); it('should pass default options correctly', function (done) { diff --git a/test/unit/defaultReporter.test.js b/test/unit/defaultReporter.test.js index a213a5da0..41722d5b4 100644 --- a/test/unit/defaultReporter.test.js +++ b/test/unit/defaultReporter.test.js @@ -1,6 +1,6 @@ const sinon = require('sinon'), expect = require('chai').expect, - newman = require('../../'); + { run } = require('../../lib/commands/run/collection-runner'); describe('Default reporter', function () { beforeEach(function () { @@ -12,7 +12,7 @@ describe('Default reporter', function () { }); it('cli can be loaded', function (done) { - newman.run({ + run({ collection: 'test/fixtures/run/single-get-request.json', reporters: ['cli'] }, function (err) { @@ -24,7 +24,7 @@ describe('Default reporter', function () { }); it('json can be loaded', function (done) { - newman.run({ + run({ collection: 'test/fixtures/run/single-get-request.json', reporters: ['json'] }, function (err) { @@ -36,7 +36,7 @@ describe('Default reporter', function () { }); it('junit can be loaded', function (done) { - newman.run({ + run({ collection: 'test/fixtures/run/single-get-request.json', reporters: ['junit'] }, function (err) { @@ -48,7 +48,7 @@ describe('Default reporter', function () { }); it('progress can be loaded', function (done) { - newman.run({ + run({ collection: 'test/fixtures/run/single-get-request.json', reporters: ['progress'] }, function (err) { @@ -60,7 +60,7 @@ describe('Default reporter', function () { }); it('emojitrain can be loaded', function (done) { - newman.run({ + run({ collection: 'test/fixtures/run/single-get-request.json', reporters: ['emojitrain'] }, function (err) { diff --git a/test/unit/dir-utils.test.js b/test/unit/dir-utils.test.js new file mode 100644 index 000000000..c76dc896e --- /dev/null +++ b/test/unit/dir-utils.test.js @@ -0,0 +1,97 @@ +const dirUtils = require('../../lib/commands/dir-utils'), + path = require('path'), + expect = require('chai').expect, + fs = require('fs'); + +describe('dir-utils tests', function () { + it('should create a temp dir', function (done) { + const dir = dirUtils.createTempDir(); + + fs.rmSync(dir, { recursive: true, force: true }); + done(); + }); + + it('should assert directory absence', function (done) { + const dir = dirUtils.createTempDir(); + + dirUtils.assertDirectoryAbsence(path.join(dir, 'barbaz')); + fs.rmSync(dir, { recursive: true, force: true }); + done(); + }); + + it('should assert directory existence', function (done) { + const dir = dirUtils.createTempDir(); + + dirUtils.assertDirectoryExistence(dir); + fs.rmSync(dir, { recursive: true, force: true }); + done(); + }); + + it('should assert file create and existence', function (done) { + const dir = dirUtils.createTempDir(), + file = path.join(dir, 'foo.txt'); + + dirUtils.createFile(file, ''); + dirUtils.assertFileExistence(file); + fs.rmSync(dir, { recursive: true, force: true }); + done(); + }); + + it('should be able to create directory', function (done) { + const dir = dirUtils.createTempDir(), + newDir = path.join(dir, 'foo'); + + dirUtils.createDir(newDir); + fs.rmSync(dir, { recursive: true, force: true }); + done(); + }); + + it('should assert collection directory', function (done) { + dirUtils.assertCollectionDir('examples/Sample Postman Collection'); + done(); + }); + + it('should sanitize slashes', function (done) { + const foo = dirUtils.sanitizePathName('/foo/'); + + expect(foo).to.equal('_slash_foo_slash_'); + done(); + }); + + it('should return same string if no slashes present', function (done) { + const foo = dirUtils.sanitizePathName('foo'); + + expect(foo).to.equal('foo'); + done(); + }); + + it('should convert directory tree to collection', function (done) { + const collectionJSON = dirUtils.dirTreeToCollectionJson('examples/Sample Postman Collection'); + + expect(collectionJSON).to.deep.equal(JSON.parse(fs.readFileSync('examples/sample-collection.json'))); + done(); + }); + + it('should convert collection to directory tree', function (done) { + const dir = dirUtils.createTempDir(), + collectionJSON = JSON.parse(fs.readFileSync('examples/sample-collection.json')), + currentDir = process.cwd(); + + process.chdir(dir); + dirUtils.traverse(collectionJSON, [], {}); + process.chdir(currentDir); + + let recreatedCollectionJSON = dirUtils.dirTreeToCollectionJson(path.join(dir, 'Sample Postman Collection')); + + expect(recreatedCollectionJSON).to.deep.equal(JSON.parse(fs.readFileSync('examples/sample-collection.json'))); + + fs.rmSync(dir, { recursive: true, force: true }); + done(); + }); + + it('should create and remove postman folders under an collection', function (done) { + dirUtils.createPostmanFolder('examples/Sample Postman Collection/foo'); + dirUtils.removePostmanFolder('examples/Sample Postman Collection/foo'); + done(); + }); +}); diff --git a/test/unit/externalReporter.test.js b/test/unit/externalReporter.test.js index 511237234..a4c34170c 100644 --- a/test/unit/externalReporter.test.js +++ b/test/unit/externalReporter.test.js @@ -1,6 +1,6 @@ const sinon = require('sinon'), expect = require('chai').expect, - newman = require('../../'); + { run } = require('../../lib/commands/run/collection-runner'); describe('External reporter', function () { beforeEach(function () { @@ -12,7 +12,7 @@ describe('External reporter', function () { }); it('warns when not found', function (done) { - newman.run({ + run({ collection: 'test/fixtures/run/single-get-request.json', reporters: ['unknownreporter'] }, function (err) { diff --git a/test/unit/options.test.js b/test/unit/options.test.js index d77c60e5a..508a852e9 100644 --- a/test/unit/options.test.js +++ b/test/unit/options.test.js @@ -1,7 +1,7 @@ const _ = require('lodash'), expect = require('chai').expect, { VariableScope } = require('postman-collection'), - options = require('../../lib/run/options'); + options = require('../../lib/commands/run/options'); describe('options', function () { describe('JSON with spaces', function () { diff --git a/test/unit/run-summary.test.js b/test/unit/run-summary.test.js index 44e2ec257..f0a298b43 100644 --- a/test/unit/run-summary.test.js +++ b/test/unit/run-summary.test.js @@ -3,8 +3,8 @@ const _ = require('lodash'), describe('run summary', function () { // @todo add test for computation of timings, transfer sizes and average response time - var Summary = require('../../lib/run/summary'), - EventEmitter = require('events'), + var Summary = require('../../lib/commands/run/summary'), + EventEmitter = require('eventemitter3'), sdk = require('postman-collection'), TRACKED_EVENTS = ['iteration', 'item', 'script', 'prerequest', 'request', 'test', 'assertion', diff --git a/test/unit/run.test.js b/test/unit/run.test.js index 9b359af35..680f25fc7 100644 --- a/test/unit/run.test.js +++ b/test/unit/run.test.js @@ -7,10 +7,12 @@ const _ = require('lodash'), runtime = require('postman-runtime'); describe('run module', function () { - var run = require('../../lib/run'); + var runCommand = require('../../lib/commands/run'); + var { run } = require('../../lib/commands/run/collection-runner'); it('should export a function', function () { - expect(run).to.be.a('function'); + expect(runCommand).to.have.property('cliSetup'); + expect(runCommand).to.have.property('action'); }); it('should start a run with no options and return error in callback', function (done) { diff --git a/test/unit/secure-fs.test.js b/test/unit/secure-fs.test.js index 1519ae7f2..fa93be1d6 100644 --- a/test/unit/secure-fs.test.js +++ b/test/unit/secure-fs.test.js @@ -1,6 +1,6 @@ const path = require('path'), expect = require('chai').expect, - SecureFs = require('../../lib/run/secure-fs'), + SecureFs = require('../../lib/commands/run/secure-fs'), POSIX_WORKING_DIR = '/Postman/files', WIN32_WORKING_DIR = 'C:\\Postman\\files';