diff --git a/package.json b/package.json index 9a4f58f1a9..2539314fe2 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "find-circular": "npm run build && madge --circular ./dist", "format": "prettier --write '{src,test,scripts}/**/*.{js,ts}'", "prepare": "npm run build", + "test:acceptance": "tap test/acceptance/cli-test.nuget.test.ts -Rspec --timeout=300 --node-arg=-r --node-arg=ts-node/register", "tap": "tap test/*.test.* test/acceptance/*.test.* test/system/*.test.* -Rspec --timeout=300 --node-arg=-r --node-arg=ts-node/register", "test": "npm run test-common && npm run tap", "test-common": "npm run check-tests && npm run build && npm run lint && node --require ts-node/register src/cli test --org=snyk", diff --git a/src/lib/monitor.ts b/src/lib/monitor.ts index 11bb948112..5e37065316 100644 --- a/src/lib/monitor.ts +++ b/src/lib/monitor.ts @@ -184,9 +184,7 @@ export async function monitor( const policy = await snyk.policy.load(policyLocations, { loose: true }); const target = await getTarget(pkg, meta); - const targetFileRelativePath = targetFile - ? path.join(path.resolve(root), targetFile) - : ''; + const targetFileRelativePath = path.join(path.resolve(root), targetFile || ''); if (target && target.branch) { analytics.add('targetBranch', target.branch); @@ -294,9 +292,7 @@ export async function monitorGraph( const policy = await snyk.policy.load(policyLocations, { loose: true }); const target = await getTarget(pkg, meta); - const targetFileRelativePath = targetFile - ? path.join(path.resolve(root), targetFile) - : ''; + const targetFileRelativePath = path.join(path.resolve(root), targetFile || ''); if (target && target.branch) { analytics.add('targetBranch', target.branch); diff --git a/test/acceptance/cli-help.test.ts b/test/acceptance/cli-help.test.ts new file mode 100644 index 0000000000..893e9adf15 --- /dev/null +++ b/test/acceptance/cli-help.test.ts @@ -0,0 +1,8 @@ +import * as tap from 'tap'; +import * as cli from '../../src/cli/commands'; + +const { test } = tap; + +test("snyk help doesn't crash", async (t) => { + t.match(await cli.help(), /Usage/); +}); diff --git a/test/acceptance/protect.test.ts b/test/acceptance/cli-protect.test.ts similarity index 100% rename from test/acceptance/protect.test.ts rename to test/acceptance/cli-protect.test.ts diff --git a/test/acceptance/cli-test.acceptance.test.ts b/test/acceptance/cli-test.acceptance.test.ts index 30c3417299..496b634e5a 100644 --- a/test/acceptance/cli-test.acceptance.test.ts +++ b/test/acceptance/cli-test.acceptance.test.ts @@ -7,7 +7,6 @@ import * as _ from 'lodash'; import * as needle from 'needle'; import * as cli from '../../src/cli/commands'; import { fakeServer } from './fake-server'; -import * as subProcess from '../../src/lib/sub-process'; import * as version from '../../src/lib/version'; // ensure this is required *after* the demo server, since this will @@ -31,3037 +30,146 @@ const after = tap.runOnly ? only : test; // Should be after `process.env` setup. import * as plugins from '../../src/lib/plugins'; -import { legacyPlugin as pluginApi } from '@snyk/cli-interface'; -function loadJson(filename: string) { - return JSON.parse(fs.readFileSync(filename, 'utf-8')); -} - -// @later: remove this config stuff. -// Was copied straight from ../src/cli-server.js -before('setup', async (t) => { - versionNumber = await version(); - - t.plan(3); - let key = await cli.config('get', 'api'); - oldkey = key; - t.pass('existing user config captured'); - - key = await cli.config('get', 'endpoint'); - oldendpoint = key; - t.pass('existing user endpoint captured'); - - await new Promise((resolve) => { - server.listen(port, resolve); - }); - t.pass('started demo server'); - t.end(); -}); - -// @later: remove this config stuff. -// Was copied straight from ../src/cli-server.js -before('prime config', async (t) => { - await cli.config('set', 'api=' + apiKey); - t.pass('api token set'); - await cli.config('unset', 'endpoint'); - t.pass('endpoint removed'); - t.end(); -}); - -test('test cli with multiple params: good and bad', async (t) => { - t.plan(6); - try { - await cli.test('/', 'semver', { registry: 'npm', org: 'EFF', json: true }); - t.fail('expect to err'); - } catch (err) { - const errObj = JSON.parse(err.message); - t.ok(errObj.length === 2, 'expecting two results'); - t.notOk(errObj[0].ok, 'first object shouldnt be ok'); - t.ok(errObj[1].ok, 'second object should be ok'); - t.ok(errObj[0].path.length > 0, 'should have path'); - t.ok(errObj[1].path.length > 0, 'should have path'); - t.pass('info on both objects'); - } - t.end(); -}); - -test('userMessage correctly bubbles with npm', async (t) => { - chdirWorkspaces(); - try { - await cli.test('npm-package', { org: 'missing-org' }); - t.fail('expect to err'); - } catch (err) { - t.equal(err.userMessage, 'cli error message', 'got correct err message'); - } - t.end(); -}); - -test('userMessage correctly bubbles with everything other than npm', async (t) => { - chdirWorkspaces(); - try { - await cli.test('ruby-app', { org: 'missing-org' }); - t.fail('expect to err'); - } catch (err) { - t.equal(err.userMessage, 'cli error message', 'got correct err message'); - } - t.end(); -}); - -/** - * Remote package `test` - */ - -test('`test semver` sends remote NPM request:', async (t) => { - // We care about the request here, not the response - const output = await cli.test('semver', { registry: 'npm', org: 'EFF' }); - const req = server.popRequest(); - t.equal(req.method, 'GET', 'makes GET request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - 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.notMatch(output, 'Remediation', 'shows no remediation advice'); - t.notMatch(output, 'snyk wizard', 'does not suggest `snyk wizard`'); -}); - -test('`test sinatra --registry=rubygems` sends remote Rubygems request:', async (t) => { - await cli.test('sinatra', { registry: 'rubygems', org: 'ACME' }); - const req = server.popRequest(); - t.equal(req.method, 'GET', 'makes GET request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/vuln/rubygems/sinatra', 'gets from correct url'); - t.equal(req.query.org, 'ACME', 'org sent as a query in request'); -}); - -/** - * Local source `test` - */ - -test('`test npm-package with custom --project-name`', async (t) => { - chdirWorkspaces(); - await cli.test('npm-package', { - 'project-name': 'custom-project-name', - }); - const req = server.popRequest(); - t.match( - req.body.projectNameOverride, - 'custom-project-name', - 'custom project name is passed', - ); - t.match(req.body.targetFile, undefined, 'target is undefined'); -}); - -test('test npm-package remoteUrl', async (t) => { - chdirWorkspaces(); - process.env.GIT_DIR = 'npm-package/gitdir'; - await cli.test('npm-package'); - const req = server.popRequest(); - t.equal( - req.body.target.remoteUrl, - 'http://github.com/snyk/npm-package', - 'git remoteUrl is passed', - ); - t.equals( - req.body.target.branch, - 'master', - 'correct branch passed to request', - ); - - delete process.env.GIT_DIR; -}); - -test('test npm-package remoteUrl with --remote-repo-url', async (t) => { - chdirWorkspaces(); - process.env.GIT_DIR = 'npm-package/gitdir'; - await cli.test('npm-package', { - 'remote-repo-url': 'foo', - }); - const req = server.popRequest(); - t.equal(req.body.target.remoteUrl, 'foo', 'specified remoteUrl is passed'); - t.equals( - req.body.target.branch, - 'master', - 'correct branch passed to request', - ); - - delete process.env.GIT_DIR; -}); - -test('`test empty --file=Gemfile`', async (t) => { - chdirWorkspaces(); - try { - await cli.test('empty', { file: 'Gemfile' }); - t.fail('should have failed'); - } catch (err) { - t.pass('throws err'); - t.match( - err.message, - 'Could not find the specified file: Gemfile', - 'shows err', - ); - } -}); - -test('`test --file=fixtures/protect/package.json`', async (t) => { - const res = await cli.test(path.resolve(__dirname, '..'), { - file: 'fixtures/protect/package.json', - }); - t.match( - res, - /Tested 1 dependencies for known vulnerabilities/, - 'should succeed in a folder', - ); -}); - -test('`test /` test for non-existent with path specified', async (t) => { - chdirWorkspaces(); - try { - await cli.test('/'); - t.fail('should have failed'); - } catch (err) { - t.pass('throws err'); - t.match( - err.message, - 'Could not detect supported target files in /.' + - '\nPlease see our documentation for supported' + - ' languages and target files: ' + - 'https://support.snyk.io/hc/en-us/articles/360000911957-Language-support' + - ' and make sure you' + - ' are in the right directory.', - ); - } -}); - -test('`test nuget-app --file=non_existent`', async (t) => { - chdirWorkspaces(); - try { - await cli.test('nuget-app', { file: 'non-existent' }); - t.fail('should have failed'); - } catch (err) { - t.pass('throws err'); - t.match( - err.message, - 'Could not find the specified file: non-existent', - 'show first part of err message', - ); - t.match( - err.message, - 'Please check that it exists and try again.', - 'show second part of err message', - ); - } -}); - -test('`test empty --file=readme.md`', async (t) => { - chdirWorkspaces(); - try { - await cli.test('empty', { file: 'readme.md' }); - t.fail('should have failed'); - } catch (err) { - t.pass('throws err'); - t.match( - err.message, - 'Could not detect package manager for file: readme.md', - 'shows err message for when file specified exists, but not supported', - ); - } -}); - -test('`test ruby-app-no-lockfile --file=Gemfile`', async (t) => { - chdirWorkspaces(); - try { - await cli.test('ruby-app-no-lockfile', { file: 'Gemfile' }); - t.fail('should have failed'); - } catch (err) { - t.pass('throws err'); - t.match(err.message, 'Please run `bundle install`', 'shows err'); - } -}); - -test('`test ruby-app --file=Gemfile.lock`', async (t) => { - chdirWorkspaces(); - await cli.test('ruby-app', { file: 'Gemfile.lock' }); - - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - - const depGraph = req.body.depGraph; - t.equal(depGraph.pkgManager.name, 'rubygems'); - t.same( - depGraph.pkgs.map((p) => p.id).sort(), - ['ruby-app@', 'json@2.0.2', 'lynx@0.4.0'].sort(), - 'depGraph looks fine', - ); -}); - -test('`test ruby-app` meta when no vulns', async (t) => { - chdirWorkspaces(); - const res = await cli.test('ruby-app'); - - 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+rubygems/, 'package manager displayed'); - t.match(meta[2], /Target file:\s+Gemfile/, 'target file displayed'); - t.match(meta[3], /Project name:\s+ruby-app/, 'project name displayed'); - t.match(meta[4], /Open source:\s+no/, 'open source displayed'); - t.match(meta[5], /Project path:\s+ruby-app/, 'path displayed'); - t.notMatch( - meta[5], - /Local Snyk policy:\s+found/, - 'local policy not displayed', - ); -}); - -test('`test ruby-app-thresholds`', async (t) => { - chdirWorkspaces(); - - server.setNextResponse( - require('./workspaces/ruby-app-thresholds/test-graph-result.json'), - ); - - try { - await cli.test('ruby-app-thresholds'); - t.fail('should have thrown'); - } catch (err) { - const res = err.message; - - t.match( - res, - 'Tested 7 dependencies for known vulnerabilities, found 6 vulnerabilities, 7 vulnerable paths', - '6 vulns', - ); - - 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+rubygems/, - 'package manager displayed', - ); - t.match(meta[2], /Target file:\s+Gemfile/, 'target file displayed'); - t.match( - meta[3], - /Project name:\s+ruby-app-thresholds/, - 'project name displayed', - ); - t.match(meta[4], /Open source:\s+no/, 'open source displayed'); - t.match(meta[5], /Project path:\s+ruby-app-thresholds/, 'path displayed'); - t.notMatch( - meta[5], - /Local Snyk policy:\s+found/, - 'local policy not displayed', - ); - } -}); - -test('`test ruby-app-thresholds --severity-threshold=low --json`', async (t) => { - chdirWorkspaces(); - - server.setNextResponse( - require('./workspaces/ruby-app-thresholds/test-graph-result-low-severity.json'), - ); - - try { - await cli.test('ruby-app-thresholds', { - severityThreshold: 'low', - json: true, - }); - t.fail('should have thrown'); - } catch (err) { - const req = server.popRequest(); - t.is(req.query.severityThreshold, 'low'); - - const res = JSON.parse(err.message); - - const expected = require('./workspaces/ruby-app-thresholds/legacy-res-json-low-severity.json'); - - t.deepEqual( - _.omit(res, ['vulnerabilities']), - _.omit(expected, ['vulnerabilities']), - 'metadata is ok', - ); - t.deepEqual( - _.sortBy(res.vulnerabilities, 'id'), - _.sortBy(expected.vulnerabilities, 'id'), - 'vulns are the same', - ); - } -}); - -test('`test ruby-app-thresholds --severity-threshold=medium`', async (t) => { - chdirWorkspaces(); - - server.setNextResponse( - require('./workspaces/ruby-app-thresholds/test-graph-result-medium-severity.json'), - ); - - try { - await cli.test('ruby-app-thresholds', { - severityThreshold: 'medium', - }); - t.fail('should have thrown'); - } catch (err) { - const req = server.popRequest(); - t.is(req.query.severityThreshold, 'medium'); - - const res = err.message; - - t.match( - res, - 'Tested 7 dependencies for known vulnerabilities, found 5 vulnerabilities, 6 vulnerable paths', - '5 vulns', - ); - } -}); - -test('`test ruby-app-thresholds --ignore-policy`', async (t) => { - chdirWorkspaces(); - - server.setNextResponse( - require('./workspaces/ruby-app-thresholds/test-graph-result-medium-severity.json'), - ); - - try { - await cli.test('ruby-app-thresholds', { - 'ignore-policy': true, - }); - t.fail('should have thrown'); - } catch (err) { - const req = server.popRequest(); - t.equal(req.query.ignorePolicy, 'true'); - t.end(); - } -}); - -test('`test ruby-app-thresholds --severity-threshold=medium --json`', async (t) => { - chdirWorkspaces(); - - server.setNextResponse( - require('./workspaces/ruby-app-thresholds/test-graph-result-medium-severity.json'), - ); - - try { - await cli.test('ruby-app-thresholds', { - severityThreshold: 'medium', - json: true, - }); - t.fail('should have thrown'); - } catch (err) { - const req = server.popRequest(); - t.is(req.query.severityThreshold, 'medium'); - - const res = JSON.parse(err.message); - - const expected = require('./workspaces/ruby-app-thresholds/legacy-res-json-medium-severity.json'); - - t.deepEqual( - _.omit(res, ['vulnerabilities']), - _.omit(expected, ['vulnerabilities']), - 'metadata is ok', - ); - t.deepEqual( - _.sortBy(res.vulnerabilities, 'id'), - _.sortBy(expected.vulnerabilities, 'id'), - 'vulns are the same', - ); - } -}); - -test('`test ruby-app-thresholds --severity-threshold=high', async (t) => { - chdirWorkspaces(); - - server.setNextResponse( - require('./workspaces/ruby-app-thresholds/test-graph-result-high-severity.json'), - ); - - try { - await cli.test('ruby-app-thresholds', { - severityThreshold: 'high', - }); - t.fail('should have thrown'); - } catch (err) { - const req = server.popRequest(); - t.is(req.query.severityThreshold, 'high'); - - const res = err.message; - - t.match( - res, - 'Tested 7 dependencies for known vulnerabilities, found 3 vulnerabilities, 4 vulnerable paths', - '3 vulns', - ); - } -}); - -test('`test ruby-app-thresholds --severity-threshold=high --json`', async (t) => { - chdirWorkspaces(); - - server.setNextResponse( - require('./workspaces/ruby-app-thresholds/test-graph-result-high-severity.json'), - ); - - try { - await cli.test('ruby-app-thresholds', { - severityThreshold: 'high', - json: true, - }); - t.fail('should have thrown'); - } catch (err) { - const req = server.popRequest(); - t.is(req.query.severityThreshold, 'high'); - - const res = JSON.parse(err.message); - - const expected = require('./workspaces/ruby-app-thresholds/legacy-res-json-high-severity.json'); - - t.deepEqual( - _.omit(res, ['vulnerabilities']), - _.omit(expected, ['vulnerabilities']), - 'metadata is ok', - ); - t.deepEqual( - _.sortBy(res.vulnerabilities, 'id'), - _.sortBy(expected.vulnerabilities, 'id'), - 'vulns are the same', - ); - } -}); - -test('`test ruby-app-policy`', async (t) => { - chdirWorkspaces(); - - server.setNextResponse( - require('./workspaces/ruby-app-policy/test-graph-result.json'), - ); - - try { - await cli.test('ruby-app-policy', { - json: true, - }); - t.fail('should have thrown'); - } catch (err) { - const res = JSON.parse(err.message); - - const expected = require('./workspaces/ruby-app-policy/legacy-res-json.json'); - - t.deepEqual( - _.omit(res, ['vulnerabilities']), - _.omit(expected, ['vulnerabilities']), - 'metadata is ok', - ); - t.deepEqual( - _.sortBy(res.vulnerabilities, 'id'), - _.sortBy(expected.vulnerabilities, 'id'), - 'vulns are the same', - ); - } -}); - -test('`test ruby-app-policy` with cloud ignores', async (t) => { - chdirWorkspaces(); - - server.setNextResponse( - require('./workspaces/ruby-app-policy/test-graph-result-cloud-ignore.json'), - ); - - try { - await cli.test('ruby-app-policy', { - json: true, - }); - t.fail('should have thrown'); - } catch (err) { - const res = JSON.parse(err.message); - - const expected = require('./workspaces/ruby-app-policy/legacy-res-json-cloud-ignore.json'); - - t.deepEqual( - _.omit(res, ['vulnerabilities']), - _.omit(expected, ['vulnerabilities']), - 'metadata is ok', - ); - t.deepEqual( - _.sortBy(res.vulnerabilities, 'id'), - _.sortBy(expected.vulnerabilities, 'id'), - 'vulns are the same', - ); - } -}); - -test('`test ruby-app-no-vulns`', async (t) => { - chdirWorkspaces(); - - server.setNextResponse( - require('./workspaces/ruby-app-no-vulns/test-graph-result.json'), - ); - - const outText = await cli.test('ruby-app-no-vulns', { - json: true, - }); - - const res = JSON.parse(outText); - - const expected = require('./workspaces/ruby-app-no-vulns/legacy-res-json.json'); - - t.deepEqual(res, expected, '--json output is the same'); -}); - -test('`test ruby-app-no-vulns`', async (t) => { - chdirWorkspaces(); - - const apiResponse = Object.assign( - {}, - require('./workspaces/ruby-app-no-vulns/test-graph-result.json'), - ); - apiResponse.meta.isPublic = true; - server.setNextResponse(apiResponse); - - const outText = await cli.test('ruby-app-no-vulns', { - json: true, - }); - - const res = JSON.parse(outText); - - const expected = Object.assign( - {}, - require('./workspaces/ruby-app-no-vulns/legacy-res-json.json'), - { isPrivate: false }, - ); - - t.deepEqual(res, expected, '--json output is the same'); -}); - -test('`test gradle-kotlin-dsl-app` returns correct meta', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { name: 'testplugin', runtime: 'testruntime' }, - }; - }, - }; - sinon.spy(plugin, 'inspect'); - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('gradle').returns(plugin); - - const res = await cli.test('gradle-kotlin-dsl-app'); - 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+gradle/, 'package manager displayed'); - t.match(meta[2], /Target file:\s+build.gradle.kts/, 'target file displayed'); - t.match(meta[3], /Open source:\s+no/, 'open source displayed'); - t.match(meta[4], /Project path:\s+gradle-kotlin-dsl-app/, 'path displayed'); - t.notMatch( - meta[5], - /Local Snyk policy:\s+found/, - 'local policy not displayed', - ); -}); - -test('`test gradle-app` returns correct meta', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { name: 'testplugin', runtime: 'testruntime' }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('gradle').returns(plugin); - - const res = await cli.test('gradle-app'); - const meta = res.slice(res.indexOf('Organization:')).split('\n'); - - t.false( - ((spyPlugin.args[0] as any)[2] as any).allSubProjects, - '`allSubProjects` option is not sent', - ); - t.match(meta[0], /Organization:\s+test-org/, 'organization displayed'); - t.match(meta[1], /Package manager:\s+gradle/, 'package manager displayed'); - t.match(meta[2], /Target file:\s+build.gradle/, 'target file displayed'); - t.match(meta[3], /Open source:\s+no/, 'open source displayed'); - t.match(meta[4], /Project path:\s+gradle-app/, 'path displayed'); - t.notMatch( - meta[5], - /Local Snyk policy:\s+found/, - 'local policy not displayed', - ); -}); - -test('`test gradle-app --all-sub-projects` sends `allSubProjects` argument to plugin', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { plugin: { name: 'gradle' }, package: {} }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('gradle').returns(plugin); - - await cli.test('gradle-app', { - allSubProjects: true, - }); - t.true(((spyPlugin.args[0] as any)[2] as any).allSubProjects); -}); - -test('`test gradle-app` plugin fails to return package or scannedProjects', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { plugin: { name: 'gradle' } }; - }, - }; - sinon.spy(plugin, 'inspect'); - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('gradle').returns(plugin); - - try { - await cli.test('gradle-app', {}); - t.fail('expected error'); - } catch (error) { - t.match( - error, - /error getting dependencies from gradle plugin: neither 'package' nor 'scannedProjects' were found/, - 'error found', - ); - } -}); - -test('`test gradle-app --all-sub-projects` returns correct multi tree meta', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect(): Promise { - return { - plugin: { name: 'gradle' }, - scannedProjects: [ - { - depTree: { - name: 'tree0', - version: '1.0.0', - dependencies: { dep1: { name: 'dep1', version: '1' } }, - }, - }, - { - depTree: { - name: 'tree1', - version: '2.0.0', - dependencies: { dep1: { name: 'dep2', version: '2' } }, - }, - }, - ], - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('gradle').returns(plugin); - - const res = await cli.test('gradle-app', { allSubProjects: true }); - t.true( - ((spyPlugin.args[0] as any)[2] as any).allSubProjects, - '`allSubProjects` option is sent', - ); - - const tests = res.split('Testing gradle-app...').filter((s) => !!s.trim()); - t.equals(tests.length, 2, 'two projects tested independently'); - t.match( - res, - /Tested 2 projects/, - 'number projects tested displayed properly', - ); - for (let i = 0; i < tests.length; i++) { - const meta = tests[i].slice(tests[i].indexOf('Organization:')).split('\n'); - t.match(meta[0], /Organization:\s+test-org/, 'organization displayed'); - t.match(meta[1], /Package manager:\s+gradle/, 'package manager displayed'); - t.match(meta[2], /Target file:\s+build.gradle/, 'target file displayed'); - t.match(meta[3], /Project name:\s+tree/, 'sub-project displayed'); - t.includes(meta[3], `tree${i}`, 'sub-project displayed'); - t.match(meta[4], /Open source:\s+no/, 'open source displayed'); - t.match(meta[5], /Project path:\s+gradle-app/, 'path displayed'); - t.notMatch( - meta[6], - /Local Snyk policy:\s+found/, - 'local policy not displayed', - ); - } -}); - -test('`test` returns correct meta when target file specified', async (t) => { - chdirWorkspaces(); - const res = await cli.test('ruby-app', { file: 'Gemfile.lock' }); - const meta = res.slice(res.indexOf('Organization:')).split('\n'); - t.match(meta[2], /Target file:\s+Gemfile.lock/, 'target file displayed'); -}); - -test('`test npm-package-policy` returns correct meta', async (t) => { - chdirWorkspaces(); - const res = await cli.test('npm-package-policy'); - 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'); - t.match(meta[2], /Target file:\s+package.json/, 'target file displayed'); - t.match( - meta[3], - /Project name:\s+custom-policy-location-package/, - 'project name displayed', - ); - t.match(meta[4], /Open source:\s+no/, 'open source displayed'); - t.match(meta[5], /Project path:\s+npm-package-policy/, 'path displayed'); - t.match(meta[6], /Local Snyk policy:\s+found/, 'local policy displayed'); -}); - -test('`test ruby-gem-no-lockfile --file=ruby-gem.gemspec`', async (t) => { - chdirWorkspaces(); - await cli.test('ruby-gem-no-lockfile', { file: 'ruby-gem.gemspec' }); - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - - const depGraph = req.body.depGraph; - t.equal(depGraph.pkgManager.name, 'rubygems'); - t.same( - depGraph.pkgs.map((p) => p.id), - ['ruby-gem-no-lockfile@'], - 'no deps as we dont really support gemspecs yet', - ); -}); - -test('`test ruby-gem --file=ruby-gem.gemspec`', async (t) => { - chdirWorkspaces(); - await cli.test('ruby-gem', { file: 'ruby-gem.gemspec' }); - - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - - const depGraph = req.body.depGraph; - t.equal(depGraph.pkgManager.name, 'rubygems'); - t.same( - depGraph.pkgs.map((p) => p.id).sort(), - ['ruby-gem@', 'ruby-gem@0.1.0', 'rake@10.5.0'].sort(), - 'depGraph looks fine', - ); -}); - -test('`test ruby-app` auto-detects Gemfile', async (t) => { - chdirWorkspaces(); - await cli.test('ruby-app'); - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - - const depGraph = req.body.depGraph; - t.equal(depGraph.pkgManager.name, 'rubygems'); - t.same( - depGraph.pkgs.map((p) => p.id).sort(), - ['ruby-app@', 'json@2.0.2', 'lynx@0.4.0'].sort(), - 'depGraph looks fine', - ); - t.equal(req.body.targetFile, 'Gemfile', 'specifies target'); -}); - -test('`test nuget-app-2 auto-detects project.assets.json`', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { - name: 'snyk-nuget-plugin', - targetFile: 'project.assets.json', - }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('nuget').returns(plugin); - - await cli.test('nuget-app-2'); - - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'nuget'); - t.same( - spyPlugin.getCall(0).args, - [ - 'nuget-app-2', - 'project.assets.json', - { - args: null, - file: 'project.assets.json', - org: null, - projectName: null, - packageManager: 'nuget', - path: 'nuget-app-2', - showVulnPaths: 'some', - }, - ], - 'calls nuget plugin', - ); -}); - -test('`test nuget-app-2.1 auto-detects obj/project.assets.json`', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { - name: 'snyk-nuget-plugin', - targetFile: 'obj/project.assets.json', - }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('nuget').returns(plugin); - - await cli.test('nuget-app-2.1'); - - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'nuget'); - t.same( - spyPlugin.getCall(0).args, - [ - 'nuget-app-2.1', - 'obj/project.assets.json', - { - args: null, - file: 'obj/project.assets.json', - org: null, - projectName: null, - packageManager: 'nuget', - path: 'nuget-app-2.1', - showVulnPaths: 'some', - }, - ], - 'calls nuget plugin', - ); -}); - -test('`test nuget-app-4 auto-detects packages.config`', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { - name: 'snyk-nuget-plugin', - targetFile: 'paket.dependencies', - targetRuntime: 'net465s', - }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('nuget').returns(plugin); - - await cli.test('nuget-app-4'); - - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'nuget'); - t.same( - spyPlugin.getCall(0).args, - [ - 'nuget-app-4', - 'packages.config', - { - args: null, - file: 'packages.config', - org: null, - projectName: null, - packageManager: 'nuget', - path: 'nuget-app-4', - showVulnPaths: 'some', - }, - ], - 'calls nuget plugin', - ); -}); - -test('`test paket-app auto-detects paket.dependencies`', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { - name: 'snyk-nuget-plugin', - targetFile: 'paket.dependencies', - targetRuntime: 'net465s', - }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('paket').returns(plugin); - - await cli.test('paket-app'); - - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'paket'); - t.same( - spyPlugin.getCall(0).args, - [ - 'paket-app', - 'paket.dependencies', - { - args: null, - file: 'paket.dependencies', - org: null, - projectName: null, - packageManager: 'paket', - path: 'paket-app', - showVulnPaths: 'some', - }, - ], - 'calls nuget plugin', - ); -}); - -test('`test paket-obj-app auto-detects obj/project.assets.json if exists`', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { - name: 'snyk-nuget-plugin', - targetFile: 'paket.dependencies', - targetRuntime: 'net465s', - }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('nuget').returns(plugin); - - await cli.test('paket-obj-app'); - - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'nuget'); - t.same( - spyPlugin.getCall(0).args, - [ - 'paket-obj-app', - 'obj/project.assets.json', - { - args: null, - file: 'obj/project.assets.json', - org: null, - projectName: null, - packageManager: 'nuget', - path: 'paket-obj-app', - showVulnPaths: 'some', - }, - ], - 'calls nuget plugin', - ); -}); - -test('`test monorepo --file=sub-ruby-app/Gemfile`', async (t) => { - chdirWorkspaces(); - await cli.test('monorepo', { file: 'sub-ruby-app/Gemfile' }); - - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - - const depGraph = req.body.depGraph; - t.equal(depGraph.pkgManager.name, 'rubygems'); - t.same( - depGraph.pkgs.map((p) => p.id).sort(), - ['monorepo@', 'json@2.0.2', 'lynx@0.4.0'].sort(), - 'depGraph looks fine', - ); - - t.equal( - req.body.targetFile, - path.join('sub-ruby-app', 'Gemfile'), - 'specifies target', - ); -}); - -test('`test maven-app --file=pom.xml --dev` sends package info', async (t) => { - chdirWorkspaces(); - stubExec(t, 'maven-app/mvn-dep-tree-stdout.txt'); - await cli.test('maven-app', { - file: 'pom.xml', - org: 'nobelprize.org', - dev: true, - }); - - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.query.org, 'nobelprize.org', 'org sent as a query in request'); - t.match(req.body.targetFile, undefined, 'target is undefined'); - - const depGraph = depGraphLib.createFromJSON(req.body.depGraph); - t.equal(depGraph.rootPkg.name, 'com.mycompany.app:maven-app', 'root name'); - const pkgs = depGraph.getPkgs().map((x) => `${x.name}@${x.version}`); - t.ok(pkgs.indexOf('com.mycompany.app:maven-app@1.0-SNAPSHOT') >= 0); - t.ok(pkgs.indexOf('axis:axis@1.4') >= 0); - t.ok(pkgs.indexOf('junit:junit@3.8.2') >= 0); -}); - -test('`test maven-app-with-jars --file=example.jar` sends package info', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { name: 'testplugin', runtime: 'testruntime' }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('maven').returns(plugin); - - await cli.test('maven-app-with-jars', { - file: 'example.jar', - }); - - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.match(req.body.targetFile, undefined, 'target is undefined'); - - t.equal(req.body.depGraph.pkgManager.name, 'maven'); - t.same( - spyPlugin.getCall(0).args, - [ - 'maven-app-with-jars', - 'example.jar', - { - args: null, - file: 'example.jar', - org: null, - projectName: null, - packageManager: 'maven', - path: 'maven-app-with-jars', - showVulnPaths: 'some', - }, - ], - 'calls mvn plugin', - ); -}); - -test('`test maven-app-with-jars --file=example.war` sends package info', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { name: 'testplugin', runtime: 'testruntime' }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('maven').returns(plugin); - - await cli.test('maven-app-with-jars', { - file: 'example.war', - }); - - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.match(req.body.targetFile, undefined, 'target is undefined'); - - t.equal(req.body.depGraph.pkgManager.name, 'maven'); - t.same( - spyPlugin.getCall(0).args, - [ - 'maven-app-with-jars', - 'example.war', - { - args: null, - file: 'example.war', - org: null, - projectName: null, - packageManager: 'maven', - path: 'maven-app-with-jars', - showVulnPaths: 'some', - }, - ], - 'calls mvn plugin', - ); -}); - -test('`test npm-package` sends pkg info', async (t) => { - chdirWorkspaces(); - await cli.test('npm-package'); - const req = server.popRequest(); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.match(req.body.targetFile, undefined, 'target is undefined'); - const depGraph = req.body.depGraph; - - t.same( - depGraph.pkgs.map((p) => p.id).sort(), - ['npm-package@1.0.0', 'ms@0.7.1', 'debug@2.2.0'].sort(), - 'depGraph looks fine', - ); -}); - -test('`test npm-package --file=package-lock.json ` sends pkg info', async (t) => { - chdirWorkspaces(); - await cli.test('npm-package', { file: 'package-lock.json' }); - const req = server.popRequest(); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.match(req.body.targetFile, undefined, 'target is undefined'); - const depGraph = req.body.depGraph; - t.same( - depGraph.pkgs.map((p) => p.id).sort(), - ['npm-package@1.0.0', 'ms@0.7.1', 'debug@2.2.0'].sort(), - 'depGraph looks fine', - ); -}); - -test('`test npm-package --file=package-lock.json --dev` sends pkg info', async (t) => { - chdirWorkspaces(); - await cli.test('npm-package', { file: 'package-lock.json', dev: true }); - const req = server.popRequest(); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.match(req.body.targetFile, undefined, 'target is undefined'); - const depGraph = req.body.depGraph; - t.same( - depGraph.pkgs.map((p) => p.id).sort(), - [ - 'npm-package@1.0.0', - 'ms@0.7.1', - 'debug@2.2.0', - 'object-assign@4.1.1', - ].sort(), - 'depGraph looks fine', - ); -}); - -test('`test npm-out-of-sync` out of sync fails', async (t) => { - chdirWorkspaces(); - try { - await cli.test('npm-out-of-sync', { dev: true }); - t.fail('Should fail'); - } catch (e) { - t.equal( - e.message, - '\nTesting npm-out-of-sync...\n\n' + - 'Dependency snyk was not found in package-lock.json.' + - ' Your package.json and package-lock.json are probably out of sync.' + - ' Please run "npm install" and try again.', - 'Contains enough info about err', - ); - } -}); - -// yarn lockfile based testing is only supported for node 4+ -test('`test yarn-out-of-sync` out of sync fails', async (t) => { - chdirWorkspaces(); - try { - await cli.test('yarn-out-of-sync', { dev: true }); - t.fail('Should fail'); - } catch (e) { - t.equal( - e.message, - '\nTesting yarn-out-of-sync...\n\n' + - 'Dependency snyk was not found in yarn.lock.' + - ' Your package.json and yarn.lock are probably out of sync.' + - ' Please run "yarn install" and try again.', - 'Contains enough info about err', - ); - } -}); - -test('`test yarn-out-of-sync --strict-out-of-sync=false` passes', async (t) => { - chdirWorkspaces(); - await cli.test('yarn-out-of-sync', { dev: true, strictOutOfSync: false }); - const req = server.popRequest(); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - const depGraph = req.body.depGraph; - t.same( - depGraph.pkgs.map((p) => p.id).sort(), - [ - 'acorn-jsx@3.0.1', - 'acorn@3.3.0', - 'acorn@5.7.3', - 'ajv-keywords@2.1.1', - 'ajv@5.5.2', - 'ansi-escapes@3.1.0', - 'ansi-regex@2.1.1', - 'ansi-regex@3.0.0', - 'ansi-styles@2.2.1', - 'ansi-styles@3.2.1', - 'argparse@1.0.10', - 'array-union@1.0.2', - 'array-uniq@1.0.3', - 'arrify@1.0.1', - 'babel-code-frame@6.26.0', - 'balanced-match@1.0.0', - 'brace-expansion@1.1.11', - 'buffer-from@1.1.1', - 'caller-path@0.1.0', - 'callsites@0.2.0', - 'chalk@1.1.3', - 'chalk@2.4.1', - 'chardet@0.4.2', - 'circular-json@0.3.3', - 'cli-cursor@2.1.0', - 'cli-width@2.2.0', - 'co@4.6.0', - 'color-convert@1.9.3', - 'color-name@1.1.3', - 'concat-map@0.0.1', - 'concat-stream@1.6.2', - 'core-util-is@1.0.2', - 'cross-spawn@5.1.0', - 'debug@3.2.5', - 'deep-is@0.1.3', - 'del@2.2.2', - 'doctrine@2.1.0', - 'escape-string-regexp@1.0.5', - 'eslint-scope@3.7.3', - 'eslint-visitor-keys@1.0.0', - 'eslint@4.19.1', - 'espree@3.5.4', - 'esprima@4.0.1', - 'esquery@1.0.1', - 'esrecurse@4.2.1', - 'estraverse@4.2.0', - 'esutils@2.0.2', - 'external-editor@2.2.0', - 'fast-deep-equal@1.1.0', - 'fast-json-stable-stringify@2.0.0', - 'fast-levenshtein@2.0.6', - 'figures@2.0.0', - 'file-entry-cache@2.0.0', - 'flat-cache@1.3.0', - 'fs.realpath@1.0.0', - 'functional-red-black-tree@1.0.1', - 'glob@7.1.3', - 'globals@11.7.0', - 'globby@5.0.0', - 'graceful-fs@4.1.11', - 'has-ansi@2.0.0', - 'has-flag@3.0.0', - 'iconv-lite@0.4.24', - 'ignore@3.3.10', - 'imurmurhash@0.1.4', - 'inflight@1.0.6', - 'inherits@2.0.3', - 'inquirer@3.3.0', - 'is-fullwidth-code-point@2.0.0', - 'is-path-cwd@1.0.0', - 'is-path-in-cwd@1.0.1', - 'is-path-inside@1.0.1', - 'is-promise@2.1.0', - 'is-resolvable@1.1.0', - 'isarray@1.0.0', - 'isexe@2.0.0', - 'js-tokens@3.0.2', - 'js-yaml@3.12.0', - 'json-schema-traverse@0.3.1', - 'json-stable-stringify-without-jsonify@1.0.1', - 'levn@0.3.0', - 'lodash@4.17.11', - 'lru-cache@4.1.3', - 'mimic-fn@1.2.0', - 'minimatch@3.0.4', - 'minimist@0.0.8', - 'mkdirp@0.5.1', - 'ms@2.1.1', - 'mute-stream@0.0.7', - 'natural-compare@1.4.0', - 'npm-package@1.0.0', - 'object-assign@4.1.1', - 'once@1.4.0', - 'onetime@2.0.1', - 'optionator@0.8.2', - 'os-tmpdir@1.0.2', - 'path-is-absolute@1.0.1', - 'path-is-inside@1.0.2', - 'pify@2.3.0', - 'pinkie-promise@2.0.1', - 'pinkie@2.0.4', - 'pluralize@7.0.0', - 'prelude-ls@1.1.2', - 'process-nextick-args@2.0.0', - 'progress@2.0.0', - 'pseudomap@1.0.2', - 'readable-stream@2.3.6', - 'regexpp@1.1.0', - 'require-uncached@1.0.3', - 'resolve-from@1.0.1', - 'restore-cursor@2.0.0', - 'rewire@4.0.1', - 'rimraf@2.6.2', - 'run-async@2.3.0', - 'rx-lite-aggregates@4.0.8', - 'rx-lite@4.0.8', - 'safe-buffer@5.1.2', - 'safer-buffer@2.1.2', - 'semver@5.5.1', - 'shebang-command@1.2.0', - 'shebang-regex@1.0.0', - 'signal-exit@3.0.2', - 'slice-ansi@1.0.0', - 'snyk@*', - 'sprintf-js@1.0.3', - 'string-width@2.1.1', - 'string_decoder@1.1.1', - 'strip-ansi@3.0.1', - 'strip-ansi@4.0.0', - 'strip-json-comments@2.0.1', - 'supports-color@2.0.0', - 'supports-color@5.5.0', - 'table@4.0.2', - 'text-table@0.2.0', - 'through@2.3.8', - 'tmp@0.0.33', - 'to-array@0.1.4', - 'type-check@0.3.2', - 'typedarray@0.0.6', - 'util-deprecate@1.0.2', - 'which@1.3.1', - 'wordwrap@1.0.0', - 'wrappy@1.0.2', - 'write@0.2.1', - 'yallist@2.1.2', - ].sort(), - 'depGraph looks fine', - ); -}); - -test('`test npm-out-of-sync --strict-out-of-sync=false` passes', async (t) => { - chdirWorkspaces(); - await cli.test('npm-out-of-sync', { dev: true, strictOutOfSync: false }); - const req = server.popRequest(); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - const depGraph = req.body.depGraph; - t.same( - depGraph.pkgs.map((p) => p.id).sort(), - [ - 'npm-package@1.0.0', - 'object-assign@4.1.1', - 'rewire@^4.0.1', - 'snyk@*', - 'to-array@0.1.4', - ].sort(), - 'depGraph looks fine', - ); -}); - -test('`test npm-package-shrinkwrap --file=package-lock.json ` with npm-shrinkwrap errors', async (t) => { - t.plan(1); - chdirWorkspaces(); - try { - await cli.test('npm-package-shrinkwrap', { file: 'package-lock.json' }); - t.fail('Should fail'); - } catch (e) { - t.includes( - e.message, - '--file=package-lock.json', - 'Contains enough info about err', - ); - } -}); - -test('`test npm-package-with-subfolder --file=package-lock.json ` picks top-level files', async (t) => { - chdirWorkspaces(); - await cli.test('npm-package-with-subfolder', { file: 'package-lock.json' }); - const req = server.popRequest(); - const depGraph = req.body.depGraph; - t.same( - depGraph.pkgs.map((p) => p.id).sort(), - ['npm-package-top-level@1.0.0', 'to-array@0.1.4'].sort(), - 'depGraph looks fine', - ); -}); - -test('`test npm-package-with-subfolder --file=subfolder/package-lock.json ` picks subfolder files', async (t) => { - chdirWorkspaces(); - await cli.test('npm-package-with-subfolder', { - file: 'subfolder/package-lock.json', - }); - const req = server.popRequest(); - const depGraph = req.body.depGraph; - t.same( - depGraph.pkgs.map((p) => p.id).sort(), - ['npm-package-subfolder@1.0.0', 'to-array@0.1.4'].sort(), - 'depGraph looks fine', - ); -}); - -test('`test yarn-package --file=yarn.lock ` sends pkg info', async (t) => { - chdirWorkspaces(); - await cli.test('yarn-package', { file: 'yarn.lock' }); - const req = server.popRequest(); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.match(req.body.targetFile, undefined, 'target is undefined'); - const depGraph = req.body.depGraph; - t.same( - depGraph.pkgs.map((p) => p.id).sort(), - ['npm-package@1.0.0', 'ms@0.7.1', 'debug@2.2.0'].sort(), - 'depGraph looks fine', - ); -}); - -test('`test yarn-package --file=yarn.lock --dev` sends pkg info', async (t) => { - chdirWorkspaces(); - await cli.test('yarn-package', { file: 'yarn.lock', dev: true }); - const req = server.popRequest(); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.match(req.body.targetFile, undefined, 'target is undefined'); - const depGraph = req.body.depGraph; - t.same( - depGraph.pkgs.map((p) => p.id).sort(), - [ - 'npm-package@1.0.0', - 'ms@0.7.1', - 'debug@2.2.0', - 'object-assign@4.1.1', - ].sort(), - 'depGraph looks fine', - ); -}); - -test('`test yarn-package-with-subfolder --file=yarn.lock ` picks top-level files', async (t) => { - chdirWorkspaces(); - await cli.test('yarn-package-with-subfolder', { file: 'yarn.lock' }); - const req = server.popRequest(); - const depGraph = req.body.depGraph; - t.same( - depGraph.pkgs.map((p) => p.id).sort(), - ['yarn-package-top-level@1.0.0', 'to-array@0.1.4'].sort(), - 'depGraph looks fine', - ); -}); - -test('`test yarn-package-with-subfolder --file=subfolder/yarn.lock ` picks subfolder files', async (t) => { - chdirWorkspaces(); - await cli.test('yarn-package-with-subfolder', { - file: 'subfolder/yarn.lock', - }); - const req = server.popRequest(); - const depGraph = req.body.depGraph; - t.same( - depGraph.pkgs.map((p) => p.id).sort(), - ['yarn-package-subfolder@1.0.0', 'to-array@0.1.4'].sort(), - 'depGraph looks fine', - ); -}); - -test('`test` on a yarn package does work and displays appropriate text', async (t) => { - chdirWorkspaces('yarn-app'); - await cli.test(); - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.match(req.body.targetFile, undefined, 'target is undefined'); - const depGraph = req.body.depGraph; - t.same( - depGraph.pkgs.map((p) => p.id).sort(), - ['yarn-app-one@1.0.0', 'marked@0.3.6', 'moment@2.18.1'].sort(), - 'depGraph looks fine', - ); -}); - -test('`test pip-app --file=requirements.txt`', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { name: 'testplugin', runtime: 'testruntime' }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('pip').returns(plugin); - - await cli.test('pip-app', { - file: 'requirements.txt', - }); - let req = server.popRequest(); - t.equal(req.method, 'GET', 'makes GET request'); - t.match( - req.url, - 'cli-config/feature-flags/pythonPinningAdvice', - 'to correct url', - ); - req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'pip'); - t.same( - spyPlugin.getCall(0).args, - [ - 'pip-app', - 'requirements.txt', - { - args: null, - file: 'requirements.txt', - org: null, - projectName: null, - packageManager: 'pip', - path: 'pip-app', - showVulnPaths: 'some', - }, - ], - 'calls python plugin', - ); -}); - -test('`test pipenv-app --file=Pipfile`', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - plugin: { - targetFile: 'Pipfile', - name: 'snyk-python-plugin', - runtime: 'Python', - }, - package: {}, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('pip').returns(plugin); - - await cli.test('pipenv-app', { - file: 'Pipfile', - }); - let req = server.popRequest(); - t.equal(req.method, 'GET', 'makes GET request'); - t.match( - req.url, - 'cli-config/feature-flags/pythonPinningAdvice', - 'to correct url', - ); - req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.targetFile, 'Pipfile', 'specifies target'); - t.equal(req.body.depGraph.pkgManager.name, 'pip'); - t.same( - spyPlugin.getCall(0).args, - [ - 'pipenv-app', - 'Pipfile', - { - args: null, - file: 'Pipfile', - org: null, - projectName: null, - packageManager: 'pip', - path: 'pipenv-app', - showVulnPaths: 'some', - }, - ], - 'calls python plugin', - ); -}); - -test('`test pip-app-transitive-vuln --file=requirements.txt (actionableCliRemediation=false)`', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return loadJson('./pip-app-transitive-vuln/inspect-result.json'); - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('pip').returns(plugin); - - server.setNextResponse( - loadJson('./pip-app-transitive-vuln/response-without-remediation.json'), - ); - try { - await cli.test('pip-app-transitive-vuln', { - file: 'requirements.txt', - }); - t.fail('should throw, since there are vulns'); - } catch (e) { - t.equals( - e.message, - fs.readFileSync('pip-app-transitive-vuln/cli-output.txt', 'utf8'), - ); - } - let req = server.popRequest(); - t.equal(req.method, 'GET', 'makes GET request'); - t.match( - req.url, - 'cli-config/feature-flags/pythonPinningAdvice', - 'to correct url', - ); - req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'pip'); - t.same( - spyPlugin.getCall(0).args, - [ - 'pip-app-transitive-vuln', - 'requirements.txt', - { - args: null, - file: 'requirements.txt', - org: null, - projectName: null, - packageManager: 'pip', - path: 'pip-app-transitive-vuln', - showVulnPaths: 'some', - }, - ], - 'calls python plugin', - ); -}); - -test('`test pip-app-transitive-vuln --file=requirements.txt (actionableCliRemediation=true)`', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return loadJson('./pip-app-transitive-vuln/inspect-result.json'); - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('pip').returns(plugin); - - server.setNextResponse( - loadJson('./pip-app-transitive-vuln/response-with-remediation.json'), - ); - try { - await cli.test('pip-app-transitive-vuln', { - file: 'requirements.txt', - }); - t.fail('should throw, since there are vulns'); - } catch (e) { - t.equals( - e.message, - fs.readFileSync( - 'pip-app-transitive-vuln/cli-output-actionable-remediation.txt', - 'utf8', - ), - ); - } - let req = server.popRequest(); - t.equal(req.method, 'GET', 'makes GET request'); - t.match( - req.url, - 'cli-config/feature-flags/pythonPinningAdvice', - 'to correct url', - ); - req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'pip'); - t.same( - spyPlugin.getCall(0).args, - [ - 'pip-app-transitive-vuln', - 'requirements.txt', - { - args: null, - file: 'requirements.txt', - org: null, - projectName: null, - packageManager: 'pip', - path: 'pip-app-transitive-vuln', - showVulnPaths: 'some', - }, - ], - 'calls python plugin', - ); -}); - -test('`test nuget-app --file=project.assets.json`', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { - name: 'snyk-nuget-plugin', - targetFile: 'project.assets.json', - targetRuntime: 'net465s', - }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('nuget').returns(plugin); - - await cli.test('nuget-app', { - file: 'project.assets.json', - }); - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.targetFile, 'project.assets.json', 'specifies target'); - t.equal(req.body.depGraph.pkgManager.name, 'nuget'); - t.same( - spyPlugin.getCall(0).args, - [ - 'nuget-app', - 'project.assets.json', - { - args: null, - file: 'project.assets.json', - org: null, - projectName: null, - packageManager: 'nuget', - path: 'nuget-app', - showVulnPaths: 'some', - }, - ], - 'calls nuget plugin', - ); -}); - -test('`test nuget-app --file=packages.config`', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { - name: 'snyk-nuget-plugin', - targetFile: 'packages.config', - targetRuntime: 'net465s', - }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('nuget').returns(plugin); - - await cli.test('nuget-app', { - file: 'packages.config', - }); - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.targetFile, 'packages.config', 'specifies target'); - t.equal(req.body.depGraph.pkgManager.name, 'nuget'); - t.same( - spyPlugin.getCall(0).args, - [ - 'nuget-app', - 'packages.config', - { - args: null, - file: 'packages.config', - org: null, - projectName: null, - packageManager: 'nuget', - path: 'nuget-app', - showVulnPaths: 'some', - }, - ], - 'calls nuget plugin', - ); -}); - -test('`test nuget-app --file=project.json`', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { - name: 'testplugin', - runtime: 'testruntime', - targetFile: 'project.json', - }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('nuget').returns(plugin); - - await cli.test('nuget-app', { - file: 'project.json', - }); - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.targetFile, 'project.json', 'specifies target'); - t.equal(req.body.depGraph.pkgManager.name, 'nuget'); - t.same( - spyPlugin.getCall(0).args, - [ - 'nuget-app', - 'project.json', - { - args: null, - file: 'project.json', - org: null, - projectName: null, - packageManager: 'nuget', - path: 'nuget-app', - showVulnPaths: 'some', - }, - ], - 'calls nuget plugin', - ); -}); - -test('`test paket-app --file=paket.dependencies`', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { - name: 'snyk-nuget-plugin', - targetFile: 'paket.dependencies', - targetRuntime: 'net465s', - }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('paket').returns(plugin); - - await cli.test('paket-app', { - file: 'paket.dependencies', - }); - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'paket'); - t.equal(req.body.targetFile, 'paket.dependencies', 'specifies target'); - t.same( - spyPlugin.getCall(0).args, - [ - 'paket-app', - 'paket.dependencies', - { - args: null, - file: 'paket.dependencies', - org: null, - projectName: null, - packageManager: 'paket', - path: 'paket-app', - showVulnPaths: 'some', - }, - ], - 'calls nuget plugin', - ); -}); - -test('`test golang-gomodules --file=go.mod`', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { - name: 'testplugin', - runtime: 'testruntime', - targetFile: 'go.mod', - }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('gomodules').returns(plugin); - - await cli.test('golang-gomodules', { - file: 'go.mod', - }); - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'gomodules'); - t.equal(req.body.targetFile, 'go.mod', 'specifies target'); - t.same( - spyPlugin.getCall(0).args, - [ - 'golang-gomodules', - 'go.mod', - { - args: null, - file: 'go.mod', - org: null, - projectName: null, - packageManager: 'gomodules', - path: 'golang-gomodules', - showVulnPaths: 'some', - }, - ], - 'calls golang plugin', - ); -}); - -test('`test golang-app` auto-detects golang-gomodules', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { - name: 'testplugin', - runtime: 'testruntime', - targetFile: 'go.mod', - }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('gomodules').returns(plugin); - - await cli.test('golang-gomodules'); - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'gomodules'); - t.equal(req.body.targetFile, 'go.mod', 'specifies target'); - t.same( - spyPlugin.getCall(0).args, - [ - 'golang-gomodules', - 'go.mod', - { - args: null, - file: 'go.mod', - org: null, - projectName: null, - packageManager: 'gomodules', - path: 'golang-gomodules', - showVulnPaths: 'some', - }, - ], - 'calls golang-gomodules plugin', - ); -}); - -test('`test golang-app --file=Gopkg.lock`', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { - name: 'testplugin', - runtime: 'testruntime', - targetFile: 'Gopkg.lock', - }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('golangdep').returns(plugin); - - await cli.test('golang-app', { - file: 'Gopkg.lock', - }); - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'golangdep'); - t.equal(req.body.targetFile, 'Gopkg.lock', 'specifies target'); - t.same( - spyPlugin.getCall(0).args, - [ - 'golang-app', - 'Gopkg.lock', - { - args: null, - file: 'Gopkg.lock', - org: null, - projectName: null, - packageManager: 'golangdep', - path: 'golang-app', - showVulnPaths: 'some', - }, - ], - 'calls golang plugin', - ); -}); - -test('`test golang-app --file=vendor/vendor.json`', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { - name: 'testplugin', - runtime: 'testruntime', - targetFile: 'vendor/vendor.json', - }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('govendor').returns(plugin); - - await cli.test('golang-app', { - file: 'vendor/vendor.json', - }); - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'govendor'); - t.equal(req.body.targetFile, 'vendor/vendor.json', 'specifies target'); - t.same( - spyPlugin.getCall(0).args, - [ - 'golang-app', - 'vendor/vendor.json', - { - args: null, - file: 'vendor/vendor.json', - org: null, - projectName: null, - packageManager: 'govendor', - path: 'golang-app', - showVulnPaths: 'some', - }, - ], - 'calls golang plugin', - ); -}); - -test('`test golang-app` auto-detects golang/dep', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { - name: 'testplugin', - runtime: 'testruntime', - targetFile: 'Gopkg.lock', - }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('golangdep').returns(plugin); - - await cli.test('golang-app'); - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'golangdep'); - t.equal(req.body.targetFile, 'Gopkg.lock', 'specifies target'); - t.same( - spyPlugin.getCall(0).args, - [ - 'golang-app', - 'Gopkg.lock', - { - args: null, - file: 'Gopkg.lock', - org: null, - projectName: null, - packageManager: 'golangdep', - path: 'golang-app', - showVulnPaths: 'some', - }, - ], - 'calls golang plugin', - ); -}); - -test('`test golang-app-govendor` auto-detects govendor', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { name: 'testplugin', runtime: 'testruntime' }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('govendor').returns(plugin); - - await cli.test('golang-app-govendor'); - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'govendor'); - t.same( - spyPlugin.getCall(0).args, - [ - 'golang-app-govendor', - 'vendor/vendor.json', - { - args: null, - file: 'vendor/vendor.json', - org: null, - projectName: null, - packageManager: 'govendor', - path: 'golang-app-govendor', - showVulnPaths: 'some', - }, - ], - 'calls golang plugin', - ); -}); - -test('`test composer-app --file=composer.lock`', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { name: 'testplugin', runtime: 'testruntime' }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('composer').returns(plugin); - - await cli.test('composer-app', { - file: 'composer.lock', - }); - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'composer'); - t.same( - spyPlugin.getCall(0).args, - [ - 'composer-app', - 'composer.lock', - { - args: null, - file: 'composer.lock', - org: null, - projectName: null, - packageManager: 'composer', - path: 'composer-app', - showVulnPaths: 'some', - }, - ], - 'calls composer plugin', - ); -}); - -test('`test composer-app` auto-detects composer.lock', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { name: 'testplugin', runtime: 'testruntime' }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('composer').returns(plugin); - - await cli.test('composer-app'); - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'composer'); - t.same( - spyPlugin.getCall(0).args, - [ - 'composer-app', - 'composer.lock', - { - args: null, - file: 'composer.lock', - org: null, - projectName: null, - packageManager: 'composer', - path: 'composer-app', - showVulnPaths: 'some', - }, - ], - 'calls composer plugin', - ); -}); - -test('`test composer-app --file=composer.lock --dev`', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { name: 'testplugin', runtime: 'testruntime' }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('composer').returns(plugin); - - await cli.test('composer-app', { - file: 'composer.lock', - dev: true, - }); - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'composer'); - t.same( - spyPlugin.getCall(0).args, - [ - 'composer-app', - 'composer.lock', - { - args: null, - dev: true, - file: 'composer.lock', - org: null, - projectName: null, - packageManager: 'composer', - path: 'composer-app', - showVulnPaths: 'some', - }, - ], - 'calls composer plugin', - ); -}); - -test('`test composer-app golang-app nuget-app` auto-detects all three projects', async (t) => { - chdirWorkspaces(); - const plugin = { - async inspect() { - return { - package: {}, - plugin: { name: 'testplugin', runtime: 'testruntime' }, - }; - }, - }; - const spyPlugin = sinon.spy(plugin, 'inspect'); - - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - t.teardown(loadPlugin.restore); - loadPlugin.withArgs('composer').returns(plugin); - loadPlugin.withArgs('golangdep').returns(plugin); - loadPlugin.withArgs('nuget').returns(plugin); - - await cli.test('composer-app', 'golang-app', 'nuget-app', { - org: 'test-org', - }); - // assert three API calls made, each with a different url - const reqs = Array.from({ length: 3 }).map(() => server.popRequest()); - - t.same( - reqs.map((r) => r.method), - ['POST', 'POST', 'POST'], - 'all post requests', - ); - - t.same( - reqs.map((r) => r.headers['x-snyk-cli-version']), - [versionNumber, versionNumber, versionNumber], - 'all send version number', - ); +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('setup', async (t) => { + versionNumber = await version(); - t.same( - reqs.map((r) => r.url), - [ - '/api/v1/test-dep-graph?org=test-org', - '/api/v1/test-dep-graph?org=test-org', - '/api/v1/test-dep-graph?org=test-org', - ], - 'all urls are present', - ); + t.plan(3); + let key = await cli.config('get', 'api'); + oldkey = key; + t.pass('existing user config captured'); - t.same( - reqs.map((r) => r.body.depGraph.pkgManager.name).sort(), - ['composer', 'golangdep', 'nuget'], - 'all urls are present', - ); + key = await cli.config('get', 'endpoint'); + oldendpoint = key; + t.pass('existing user endpoint captured'); - // assert three spyPlugin calls, each with a different app - const calls = spyPlugin.getCalls().sort((call1: any, call2: any) => { - return call1.args[0] < call2.args[1] - ? -1 - : call1.args[0] > call2.args[0] - ? 1 - : 0; + await new Promise((resolve) => { + server.listen(port, resolve); }); - t.same( - calls[0].args, - [ - 'composer-app', - 'composer.lock', - { - args: null, - org: 'test-org', - file: 'composer.lock', - projectName: null, - packageManager: 'composer', - path: 'composer-app', - showVulnPaths: 'some', - }, - ], - 'calls composer plugin', - ); - t.same( - calls[1].args, - [ - 'golang-app', - 'Gopkg.lock', - { - args: null, - org: 'test-org', - file: 'Gopkg.lock', - projectName: null, - packageManager: 'golangdep', - path: 'golang-app', - showVulnPaths: 'some', - }, - ], - 'calls golangdep plugin', - ); - t.same( - calls[2].args, - [ - 'nuget-app', - 'project.assets.json', - { - args: null, - org: 'test-org', - file: 'project.assets.json', - projectName: null, - packageManager: 'nuget', - path: 'nuget-app', - showVulnPaths: 'some', - }, - ], - 'calls nuget plugin', - ); + t.pass('started demo server'); + t.end(); }); -test('`test foo:latest --docker`', async (t) => { - const spyPlugin = stubDockerPluginResponse( - { - plugin: { - packageManager: 'deb', - }, - package: {}, - }, - t, - ); - - await cli.test('foo:latest', { - docker: true, - org: 'explicit-org', - }); - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'deb'); - t.same( - spyPlugin.getCall(0).args, - [ - 'foo:latest', - null, - { - args: null, - file: null, - docker: true, - org: 'explicit-org', - projectName: null, - packageManager: null, - path: 'foo:latest', - showVulnPaths: 'some', - }, - ], - 'calls docker plugin with expected arguments', - ); +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('prime config', async (t) => { + await cli.config('set', 'api=' + apiKey); + t.pass('api token set'); + await cli.config('unset', 'endpoint'); + t.pass('endpoint removed'); + t.end(); }); -test('`test foo:latest --docker vulnerable paths`', async (t) => { - stubDockerPluginResponse( - { - plugin: { - packageManager: 'deb', - }, - package: { - name: 'docker-image', - dependencies: { - 'apt/libapt-pkg5.0': { - version: '1.6.3ubuntu0.1', - dependencies: { - 'bzip2/libbz2-1.0': { - version: '1.0.6-8.1', - }, - }, - }, - 'bzip2/libbz2-1.0': { - version: '1.0.6-8.1', - }, - }, - }, - }, - t, - ); - - const vulns = require('./fixtures/docker/find-result.json'); - server.setNextResponse(vulns); - +test('test cli with multiple params: good and bad', async (t) => { + t.plan(6); try { - await cli.test('foo:latest', { - docker: true, - org: 'explicit-org', - }); - t.fail('should have found vuln'); + await cli.test('/', 'semver', { registry: 'npm', org: 'EFF', json: true }); + t.fail('expect to err'); } catch (err) { - const msg = err.message; - t.match( - msg, - 'Tested 2 dependencies for known vulnerabilities, found 1 vulnerability', - ); - t.match(msg, 'From: bzip2/libbz2-1.0@1.0.6-8.1'); - t.match( - msg, - 'From: apt/libapt-pkg5.0@1.6.3ubuntu0.1 > bzip2/libbz2-1.0@1.0.6-8.1', - ); - t.false( - msg.includes('vulnerable paths'), - 'docker should not includes number of vulnerable paths', - ); + const errObj = JSON.parse(err.message); + t.ok(errObj.length === 2, 'expecting two results'); + t.notOk(errObj[0].ok, 'first object shouldnt be ok'); + t.ok(errObj[1].ok, 'second object should be ok'); + t.ok(errObj[0].path.length > 0, 'should have path'); + t.ok(errObj[1].path.length > 0, 'should have path'); + t.pass('info on both objects'); } + t.end(); }); -test('`test foo:latest --docker --file=Dockerfile`', async (t) => { - const spyPlugin = stubDockerPluginResponse( - { - plugin: { - packageManager: 'deb', - }, - package: { - docker: { - baseImage: 'ubuntu:14.04', - }, - }, - }, - t, - ); - - await cli.test('foo:latest', { - docker: true, - org: 'explicit-org', - file: 'Dockerfile', - }); - - const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); - t.equal( - req.headers['x-snyk-cli-version'], - versionNumber, - 'sends version number', - ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'deb'); - t.equal(req.body.docker.baseImage, 'ubuntu:14.04', 'posts docker baseImage'); - t.same( - spyPlugin.getCall(0).args, - [ - 'foo:latest', - 'Dockerfile', - { - args: null, - file: 'Dockerfile', - docker: true, - org: 'explicit-org', - projectName: null, - packageManager: null, - path: 'foo:latest', - showVulnPaths: 'some', - }, - ], - 'calls docker plugin with expected arguments', - ); +test('userMessage correctly bubbles with npm', async (t) => { + chdirWorkspaces(); + try { + await cli.test('npm-package', { org: 'missing-org' }); + t.fail('expect to err'); + } catch (err) { + t.equal(err.userMessage, 'cli error message', 'got correct err message'); + } + t.end(); }); -test('`test foo:latest --docker --file=Dockerfile remediation advice`', async (t) => { - stubDockerPluginResponse('./fixtures/docker/plugin-multiple-deps', t); - const vulns = require('./fixtures/docker/find-result-remediation.json'); - server.setNextResponse(vulns); - +test('userMessage correctly bubbles with everything other than npm', async (t) => { + chdirWorkspaces(); try { - await cli.test('foo:latest', { - docker: true, - org: 'explicit-org', - file: 'Dockerfile', - }); - t.fail('should have found vuln'); + await cli.test('ruby-app', { org: 'missing-org' }); + t.fail('expect to err'); } catch (err) { - const msg = err.message; - t.match(msg, 'Base Image'); - t.match(msg, 'Recommendations for base image upgrade'); + t.equal(err.userMessage, 'cli error message', 'got correct err message'); } + t.end(); }); -test('`test foo:latest --docker` doesnt collect policy from cwd', async (t) => { - chdirWorkspaces('npm-package-policy'); - const spyPlugin = stubDockerPluginResponse( - { - plugin: { - packageManager: 'deb', - }, - package: {}, - }, - t, - ); +/** + * Remote package `test` + */ - await cli.test('foo:latest', { - docker: true, - org: 'explicit-org', - }); +test('`test semver` sends remote NPM request:', async (t) => { + // We care about the request here, not the response + const output = await cli.test('semver', { registry: 'npm', org: 'EFF' }); const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); + t.equal(req.method, 'GET', 'makes GET request'); t.equal( req.headers['x-snyk-cli-version'], versionNumber, 'sends version number', ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'deb'); - t.same( - spyPlugin.getCall(0).args, - [ - 'foo:latest', - null, - { - args: null, - file: null, - docker: true, - org: 'explicit-org', - projectName: null, - packageManager: null, - path: 'foo:latest', - showVulnPaths: 'some', - }, - ], - 'calls docker plugin with expected arguments', - ); - const policyString = req.body.policy; - t.false(policyString, 'policy not sent'); -}); - -test('`test foo:latest --docker` supports custom policy', async (t) => { - chdirWorkspaces(); - const spyPlugin = stubDockerPluginResponse( - { - plugin: { - packageManager: 'deb', - }, - package: {}, - }, - t, - ); - - await cli.test('foo:latest', { - docker: true, - org: 'explicit-org', - 'policy-path': 'npm-package-policy/custom-location', - }); - const req = server.popRequest(); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'deb'); - t.same( - spyPlugin.getCall(0).args, - [ - 'foo:latest', - null, - { - args: null, - file: null, - docker: true, - org: 'explicit-org', - projectName: null, - packageManager: null, - path: 'foo:latest', - showVulnPaths: 'some', - 'policy-path': 'npm-package-policy/custom-location', - }, - ], - 'calls docker plugin with expected arguments', - ); - - const expected = fs.readFileSync( - path.join('npm-package-policy/custom-location', '.snyk'), - 'utf8', - ); - const policyString = req.body.policy; - t.equal(policyString, expected, 'sends correct policy'); + 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.notMatch(output, 'Remediation', 'shows no remediation advice'); + t.notMatch(output, 'snyk wizard', 'does not suggest `snyk wizard`'); }); -test('`test foo:latest --docker with binaries`', async (t) => { - const spyPlugin = stubDockerPluginResponse( - { - plugin: { - packageManager: 'deb', - }, - package: { - docker: { - binaries: [{ name: 'node', version: '5.10.1' }], - }, - }, - }, - t, - ); - - await cli.test('foo:latest', { - docker: true, - org: 'explicit-org', - }); +test('`test sinatra --registry=rubygems` sends remote Rubygems request:', async (t) => { + await cli.test('sinatra', { registry: 'rubygems', org: 'ACME' }); const req = server.popRequest(); - t.equal(req.method, 'POST', 'makes POST request'); + t.equal(req.method, 'GET', 'makes GET request'); t.equal( req.headers['x-snyk-cli-version'], versionNumber, 'sends version number', ); - t.match(req.url, '/test-dep-graph', 'posts to correct url'); - t.equal(req.body.depGraph.pkgManager.name, 'deb'); - t.same( - req.body.docker.binaries, - [{ name: 'node', version: '5.10.1' }], - 'posts docker binaries', - ); - t.same( - spyPlugin.getCall(0).args, - [ - 'foo:latest', - null, - { - args: null, - file: null, - docker: true, - org: 'explicit-org', - projectName: null, - packageManager: null, - path: 'foo:latest', - showVulnPaths: 'some', - }, - ], - 'calls docker plugin with expected arguments', - ); + t.match(req.url, '/vuln/rubygems/sinatra', 'gets from correct url'); + t.equal(req.query.org, 'ACME', 'org sent as a query in request'); }); -test('`test foo:latest --docker with binaries vulnerabilities`', async (t) => { - stubDockerPluginResponse( - { - plugin: { - packageManager: 'deb', - }, - package: { - name: 'docker-image', - dependencies: { - 'apt/libapt-pkg5.0': { - version: '1.6.3ubuntu0.1', - dependencies: { - 'bzip2/libbz2-1.0': { - version: '1.0.6-8.1', - }, - }, - }, - 'bzip2/libbz2-1.0': { - version: '1.0.6-8.1', - }, - 'bzr/libbz2-1.0': { - version: '1.0.6-8.1', - }, - }, - docker: { - binaries: { - Analysis: [{ name: 'node', version: '5.10.1' }], - }, - }, - }, - }, - t, - ); - - const vulns = require('./fixtures/docker/find-result-binaries.json'); - server.setNextResponse(vulns); +/** + * Local source `test` + */ +test('`test /` test for non-existent with path specified', async (t) => { + chdirWorkspaces(); try { - await cli.test('foo:latest', { - docker: true, - org: 'explicit-org', - }); - t.fail('should have found vuln'); + await cli.test('/'); + t.fail('should have failed'); } catch (err) { - const msg = err.message; + t.pass('throws err'); t.match( - msg, - 'Tested 3 dependencies for known vulnerabilities, found 3 vulnerabilities', + err.message, + 'Could not detect supported target files in /.' + + '\nPlease see our documentation for supported' + + ' languages and target files: ' + + 'https://support.snyk.io/hc/en-us/articles/360000911957-Language-support' + + ' and make sure you' + + ' are in the right directory.', ); - t.match(msg, 'From: bzip2/libbz2-1.0@1.0.6-8.1'); + } +}); + +test('`test empty --file=readme.md`', async (t) => { + chdirWorkspaces(); + try { + await cli.test('empty', { file: 'readme.md' }); + t.fail('should have failed'); + } catch (err) { + t.pass('throws err'); t.match( - msg, - 'From: apt/libapt-pkg5.0@1.6.3ubuntu0.1 > bzip2/libbz2-1.0@1.0.6-8.1', - ); - t.match(msg, 'Info: http://localhost:12345/vuln/SNYK-UPSTREAM-NODE-72359'); - t.false( - msg.includes('vulnerable paths'), - 'docker should not includes number of vulnerable paths', + err.message, + 'Could not detect package manager for file: readme.md', + 'shows err message for when file specified exists, but not supported', ); - t.match(msg, 'Detected 2 vulnerabilities for node@5.10.1'); - t.match(msg, 'High severity vulnerability found in node'); - t.match(msg, 'Fixed in: 5.13.1'); - t.match(msg, 'Fixed in: 5.15.1'); } }); @@ -3166,56 +274,6 @@ test('`test npm-package-with-git-url ` handles git url with patch policy', async } }); -test('`test sbt-simple-struts`', async (t) => { - chdirWorkspaces(); - - const plugin = { - async inspect() { - return { - plugin: { name: 'sbt' }, - package: require('./workspaces/sbt-simple-struts/dep-tree.json'), - }; - }, - }; - const loadPlugin = sinon.stub(plugins, 'loadPlugin'); - loadPlugin.returns(plugin); - - t.teardown(() => { - loadPlugin.restore(); - }); - - server.setNextResponse( - require('./workspaces/sbt-simple-struts/test-graph-result.json'), - ); - - try { - await cli.test('sbt-simple-struts', { json: true }); - - t.fail('should have thrown'); - } catch (err) { - const res = JSON.parse(err.message); - - const expected = require('./workspaces/sbt-simple-struts/legacy-res-json.json'); - - t.deepEqual( - _.omit(res, ['vulnerabilities', 'packageManager']), - _.omit(expected, ['vulnerabilities', 'packageManager']), - 'metadata is ok', - ); - // NOTE: decided to keep this discrepancy - t.is( - res.packageManager, - 'sbt', - 'pacakgeManager is sbt, altough it was mavn with the legacy api', - ); - t.deepEqual( - _.sortBy(res.vulnerabilities, 'id'), - _.sortBy(expected.vulnerabilities, 'id'), - 'vulns are the same', - ); - } -}); - test('`test --insecure`', async (tt) => { tt.plan(2); chdirWorkspaces('npm-package'); @@ -3271,24 +329,6 @@ test('`test --insecure`', async (tt) => { }); }); -test("snyk help doesn't crash", async (t) => { - t.match(await cli.help(), /Usage/); -}); - -/** - * We can't expect all test environments to have Maven installed - * So, hijack the system exec call and return the expected output - */ -function stubExec(t, execOutputFile) { - const stub = sinon.stub(subProcess, 'execute').callsFake(() => { - const stdout = fs.readFileSync(path.join(execOutputFile), 'utf8'); - return Promise.resolve(stdout); - }); - t.teardown(() => { - stub.restore(); - }); -} - test('error 401 handling', async (t) => { chdirWorkspaces(); diff --git a/test/acceptance/cli-test.composer.test.ts b/test/acceptance/cli-test.composer.test.ts new file mode 100644 index 0000000000..fd5e7dd640 --- /dev/null +++ b/test/acceptance/cli-test.composer.test.ts @@ -0,0 +1,344 @@ +import * as tap from 'tap'; +import * as sinon from 'sinon'; +import * as cli from '../../src/cli/commands'; +import { fakeServer } from './fake-server'; +import * as version from '../../src/lib/version'; + +const { test, only } = tap; +(tap as any).runOnly = false; // <- for debug. set to true, and replace a test to only(..) + +const port = (process.env.PORT = process.env.SNYK_PORT = '12345'); +process.env.SNYK_API = 'http://localhost:' + port + '/api/v1'; +process.env.SNYK_HOST = 'http://localhost:' + port; +process.env.LOG_LEVEL = '0'; +const apiKey = '123456789'; +let oldkey; +let oldendpoint; +let versionNumber; +const server = fakeServer(process.env.SNYK_API, apiKey); +const before = tap.runOnly ? only : test; +const after = tap.runOnly ? only : test; + +// Should be after `process.env` setup. +import * as plugins from '../../src/lib/plugins'; + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('setup', async (t) => { + versionNumber = await version(); + + t.plan(3); + let key = await cli.config('get', 'api'); + oldkey = key; + t.pass('existing user config captured'); + + key = await cli.config('get', 'endpoint'); + oldendpoint = key; + t.pass('existing user endpoint captured'); + + await new Promise((resolve) => { + server.listen(port, resolve); + }); + t.pass('started demo server'); + t.end(); +}); + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('prime config', async (t) => { + await cli.config('set', 'api=' + apiKey); + t.pass('api token set'); + await cli.config('unset', 'endpoint'); + t.pass('endpoint removed'); + t.end(); +}); + +test('`test composer-app --file=composer.lock`', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { name: 'testplugin', runtime: 'testruntime' }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('composer').returns(plugin); + + await cli.test('composer-app', { + file: 'composer.lock', + }); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'composer'); + t.same( + spyPlugin.getCall(0).args, + [ + 'composer-app', + 'composer.lock', + { + args: null, + file: 'composer.lock', + org: null, + projectName: null, + packageManager: 'composer', + path: 'composer-app', + showVulnPaths: 'some', + }, + ], + 'calls composer plugin', + ); +}); + +test('`test composer-app` auto-detects composer.lock', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { name: 'testplugin', runtime: 'testruntime' }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('composer').returns(plugin); + + await cli.test('composer-app'); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'composer'); + t.same( + spyPlugin.getCall(0).args, + [ + 'composer-app', + 'composer.lock', + { + args: null, + file: 'composer.lock', + org: null, + projectName: null, + packageManager: 'composer', + path: 'composer-app', + showVulnPaths: 'some', + }, + ], + 'calls composer plugin', + ); +}); + +test('`test composer-app --file=composer.lock --dev`', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { name: 'testplugin', runtime: 'testruntime' }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('composer').returns(plugin); + + await cli.test('composer-app', { + file: 'composer.lock', + dev: true, + }); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'composer'); + t.same( + spyPlugin.getCall(0).args, + [ + 'composer-app', + 'composer.lock', + { + args: null, + dev: true, + file: 'composer.lock', + org: null, + projectName: null, + packageManager: 'composer', + path: 'composer-app', + showVulnPaths: 'some', + }, + ], + 'calls composer plugin', + ); +}); + +test('`test composer-app golang-app nuget-app` auto-detects all three projects', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { name: 'testplugin', runtime: 'testruntime' }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('composer').returns(plugin); + loadPlugin.withArgs('golangdep').returns(plugin); + loadPlugin.withArgs('nuget').returns(plugin); + + await cli.test('composer-app', 'golang-app', 'nuget-app', { + org: 'test-org', + }); + // assert three API calls made, each with a different url + const reqs = Array.from({ length: 3 }).map(() => server.popRequest()); + + t.same( + reqs.map((r) => r.method), + ['POST', 'POST', 'POST'], + 'all post requests', + ); + + t.same( + reqs.map((r) => r.headers['x-snyk-cli-version']), + [versionNumber, versionNumber, versionNumber], + 'all send version number', + ); + + t.same( + reqs.map((r) => r.url), + [ + '/api/v1/test-dep-graph?org=test-org', + '/api/v1/test-dep-graph?org=test-org', + '/api/v1/test-dep-graph?org=test-org', + ], + 'all urls are present', + ); + + t.same( + reqs.map((r) => r.body.depGraph.pkgManager.name).sort(), + ['composer', 'golangdep', 'nuget'], + 'all urls are present', + ); + + // assert three spyPlugin calls, each with a different app + const calls = spyPlugin.getCalls().sort((call1: any, call2: any) => { + return call1.args[0] < call2.args[1] + ? -1 + : call1.args[0] > call2.args[0] + ? 1 + : 0; + }); + t.same( + calls[0].args, + [ + 'composer-app', + 'composer.lock', + { + args: null, + org: 'test-org', + file: 'composer.lock', + projectName: null, + packageManager: 'composer', + path: 'composer-app', + showVulnPaths: 'some', + }, + ], + 'calls composer plugin', + ); + t.same( + calls[1].args, + [ + 'golang-app', + 'Gopkg.lock', + { + args: null, + org: 'test-org', + file: 'Gopkg.lock', + projectName: null, + packageManager: 'golangdep', + path: 'golang-app', + showVulnPaths: 'some', + }, + ], + 'calls golangdep plugin', + ); + t.same( + calls[2].args, + [ + 'nuget-app', + 'project.assets.json', + { + args: null, + org: 'test-org', + file: 'project.assets.json', + projectName: null, + packageManager: 'nuget', + path: 'nuget-app', + showVulnPaths: 'some', + }, + ], + 'calls nuget plugin', + ); +}); + +// @later: try and remove this config stuff +// Was copied straight from ../src/cli-server.js +after('teardown', async (t) => { + t.plan(4); + + delete process.env.SNYK_API; + delete process.env.SNYK_HOST; + delete process.env.SNYK_PORT; + t.notOk(process.env.SNYK_PORT, 'fake env values cleared'); + + await new Promise((resolve) => { + server.close(resolve); + }); + t.pass('server shutdown'); + let key = 'set'; + let value = 'api=' + oldkey; + if (!oldkey) { + key = 'unset'; + value = 'api'; + } + await cli.config(key, value); + t.pass('user config restored'); + if (oldendpoint) { + await cli.config('endpoint', oldendpoint); + t.pass('user endpoint restored'); + t.end(); + } else { + t.pass('no endpoint'); + t.end(); + } +}); + +function chdirWorkspaces(subdir = '') { + process.chdir(__dirname + '/workspaces' + (subdir ? '/' + subdir : '')); +} diff --git a/test/acceptance/cli-test.docker.test.ts b/test/acceptance/cli-test.docker.test.ts new file mode 100644 index 0000000000..30e365da48 --- /dev/null +++ b/test/acceptance/cli-test.docker.test.ts @@ -0,0 +1,491 @@ +import * as tap from 'tap'; +import * as sinon from 'sinon'; +import * as cli from '../../src/cli/commands'; +import { fakeServer } from './fake-server'; +import * as version from '../../src/lib/version'; +import * as fs from 'fs'; +import * as path from 'path'; + +const { test, only } = tap; +(tap as any).runOnly = false; // <- for debug. set to true, and replace a test to only(..) + +const port = (process.env.PORT = process.env.SNYK_PORT = '12345'); +process.env.SNYK_API = 'http://localhost:' + port + '/api/v1'; +process.env.SNYK_HOST = 'http://localhost:' + port; +process.env.LOG_LEVEL = '0'; +const apiKey = '123456789'; +let oldkey; +let oldendpoint; +let versionNumber; +const server = fakeServer(process.env.SNYK_API, apiKey); +const before = tap.runOnly ? only : test; +const after = tap.runOnly ? only : test; + +// Should be after `process.env` setup. +import * as plugins from '../../src/lib/plugins'; +import { legacyPlugin as pluginApi } from '@snyk/cli-interface'; + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('setup', async (t) => { + versionNumber = await version(); + + t.plan(3); + let key = await cli.config('get', 'api'); + oldkey = key; + t.pass('existing user config captured'); + + key = await cli.config('get', 'endpoint'); + oldendpoint = key; + t.pass('existing user endpoint captured'); + + await new Promise((resolve) => { + server.listen(port, resolve); + }); + t.pass('started demo server'); + t.end(); +}); + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('prime config', async (t) => { + await cli.config('set', 'api=' + apiKey); + t.pass('api token set'); + await cli.config('unset', 'endpoint'); + t.pass('endpoint removed'); + t.end(); +}); + +test('`test foo:latest --docker`', async (t) => { + const spyPlugin = stubDockerPluginResponse( + { + plugin: { + packageManager: 'deb', + }, + package: {}, + }, + t, + ); + + await cli.test('foo:latest', { + docker: true, + org: 'explicit-org', + }); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'deb'); + t.same( + spyPlugin.getCall(0).args, + [ + 'foo:latest', + null, + { + args: null, + file: null, + docker: true, + org: 'explicit-org', + projectName: null, + packageManager: null, + path: 'foo:latest', + showVulnPaths: 'some', + }, + ], + 'calls docker plugin with expected arguments', + ); +}); + +test('`test foo:latest --docker vulnerable paths`', async (t) => { + stubDockerPluginResponse( + { + plugin: { + packageManager: 'deb', + }, + package: { + name: 'docker-image', + dependencies: { + 'apt/libapt-pkg5.0': { + version: '1.6.3ubuntu0.1', + dependencies: { + 'bzip2/libbz2-1.0': { + version: '1.0.6-8.1', + }, + }, + }, + 'bzip2/libbz2-1.0': { + version: '1.0.6-8.1', + }, + }, + }, + }, + t, + ); + + const vulns = require('./fixtures/docker/find-result.json'); + server.setNextResponse(vulns); + + try { + await cli.test('foo:latest', { + docker: true, + org: 'explicit-org', + }); + t.fail('should have found vuln'); + } catch (err) { + const msg = err.message; + t.match( + msg, + 'Tested 2 dependencies for known vulnerabilities, found 1 vulnerability', + ); + t.match(msg, 'From: bzip2/libbz2-1.0@1.0.6-8.1'); + t.match( + msg, + 'From: apt/libapt-pkg5.0@1.6.3ubuntu0.1 > bzip2/libbz2-1.0@1.0.6-8.1', + ); + t.false( + msg.includes('vulnerable paths'), + 'docker should not includes number of vulnerable paths', + ); + } +}); + +test('`test foo:latest --docker --file=Dockerfile`', async (t) => { + const spyPlugin = stubDockerPluginResponse( + { + plugin: { + packageManager: 'deb', + }, + package: { + docker: { + baseImage: 'ubuntu:14.04', + }, + }, + }, + t, + ); + + await cli.test('foo:latest', { + docker: true, + org: 'explicit-org', + file: 'Dockerfile', + }); + + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'deb'); + t.equal(req.body.docker.baseImage, 'ubuntu:14.04', 'posts docker baseImage'); + t.same( + spyPlugin.getCall(0).args, + [ + 'foo:latest', + 'Dockerfile', + { + args: null, + file: 'Dockerfile', + docker: true, + org: 'explicit-org', + projectName: null, + packageManager: null, + path: 'foo:latest', + showVulnPaths: 'some', + }, + ], + 'calls docker plugin with expected arguments', + ); +}); + +test('`test foo:latest --docker --file=Dockerfile remediation advice`', async (t) => { + stubDockerPluginResponse('./fixtures/docker/plugin-multiple-deps', t); + const vulns = require('./fixtures/docker/find-result-remediation.json'); + server.setNextResponse(vulns); + + try { + await cli.test('foo:latest', { + docker: true, + org: 'explicit-org', + file: 'Dockerfile', + }); + t.fail('should have found vuln'); + } catch (err) { + const msg = err.message; + t.match(msg, 'Base Image'); + t.match(msg, 'Recommendations for base image upgrade'); + } +}); + +test('`test foo:latest --docker` doesnt collect policy from cwd', async (t) => { + chdirWorkspaces('npm-package-policy'); + const spyPlugin = stubDockerPluginResponse( + { + plugin: { + packageManager: 'deb', + }, + package: {}, + }, + t, + ); + + await cli.test('foo:latest', { + docker: true, + org: 'explicit-org', + }); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'deb'); + t.same( + spyPlugin.getCall(0).args, + [ + 'foo:latest', + null, + { + args: null, + file: null, + docker: true, + org: 'explicit-org', + projectName: null, + packageManager: null, + path: 'foo:latest', + showVulnPaths: 'some', + }, + ], + 'calls docker plugin with expected arguments', + ); + const policyString = req.body.policy; + t.false(policyString, 'policy not sent'); +}); + +test('`test foo:latest --docker` supports custom policy', async (t) => { + chdirWorkspaces(); + const spyPlugin = stubDockerPluginResponse( + { + plugin: { + packageManager: 'deb', + }, + package: {}, + }, + t, + ); + + await cli.test('foo:latest', { + docker: true, + org: 'explicit-org', + 'policy-path': 'npm-package-policy/custom-location', + }); + const req = server.popRequest(); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'deb'); + t.same( + spyPlugin.getCall(0).args, + [ + 'foo:latest', + null, + { + args: null, + file: null, + docker: true, + org: 'explicit-org', + projectName: null, + packageManager: null, + path: 'foo:latest', + showVulnPaths: 'some', + 'policy-path': 'npm-package-policy/custom-location', + }, + ], + 'calls docker plugin with expected arguments', + ); + + const expected = fs.readFileSync( + path.join('npm-package-policy/custom-location', '.snyk'), + 'utf8', + ); + const policyString = req.body.policy; + t.equal(policyString, expected, 'sends correct policy'); +}); + +test('`test foo:latest --docker with binaries`', async (t) => { + const spyPlugin = stubDockerPluginResponse( + { + plugin: { + packageManager: 'deb', + }, + package: { + docker: { + binaries: [{ name: 'node', version: '5.10.1' }], + }, + }, + }, + t, + ); + + await cli.test('foo:latest', { + docker: true, + org: 'explicit-org', + }); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'deb'); + t.same( + req.body.docker.binaries, + [{ name: 'node', version: '5.10.1' }], + 'posts docker binaries', + ); + t.same( + spyPlugin.getCall(0).args, + [ + 'foo:latest', + null, + { + args: null, + file: null, + docker: true, + org: 'explicit-org', + projectName: null, + packageManager: null, + path: 'foo:latest', + showVulnPaths: 'some', + }, + ], + 'calls docker plugin with expected arguments', + ); +}); + +test('`test foo:latest --docker with binaries vulnerabilities`', async (t) => { + stubDockerPluginResponse( + { + plugin: { + packageManager: 'deb', + }, + package: { + name: 'docker-image', + dependencies: { + 'apt/libapt-pkg5.0': { + version: '1.6.3ubuntu0.1', + dependencies: { + 'bzip2/libbz2-1.0': { + version: '1.0.6-8.1', + }, + }, + }, + 'bzip2/libbz2-1.0': { + version: '1.0.6-8.1', + }, + 'bzr/libbz2-1.0': { + version: '1.0.6-8.1', + }, + }, + docker: { + binaries: { + Analysis: [{ name: 'node', version: '5.10.1' }], + }, + }, + }, + }, + t, + ); + + const vulns = require('./fixtures/docker/find-result-binaries.json'); + server.setNextResponse(vulns); + + try { + await cli.test('foo:latest', { + docker: true, + org: 'explicit-org', + }); + t.fail('should have found vuln'); + } catch (err) { + const msg = err.message; + t.match( + msg, + 'Tested 3 dependencies for known vulnerabilities, found 3 vulnerabilities', + ); + t.match(msg, 'From: bzip2/libbz2-1.0@1.0.6-8.1'); + t.match( + msg, + 'From: apt/libapt-pkg5.0@1.6.3ubuntu0.1 > bzip2/libbz2-1.0@1.0.6-8.1', + ); + t.match(msg, 'Info: http://localhost:12345/vuln/SNYK-UPSTREAM-NODE-72359'); + t.false( + msg.includes('vulnerable paths'), + 'docker should not includes number of vulnerable paths', + ); + t.match(msg, 'Detected 2 vulnerabilities for node@5.10.1'); + t.match(msg, 'High severity vulnerability found in node'); + t.match(msg, 'Fixed in: 5.13.1'); + t.match(msg, 'Fixed in: 5.15.1'); + } +}); + +// @later: try and remove this config stuff +// Was copied straight from ../src/cli-server.js +after('teardown', async (t) => { + t.plan(4); + + delete process.env.SNYK_API; + delete process.env.SNYK_HOST; + delete process.env.SNYK_PORT; + t.notOk(process.env.SNYK_PORT, 'fake env values cleared'); + + await new Promise((resolve) => { + server.close(resolve); + }); + t.pass('server shutdown'); + let key = 'set'; + let value = 'api=' + oldkey; + if (!oldkey) { + key = 'unset'; + value = 'api'; + } + await cli.config(key, value); + t.pass('user config restored'); + if (oldendpoint) { + await cli.config('endpoint', oldendpoint); + t.pass('user endpoint restored'); + t.end(); + } else { + t.pass('no endpoint'); + t.end(); + } +}); + +function chdirWorkspaces(subdir = '') { + process.chdir(__dirname + '/workspaces' + (subdir ? '/' + subdir : '')); +} + +// fixture can be fixture path or object +function stubDockerPluginResponse(fixture: string | object, t) { + const plugin = { + async inspect() { + return typeof fixture === 'object' ? fixture : require(fixture); + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + loadPlugin + .withArgs(sinon.match.any, sinon.match({ docker: true })) + .returns(plugin); + t.teardown(loadPlugin.restore); + + return spyPlugin; +} diff --git a/test/acceptance/cli-test.go.test.ts b/test/acceptance/cli-test.go.test.ts new file mode 100644 index 0000000000..d8acb167a8 --- /dev/null +++ b/test/acceptance/cli-test.go.test.ts @@ -0,0 +1,391 @@ +import * as tap from 'tap'; +import * as sinon from 'sinon'; +import * as cli from '../../src/cli/commands'; +import { fakeServer } from './fake-server'; +import * as version from '../../src/lib/version'; + +const { test, only } = tap; +(tap as any).runOnly = false; // <- for debug. set to true, and replace a test to only(..) + +const port = (process.env.PORT = process.env.SNYK_PORT = '12345'); +process.env.SNYK_API = 'http://localhost:' + port + '/api/v1'; +process.env.SNYK_HOST = 'http://localhost:' + port; +process.env.LOG_LEVEL = '0'; +const apiKey = '123456789'; +let oldkey; +let oldendpoint; +let versionNumber; +const server = fakeServer(process.env.SNYK_API, apiKey); +const before = tap.runOnly ? only : test; +const after = tap.runOnly ? only : test; + +// Should be after `process.env` setup. +import * as plugins from '../../src/lib/plugins'; + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('setup', async (t) => { + versionNumber = await version(); + + t.plan(3); + let key = await cli.config('get', 'api'); + oldkey = key; + t.pass('existing user config captured'); + + key = await cli.config('get', 'endpoint'); + oldendpoint = key; + t.pass('existing user endpoint captured'); + + await new Promise((resolve) => { + server.listen(port, resolve); + }); + t.pass('started demo server'); + t.end(); +}); + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('prime config', async (t) => { + await cli.config('set', 'api=' + apiKey); + t.pass('api token set'); + await cli.config('unset', 'endpoint'); + t.pass('endpoint removed'); + t.end(); +}); + +test('`test golang-gomodules --file=go.mod`', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { + name: 'testplugin', + runtime: 'testruntime', + targetFile: 'go.mod', + }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('gomodules').returns(plugin); + + await cli.test('golang-gomodules', { + file: 'go.mod', + }); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'gomodules'); + t.equal(req.body.targetFile, 'go.mod', 'specifies target'); + t.same( + spyPlugin.getCall(0).args, + [ + 'golang-gomodules', + 'go.mod', + { + args: null, + file: 'go.mod', + org: null, + projectName: null, + packageManager: 'gomodules', + path: 'golang-gomodules', + showVulnPaths: 'some', + }, + ], + 'calls golang plugin', + ); +}); + +test('`test golang-app` auto-detects golang-gomodules', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { + name: 'testplugin', + runtime: 'testruntime', + targetFile: 'go.mod', + }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('gomodules').returns(plugin); + + await cli.test('golang-gomodules'); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'gomodules'); + t.equal(req.body.targetFile, 'go.mod', 'specifies target'); + t.same( + spyPlugin.getCall(0).args, + [ + 'golang-gomodules', + 'go.mod', + { + args: null, + file: 'go.mod', + org: null, + projectName: null, + packageManager: 'gomodules', + path: 'golang-gomodules', + showVulnPaths: 'some', + }, + ], + 'calls golang-gomodules plugin', + ); +}); + +test('`test golang-app --file=Gopkg.lock`', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { + name: 'testplugin', + runtime: 'testruntime', + targetFile: 'Gopkg.lock', + }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('golangdep').returns(plugin); + + await cli.test('golang-app', { + file: 'Gopkg.lock', + }); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'golangdep'); + t.equal(req.body.targetFile, 'Gopkg.lock', 'specifies target'); + t.same( + spyPlugin.getCall(0).args, + [ + 'golang-app', + 'Gopkg.lock', + { + args: null, + file: 'Gopkg.lock', + org: null, + projectName: null, + packageManager: 'golangdep', + path: 'golang-app', + showVulnPaths: 'some', + }, + ], + 'calls golang plugin', + ); +}); + +test('`test golang-app --file=vendor/vendor.json`', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { + name: 'testplugin', + runtime: 'testruntime', + targetFile: 'vendor/vendor.json', + }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('govendor').returns(plugin); + + await cli.test('golang-app', { + file: 'vendor/vendor.json', + }); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'govendor'); + t.equal(req.body.targetFile, 'vendor/vendor.json', 'specifies target'); + t.same( + spyPlugin.getCall(0).args, + [ + 'golang-app', + 'vendor/vendor.json', + { + args: null, + file: 'vendor/vendor.json', + org: null, + projectName: null, + packageManager: 'govendor', + path: 'golang-app', + showVulnPaths: 'some', + }, + ], + 'calls golang plugin', + ); +}); + +test('`test golang-app` auto-detects golang/dep', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { + name: 'testplugin', + runtime: 'testruntime', + targetFile: 'Gopkg.lock', + }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('golangdep').returns(plugin); + + await cli.test('golang-app'); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'golangdep'); + t.equal(req.body.targetFile, 'Gopkg.lock', 'specifies target'); + t.same( + spyPlugin.getCall(0).args, + [ + 'golang-app', + 'Gopkg.lock', + { + args: null, + file: 'Gopkg.lock', + org: null, + projectName: null, + packageManager: 'golangdep', + path: 'golang-app', + showVulnPaths: 'some', + }, + ], + 'calls golang plugin', + ); +}); + +test('`test golang-app-govendor` auto-detects govendor', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { name: 'testplugin', runtime: 'testruntime' }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('govendor').returns(plugin); + + await cli.test('golang-app-govendor'); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'govendor'); + t.same( + spyPlugin.getCall(0).args, + [ + 'golang-app-govendor', + 'vendor/vendor.json', + { + args: null, + file: 'vendor/vendor.json', + org: null, + projectName: null, + packageManager: 'govendor', + path: 'golang-app-govendor', + showVulnPaths: 'some', + }, + ], + 'calls golang plugin', + ); +}); + +// @later: try and remove this config stuff +// Was copied straight from ../src/cli-server.js +after('teardown', async (t) => { + t.plan(4); + + delete process.env.SNYK_API; + delete process.env.SNYK_HOST; + delete process.env.SNYK_PORT; + t.notOk(process.env.SNYK_PORT, 'fake env values cleared'); + + await new Promise((resolve) => { + server.close(resolve); + }); + t.pass('server shutdown'); + let key = 'set'; + let value = 'api=' + oldkey; + if (!oldkey) { + key = 'unset'; + value = 'api'; + } + await cli.config(key, value); + t.pass('user config restored'); + if (oldendpoint) { + await cli.config('endpoint', oldendpoint); + t.pass('user endpoint restored'); + t.end(); + } else { + t.pass('no endpoint'); + t.end(); + } +}); + +function chdirWorkspaces(subdir = '') { + process.chdir(__dirname + '/workspaces' + (subdir ? '/' + subdir : '')); +} diff --git a/test/acceptance/cli-test.gradle.test.ts b/test/acceptance/cli-test.gradle.test.ts new file mode 100644 index 0000000000..52c66c4059 --- /dev/null +++ b/test/acceptance/cli-test.gradle.test.ts @@ -0,0 +1,256 @@ +import * as tap from 'tap'; +import * as sinon from 'sinon'; +import * as cli from '../../src/cli/commands'; +import { fakeServer } from './fake-server'; +import * as version from '../../src/lib/version'; + +const { test, only } = tap; +(tap as any).runOnly = false; // <- for debug. set to true, and replace a test to only(..) + +const port = (process.env.PORT = process.env.SNYK_PORT = '12345'); +process.env.SNYK_API = 'http://localhost:' + port + '/api/v1'; +process.env.SNYK_HOST = 'http://localhost:' + port; +process.env.LOG_LEVEL = '0'; +const apiKey = '123456789'; +let oldkey; +let oldendpoint; +let versionNumber; +const server = fakeServer(process.env.SNYK_API, apiKey); +const before = tap.runOnly ? only : test; +const after = tap.runOnly ? only : test; + +// Should be after `process.env` setup. +import * as plugins from '../../src/lib/plugins'; +import { legacyPlugin as pluginApi } from '@snyk/cli-interface'; + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('setup', async (t) => { + versionNumber = await version(); + + t.plan(3); + let key = await cli.config('get', 'api'); + oldkey = key; + t.pass('existing user config captured'); + + key = await cli.config('get', 'endpoint'); + oldendpoint = key; + t.pass('existing user endpoint captured'); + + await new Promise((resolve) => { + server.listen(port, resolve); + }); + t.pass('started demo server'); + t.end(); +}); + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('prime config', async (t) => { + await cli.config('set', 'api=' + apiKey); + t.pass('api token set'); + await cli.config('unset', 'endpoint'); + t.pass('endpoint removed'); + t.end(); +}); + +test('`test gradle-kotlin-dsl-app` returns correct meta', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { name: 'testplugin', runtime: 'testruntime' }, + }; + }, + }; + sinon.spy(plugin, 'inspect'); + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('gradle').returns(plugin); + + const res = await cli.test('gradle-kotlin-dsl-app'); + 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+gradle/, 'package manager displayed'); + t.match(meta[2], /Target file:\s+build.gradle.kts/, 'target file displayed'); + t.match(meta[3], /Open source:\s+no/, 'open source displayed'); + t.match(meta[4], /Project path:\s+gradle-kotlin-dsl-app/, 'path displayed'); + t.notMatch( + meta[5], + /Local Snyk policy:\s+found/, + 'local policy not displayed', + ); +}); + +test('`test gradle-app` returns correct meta', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { name: 'testplugin', runtime: 'testruntime' }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('gradle').returns(plugin); + + const res = await cli.test('gradle-app'); + const meta = res.slice(res.indexOf('Organization:')).split('\n'); + + t.false( + ((spyPlugin.args[0] as any)[2] as any).allSubProjects, + '`allSubProjects` option is not sent', + ); + t.match(meta[0], /Organization:\s+test-org/, 'organization displayed'); + t.match(meta[1], /Package manager:\s+gradle/, 'package manager displayed'); + t.match(meta[2], /Target file:\s+build.gradle/, 'target file displayed'); + t.match(meta[3], /Open source:\s+no/, 'open source displayed'); + t.match(meta[4], /Project path:\s+gradle-app/, 'path displayed'); + t.notMatch( + meta[5], + /Local Snyk policy:\s+found/, + 'local policy not displayed', + ); +}); + +test('`test gradle-app --all-sub-projects` sends `allSubProjects` argument to plugin', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { plugin: { name: 'gradle' }, package: {} }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('gradle').returns(plugin); + + await cli.test('gradle-app', { + allSubProjects: true, + }); + t.true(((spyPlugin.args[0] as any)[2] as any).allSubProjects); +}); + +test('`test gradle-app` plugin fails to return package or scannedProjects', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { plugin: { name: 'gradle' } }; + }, + }; + sinon.spy(plugin, 'inspect'); + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('gradle').returns(plugin); + + try { + await cli.test('gradle-app', {}); + t.fail('expected error'); + } catch (error) { + t.match( + error, + /error getting dependencies from gradle plugin: neither 'package' nor 'scannedProjects' were found/, + 'error found', + ); + } +}); + +test('`test gradle-app --all-sub-projects` returns correct multi tree meta', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect(): Promise { + return { + plugin: { name: 'gradle' }, + scannedProjects: [ + { + depTree: { + name: 'tree0', + version: '1.0.0', + dependencies: { dep1: { name: 'dep1', version: '1' } }, + }, + }, + { + depTree: { + name: 'tree1', + version: '2.0.0', + dependencies: { dep1: { name: 'dep2', version: '2' } }, + }, + }, + ], + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('gradle').returns(plugin); + + const res = await cli.test('gradle-app', { allSubProjects: true }); + t.true( + ((spyPlugin.args[0] as any)[2] as any).allSubProjects, + '`allSubProjects` option is sent', + ); + + const tests = res.split('Testing gradle-app...').filter((s) => !!s.trim()); + t.equals(tests.length, 2, 'two projects tested independently'); + t.match( + res, + /Tested 2 projects/, + 'number projects tested displayed properly', + ); + for (let i = 0; i < tests.length; i++) { + const meta = tests[i].slice(tests[i].indexOf('Organization:')).split('\n'); + t.match(meta[0], /Organization:\s+test-org/, 'organization displayed'); + t.match(meta[1], /Package manager:\s+gradle/, 'package manager displayed'); + t.match(meta[2], /Target file:\s+build.gradle/, 'target file displayed'); + t.match(meta[3], /Project name:\s+tree/, 'sub-project displayed'); + t.includes(meta[3], `tree${i}`, 'sub-project displayed'); + t.match(meta[4], /Open source:\s+no/, 'open source displayed'); + t.match(meta[5], /Project path:\s+gradle-app/, 'path displayed'); + t.notMatch( + meta[6], + /Local Snyk policy:\s+found/, + 'local policy not displayed', + ); + } +}); + +// @later: try and remove this config stuff +// Was copied straight from ../src/cli-server.js +after('teardown', async (t) => { + t.plan(4); + + delete process.env.SNYK_API; + delete process.env.SNYK_HOST; + delete process.env.SNYK_PORT; + t.notOk(process.env.SNYK_PORT, 'fake env values cleared'); + + await new Promise((resolve) => { + server.close(resolve); + }); + t.pass('server shutdown'); + let key = 'set'; + let value = 'api=' + oldkey; + if (!oldkey) { + key = 'unset'; + value = 'api'; + } + await cli.config(key, value); + t.pass('user config restored'); + if (oldendpoint) { + await cli.config('endpoint', oldendpoint); + t.pass('user endpoint restored'); + t.end(); + } else { + t.pass('no endpoint'); + t.end(); + } +}); + +function chdirWorkspaces(subdir = '') { + process.chdir(__dirname + '/workspaces' + (subdir ? '/' + subdir : '')); +} diff --git a/test/acceptance/cli-test.maven.test.ts b/test/acceptance/cli-test.maven.test.ts new file mode 100644 index 0000000000..b1f639c866 --- /dev/null +++ b/test/acceptance/cli-test.maven.test.ts @@ -0,0 +1,282 @@ +import * as tap from 'tap'; +import * as sinon from 'sinon'; +import * as cli from '../../src/cli/commands'; +import { fakeServer } from './fake-server'; +import * as version from '../../src/lib/version'; +import * as subProcess from '../../src/lib/sub-process'; +import * as path from 'path'; +import * as fs from 'fs'; + +const { test, only } = tap; +(tap as any).runOnly = false; // <- for debug. set to true, and replace a test to only(..) + +const port = (process.env.PORT = process.env.SNYK_PORT = '12345'); +process.env.SNYK_API = 'http://localhost:' + port + '/api/v1'; +process.env.SNYK_HOST = 'http://localhost:' + port; +process.env.LOG_LEVEL = '0'; +const apiKey = '123456789'; +let oldkey; +let oldendpoint; +let versionNumber; +const server = fakeServer(process.env.SNYK_API, apiKey); +const before = tap.runOnly ? only : test; +const after = tap.runOnly ? only : test; + +// Should be after `process.env` setup. +import * as plugins from '../../src/lib/plugins'; +import * as depGraphLib from '@snyk/dep-graph'; + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('setup', async (t) => { + versionNumber = await version(); + + t.plan(3); + let key = await cli.config('get', 'api'); + oldkey = key; + t.pass('existing user config captured'); + + key = await cli.config('get', 'endpoint'); + oldendpoint = key; + t.pass('existing user endpoint captured'); + + await new Promise((resolve) => { + server.listen(port, resolve); + }); + t.pass('started demo server'); + t.end(); +}); + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('prime config', async (t) => { + await cli.config('set', 'api=' + apiKey); + t.pass('api token set'); + await cli.config('unset', 'endpoint'); + t.pass('endpoint removed'); + t.end(); +}); + +test('`test maven-app --file=pom.xml --dev` sends package info', async (t) => { + chdirWorkspaces(); + stubExec(t, 'maven-app/mvn-dep-tree-stdout.txt'); + await cli.test('maven-app', { + file: 'pom.xml', + org: 'nobelprize.org', + dev: true, + }); + + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.query.org, 'nobelprize.org', 'org sent as a query in request'); + t.match(req.body.targetFile, undefined, 'target is undefined'); + + const depGraph = depGraphLib.createFromJSON(req.body.depGraph); + t.equal(depGraph.rootPkg.name, 'com.mycompany.app:maven-app', 'root name'); + const pkgs = depGraph.getPkgs().map((x) => `${x.name}@${x.version}`); + t.ok(pkgs.indexOf('com.mycompany.app:maven-app@1.0-SNAPSHOT') >= 0); + t.ok(pkgs.indexOf('axis:axis@1.4') >= 0); + t.ok(pkgs.indexOf('junit:junit@3.8.2') >= 0); +}); + +test('`test maven-app-with-jars --file=example.jar` sends package info', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { name: 'testplugin', runtime: 'testruntime' }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('maven').returns(plugin); + + await cli.test('maven-app-with-jars', { + file: 'example.jar', + }); + + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.match(req.body.targetFile, undefined, 'target is undefined'); + + t.equal(req.body.depGraph.pkgManager.name, 'maven'); + t.same( + spyPlugin.getCall(0).args, + [ + 'maven-app-with-jars', + 'example.jar', + { + args: null, + file: 'example.jar', + org: null, + projectName: null, + packageManager: 'maven', + path: 'maven-app-with-jars', + showVulnPaths: 'some', + }, + ], + 'calls mvn plugin', + ); +}); + +test('`test maven-app-with-jars --file=example.war` sends package info', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { name: 'testplugin', runtime: 'testruntime' }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('maven').returns(plugin); + + await cli.test('maven-app-with-jars', { + file: 'example.war', + }); + + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.match(req.body.targetFile, undefined, 'target is undefined'); + + t.equal(req.body.depGraph.pkgManager.name, 'maven'); + t.same( + spyPlugin.getCall(0).args, + [ + 'maven-app-with-jars', + 'example.war', + { + args: null, + file: 'example.war', + org: null, + projectName: null, + packageManager: 'maven', + path: 'maven-app-with-jars', + showVulnPaths: 'some', + }, + ], + 'calls mvn plugin', + ); +}); + +test('`test maven-app-with-jars --all-jars` sends package info', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { name: 'testplugin', runtime: 'testruntime' }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('maven').returns(plugin); + + await cli.test('maven-app-with-jars', { + 'all-jars': true, + }); + + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.match(req.body.targetFile, undefined, 'target is undefined'); + + t.equal(req.body.depGraph.pkgManager.name, 'maven'); + t.same( + spyPlugin.getCall(0).args, + [ + 'maven-app-with-jars', + undefined, + { + args: null, + org: null, + projectName: null, + packageManager: 'maven', + path: 'maven-app-with-jars', + showVulnPaths: 'some', + }, + ], + 'calls mvn plugin', + ); +}); + +// @later: try and remove this config stuff +// Was copied straight from ../src/cli-server.js +after('teardown', async (t) => { + t.plan(4); + + delete process.env.SNYK_API; + delete process.env.SNYK_HOST; + delete process.env.SNYK_PORT; + t.notOk(process.env.SNYK_PORT, 'fake env values cleared'); + + await new Promise((resolve) => { + server.close(resolve); + }); + t.pass('server shutdown'); + let key = 'set'; + let value = 'api=' + oldkey; + if (!oldkey) { + key = 'unset'; + value = 'api'; + } + await cli.config(key, value); + t.pass('user config restored'); + if (oldendpoint) { + await cli.config('endpoint', oldendpoint); + t.pass('user endpoint restored'); + t.end(); + } else { + t.pass('no endpoint'); + t.end(); + } +}); + +function chdirWorkspaces(subdir = '') { + process.chdir(__dirname + '/workspaces' + (subdir ? '/' + subdir : '')); +} + +/** + * We can't expect all test environments to have Maven installed + * So, hijack the system exec call and return the expected output + */ +function stubExec(t, execOutputFile) { + const stub = sinon.stub(subProcess, 'execute').callsFake(() => { + const stdout = fs.readFileSync(path.join(execOutputFile), 'utf8'); + return Promise.resolve(stdout); + }); + t.teardown(() => { + stub.restore(); + }); +} diff --git a/test/acceptance/cli-test.npm.test.ts b/test/acceptance/cli-test.npm.test.ts new file mode 100644 index 0000000000..f5a4c1c4b9 --- /dev/null +++ b/test/acceptance/cli-test.npm.test.ts @@ -0,0 +1,294 @@ +import * as tap from 'tap'; +import * as sinon from 'sinon'; +import * as cli from '../../src/cli/commands'; +import { fakeServer } from './fake-server'; +import * as version from '../../src/lib/version'; + +const { test, only } = tap; +(tap as any).runOnly = false; // <- for debug. set to true, and replace a test to only(..) + +const port = (process.env.PORT = process.env.SNYK_PORT = '12345'); +process.env.SNYK_API = 'http://localhost:' + port + '/api/v1'; +process.env.SNYK_HOST = 'http://localhost:' + port; +process.env.LOG_LEVEL = '0'; +const apiKey = '123456789'; +let oldkey; +let oldendpoint; +let versionNumber; +const server = fakeServer(process.env.SNYK_API, apiKey); +const before = tap.runOnly ? only : test; +const after = tap.runOnly ? only : test; + +// Should be after `process.env` setup. +import * as plugins from '../../src/lib/plugins'; +import { legacyPlugin as pluginApi } from '@snyk/cli-interface'; + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('setup', async (t) => { + versionNumber = await version(); + + t.plan(3); + let key = await cli.config('get', 'api'); + oldkey = key; + t.pass('existing user config captured'); + + key = await cli.config('get', 'endpoint'); + oldendpoint = key; + t.pass('existing user endpoint captured'); + + await new Promise((resolve) => { + server.listen(port, resolve); + }); + t.pass('started demo server'); + t.end(); +}); + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('prime config', async (t) => { + await cli.config('set', 'api=' + apiKey); + t.pass('api token set'); + await cli.config('unset', 'endpoint'); + t.pass('endpoint removed'); + t.end(); +}); + +test('`test npm-package with custom --project-name`', async (t) => { + chdirWorkspaces(); + await cli.test('npm-package', { + 'project-name': 'custom-project-name', + }); + const req = server.popRequest(); + t.match( + req.body.projectNameOverride, + 'custom-project-name', + 'custom project name is passed', + ); + t.match(req.body.targetFile, undefined, 'target is undefined'); +}); + +test('test npm-package remoteUrl', async (t) => { + chdirWorkspaces(); + process.env.GIT_DIR = 'npm-package/gitdir'; + await cli.test('npm-package'); + const req = server.popRequest(); + t.equal( + req.body.target.remoteUrl, + 'http://github.com/snyk/npm-package', + 'git remoteUrl is passed', + ); + t.equals( + req.body.target.branch, + 'master', + 'correct branch passed to request', + ); + + delete process.env.GIT_DIR; +}); + +test('test npm-package remoteUrl with --remote-repo-url', async (t) => { + chdirWorkspaces(); + process.env.GIT_DIR = 'npm-package/gitdir'; + await cli.test('npm-package', { + 'remote-repo-url': 'foo', + }); + const req = server.popRequest(); + t.equal(req.body.target.remoteUrl, 'foo', 'specified remoteUrl is passed'); + t.equals( + req.body.target.branch, + 'master', + 'correct branch passed to request', + ); + + delete process.env.GIT_DIR; +}); + +test('`test --file=fixtures/protect/package.json`', async (t) => { + const res = await cli.test(path.resolve(__dirname, '..'), { + file: 'fixtures/protect/package.json', + }); + t.match( + res, + /Tested 1 dependencies for known vulnerabilities/, + 'should succeed in a folder', + ); +}); + +test('`test npm-package-policy` returns correct meta', async (t) => { + chdirWorkspaces(); + const res = await cli.test('npm-package-policy'); + 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'); + t.match(meta[2], /Target file:\s+package.json/, 'target file displayed'); + t.match( + meta[3], + /Project name:\s+custom-policy-location-package/, + 'project name displayed', + ); + t.match(meta[4], /Open source:\s+no/, 'open source displayed'); + t.match(meta[5], /Project path:\s+npm-package-policy/, 'path displayed'); + t.match(meta[6], /Local Snyk policy:\s+found/, 'local policy displayed'); +}); + +test('`test npm-package` sends pkg info', async (t) => { + chdirWorkspaces(); + await cli.test('npm-package'); + const req = server.popRequest(); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.match(req.body.targetFile, undefined, 'target is undefined'); + const depGraph = req.body.depGraph; + + t.same( + depGraph.pkgs.map((p) => p.id).sort(), + ['npm-package@1.0.0', 'ms@0.7.1', 'debug@2.2.0'].sort(), + 'depGraph looks fine', + ); +}); + +test('`test npm-package --file=package-lock.json ` sends pkg info', async (t) => { + chdirWorkspaces(); + await cli.test('npm-package', { file: 'package-lock.json' }); + const req = server.popRequest(); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.match(req.body.targetFile, undefined, 'target is undefined'); + const depGraph = req.body.depGraph; + t.same( + depGraph.pkgs.map((p) => p.id).sort(), + ['npm-package@1.0.0', 'ms@0.7.1', 'debug@2.2.0'].sort(), + 'depGraph looks fine', + ); +}); + +test('`test npm-package --file=package-lock.json --dev` sends pkg info', async (t) => { + chdirWorkspaces(); + await cli.test('npm-package', { file: 'package-lock.json', dev: true }); + const req = server.popRequest(); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.match(req.body.targetFile, undefined, 'target is undefined'); + const depGraph = req.body.depGraph; + t.same( + depGraph.pkgs.map((p) => p.id).sort(), + [ + 'npm-package@1.0.0', + 'ms@0.7.1', + 'debug@2.2.0', + 'object-assign@4.1.1', + ].sort(), + 'depGraph looks fine', + ); +}); + +test('`test npm-out-of-sync` out of sync fails', async (t) => { + chdirWorkspaces(); + try { + await cli.test('npm-out-of-sync', { dev: true }); + t.fail('Should fail'); + } catch (e) { + t.equal( + e.message, + '\nTesting npm-out-of-sync...\n\n' + + 'Dependency snyk was not found in package-lock.json.' + + ' Your package.json and package-lock.json are probably out of sync.' + + ' Please run "npm install" and try again.', + 'Contains enough info about err', + ); + } +}); + +test('`test npm-out-of-sync --strict-out-of-sync=false` passes', async (t) => { + chdirWorkspaces(); + await cli.test('npm-out-of-sync', { dev: true, strictOutOfSync: false }); + const req = server.popRequest(); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + const depGraph = req.body.depGraph; + t.same( + depGraph.pkgs.map((p) => p.id).sort(), + [ + 'npm-package@1.0.0', + 'object-assign@4.1.1', + 'rewire@^4.0.1', + 'snyk@*', + 'to-array@0.1.4', + ].sort(), + 'depGraph looks fine', + ); +}); + +test('`test npm-package-shrinkwrap --file=package-lock.json ` with npm-shrinkwrap errors', async (t) => { + t.plan(1); + chdirWorkspaces(); + try { + await cli.test('npm-package-shrinkwrap', { file: 'package-lock.json' }); + t.fail('Should fail'); + } catch (e) { + t.includes( + e.message, + '--file=package-lock.json', + 'Contains enough info about err', + ); + } +}); + +test('`test npm-package-with-subfolder --file=package-lock.json ` picks top-level files', async (t) => { + chdirWorkspaces(); + await cli.test('npm-package-with-subfolder', { file: 'package-lock.json' }); + const req = server.popRequest(); + const depGraph = req.body.depGraph; + t.same( + depGraph.pkgs.map((p) => p.id).sort(), + ['npm-package-top-level@1.0.0', 'to-array@0.1.4'].sort(), + 'depGraph looks fine', + ); +}); + +test('`test npm-package-with-subfolder --file=subfolder/package-lock.json ` picks subfolder files', async (t) => { + chdirWorkspaces(); + await cli.test('npm-package-with-subfolder', { + file: 'subfolder/package-lock.json', + }); + const req = server.popRequest(); + const depGraph = req.body.depGraph; + t.same( + depGraph.pkgs.map((p) => p.id).sort(), + ['npm-package-subfolder@1.0.0', 'to-array@0.1.4'].sort(), + 'depGraph looks fine', + ); +}); + +// @later: try and remove this config stuff +// Was copied straight from ../src/cli-server.js +after('teardown', async (t) => { + t.plan(4); + + delete process.env.SNYK_API; + delete process.env.SNYK_HOST; + delete process.env.SNYK_PORT; + t.notOk(process.env.SNYK_PORT, 'fake env values cleared'); + + await new Promise((resolve) => { + server.close(resolve); + }); + t.pass('server shutdown'); + let key = 'set'; + let value = 'api=' + oldkey; + if (!oldkey) { + key = 'unset'; + value = 'api'; + } + await cli.config(key, value); + t.pass('user config restored'); + if (oldendpoint) { + await cli.config('endpoint', oldendpoint); + t.pass('user endpoint restored'); + t.end(); + } else { + t.pass('no endpoint'); + t.end(); + } +}); + +function chdirWorkspaces(subdir = '') { + process.chdir(__dirname + '/workspaces' + (subdir ? '/' + subdir : '')); +} diff --git a/test/acceptance/cli-test.nuget.test.ts b/test/acceptance/cli-test.nuget.test.ts new file mode 100644 index 0000000000..9e2c1aa4bb --- /dev/null +++ b/test/acceptance/cli-test.nuget.test.ts @@ -0,0 +1,566 @@ +import * as tap from 'tap'; +import * as sinon from 'sinon'; +import * as cli from '../../src/cli/commands'; +import { fakeServer } from './fake-server'; +import * as version from '../../src/lib/version'; + +const { test, only } = tap; +(tap as any).runOnly = false; // <- for debug. set to true, and replace a test to only(..) + +const port = (process.env.PORT = process.env.SNYK_PORT = '12345'); +process.env.SNYK_API = 'http://localhost:' + port + '/api/v1'; +process.env.SNYK_HOST = 'http://localhost:' + port; +process.env.LOG_LEVEL = '0'; +const apiKey = '123456789'; +let oldkey; +let oldendpoint; +let versionNumber; +const server = fakeServer(process.env.SNYK_API, apiKey); +const before = tap.runOnly ? only : test; +const after = tap.runOnly ? only : test; + +// Should be after `process.env` setup. +import * as plugins from '../../src/lib/plugins'; + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('setup', async (t) => { + versionNumber = await version(); + + t.plan(3); + let key = await cli.config('get', 'api'); + oldkey = key; + t.pass('existing user config captured'); + + key = await cli.config('get', 'endpoint'); + oldendpoint = key; + t.pass('existing user endpoint captured'); + + await new Promise((resolve) => { + server.listen(port, resolve); + }); + t.pass('started demo server'); + t.end(); +}); + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('prime config', async (t) => { + await cli.config('set', 'api=' + apiKey); + t.pass('api token set'); + await cli.config('unset', 'endpoint'); + t.pass('endpoint removed'); + t.end(); +}); + +test('`test nuget-app --file=non_existent`', async (t) => { + chdirWorkspaces(); + try { + await cli.test('nuget-app', { file: 'non-existent' }); + t.fail('should have failed'); + } catch (err) { + t.pass('throws err'); + t.match( + err.message, + 'Could not find the specified file: non-existent', + 'show first part of err message', + ); + t.match( + err.message, + 'Please check that it exists and try again.', + 'show second part of err message', + ); + } +}); + +test('`test nuget-app-2 auto-detects project.assets.json`', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { + name: 'snyk-nuget-plugin', + targetFile: 'project.assets.json', + }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('nuget').returns(plugin); + + await cli.test('nuget-app-2'); + + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'nuget'); + t.same( + spyPlugin.getCall(0).args, + [ + 'nuget-app-2', + 'project.assets.json', + { + args: null, + file: 'project.assets.json', + org: null, + projectName: null, + packageManager: 'nuget', + path: 'nuget-app-2', + showVulnPaths: 'some', + }, + ], + 'calls nuget plugin', + ); +}); + +test('`test nuget-app-2.1 auto-detects obj/project.assets.json`', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { + name: 'snyk-nuget-plugin', + targetFile: 'obj/project.assets.json', + }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('nuget').returns(plugin); + + await cli.test('nuget-app-2.1'); + + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'nuget'); + t.same( + spyPlugin.getCall(0).args, + [ + 'nuget-app-2.1', + 'obj/project.assets.json', + { + args: null, + file: 'obj/project.assets.json', + org: null, + projectName: null, + packageManager: 'nuget', + path: 'nuget-app-2.1', + showVulnPaths: 'some', + }, + ], + 'calls nuget plugin', + ); +}); + +test('`test nuget-app-4 auto-detects packages.config`', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { + name: 'snyk-nuget-plugin', + targetFile: 'paket.dependencies', + targetRuntime: 'net465s', + }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('nuget').returns(plugin); + + await cli.test('nuget-app-4'); + + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'nuget'); + t.same( + spyPlugin.getCall(0).args, + [ + 'nuget-app-4', + 'packages.config', + { + args: null, + file: 'packages.config', + org: null, + projectName: null, + packageManager: 'nuget', + path: 'nuget-app-4', + showVulnPaths: 'some', + }, + ], + 'calls nuget plugin', + ); +}); + +test('`test nuget-app --file=project.assets.json`', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { + name: 'snyk-nuget-plugin', + targetFile: 'project.assets.json', + targetRuntime: 'net465s', + }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('nuget').returns(plugin); + + await cli.test('nuget-app', { + file: 'project.assets.json', + }); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.targetFile, 'project.assets.json', 'specifies target'); + t.equal(req.body.depGraph.pkgManager.name, 'nuget'); + t.same( + spyPlugin.getCall(0).args, + [ + 'nuget-app', + 'project.assets.json', + { + args: null, + file: 'project.assets.json', + org: null, + projectName: null, + packageManager: 'nuget', + path: 'nuget-app', + showVulnPaths: 'some', + }, + ], + 'calls nuget plugin', + ); +}); + +test('`test nuget-app --file=packages.config`', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { + name: 'snyk-nuget-plugin', + targetFile: 'packages.config', + targetRuntime: 'net465s', + }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('nuget').returns(plugin); + + await cli.test('nuget-app', { + file: 'packages.config', + }); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.targetFile, 'packages.config', 'specifies target'); + t.equal(req.body.depGraph.pkgManager.name, 'nuget'); + t.same( + spyPlugin.getCall(0).args, + [ + 'nuget-app', + 'packages.config', + { + args: null, + file: 'packages.config', + org: null, + projectName: null, + packageManager: 'nuget', + path: 'nuget-app', + showVulnPaths: 'some', + }, + ], + 'calls nuget plugin', + ); +}); + +test('`test nuget-app --file=project.json`', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { + name: 'testplugin', + runtime: 'testruntime', + targetFile: 'project.json', + }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('nuget').returns(plugin); + + await cli.test('nuget-app', { + file: 'project.json', + }); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.targetFile, 'project.json', 'specifies target'); + t.equal(req.body.depGraph.pkgManager.name, 'nuget'); + t.same( + spyPlugin.getCall(0).args, + [ + 'nuget-app', + 'project.json', + { + args: null, + file: 'project.json', + org: null, + projectName: null, + packageManager: 'nuget', + path: 'nuget-app', + showVulnPaths: 'some', + }, + ], + 'calls nuget plugin', + ); +}); + +test('`test paket-app auto-detects paket.dependencies`', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { + name: 'snyk-nuget-plugin', + targetFile: 'paket.dependencies', + targetRuntime: 'net465s', + }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('paket').returns(plugin); + + await cli.test('paket-app'); + + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'paket'); + t.same( + spyPlugin.getCall(0).args, + [ + 'paket-app', + 'paket.dependencies', + { + args: null, + file: 'paket.dependencies', + org: null, + projectName: null, + packageManager: 'paket', + path: 'paket-app', + showVulnPaths: 'some', + }, + ], + 'calls nuget plugin', + ); +}); + +test('`test paket-obj-app auto-detects obj/project.assets.json if exists`', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { + name: 'snyk-nuget-plugin', + targetFile: 'paket.dependencies', + targetRuntime: 'net465s', + }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('nuget').returns(plugin); + + await cli.test('paket-obj-app'); + + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'nuget'); + t.same( + spyPlugin.getCall(0).args, + [ + 'paket-obj-app', + 'obj/project.assets.json', + { + args: null, + file: 'obj/project.assets.json', + org: null, + projectName: null, + packageManager: 'nuget', + path: 'paket-obj-app', + showVulnPaths: 'some', + }, + ], + 'calls nuget plugin', + ); +}); + +test('`test paket-app --file=paket.dependencies`', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { + name: 'snyk-nuget-plugin', + targetFile: 'paket.dependencies', + targetRuntime: 'net465s', + }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('paket').returns(plugin); + + await cli.test('paket-app', { + file: 'paket.dependencies', + }); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'paket'); + t.equal(req.body.targetFile, 'paket.dependencies', 'specifies target'); + t.same( + spyPlugin.getCall(0).args, + [ + 'paket-app', + 'paket.dependencies', + { + args: null, + file: 'paket.dependencies', + org: null, + projectName: null, + packageManager: 'paket', + path: 'paket-app', + showVulnPaths: 'some', + }, + ], + 'calls nuget plugin', + ); +}); + +// @later: try and remove this config stuff +// Was copied straight from ../src/cli-server.js +after('teardown', async (t) => { + t.plan(4); + + delete process.env.SNYK_API; + delete process.env.SNYK_HOST; + delete process.env.SNYK_PORT; + t.notOk(process.env.SNYK_PORT, 'fake env values cleared'); + + await new Promise((resolve) => { + server.close(resolve); + }); + t.pass('server shutdown'); + let key = 'set'; + let value = 'api=' + oldkey; + if (!oldkey) { + key = 'unset'; + value = 'api'; + } + await cli.config(key, value); + t.pass('user config restored'); + if (oldendpoint) { + await cli.config('endpoint', oldendpoint); + t.pass('user endpoint restored'); + t.end(); + } else { + t.pass('no endpoint'); + t.end(); + } +}); + +function chdirWorkspaces(subdir = '') { + process.chdir(__dirname + '/workspaces' + (subdir ? '/' + subdir : '')); +} diff --git a/test/acceptance/cli-test.python.test.ts b/test/acceptance/cli-test.python.test.ts new file mode 100644 index 0000000000..d7058acff5 --- /dev/null +++ b/test/acceptance/cli-test.python.test.ts @@ -0,0 +1,335 @@ +import * as tap from 'tap'; +import * as sinon from 'sinon'; +import * as cli from '../../src/cli/commands'; +import { fakeServer } from './fake-server'; +import * as version from '../../src/lib/version'; +import * as fs from 'fs'; + +const { test, only } = tap; +(tap as any).runOnly = false; // <- for debug. set to true, and replace a test to only(..) + +const port = (process.env.PORT = process.env.SNYK_PORT = '12345'); +process.env.SNYK_API = 'http://localhost:' + port + '/api/v1'; +process.env.SNYK_HOST = 'http://localhost:' + port; +process.env.LOG_LEVEL = '0'; +const apiKey = '123456789'; +let oldkey; +let oldendpoint; +let versionNumber; +const server = fakeServer(process.env.SNYK_API, apiKey); +const before = tap.runOnly ? only : test; +const after = tap.runOnly ? only : test; + +// Should be after `process.env` setup. +import * as plugins from '../../src/lib/plugins'; + +function loadJson(filename: string) { + return JSON.parse(fs.readFileSync(filename, 'utf-8')); +} + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('setup', async (t) => { + versionNumber = await version(); + + t.plan(3); + let key = await cli.config('get', 'api'); + oldkey = key; + t.pass('existing user config captured'); + + key = await cli.config('get', 'endpoint'); + oldendpoint = key; + t.pass('existing user endpoint captured'); + + await new Promise((resolve) => { + server.listen(port, resolve); + }); + t.pass('started demo server'); + t.end(); +}); + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('prime config', async (t) => { + await cli.config('set', 'api=' + apiKey); + t.pass('api token set'); + await cli.config('unset', 'endpoint'); + t.pass('endpoint removed'); + t.end(); +}); + +test('`test pip-app --file=requirements.txt`', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + package: {}, + plugin: { name: 'testplugin', runtime: 'testruntime' }, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('pip').returns(plugin); + + await cli.test('pip-app', { + file: 'requirements.txt', + }); + let req = server.popRequest(); + t.equal(req.method, 'GET', 'makes GET request'); + t.match( + req.url, + 'cli-config/feature-flags/pythonPinningAdvice', + 'to correct url', + ); + req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'pip'); + t.same( + spyPlugin.getCall(0).args, + [ + 'pip-app', + 'requirements.txt', + { + args: null, + file: 'requirements.txt', + org: null, + projectName: null, + packageManager: 'pip', + path: 'pip-app', + showVulnPaths: 'some', + }, + ], + 'calls python plugin', + ); +}); + +test('`test pipenv-app --file=Pipfile`', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return { + plugin: { + targetFile: 'Pipfile', + name: 'snyk-python-plugin', + runtime: 'Python', + }, + package: {}, + }; + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('pip').returns(plugin); + + await cli.test('pipenv-app', { + file: 'Pipfile', + }); + let req = server.popRequest(); + t.equal(req.method, 'GET', 'makes GET request'); + t.match( + req.url, + 'cli-config/feature-flags/pythonPinningAdvice', + 'to correct url', + ); + req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.targetFile, 'Pipfile', 'specifies target'); + t.equal(req.body.depGraph.pkgManager.name, 'pip'); + t.same( + spyPlugin.getCall(0).args, + [ + 'pipenv-app', + 'Pipfile', + { + args: null, + file: 'Pipfile', + org: null, + projectName: null, + packageManager: 'pip', + path: 'pipenv-app', + showVulnPaths: 'some', + }, + ], + 'calls python plugin', + ); +}); + +test('`test pip-app-transitive-vuln --file=requirements.txt (actionableCliRemediation=false)`', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return loadJson('./pip-app-transitive-vuln/inspect-result.json'); + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('pip').returns(plugin); + + server.setNextResponse( + loadJson('./pip-app-transitive-vuln/response-without-remediation.json'), + ); + try { + await cli.test('pip-app-transitive-vuln', { + file: 'requirements.txt', + }); + t.fail('should throw, since there are vulns'); + } catch (e) { + t.equals( + e.message, + fs.readFileSync('pip-app-transitive-vuln/cli-output.txt', 'utf8'), + ); + } + let req = server.popRequest(); + t.equal(req.method, 'GET', 'makes GET request'); + t.match( + req.url, + 'cli-config/feature-flags/pythonPinningAdvice', + 'to correct url', + ); + req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'pip'); + t.same( + spyPlugin.getCall(0).args, + [ + 'pip-app-transitive-vuln', + 'requirements.txt', + { + args: null, + file: 'requirements.txt', + org: null, + projectName: null, + packageManager: 'pip', + path: 'pip-app-transitive-vuln', + showVulnPaths: 'some', + }, + ], + 'calls python plugin', + ); +}); + +test('`test pip-app-transitive-vuln --file=requirements.txt (actionableCliRemediation=true)`', async (t) => { + chdirWorkspaces(); + const plugin = { + async inspect() { + return loadJson('./pip-app-transitive-vuln/inspect-result.json'); + }, + }; + const spyPlugin = sinon.spy(plugin, 'inspect'); + + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('pip').returns(plugin); + + server.setNextResponse( + loadJson('./pip-app-transitive-vuln/response-with-remediation.json'), + ); + try { + await cli.test('pip-app-transitive-vuln', { + file: 'requirements.txt', + }); + t.fail('should throw, since there are vulns'); + } catch (e) { + t.equals( + e.message, + fs.readFileSync( + 'pip-app-transitive-vuln/cli-output-actionable-remediation.txt', + 'utf8', + ), + ); + } + let req = server.popRequest(); + t.equal(req.method, 'GET', 'makes GET request'); + t.match( + req.url, + 'cli-config/feature-flags/pythonPinningAdvice', + 'to correct url', + ); + req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.equal(req.body.depGraph.pkgManager.name, 'pip'); + t.same( + spyPlugin.getCall(0).args, + [ + 'pip-app-transitive-vuln', + 'requirements.txt', + { + args: null, + file: 'requirements.txt', + org: null, + projectName: null, + packageManager: 'pip', + path: 'pip-app-transitive-vuln', + showVulnPaths: 'some', + }, + ], + 'calls python plugin', + ); +}); + +// @later: try and remove this config stuff +// Was copied straight from ../src/cli-server.js +after('teardown', async (t) => { + t.plan(4); + + delete process.env.SNYK_API; + delete process.env.SNYK_HOST; + delete process.env.SNYK_PORT; + t.notOk(process.env.SNYK_PORT, 'fake env values cleared'); + + await new Promise((resolve) => { + server.close(resolve); + }); + t.pass('server shutdown'); + let key = 'set'; + let value = 'api=' + oldkey; + if (!oldkey) { + key = 'unset'; + value = 'api'; + } + await cli.config(key, value); + t.pass('user config restored'); + if (oldendpoint) { + await cli.config('endpoint', oldendpoint); + t.pass('user endpoint restored'); + t.end(); + } else { + t.pass('no endpoint'); + t.end(); + } +}); + +function chdirWorkspaces(subdir = '') { + process.chdir(__dirname + '/workspaces' + (subdir ? '/' + subdir : '')); +} diff --git a/test/acceptance/cli-test.ruby.test.ts b/test/acceptance/cli-test.ruby.test.ts new file mode 100644 index 0000000000..328c694f6d --- /dev/null +++ b/test/acceptance/cli-test.ruby.test.ts @@ -0,0 +1,573 @@ +import * as tap from 'tap'; +import * as cli from '../../src/cli/commands'; +import { fakeServer } from './fake-server'; +import * as version from '../../src/lib/version'; +import * as path from 'path'; + +const { test, only } = tap; +(tap as any).runOnly = false; // <- for debug. set to true, and replace a test to only(..) + +const port = (process.env.PORT = process.env.SNYK_PORT = '12345'); +process.env.SNYK_API = 'http://localhost:' + port + '/api/v1'; +process.env.SNYK_HOST = 'http://localhost:' + port; +process.env.LOG_LEVEL = '0'; +const apiKey = '123456789'; +let oldkey; +let oldendpoint; +let versionNumber; +const server = fakeServer(process.env.SNYK_API, apiKey); +const before = tap.runOnly ? only : test; +const after = tap.runOnly ? only : test; + +import * as _ from 'lodash'; + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('setup', async (t) => { + versionNumber = await version(); + + t.plan(3); + let key = await cli.config('get', 'api'); + oldkey = key; + t.pass('existing user config captured'); + + key = await cli.config('get', 'endpoint'); + oldendpoint = key; + t.pass('existing user endpoint captured'); + + await new Promise((resolve) => { + server.listen(port, resolve); + }); + t.pass('started demo server'); + t.end(); +}); + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('prime config', async (t) => { + await cli.config('set', 'api=' + apiKey); + t.pass('api token set'); + await cli.config('unset', 'endpoint'); + t.pass('endpoint removed'); + t.end(); +}); + +test('`test ruby-app-no-lockfile --file=Gemfile`', async (t) => { + chdirWorkspaces(); + try { + await cli.test('ruby-app-no-lockfile', { file: 'Gemfile' }); + t.fail('should have failed'); + } catch (err) { + t.pass('throws err'); + t.match(err.message, 'Please run `bundle install`', 'shows err'); + } +}); + +test('`test ruby-app --file=Gemfile.lock`', async (t) => { + chdirWorkspaces(); + await cli.test('ruby-app', { file: 'Gemfile.lock' }); + + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + + const depGraph = req.body.depGraph; + t.equal(depGraph.pkgManager.name, 'rubygems'); + t.same( + depGraph.pkgs.map((p) => p.id).sort(), + ['ruby-app@', 'json@2.0.2', 'lynx@0.4.0'].sort(), + 'depGraph looks fine', + ); +}); + +test('`test ruby-app` meta when no vulns', async (t) => { + chdirWorkspaces(); + const res = await cli.test('ruby-app'); + + 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+rubygems/, 'package manager displayed'); + t.match(meta[2], /Target file:\s+Gemfile/, 'target file displayed'); + t.match(meta[3], /Project name:\s+ruby-app/, 'project name displayed'); + t.match(meta[4], /Open source:\s+no/, 'open source displayed'); + t.match(meta[5], /Project path:\s+ruby-app/, 'path displayed'); + t.notMatch( + meta[5], + /Local Snyk policy:\s+found/, + 'local policy not displayed', + ); +}); + +test('`test ruby-app-thresholds`', async (t) => { + chdirWorkspaces(); + + server.setNextResponse( + require('./workspaces/ruby-app-thresholds/test-graph-result.json'), + ); + + try { + await cli.test('ruby-app-thresholds'); + t.fail('should have thrown'); + } catch (err) { + const res = err.message; + + t.match( + res, + 'Tested 7 dependencies for known vulnerabilities, found 6 vulnerabilities, 7 vulnerable paths', + '6 vulns', + ); + + 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+rubygems/, + 'package manager displayed', + ); + t.match(meta[2], /Target file:\s+Gemfile/, 'target file displayed'); + t.match( + meta[3], + /Project name:\s+ruby-app-thresholds/, + 'project name displayed', + ); + t.match(meta[4], /Open source:\s+no/, 'open source displayed'); + t.match(meta[5], /Project path:\s+ruby-app-thresholds/, 'path displayed'); + t.notMatch( + meta[5], + /Local Snyk policy:\s+found/, + 'local policy not displayed', + ); + } +}); + +test('`test ruby-app-thresholds --severity-threshold=low --json`', async (t) => { + chdirWorkspaces(); + + server.setNextResponse( + require('./workspaces/ruby-app-thresholds/test-graph-result-low-severity.json'), + ); + + try { + await cli.test('ruby-app-thresholds', { + severityThreshold: 'low', + json: true, + }); + t.fail('should have thrown'); + } catch (err) { + const req = server.popRequest(); + t.is(req.query.severityThreshold, 'low'); + + const res = JSON.parse(err.message); + + const expected = require('./workspaces/ruby-app-thresholds/legacy-res-json-low-severity.json'); + + t.deepEqual( + _.omit(res, ['vulnerabilities']), + _.omit(expected, ['vulnerabilities']), + 'metadata is ok', + ); + t.deepEqual( + _.sortBy(res.vulnerabilities, 'id'), + _.sortBy(expected.vulnerabilities, 'id'), + 'vulns are the same', + ); + } +}); + +test('`test ruby-app-thresholds --severity-threshold=medium`', async (t) => { + chdirWorkspaces(); + + server.setNextResponse( + require('./workspaces/ruby-app-thresholds/test-graph-result-medium-severity.json'), + ); + + try { + await cli.test('ruby-app-thresholds', { + severityThreshold: 'medium', + }); + t.fail('should have thrown'); + } catch (err) { + const req = server.popRequest(); + t.is(req.query.severityThreshold, 'medium'); + + const res = err.message; + + t.match( + res, + 'Tested 7 dependencies for known vulnerabilities, found 5 vulnerabilities, 6 vulnerable paths', + '5 vulns', + ); + } +}); + +test('`test ruby-app-thresholds --ignore-policy`', async (t) => { + chdirWorkspaces(); + + server.setNextResponse( + require('./workspaces/ruby-app-thresholds/test-graph-result-medium-severity.json'), + ); + + try { + await cli.test('ruby-app-thresholds', { + 'ignore-policy': true, + }); + t.fail('should have thrown'); + } catch (err) { + const req = server.popRequest(); + t.equal(req.query.ignorePolicy, 'true'); + t.end(); + } +}); + +test('`test ruby-app-thresholds --severity-threshold=medium --json`', async (t) => { + chdirWorkspaces(); + + server.setNextResponse( + require('./workspaces/ruby-app-thresholds/test-graph-result-medium-severity.json'), + ); + + try { + await cli.test('ruby-app-thresholds', { + severityThreshold: 'medium', + json: true, + }); + t.fail('should have thrown'); + } catch (err) { + const req = server.popRequest(); + t.is(req.query.severityThreshold, 'medium'); + + const res = JSON.parse(err.message); + + const expected = require('./workspaces/ruby-app-thresholds/legacy-res-json-medium-severity.json'); + + t.deepEqual( + _.omit(res, ['vulnerabilities']), + _.omit(expected, ['vulnerabilities']), + 'metadata is ok', + ); + t.deepEqual( + _.sortBy(res.vulnerabilities, 'id'), + _.sortBy(expected.vulnerabilities, 'id'), + 'vulns are the same', + ); + } +}); + +test('`test ruby-app-thresholds --severity-threshold=high', async (t) => { + chdirWorkspaces(); + + server.setNextResponse( + require('./workspaces/ruby-app-thresholds/test-graph-result-high-severity.json'), + ); + + try { + await cli.test('ruby-app-thresholds', { + severityThreshold: 'high', + }); + t.fail('should have thrown'); + } catch (err) { + const req = server.popRequest(); + t.is(req.query.severityThreshold, 'high'); + + const res = err.message; + + t.match( + res, + 'Tested 7 dependencies for known vulnerabilities, found 3 vulnerabilities, 4 vulnerable paths', + '3 vulns', + ); + } +}); + +test('`test ruby-app-thresholds --severity-threshold=high --json`', async (t) => { + chdirWorkspaces(); + + server.setNextResponse( + require('./workspaces/ruby-app-thresholds/test-graph-result-high-severity.json'), + ); + + try { + await cli.test('ruby-app-thresholds', { + severityThreshold: 'high', + json: true, + }); + t.fail('should have thrown'); + } catch (err) { + const req = server.popRequest(); + t.is(req.query.severityThreshold, 'high'); + + const res = JSON.parse(err.message); + + const expected = require('./workspaces/ruby-app-thresholds/legacy-res-json-high-severity.json'); + + t.deepEqual( + _.omit(res, ['vulnerabilities']), + _.omit(expected, ['vulnerabilities']), + 'metadata is ok', + ); + t.deepEqual( + _.sortBy(res.vulnerabilities, 'id'), + _.sortBy(expected.vulnerabilities, 'id'), + 'vulns are the same', + ); + } +}); + +test('`test ruby-app-policy`', async (t) => { + chdirWorkspaces(); + + server.setNextResponse( + require('./workspaces/ruby-app-policy/test-graph-result.json'), + ); + + try { + await cli.test('ruby-app-policy', { + json: true, + }); + t.fail('should have thrown'); + } catch (err) { + const res = JSON.parse(err.message); + + const expected = require('./workspaces/ruby-app-policy/legacy-res-json.json'); + + t.deepEqual( + _.omit(res, ['vulnerabilities']), + _.omit(expected, ['vulnerabilities']), + 'metadata is ok', + ); + t.deepEqual( + _.sortBy(res.vulnerabilities, 'id'), + _.sortBy(expected.vulnerabilities, 'id'), + 'vulns are the same', + ); + } +}); + +test('`test ruby-app-policy` with cloud ignores', async (t) => { + chdirWorkspaces(); + + server.setNextResponse( + require('./workspaces/ruby-app-policy/test-graph-result-cloud-ignore.json'), + ); + + try { + await cli.test('ruby-app-policy', { + json: true, + }); + t.fail('should have thrown'); + } catch (err) { + const res = JSON.parse(err.message); + + const expected = require('./workspaces/ruby-app-policy/legacy-res-json-cloud-ignore.json'); + + t.deepEqual( + _.omit(res, ['vulnerabilities']), + _.omit(expected, ['vulnerabilities']), + 'metadata is ok', + ); + t.deepEqual( + _.sortBy(res.vulnerabilities, 'id'), + _.sortBy(expected.vulnerabilities, 'id'), + 'vulns are the same', + ); + } +}); + +test('`test ruby-app-no-vulns`', async (t) => { + chdirWorkspaces(); + + server.setNextResponse( + require('./workspaces/ruby-app-no-vulns/test-graph-result.json'), + ); + + const outText = await cli.test('ruby-app-no-vulns', { + json: true, + }); + + const res = JSON.parse(outText); + + const expected = require('./workspaces/ruby-app-no-vulns/legacy-res-json.json'); + + t.deepEqual(res, expected, '--json output is the same'); +}); + +test('`test ruby-app-no-vulns`', async (t) => { + chdirWorkspaces(); + + const apiResponse = Object.assign( + {}, + require('./workspaces/ruby-app-no-vulns/test-graph-result.json'), + ); + apiResponse.meta.isPublic = true; + server.setNextResponse(apiResponse); + + const outText = await cli.test('ruby-app-no-vulns', { + json: true, + }); + + const res = JSON.parse(outText); + + const expected = Object.assign( + {}, + require('./workspaces/ruby-app-no-vulns/legacy-res-json.json'), + { isPrivate: false }, + ); + + t.deepEqual(res, expected, '--json output is the same'); +}); + +test('`test` returns correct meta when target file specified', async (t) => { + chdirWorkspaces(); + const res = await cli.test('ruby-app', { file: 'Gemfile.lock' }); + const meta = res.slice(res.indexOf('Organization:')).split('\n'); + t.match(meta[2], /Target file:\s+Gemfile.lock/, 'target file displayed'); +}); + +test('`test ruby-gem-no-lockfile --file=ruby-gem.gemspec`', async (t) => { + chdirWorkspaces(); + await cli.test('ruby-gem-no-lockfile', { file: 'ruby-gem.gemspec' }); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + + const depGraph = req.body.depGraph; + t.equal(depGraph.pkgManager.name, 'rubygems'); + t.same( + depGraph.pkgs.map((p) => p.id), + ['ruby-gem-no-lockfile@'], + 'no deps as we dont really support gemspecs yet', + ); +}); + +test('`test ruby-gem --file=ruby-gem.gemspec`', async (t) => { + chdirWorkspaces(); + await cli.test('ruby-gem', { file: 'ruby-gem.gemspec' }); + + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + + const depGraph = req.body.depGraph; + t.equal(depGraph.pkgManager.name, 'rubygems'); + t.same( + depGraph.pkgs.map((p) => p.id).sort(), + ['ruby-gem@', 'ruby-gem@0.1.0', 'rake@10.5.0'].sort(), + 'depGraph looks fine', + ); +}); + +test('`test ruby-app` auto-detects Gemfile', async (t) => { + chdirWorkspaces(); + await cli.test('ruby-app'); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + + const depGraph = req.body.depGraph; + t.equal(depGraph.pkgManager.name, 'rubygems'); + t.same( + depGraph.pkgs.map((p) => p.id).sort(), + ['ruby-app@', 'json@2.0.2', 'lynx@0.4.0'].sort(), + 'depGraph looks fine', + ); + t.equal(req.body.targetFile, 'Gemfile', 'specifies target'); +}); + +test('`test monorepo --file=sub-ruby-app/Gemfile`', async (t) => { + chdirWorkspaces(); + await cli.test('monorepo', { file: 'sub-ruby-app/Gemfile' }); + + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + + const depGraph = req.body.depGraph; + t.equal(depGraph.pkgManager.name, 'rubygems'); + t.same( + depGraph.pkgs.map((p) => p.id).sort(), + ['monorepo@', 'json@2.0.2', 'lynx@0.4.0'].sort(), + 'depGraph looks fine', + ); + + t.equal( + req.body.targetFile, + path.join('sub-ruby-app', 'Gemfile'), + 'specifies target', + ); +}); + +test('`test empty --file=Gemfile`', async (t) => { + chdirWorkspaces(); + try { + await cli.test('empty', { file: 'Gemfile' }); + t.fail('should have failed'); + } catch (err) { + t.pass('throws err'); + t.match( + err.message, + 'Could not find the specified file: Gemfile', + 'shows err', + ); + } +}); + +// @later: try and remove this config stuff +// Was copied straight from ../src/cli-server.js +after('teardown', async (t) => { + t.plan(4); + + delete process.env.SNYK_API; + delete process.env.SNYK_HOST; + delete process.env.SNYK_PORT; + t.notOk(process.env.SNYK_PORT, 'fake env values cleared'); + + await new Promise((resolve) => { + server.close(resolve); + }); + t.pass('server shutdown'); + let key = 'set'; + let value = 'api=' + oldkey; + if (!oldkey) { + key = 'unset'; + value = 'api'; + } + await cli.config(key, value); + t.pass('user config restored'); + if (oldendpoint) { + await cli.config('endpoint', oldendpoint); + t.pass('user endpoint restored'); + t.end(); + } else { + t.pass('no endpoint'); + t.end(); + } +}); + +function chdirWorkspaces(subdir = '') { + process.chdir(__dirname + '/workspaces' + (subdir ? '/' + subdir : '')); +} diff --git a/test/acceptance/cli-test.sbt.test.ts b/test/acceptance/cli-test.sbt.test.ts new file mode 100644 index 0000000000..78ac735717 --- /dev/null +++ b/test/acceptance/cli-test.sbt.test.ts @@ -0,0 +1,141 @@ +import * as tap from 'tap'; +import * as sinon from 'sinon'; +import * as cli from '../../src/cli/commands'; +import { fakeServer } from './fake-server'; +import * as version from '../../src/lib/version'; + +const { test, only } = tap; +(tap as any).runOnly = false; // <- for debug. set to true, and replace a test to only(..) + +const port = (process.env.PORT = process.env.SNYK_PORT = '12345'); +process.env.SNYK_API = 'http://localhost:' + port + '/api/v1'; +process.env.SNYK_HOST = 'http://localhost:' + port; +process.env.LOG_LEVEL = '0'; +const apiKey = '123456789'; +let oldkey; +let oldendpoint; +let versionNumber; +const server = fakeServer(process.env.SNYK_API, apiKey); +const before = tap.runOnly ? only : test; +const after = tap.runOnly ? only : test; + +// Should be after `process.env` setup. +import * as plugins from '../../src/lib/plugins'; +import * as _ from 'lodash'; + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('setup', async (t) => { + versionNumber = await version(); + + t.plan(3); + let key = await cli.config('get', 'api'); + oldkey = key; + t.pass('existing user config captured'); + + key = await cli.config('get', 'endpoint'); + oldendpoint = key; + t.pass('existing user endpoint captured'); + + await new Promise((resolve) => { + server.listen(port, resolve); + }); + t.pass('started demo server'); + t.end(); +}); + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('prime config', async (t) => { + await cli.config('set', 'api=' + apiKey); + t.pass('api token set'); + await cli.config('unset', 'endpoint'); + t.pass('endpoint removed'); + t.end(); +}); + +test('`test sbt-simple-struts`', async (t) => { + chdirWorkspaces(); + + const plugin = { + async inspect() { + return { + plugin: { name: 'sbt' }, + package: require('./workspaces/sbt-simple-struts/dep-tree.json'), + }; + }, + }; + const loadPlugin = sinon.stub(plugins, 'loadPlugin'); + loadPlugin.returns(plugin); + + t.teardown(() => { + loadPlugin.restore(); + }); + + server.setNextResponse( + require('./workspaces/sbt-simple-struts/test-graph-result.json'), + ); + + try { + await cli.test('sbt-simple-struts', { json: true }); + + t.fail('should have thrown'); + } catch (err) { + const res = JSON.parse(err.message); + + const expected = require('./workspaces/sbt-simple-struts/legacy-res-json.json'); + + t.deepEqual( + _.omit(res, ['vulnerabilities', 'packageManager']), + _.omit(expected, ['vulnerabilities', 'packageManager']), + 'metadata is ok', + ); + // NOTE: decided to keep this discrepancy + t.is( + res.packageManager, + 'sbt', + 'pacakgeManager is sbt, altough it was mavn with the legacy api', + ); + t.deepEqual( + _.sortBy(res.vulnerabilities, 'id'), + _.sortBy(expected.vulnerabilities, 'id'), + 'vulns are the same', + ); + } +}); + +// @later: try and remove this config stuff +// Was copied straight from ../src/cli-server.js +after('teardown', async (t) => { + t.plan(4); + + delete process.env.SNYK_API; + delete process.env.SNYK_HOST; + delete process.env.SNYK_PORT; + t.notOk(process.env.SNYK_PORT, 'fake env values cleared'); + + await new Promise((resolve) => { + server.close(resolve); + }); + t.pass('server shutdown'); + let key = 'set'; + let value = 'api=' + oldkey; + if (!oldkey) { + key = 'unset'; + value = 'api'; + } + await cli.config(key, value); + t.pass('user config restored'); + if (oldendpoint) { + await cli.config('endpoint', oldendpoint); + t.pass('user endpoint restored'); + t.end(); + } else { + t.pass('no endpoint'); + t.end(); + } +}); + +function chdirWorkspaces(subdir = '') { + process.chdir(__dirname + '/workspaces' + (subdir ? '/' + subdir : '')); +} diff --git a/test/acceptance/cli-test.yarn.test.ts b/test/acceptance/cli-test.yarn.test.ts new file mode 100644 index 0000000000..830b497df1 --- /dev/null +++ b/test/acceptance/cli-test.yarn.test.ts @@ -0,0 +1,342 @@ +import * as tap from 'tap'; +import * as cli from '../../src/cli/commands'; +import { fakeServer } from './fake-server'; +import * as version from '../../src/lib/version'; + +const { test, only } = tap; +(tap as any).runOnly = false; // <- for debug. set to true, and replace a test to only(..) + +const port = (process.env.PORT = process.env.SNYK_PORT = '12345'); +process.env.SNYK_API = 'http://localhost:' + port + '/api/v1'; +process.env.SNYK_HOST = 'http://localhost:' + port; +process.env.LOG_LEVEL = '0'; +const apiKey = '123456789'; +let oldkey; +let oldendpoint; +let versionNumber; +const server = fakeServer(process.env.SNYK_API, apiKey); +const before = tap.runOnly ? only : test; +const after = tap.runOnly ? only : test; + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('setup', async (t) => { + versionNumber = await version(); + + t.plan(3); + let key = await cli.config('get', 'api'); + oldkey = key; + t.pass('existing user config captured'); + + key = await cli.config('get', 'endpoint'); + oldendpoint = key; + t.pass('existing user endpoint captured'); + + await new Promise((resolve) => { + server.listen(port, resolve); + }); + t.pass('started demo server'); + t.end(); +}); + +// @later: remove this config stuff. +// Was copied straight from ../src/cli-server.js +before('prime config', async (t) => { + await cli.config('set', 'api=' + apiKey); + t.pass('api token set'); + await cli.config('unset', 'endpoint'); + t.pass('endpoint removed'); + t.end(); +}); + +// yarn lockfile based testing is only supported for node 4+ +test('`test yarn-out-of-sync` out of sync fails', async (t) => { + chdirWorkspaces(); + try { + await cli.test('yarn-out-of-sync', { dev: true }); + t.fail('Should fail'); + } catch (e) { + t.equal( + e.message, + '\nTesting yarn-out-of-sync...\n\n' + + 'Dependency snyk was not found in yarn.lock.' + + ' Your package.json and yarn.lock are probably out of sync.' + + ' Please run "yarn install" and try again.', + 'Contains enough info about err', + ); + } +}); + +test('`test yarn-out-of-sync --strict-out-of-sync=false` passes', async (t) => { + chdirWorkspaces(); + await cli.test('yarn-out-of-sync', { dev: true, strictOutOfSync: false }); + const req = server.popRequest(); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + const depGraph = req.body.depGraph; + t.same( + depGraph.pkgs.map((p) => p.id).sort(), + [ + 'acorn-jsx@3.0.1', + 'acorn@3.3.0', + 'acorn@5.7.3', + 'ajv-keywords@2.1.1', + 'ajv@5.5.2', + 'ansi-escapes@3.1.0', + 'ansi-regex@2.1.1', + 'ansi-regex@3.0.0', + 'ansi-styles@2.2.1', + 'ansi-styles@3.2.1', + 'argparse@1.0.10', + 'array-union@1.0.2', + 'array-uniq@1.0.3', + 'arrify@1.0.1', + 'babel-code-frame@6.26.0', + 'balanced-match@1.0.0', + 'brace-expansion@1.1.11', + 'buffer-from@1.1.1', + 'caller-path@0.1.0', + 'callsites@0.2.0', + 'chalk@1.1.3', + 'chalk@2.4.1', + 'chardet@0.4.2', + 'circular-json@0.3.3', + 'cli-cursor@2.1.0', + 'cli-width@2.2.0', + 'co@4.6.0', + 'color-convert@1.9.3', + 'color-name@1.1.3', + 'concat-map@0.0.1', + 'concat-stream@1.6.2', + 'core-util-is@1.0.2', + 'cross-spawn@5.1.0', + 'debug@3.2.5', + 'deep-is@0.1.3', + 'del@2.2.2', + 'doctrine@2.1.0', + 'escape-string-regexp@1.0.5', + 'eslint-scope@3.7.3', + 'eslint-visitor-keys@1.0.0', + 'eslint@4.19.1', + 'espree@3.5.4', + 'esprima@4.0.1', + 'esquery@1.0.1', + 'esrecurse@4.2.1', + 'estraverse@4.2.0', + 'esutils@2.0.2', + 'external-editor@2.2.0', + 'fast-deep-equal@1.1.0', + 'fast-json-stable-stringify@2.0.0', + 'fast-levenshtein@2.0.6', + 'figures@2.0.0', + 'file-entry-cache@2.0.0', + 'flat-cache@1.3.0', + 'fs.realpath@1.0.0', + 'functional-red-black-tree@1.0.1', + 'glob@7.1.3', + 'globals@11.7.0', + 'globby@5.0.0', + 'graceful-fs@4.1.11', + 'has-ansi@2.0.0', + 'has-flag@3.0.0', + 'iconv-lite@0.4.24', + 'ignore@3.3.10', + 'imurmurhash@0.1.4', + 'inflight@1.0.6', + 'inherits@2.0.3', + 'inquirer@3.3.0', + 'is-fullwidth-code-point@2.0.0', + 'is-path-cwd@1.0.0', + 'is-path-in-cwd@1.0.1', + 'is-path-inside@1.0.1', + 'is-promise@2.1.0', + 'is-resolvable@1.1.0', + 'isarray@1.0.0', + 'isexe@2.0.0', + 'js-tokens@3.0.2', + 'js-yaml@3.12.0', + 'json-schema-traverse@0.3.1', + 'json-stable-stringify-without-jsonify@1.0.1', + 'levn@0.3.0', + 'lodash@4.17.11', + 'lru-cache@4.1.3', + 'mimic-fn@1.2.0', + 'minimatch@3.0.4', + 'minimist@0.0.8', + 'mkdirp@0.5.1', + 'ms@2.1.1', + 'mute-stream@0.0.7', + 'natural-compare@1.4.0', + 'npm-package@1.0.0', + 'object-assign@4.1.1', + 'once@1.4.0', + 'onetime@2.0.1', + 'optionator@0.8.2', + 'os-tmpdir@1.0.2', + 'path-is-absolute@1.0.1', + 'path-is-inside@1.0.2', + 'pify@2.3.0', + 'pinkie-promise@2.0.1', + 'pinkie@2.0.4', + 'pluralize@7.0.0', + 'prelude-ls@1.1.2', + 'process-nextick-args@2.0.0', + 'progress@2.0.0', + 'pseudomap@1.0.2', + 'readable-stream@2.3.6', + 'regexpp@1.1.0', + 'require-uncached@1.0.3', + 'resolve-from@1.0.1', + 'restore-cursor@2.0.0', + 'rewire@4.0.1', + 'rimraf@2.6.2', + 'run-async@2.3.0', + 'rx-lite-aggregates@4.0.8', + 'rx-lite@4.0.8', + 'safe-buffer@5.1.2', + 'safer-buffer@2.1.2', + 'semver@5.5.1', + 'shebang-command@1.2.0', + 'shebang-regex@1.0.0', + 'signal-exit@3.0.2', + 'slice-ansi@1.0.0', + 'snyk@*', + 'sprintf-js@1.0.3', + 'string-width@2.1.1', + 'string_decoder@1.1.1', + 'strip-ansi@3.0.1', + 'strip-ansi@4.0.0', + 'strip-json-comments@2.0.1', + 'supports-color@2.0.0', + 'supports-color@5.5.0', + 'table@4.0.2', + 'text-table@0.2.0', + 'through@2.3.8', + 'tmp@0.0.33', + 'to-array@0.1.4', + 'type-check@0.3.2', + 'typedarray@0.0.6', + 'util-deprecate@1.0.2', + 'which@1.3.1', + 'wordwrap@1.0.0', + 'wrappy@1.0.2', + 'write@0.2.1', + 'yallist@2.1.2', + ].sort(), + 'depGraph looks fine', + ); +}); + +test('`test yarn-package --file=yarn.lock ` sends pkg info', async (t) => { + chdirWorkspaces(); + await cli.test('yarn-package', { file: 'yarn.lock' }); + const req = server.popRequest(); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.match(req.body.targetFile, undefined, 'target is undefined'); + const depGraph = req.body.depGraph; + t.same( + depGraph.pkgs.map((p) => p.id).sort(), + ['npm-package@1.0.0', 'ms@0.7.1', 'debug@2.2.0'].sort(), + 'depGraph looks fine', + ); +}); + +test('`test yarn-package --file=yarn.lock --dev` sends pkg info', async (t) => { + chdirWorkspaces(); + await cli.test('yarn-package', { file: 'yarn.lock', dev: true }); + const req = server.popRequest(); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.match(req.body.targetFile, undefined, 'target is undefined'); + const depGraph = req.body.depGraph; + t.same( + depGraph.pkgs.map((p) => p.id).sort(), + [ + 'npm-package@1.0.0', + 'ms@0.7.1', + 'debug@2.2.0', + 'object-assign@4.1.1', + ].sort(), + 'depGraph looks fine', + ); +}); + +test('`test yarn-package-with-subfolder --file=yarn.lock ` picks top-level files', async (t) => { + chdirWorkspaces(); + await cli.test('yarn-package-with-subfolder', { file: 'yarn.lock' }); + const req = server.popRequest(); + const depGraph = req.body.depGraph; + t.same( + depGraph.pkgs.map((p) => p.id).sort(), + ['yarn-package-top-level@1.0.0', 'to-array@0.1.4'].sort(), + 'depGraph looks fine', + ); +}); + +test('`test yarn-package-with-subfolder --file=subfolder/yarn.lock ` picks subfolder files', async (t) => { + chdirWorkspaces(); + await cli.test('yarn-package-with-subfolder', { + file: 'subfolder/yarn.lock', + }); + const req = server.popRequest(); + const depGraph = req.body.depGraph; + t.same( + depGraph.pkgs.map((p) => p.id).sort(), + ['yarn-package-subfolder@1.0.0', 'to-array@0.1.4'].sort(), + 'depGraph looks fine', + ); +}); + +test('`test` on a yarn package does work and displays appropriate text', async (t) => { + chdirWorkspaces('yarn-app'); + await cli.test(); + const req = server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-dep-graph', 'posts to correct url'); + t.match(req.body.targetFile, undefined, 'target is undefined'); + const depGraph = req.body.depGraph; + t.same( + depGraph.pkgs.map((p) => p.id).sort(), + ['yarn-app-one@1.0.0', 'marked@0.3.6', 'moment@2.18.1'].sort(), + 'depGraph looks fine', + ); +}); + +// @later: try and remove this config stuff +// Was copied straight from ../src/cli-server.js +after('teardown', async (t) => { + t.plan(4); + + delete process.env.SNYK_API; + delete process.env.SNYK_HOST; + delete process.env.SNYK_PORT; + t.notOk(process.env.SNYK_PORT, 'fake env values cleared'); + + await new Promise((resolve) => { + server.close(resolve); + }); + t.pass('server shutdown'); + let key = 'set'; + let value = 'api=' + oldkey; + if (!oldkey) { + key = 'unset'; + value = 'api'; + } + await cli.config(key, value); + t.pass('user config restored'); + if (oldendpoint) { + await cli.config('endpoint', oldendpoint); + t.pass('user endpoint restored'); + t.end(); + } else { + t.pass('no endpoint'); + t.end(); + } +}); + +function chdirWorkspaces(subdir = '') { + process.chdir(__dirname + '/workspaces' + (subdir ? '/' + subdir : '')); +}