diff --git a/src/cli/args.ts b/src/cli/args.ts index bd1fa9b7f9..4fa336d1fd 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -1,4 +1,5 @@ import * as abbrev from 'abbrev'; +import { CommandResult } from './commands/types'; import debugModule = require('debug'); @@ -37,7 +38,7 @@ function dashToCamelCase(dash) { // Last item is ArgsOptions, the rest are strings (positional arguments, e.g. paths) export type MethodArgs = Array; -export type Method = (...args: MethodArgs) => Promise; +export type Method = (...args: MethodArgs) => Promise; export interface Args { command: string; @@ -143,7 +144,7 @@ export function args(rawArgv: string[]): Args { argv._.unshift(tmp.shift()!); } - let method: () => Promise = cli[command]; + let method: () => Promise = cli[command]; if (!method) { // if we failed to find a command, then default to an error diff --git a/src/cli/commands/auth/index.ts b/src/cli/commands/auth/index.ts index c1dd76076b..21b5504b97 100644 --- a/src/cli/commands/auth/index.ts +++ b/src/cli/commands/auth/index.ts @@ -13,6 +13,7 @@ import { AuthFailedError } from '../../../lib/errors/authentication-failed-error import { verifyAPI } from './is-authed'; import { CustomError } from '../../../lib/errors'; import { getUtmsAsString } from '../../../lib/utm'; +import { CommandResult } from '../types'; export = auth; @@ -117,7 +118,10 @@ async function testAuthComplete(token: string): Promise<{ res; body }> { }); } -async function auth(apiToken: string, via: AuthCliCommands) { +async function auth( + apiToken: string, + via: AuthCliCommands, +): Promise { let promise; resetAttempts(); if (apiToken) { @@ -134,9 +138,9 @@ async function auth(apiToken: string, via: AuthCliCommands) { if (res.statusCode === 200 || res.statusCode === 201) { snyk.config.set('api', body.api); - return ( + return new CommandResult( '\nYour account has been authenticated. Snyk is now ready to ' + - 'be used.\n' + 'be used.\n', ); } throw errorForFailedAuthAttempt(res, body); diff --git a/src/cli/commands/test/index.ts b/src/cli/commands/test/index.ts index a922026de1..a8d295acec 100644 --- a/src/cli/commands/test/index.ts +++ b/src/cli/commands/test/index.ts @@ -16,6 +16,7 @@ import { } from '../../../lib/types'; import { isLocalFolder } from '../../../lib/detect'; import { MethodArgs } from '../../args'; +import { TestCommandResult } from '../../commands/types'; import { GroupedVuln, LegacyVulnApiResult, @@ -55,9 +56,9 @@ const showVulnPathsMapping: Record = { // TODO: avoid using `as any` whenever it's possible -async function test(...args: MethodArgs): Promise { +async function test(...args: MethodArgs): Promise { const resultOptions = [] as any[]; - let results = [] as any[]; + const results = [] as any[]; let options = ({} as any) as Options & TestOptions; if (typeof args[args.length - 1] === 'object') { @@ -157,25 +158,18 @@ async function test(...args: MethodArgs): Promise { // resultOptions is now an array of 1 or more options used for // the tests results is now an array of 1 or more test results // values depend on `options.json` value - string or object - if (options.json) { - results = results.map((result) => { - // add json for when thrown exception - if (result instanceof Error) { - return { - ok: false, - error: result.message, - path: (result as any).path, - }; - } - return result; - }); + const errorMappedResults = createErrorMappedResultsForJsonOutput(results); + // backwards compat - strip array IFF only one result + const dataToSend = + errorMappedResults.length === 1 + ? errorMappedResults[0] + : errorMappedResults; + const stringifiedData = JSON.stringify(dataToSend, null, 2); - // backwards compat - strip array IFF only one result - const dataToSend = results.length === 1 ? results[0] : results; - const stringifiedData = JSON.stringify(dataToSend, null, 2); - - if (results.every((res) => res.ok)) { - return stringifiedData; + if (options.json) { + // if all results are ok (.ok == true) then return the json + if (errorMappedResults.every((res) => res.ok)) { + return TestCommandResult.createJsonTestCommandResult(stringifiedData); } const err = new Error(stringifiedData) as any; @@ -185,7 +179,7 @@ async function test(...args: MethodArgs): Promise { const fail = shouldFail(vulnerableResults, options.failOn); if (!fail) { // return here to prevent failure - return stringifiedData; + return TestCommandResult.createJsonTestCommandResult(stringifiedData); } } err.code = 'VULNS'; @@ -195,6 +189,7 @@ async function test(...args: MethodArgs): Promise { } err.json = stringifiedData; + err.jsonStringifiedResults = stringifiedData; throw err; } @@ -246,7 +241,10 @@ async function test(...args: MethodArgs): Promise { if (!fail) { // return here to prevent throwing failure response += chalk.bold.green(summaryMessage); - return response; + return TestCommandResult.createHumanReadableTestCommandResult( + response, + stringifiedData, + ); } } @@ -258,11 +256,31 @@ async function test(...args: MethodArgs): Promise { // first one error.code = vulnerableResults[0].code || 'VULNS'; error.userMessage = vulnerableResults[0].userMessage; + error.jsonStringifiedResults = stringifiedData; throw error; } response += chalk.bold.green(summaryMessage); - return response; + return TestCommandResult.createHumanReadableTestCommandResult( + response, + stringifiedData, + ); +} + +function createErrorMappedResultsForJsonOutput(results) { + const errorMappedResults = results.map((result) => { + // add json for when thrown exception + if (result instanceof Error) { + return { + ok: false, + error: result.message, + path: (result as any).path, + }; + } + return result; + }); + + return errorMappedResults; } function shouldFail(vulnerableResults: any[], failOn: FailOn) { diff --git a/src/cli/commands/types.ts b/src/cli/commands/types.ts new file mode 100644 index 0000000000..e42e6b3530 --- /dev/null +++ b/src/cli/commands/types.ts @@ -0,0 +1,57 @@ +export class CommandResult { + result: string; + constructor(result: string) { + this.result = result; + } + + public toString(): string { + return this.result; + } + + public getDisplayResults() { + return this.result; + } +} + +export abstract class TestCommandResult extends CommandResult { + protected jsonResult = ''; + public getJsonResult(): string { + return this.jsonResult; + } + + public static createHumanReadableTestCommandResult( + humanReadableResult: string, + jsonResult: string, + ): HumanReadableTestCommandResult { + return new HumanReadableTestCommandResult(humanReadableResult, jsonResult); + } + + public static createJsonTestCommandResult( + jsonResult: string, + ): JsonTestCommandResult { + return new JsonTestCommandResult(jsonResult); + } +} + +class HumanReadableTestCommandResult extends TestCommandResult { + protected jsonResult = ''; + + constructor(humanReadableResult: string, jsonResult: string) { + super(humanReadableResult); + this.jsonResult = jsonResult; + } + + public getJsonResult(): string { + return this.jsonResult; + } +} + +class JsonTestCommandResult extends TestCommandResult { + constructor(jsonResult: string) { + super(jsonResult); + } + + public getJsonResult(): string { + return this.result; + } +} diff --git a/src/cli/index.ts b/src/cli/index.ts index 4d339255c7..dbdcc0c3e1 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -2,6 +2,7 @@ import 'source-map-support/register'; import * as Debug from 'debug'; import * as pathLib from 'path'; +import { writeFileSync } from 'fs'; // assert supported node runtime version import * as runtime from './runtime'; @@ -10,6 +11,7 @@ import * as analytics from '../lib/analytics'; import * as alerts from '../lib/alerts'; import * as sln from '../lib/sln'; import { args as argsLib, Args } from './args'; +import { CommandResult, TestCommandResult } from './commands/types'; import { copy } from './copy'; import spinner = require('../lib/spinner'); import errors = require('../lib/errors/legacy-errors'); @@ -33,13 +35,15 @@ const EXIT_CODES = { }; async function runCommand(args: Args) { - const result = await args.method(...args.options._); + const commandResult: CommandResult = await args.method(...args.options._); const res = analytics({ args: args.options._, command: args.command, org: args.options.org, }); + const result = commandResult.getDisplayResults(); + if (result && !args.options.quiet) { if (args.options.copy) { copy(result); @@ -49,6 +53,18 @@ async function runCommand(args: Args) { } } + // also save the json (in error.json) to file if option is set + if (args.command === 'test') { + const jsonOutputFile = args.options['json-file-output']; + if (jsonOutputFile) { + const jsonOutputFileStr = jsonOutputFile as string; + saveJsonResultsToFile( + stripAnsi((commandResult as TestCommandResult).getJsonResult()), + jsonOutputFileStr, + ); + } + } + return res; } @@ -86,6 +102,15 @@ async function handleError(args, error) { } } + // also save the json (in error.json) to file if `--json-file-output` option is set + const jsonOutputFile = args.options['json-file-output']; + if (jsonOutputFile && error.jsonStringifiedResults) { + saveJsonResultsToFile( + stripAnsi(error.jsonStringifiedResults), + jsonOutputFile, + ); + } + const analyticsError = vulnsFound ? { stack: error.jsonNoVulns, @@ -120,6 +145,17 @@ async function handleError(args, error) { return { res, exitCode }; } +function saveJsonResultsToFile( + stringifiedJson: string, + jsonOutputFile: string, +) { + try { + writeFileSync(jsonOutputFile, stringifiedJson); + } catch (err) { + console.error(err); + } +} + function checkRuntime() { if (!runtime.isSupported(process.versions.node)) { console.error( diff --git a/test/acceptance/cli-test/cli-test.all-projects.spec.ts b/test/acceptance/cli-test/cli-test.all-projects.spec.ts index 6ea1e11a7d..e7493c8ff8 100644 --- a/test/acceptance/cli-test/cli-test.all-projects.spec.ts +++ b/test/acceptance/cli-test/cli-test.all-projects.spec.ts @@ -2,6 +2,7 @@ import { AcceptanceTests } from './cli-test.acceptance.test'; import { getWorkspaceJSON } from '../workspace-helper'; import * as path from 'path'; import * as sinon from 'sinon'; +import { CommandResult } from '../../../src/cli/commands/types'; export const AllProjectsTests: AcceptanceTests = { language: 'Mixed', @@ -29,7 +30,7 @@ export const AllProjectsTests: AcceptanceTests = { loadPlugin.withArgs('pip').returns(mockPlugin); loadPlugin.callThrough(); // don't mock other plugins - const result = await params.cli.test('mono-repo-project', { + const result: CommandResult = await params.cli.test('mono-repo-project', { allProjects: true, detectionDepth: 1, skipUnresolved: true, @@ -59,38 +60,42 @@ export const AllProjectsTests: AcceptanceTests = { // results should contain test results from both package managers t.match( - result, + result.getDisplayResults(), 'Package manager: rubygems', 'contains package manager rubygems', ); t.match( - result, + result.getDisplayResults(), 'Target file: Gemfile.lock', 'contains target file Gemfile.lock', ); t.match( - result, + result.getDisplayResults(), 'Project name: shallow-goof', 'contains correct project name for npm', ); - t.match(result, 'Package manager: npm', 'contains package manager npm'); t.match( - result, + result.getDisplayResults(), + 'Package manager: npm', + 'contains package manager npm', + ); + t.match( + result.getDisplayResults(), 'Target file: package-lock.json', 'contains target file package-lock.json', ); t.match( - result, + result.getDisplayResults(), 'Package manager: maven', 'contains package manager maven', ); t.match( - result, + result.getDisplayResults(), 'Target file: pom.xml', 'contains target file pom.xml', ); t.match( - result, + result.getDisplayResults(), 'Target file: Pipfile', 'contains target file Pipfile', ); @@ -138,7 +143,7 @@ export const AllProjectsTests: AcceptanceTests = { .returns(mockRequirements); loadPlugin.callThrough(); // don't mock other plugins - const result = await params.cli.test('mono-repo-project', { + const result: CommandResult = await params.cli.test('mono-repo-project', { allProjects: true, detectionDepth: 3, allowMissing: true, // allow requirements.txt to pass when deps not installed @@ -173,89 +178,97 @@ export const AllProjectsTests: AcceptanceTests = { // ruby t.match( - result, + result.getDisplayResults(), 'Package manager: rubygems', 'contains package manager rubygems', ); t.match( - result, + result.getDisplayResults(), 'Target file: Gemfile.lock', 'contains target file Gemfile.lock', ); t.match( - result, + result.getDisplayResults(), `Target file: bundler-app${path.sep}Gemfile.lock`, `contains target file bundler-app${path.sep}Gemfile.lock`, ); // npm t.match( - result, + result.getDisplayResults(), 'Project name: shallow-goof', 'contains correct project name for npm', ); t.match( - result, + result.getDisplayResults(), 'Project name: goof', 'contains correct project name for npm', ); - t.match(result, 'Package manager: npm', 'contains package manager npm'); t.match( - result, + result.getDisplayResults(), + 'Package manager: npm', + 'contains package manager npm', + ); + t.match( + result.getDisplayResults(), 'Target file: package-lock.json', 'contains target file package-lock.json', ); t.match( - result, + result.getDisplayResults(), `Target file: npm-project${path.sep}package.json`, `contains target file npm-project${path.sep}package.json`, ); // maven t.match( - result, + result.getDisplayResults(), 'Package manager: maven', 'contains package manager maven', ); t.match( - result, + result.getDisplayResults(), 'Target file: pom.xml', 'contains target file pom.xml', ); // nuget t.match( - result, + result.getDisplayResults(), 'Package manager: nuget', 'contains package manager nuget', ); t.match( - result, + result.getDisplayResults(), 'Target file: packages.config', 'contains target file packages.config', ); // paket t.match( - result, + result.getDisplayResults(), 'Package manager: paket', 'contains package manager paket', ); t.match( - result, + result.getDisplayResults(), 'Target file: paket.dependencies', 'contains target file paket.dependencies', ); // pip - t.match(result, 'Package manager: pip', 'contains package manager pip'); t.match( - result, + result.getDisplayResults(), + 'Package manager: pip', + 'contains package manager pip', + ); + t.match( + result.getDisplayResults(), 'Target file: Pipfile', 'contains target file Pipfile', ); t.match( - result, + result.getDisplayResults(), `Target file: python-app-with-req-file${path.sep}requirements.txt`, `contains target file python-app-with-req-file${path.sep}requirements.txt`, ); @@ -385,7 +398,7 @@ export const AllProjectsTests: AcceptanceTests = { const spyPlugin = sinon.spy(params.plugins, 'loadPlugin'); t.teardown(spyPlugin.restore); - const result = await params.cli.test('maven-multi-app', { + const result: CommandResult = await params.cli.test('maven-multi-app', { allProjects: true, detectionDepth: 2, }); @@ -412,17 +425,17 @@ export const AllProjectsTests: AcceptanceTests = { ); }); t.match( - result, + result.getDisplayResults(), 'Package manager: maven', 'contains package manager maven', ); t.match( - result, + result.getDisplayResults(), 'Target file: pom.xml', 'contains target file pom.xml', ); t.match( - result, + result.getDisplayResults(), `Target file: simple-child${path.sep}pom.xml`, `contains target file simple-child${path.sep}pom.xml`, ); @@ -489,9 +502,12 @@ export const AllProjectsTests: AcceptanceTests = { utils, ) => async (t) => { utils.chdirWorkspaces(); - const result = await params.cli.test('mono-repo-project-manifests-only', { - allProjects: true, - }); + const result: CommandResult = await params.cli.test( + 'mono-repo-project-manifests-only', + { + allProjects: true, + }, + ); params.server.popRequests(3).forEach((req) => { t.equal(req.method, 'POST', 'makes POST request'); t.equal( @@ -510,28 +526,32 @@ export const AllProjectsTests: AcceptanceTests = { // results should contain test results from all package managers t.match( - result, + result.getDisplayResults(), 'Package manager: rubygems', 'contains package manager rubygems', ); t.match( - result, + result.getDisplayResults(), 'Target file: Gemfile.lock', 'contains target file Gemfile.lock', ); - t.match(result, 'Package manager: npm', 'contains package manager npm'); t.match( - result, + result.getDisplayResults(), + 'Package manager: npm', + 'contains package manager npm', + ); + t.match( + result.getDisplayResults(), 'Target file: package-lock.json', 'contains target file package-lock.json', ); t.match( - result, + result.getDisplayResults(), 'Package manager: maven', 'contains package manager maven', ); t.match( - result, + result.getDisplayResults(), 'Target file: pom.xml', 'contains target file pom.xml', ); @@ -542,7 +562,9 @@ export const AllProjectsTests: AcceptanceTests = { const spyPlugin = sinon.spy(params.plugins, 'loadPlugin'); t.teardown(spyPlugin.restore); - const res = await params.cli.test('ruby-app', { allProjects: true }); + const res: CommandResult = await params.cli.test('ruby-app', { + allProjects: true, + }); t.ok(spyPlugin.withArgs('rubygems').calledOnce, 'calls rubygems plugin'); t.notOk(spyPlugin.withArgs('npm').calledOnce, "doesn't call npm plugin"); @@ -552,12 +574,12 @@ export const AllProjectsTests: AcceptanceTests = { ); t.match( - res, + res.getDisplayResults(), 'Package manager: rubygems', 'contains package manager rubygems', ); t.match( - res, + res.getDisplayResults(), 'Target file: Gemfile.lock', 'contains target file Gemfile.lock', ); @@ -660,10 +682,13 @@ export const AllProjectsTests: AcceptanceTests = { loadPlugin.callThrough(); // don't mock other plugins try { - const res = await params.cli.test('monorepo-with-nuget', { - allProjects: true, - detectionDepth: 4, - }); + const res: CommandResult = await params.cli.test( + 'monorepo-with-nuget', + { + allProjects: true, + detectionDepth: 4, + }, + ); t.equal( loadPlugin.withArgs('nuget').callCount, 2, @@ -680,49 +705,61 @@ export const AllProjectsTests: AcceptanceTests = { ); t.ok(loadPlugin.withArgs('paket').calledOnce, 'calls nuget plugin'); t.match( - res, + res.getDisplayResults(), /Tested 6 projects, no vulnerable paths were found./, 'Six projects tested', ); t.match( - res, + res.getDisplayResults(), `Target file: src${path.sep}paymentservice${path.sep}package-lock.json`, 'Npm project targetFile is as expected', ); t.match( - res, + res.getDisplayResults(), `Target file: src${path.sep}cocoapods-app${path.sep}Podfile`, 'Cocoapods project targetFile is as expected', ); t.match( - res, + res.getDisplayResults(), `Target file: src${path.sep}frontend${path.sep}Gopkg.lock`, 'Go dep project targetFile is as expected', ); t.match( - res, + res.getDisplayResults(), `Target file: src${path.sep}cartservice-nuget${path.sep}obj${path.sep}project.assets.json`, 'Nuget project targetFile is as expected', ); t.match( - res, + res.getDisplayResults(), `Target file: test${path.sep}nuget-app-4${path.sep}packages.config`, 'Nuget project targetFile is as expected', ); t.match( - res, + res.getDisplayResults(), `Target file: test${path.sep}paket-app${path.sep}paket.dependencies`, 'Paket project targetFile is as expected', ); - t.match(res, 'Package manager: nuget', 'Nuget package manager'); t.match( - res, + res.getDisplayResults(), + 'Package manager: nuget', + 'Nuget package manager', + ); + t.match( + res.getDisplayResults(), 'Package manager: cocoapods', 'Cocoapods package manager', ); - t.match(res, 'Package manager: npm', 'Npm package manager'); - t.match(res, 'Package manager: golangdep', 'Go dep package manager'); + t.match( + res.getDisplayResults(), + 'Package manager: npm', + 'Npm package manager', + ); + t.match( + res.getDisplayResults(), + 'Package manager: golangdep', + 'Go dep package manager', + ); } catch (err) { t.fail('expected to pass'); } @@ -732,7 +769,7 @@ export const AllProjectsTests: AcceptanceTests = { const spyPlugin = sinon.spy(params.plugins, 'loadPlugin'); t.teardown(spyPlugin.restore); - const result = await params.cli.test('composer-app', { + const result: CommandResult = await params.cli.test('composer-app', { allProjects: true, }); @@ -748,12 +785,12 @@ export const AllProjectsTests: AcceptanceTests = { t.match(req.url, '/api/v1/test', 'posts to correct url'); }); t.match( - result, + result.getDisplayResults(), 'Package manager: composer', 'contains package manager composer', ); t.match( - result, + result.getDisplayResults(), 'Target file: composer.lock', 'contains target file composer.lock', ); @@ -781,7 +818,7 @@ export const AllProjectsTests: AcceptanceTests = { loadPlugin.withArgs('govendor').returns(mockPlugin); loadPlugin.callThrough(); // don't mock npm plugin - const res = await params.cli.test('mono-repo-go', { + const res: CommandResult = await params.cli.test('mono-repo-go', { allProjects: true, detectionDepth: 3, }); @@ -793,34 +830,50 @@ export const AllProjectsTests: AcceptanceTests = { 'calls go vendor plugin', ); t.match( - res, + res.getDisplayResults(), /Tested 4 projects, no vulnerable paths were found./, 'Four projects tested', ); t.match( - res, + res.getDisplayResults(), `Target file: hello-dep${path.sep}Gopkg.lock`, 'Go dep project targetFile is as expected', ); t.match( - res, + res.getDisplayResults(), `Target file: hello-mod${path.sep}go.mod`, 'Go mod project targetFile is as expected', ); t.match( - res, + res.getDisplayResults(), `Target file: hello-node${path.sep}package-lock.json`, 'Npm project targetFile is as expected', ); t.match( - res, + res.getDisplayResults(), `Target file: hello-vendor${path.sep}vendor${path.sep}vendor.json`, 'Go vendor project targetFile is as expected', ); - t.match(res, 'Package manager: golangdep', 'Nuget package manager'); - t.match(res, 'Package manager: gomodules', 'Nuget package manager'); - t.match(res, 'Package manager: npm', 'Npm package manager'); - t.match(res, 'Package manager: govendor', 'Go dep package manager'); + t.match( + res.getDisplayResults(), + 'Package manager: golangdep', + 'Nuget package manager', + ); + t.match( + res.getDisplayResults(), + 'Package manager: gomodules', + 'Nuget package manager', + ); + t.match( + res.getDisplayResults(), + 'Package manager: npm', + 'Npm package manager', + ); + t.match( + res.getDisplayResults(), + 'Package manager: govendor', + 'Go dep package manager', + ); }, }, }; diff --git a/test/acceptance/cli-test/cli-test.generic.spec.ts b/test/acceptance/cli-test/cli-test.generic.spec.ts index f236027a5b..c8895bbadf 100644 --- a/test/acceptance/cli-test/cli-test.generic.spec.ts +++ b/test/acceptance/cli-test/cli-test.generic.spec.ts @@ -84,7 +84,11 @@ export const GenericTests: AcceptanceTests = { ); t.match(req.url, '/vuln/npm/semver', 'gets from correct url'); t.equal(req.query.org, 'EFF', 'org sent as a query in request'); - t.match(output, 'Testing semver', 'has "Testing semver" message'); + t.match( + output.getDisplayResults(), + 'Testing semver', + 'has "Testing semver" message', + ); t.notMatch(output, 'Remediation', 'shows no remediation advice'); t.notMatch(output, 'snyk wizard', 'does not suggest `snyk wizard`'); }, diff --git a/test/acceptance/cli-test/cli-test.gradle.spec.ts b/test/acceptance/cli-test/cli-test.gradle.spec.ts index ddc5a5d6dd..323e3db499 100644 --- a/test/acceptance/cli-test/cli-test.gradle.spec.ts +++ b/test/acceptance/cli-test/cli-test.gradle.spec.ts @@ -1,6 +1,7 @@ import * as sinon from 'sinon'; import { legacyPlugin as pluginApi } from '@snyk/cli-interface'; import { AcceptanceTests } from './cli-test.acceptance.test'; +import { CommandResult } from '../../../src/cli/commands/types'; export const GradleTests: AcceptanceTests = { language: 'Gradle', @@ -23,7 +24,10 @@ export const GradleTests: AcceptanceTests = { t.teardown(loadPlugin.restore); loadPlugin.withArgs('gradle').returns(plugin); - const res = await params.cli.test('gradle-kotlin-dsl-app'); + const commandResult: CommandResult = await params.cli.test( + 'gradle-kotlin-dsl-app', + ); + const res: string = commandResult.getDisplayResults(); const meta = res.slice(res.indexOf('Organization:')).split('\n'); t.match(meta[0], /Organization:\s+test-org/, 'organization displayed'); t.match( @@ -64,7 +68,8 @@ export const GradleTests: AcceptanceTests = { t.teardown(loadPlugin.restore); loadPlugin.withArgs('gradle').returns(plugin); - const res = await params.cli.test('gradle-app'); + const commandResult: CommandResult = await params.cli.test('gradle-app'); + const res = commandResult.getDisplayResults(); const meta = res.slice(res.indexOf('Organization:')).split('\n'); t.false( @@ -168,7 +173,10 @@ export const GradleTests: AcceptanceTests = { t.teardown(loadPlugin.restore); loadPlugin.withArgs('gradle').returns(plugin); - const res = await params.cli.test('gradle-app', { allSubProjects: true }); + const commandResult: CommandResult = await params.cli.test('gradle-app', { + allSubProjects: true, + }); + const res = commandResult.getDisplayResults(); t.true( ((spyPlugin.args[0] as any)[2] as any).allSubProjects, '`allSubProjects` option is sent', diff --git a/test/acceptance/cli-test/cli-test.npm.spec.ts b/test/acceptance/cli-test/cli-test.npm.spec.ts index a7d190d656..761b1fe643 100644 --- a/test/acceptance/cli-test/cli-test.npm.spec.ts +++ b/test/acceptance/cli-test/cli-test.npm.spec.ts @@ -1,5 +1,6 @@ import * as path from 'path'; import { AcceptanceTests } from './cli-test.acceptance.test'; +import { CommandResult } from '../../../src/cli/commands/types'; export const NpmTests: AcceptanceTests = { language: 'NPM', @@ -78,7 +79,10 @@ export const NpmTests: AcceptanceTests = { t, ) => { utils.chdirWorkspaces(); - const res = await params.cli.test('npm-package-policy'); + const commandResult: CommandResult = await params.cli.test( + 'npm-package-policy', + ); + const res = commandResult.getDisplayResults(); const meta = res.slice(res.indexOf('Organization:')).split('\n'); t.match(meta[0], /Organization:\s+test-org/, 'organization displayed'); t.match(meta[1], /Package manager:\s+npm/, 'package manager displayed'); diff --git a/test/acceptance/cli-test/cli-test.ruby.spec.ts b/test/acceptance/cli-test/cli-test.ruby.spec.ts index 97f4b0f43f..e765337ced 100644 --- a/test/acceptance/cli-test/cli-test.ruby.spec.ts +++ b/test/acceptance/cli-test/cli-test.ruby.spec.ts @@ -2,6 +2,7 @@ import * as path from 'path'; import * as _ from '@snyk/lodash'; import { AcceptanceTests } from './cli-test.acceptance.test'; import { getWorkspaceJSON } from '../workspace-helper'; +import { CommandResult } from '../../../src/cli/commands/types'; export const RubyTests: AcceptanceTests = { language: 'Ruby', @@ -43,7 +44,8 @@ export const RubyTests: AcceptanceTests = { '`test ruby-app` meta when no vulns': (params, utils) => async (t) => { utils.chdirWorkspaces(); - const res = await params.cli.test('ruby-app'); + const commandResult: CommandResult = await params.cli.test('ruby-app'); + const res = commandResult.getDisplayResults(); const meta = res.slice(res.indexOf('Organization:')).split('\n'); t.match(meta[0], /Organization:\s+test-org/, 'organization displayed'); @@ -448,7 +450,10 @@ export const RubyTests: AcceptanceTests = { utils, ) => async (t) => { utils.chdirWorkspaces(); - const res = await params.cli.test('ruby-app', { file: 'Gemfile.lock' }); + const commandResult: CommandResult = await params.cli.test('ruby-app', { + file: 'Gemfile.lock', + }); + const res = commandResult.getDisplayResults(); const meta = res.slice(res.indexOf('Organization:')).split('\n'); t.match(meta[2], /Target file:\s+Gemfile.lock/, 'target file displayed'); }, diff --git a/test/acceptance/cli-test/jeff-cli-test.acceptance.test.ts b/test/acceptance/cli-test/jeff-cli-test.acceptance.test.ts new file mode 100644 index 0000000000..8e00f0dac4 --- /dev/null +++ b/test/acceptance/cli-test/jeff-cli-test.acceptance.test.ts @@ -0,0 +1,31 @@ +import { test } from 'tap'; +import { exec } from 'child_process'; +import { sep } from 'path'; +import { fstat, readFileSync, unlinkSync } from 'fs'; +const osName = require('os-name'); + +const main = './dist/cli/index.js'.replace(/\//g, sep); + +// jeff +// this should go in test/acceptance/cli-args.test.ts but I can't get that test to run (locally) +test('`test --json-file-output produces same JSON output as normal JSON output to stdout`', (t) => { + t.plan(1); + + exec( + `node ${main} test --json --json-file-output=snyk-direct-json-test-output.json`, + (err, stdout) => { + if (err) { + throw err; + } + + const stdoutJson = stdout.trim(); + const outputFileContents = readFileSync( + 'snyk-direct-json-test-output.json', + 'utf-8', + ).trim(); + unlinkSync('./snyk-direct-json-test-output.json'); + + t.equals(stdoutJson, outputFileContents); + }, + ); +}); diff --git a/test/system/remote-package.test.ts b/test/system/remote-package.test.ts index 101bd26a7a..4eb73872b2 100644 --- a/test/system/remote-package.test.ts +++ b/test/system/remote-package.test.ts @@ -20,6 +20,7 @@ const server = require('../cli-server')(BASE_API, apiKey, notAuthorizedApiKey); // ensure this is required *after* the demo server, since this will // configure our fake configuration too import * as cli from '../../src/cli/commands'; +import { CommandResult } from '../../src/cli/commands/types'; const before = test; const after = test; @@ -77,7 +78,8 @@ test('cli tests for online repos', async (t) => { test('multiple test arguments', async (t) => { try { - const res = await cli.test('semver@4', 'qs@6'); + const commandResult: CommandResult = await cli.test('semver@4', 'qs@6'); + const res = commandResult.getDisplayResults(); const lastLine = res .trim() .split('\n') @@ -142,7 +144,10 @@ test('multiple test arguments', async (t) => { test('test for existing remote package with dev-deps only with --dev', async (t) => { try { - const res = await cli.test('lodash@4.17.11', { dev: true }); + const commandResult: CommandResult = await cli.test('lodash@4.17.11', { + dev: true, + }); + const res = commandResult.getDisplayResults(); const lastLine = res .trim() .split('\n') @@ -163,7 +168,10 @@ test('test for existing remote package with dev-deps only', async (t) => { ciCheckerStub.returns(false); t.teardown(ciCheckerStub.restore); - const res = await cli.test('lodash@4.17.11', { dev: false }); + const commandResult: CommandResult = await cli.test('lodash@4.17.11', { + dev: false, + }); + const res = commandResult.getDisplayResults(); const lastLine = res .trim() .split('\n')