diff --git a/test/acceptance/fake-server.ts b/test/acceptance/fake-server.ts index 4be36acdd8..9cd3377b08 100644 --- a/test/acceptance/fake-server.ts +++ b/test/acceptance/fake-server.ts @@ -14,6 +14,8 @@ type FakeServer = { setNextResponse: (r: any) => void; setNextStatusCode: (c: number) => void; setFeatureFlag: (featureFlag: string, enabled: boolean) => void; + unauthorizeAction: (action: string, reason?: string) => void; + getSnykToken: () => string; listen: (port: string | number, callback: () => void) => void; restore: () => void; close: (callback: () => void) => void; @@ -23,6 +25,7 @@ type FakeServer = { export const fakeServer = (basePath: string, snykToken: string): FakeServer => { let requests: express.Request[] = []; let featureFlags: Map = featureFlagDefaults(); + let unauthorizedActions = new Map(); let nextStatusCode: number | undefined = undefined; let nextResponse: any = undefined; let depGraphResponse: Record | undefined = undefined; @@ -32,6 +35,7 @@ export const fakeServer = (basePath: string, snykToken: string): FakeServer => { requests = []; depGraphResponse = undefined; featureFlags = featureFlagDefaults(); + unauthorizedActions = new Map(); }; const getRequests = () => { @@ -66,6 +70,18 @@ export const fakeServer = (basePath: string, snykToken: string): FakeServer => { featureFlags.set(featureFlag, enabled); }; + const getSnykToken = (): string => snykToken; + + const unauthorizeAction = ( + action: string, + reason = 'unauthorized by test', + ) => { + unauthorizedActions.set(action, { + allowed: false, + reason, + }); + }; + const app = express(); app.use(bodyParser.json({ limit: '50mb' })); app.use((req, res, next) => { @@ -383,9 +399,12 @@ export const fakeServer = (basePath: string, snykToken: string): FakeServer => { res.status(200).send(baseResponse); }); - app.get(basePath + '/authorization/:action', (req, res, next) => { - res.send({ result: { allowed: true } }); - return next(); + app.get(basePath + '/authorization/:action', (req, res) => { + const result = unauthorizedActions.get(req.params.action) || { + allowed: true, + reason: 'Default fake server response.', + }; + res.send({ result }); }); app.put(basePath + '/monitor/:registry/graph', (req, res, next) => { @@ -442,6 +461,8 @@ export const fakeServer = (basePath: string, snykToken: string): FakeServer => { setNextResponse, setNextStatusCode, setFeatureFlag, + unauthorizeAction, + getSnykToken, listen, restore, close, diff --git a/test/acceptance/workspaces/policy/.snyk b/test/acceptance/workspaces/policy/.snyk new file mode 100644 index 0000000000..6034f790f8 --- /dev/null +++ b/test/acceptance/workspaces/policy/.snyk @@ -0,0 +1,9 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.22.1 +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + 'npm:marked:20170907': + - '*': + reason: Default policy location test + expires: 2027-11-19T14:12:53.987Z +patch: {} diff --git a/test/jest/acceptance/snyk-auth/snyk-auth.spec.ts b/test/jest/acceptance/snyk-auth/snyk-auth.spec.ts new file mode 100644 index 0000000000..f57f2ad8f9 --- /dev/null +++ b/test/jest/acceptance/snyk-auth/snyk-auth.spec.ts @@ -0,0 +1,59 @@ +import { fakeServer } from '../../../acceptance/fake-server'; +import { createProjectFromWorkspace } from '../../util/createProject'; +import { runSnykCLI } from '../../util/runSnykCLI'; +import { removeAuth } from '../../util/removeAuth'; + +jest.setTimeout(1000 * 60); + +describe('snyk auth', () => { + let server: ReturnType; + let env: Record; + + beforeAll((done) => { + const apiPath = '/api/v1'; + const apiPort = process.env.PORT || process.env.SNYK_PORT || '12345'; + env = { + ...process.env, + SNYK_API: 'http://localhost:' + apiPort + apiPath, + SNYK_TOKEN: '123456789', // replace token from process.env + SNYK_DISABLE_ANALYTICS: '1', + }; + + server = fakeServer(apiPath, env.SNYK_TOKEN); + server.listen(apiPort, () => done()); + }); + + afterEach(() => { + server.restore(); + }); + + afterAll((done) => { + server.close(() => done()); + }); + + it('accepts valid token', async () => { + const project = await createProjectFromWorkspace('fail-on/no-vulns'); + server.setDepGraphResponse(await project.readJSON('vulns-result.json')); + + const { code, stdout } = await runSnykCLI(`auth ${server.getSnykToken()}`, { + cwd: project.path(), + env: removeAuth(project, env), + }); + + expect(code).toEqual(0); + expect(stdout).toMatch('Your account has been authenticated.'); + }); + + it('rejects invalid token', async () => { + const project = await createProjectFromWorkspace('fail-on/no-vulns'); + server.setDepGraphResponse(await project.readJSON('vulns-result.json')); + + const { code, stdout } = await runSnykCLI(`auth invalid-token`, { + cwd: project.path(), + env: removeAuth(project, env), + }); + + expect(code).toEqual(2); + expect(stdout).toMatch('Authentication failed.'); + }); +}); diff --git a/test/jest/acceptance/snyk-ignore/snyk-ignore.spec.ts b/test/jest/acceptance/snyk-ignore/snyk-ignore.spec.ts new file mode 100644 index 0000000000..d68c475700 --- /dev/null +++ b/test/jest/acceptance/snyk-ignore/snyk-ignore.spec.ts @@ -0,0 +1,109 @@ +import { load as loadPolicy } from 'snyk-policy'; +import { fakeServer } from '../../../acceptance/fake-server'; +import { createProjectFromWorkspace } from '../../util/createProject'; +import { runSnykCLI } from '../../util/runSnykCLI'; + +jest.setTimeout(1000 * 60); + +describe('snyk ignore', () => { + let server: ReturnType; + let env: Record; + + beforeAll((done) => { + const apiPath = '/api/v1'; + const apiPort = process.env.PORT || process.env.SNYK_PORT || '12345'; + env = { + ...process.env, + SNYK_API: 'http://localhost:' + apiPort + apiPath, + SNYK_TOKEN: '123456789', // replace token from process.env + SNYK_DISABLE_ANALYTICS: '1', + }; + + server = fakeServer(apiPath, env.SNYK_TOKEN); + server.listen(apiPort, () => done()); + }); + + afterEach(() => { + server.restore(); + }); + + afterAll((done) => { + server.close(() => done()); + }); + + it('creates a policy file using minimal options', async () => { + const project = await createProjectFromWorkspace('empty'); + const { code } = await runSnykCLI(`ignore --id=ID`, { + cwd: project.path(), + env: env, + }); + + expect(code).toEqual(0); + + const policy = await loadPolicy(project.path()); + expect(policy).toMatchObject({ + ignore: { + ID: [ + { + '*': { + reason: 'None Given', + expires: expect.any(Date), + created: expect.any(Date), + }, + }, + ], + }, + }); + }); + + it('creates a policy file using provided options', async () => { + const project = await createProjectFromWorkspace('empty'); + const { code } = await runSnykCLI( + `ignore --id=ID --reason=REASON --expiry=2017-10-07 --policy-path=${project.path()}`, + { + cwd: project.path(), + env: env, + }, + ); + + expect(code).toEqual(0); + const policy = await loadPolicy(project.path()); + expect(policy).toMatchObject({ + ignore: { + ID: [ + { + '*': { + reason: 'REASON', + expires: new Date('2017-10-07'), + created: expect.any(Date), + }, + }, + ], + }, + }); + }); + + it('fails on missing ID', async () => { + const project = await createProjectFromWorkspace('empty'); + const { code, stdout } = await runSnykCLI(`ignore --reason=REASON`, { + cwd: project.path(), + env: env, + }); + + expect(code).toEqual(2); + expect(stdout).toMatch('id is a required field'); + }); + + it('errors when user is not authorized to ignore', async () => { + const project = await createProjectFromWorkspace('empty'); + server.unauthorizeAction('cliIgnore', 'not allowed'); + + const { code, stdout } = await runSnykCLI(`ignore --id=ID`, { + cwd: project.path(), + env, + }); + + expect(code).toEqual(0); + expect(stdout).toMatch('not allowed'); + }); +}); diff --git a/test/jest/acceptance/snyk-monitor/json-output.spec.ts b/test/jest/acceptance/snyk-monitor/json-output.spec.ts new file mode 100644 index 0000000000..a0447579c3 --- /dev/null +++ b/test/jest/acceptance/snyk-monitor/json-output.spec.ts @@ -0,0 +1,64 @@ +import { fakeServer } from '../../../acceptance/fake-server'; +import { createProjectFromWorkspace } from '../../util/createProject'; +import { runSnykCLI } from '../../util/runSnykCLI'; + +jest.setTimeout(1000 * 60); + +describe('snyk monitor --json', () => { + let server: ReturnType; + let env: Record; + + beforeAll((done) => { + const apiPath = '/api/v1'; + const apiPort = process.env.PORT || process.env.SNYK_PORT || '12345'; + env = { + ...process.env, + SNYK_API: 'http://localhost:' + apiPort + apiPath, + SNYK_TOKEN: '123456789', // replace token from process.env + SNYK_DISABLE_ANALYTICS: '1', + }; + + server = fakeServer(apiPath, env.SNYK_TOKEN); + server.listen(apiPort, () => done()); + }); + + afterEach(() => { + server.restore(); + }); + + afterAll((done) => { + server.close(() => done()); + }); + + it('includes result details', async () => { + const project = await createProjectFromWorkspace('no-vulns'); + const { code, stdout } = await runSnykCLI(`monitor --json`, { + cwd: project.path(), + env: env, + }); + + expect(code).toEqual(0); + expect(JSON.parse(stdout)).toMatchObject({ + packageManager: 'npm', + manageUrl: 'http://localhost:12345/manage', + }); + }); + + it('includes path errors', async () => { + const project = await createProjectFromWorkspace( + 'no-supported-target-files', + ); + const { code, stdout } = await runSnykCLI(`monitor --json`, { + cwd: project.path(), + env: env, + }); + + expect(code).toEqual(3); + expect(JSON.parse(stdout)).toMatchObject({ + path: project.path(), + error: expect.stringMatching( + `Could not detect supported target files in ${project.path()}.`, + ), + }); + }); +}); diff --git a/test/jest/acceptance/snyk-policy/snyk-policy.spec.ts b/test/jest/acceptance/snyk-policy/snyk-policy.spec.ts new file mode 100644 index 0000000000..af12a9a7fd --- /dev/null +++ b/test/jest/acceptance/snyk-policy/snyk-policy.spec.ts @@ -0,0 +1,54 @@ +import { fakeServer } from '../../../acceptance/fake-server'; +import { createProjectFromWorkspace } from '../../util/createProject'; +import { runSnykCLI } from '../../util/runSnykCLI'; + +jest.setTimeout(1000 * 60); + +describe('snyk policy', () => { + let server: ReturnType; + let env: Record; + + beforeAll((done) => { + const apiPath = '/api/v1'; + const apiPort = process.env.PORT || process.env.SNYK_PORT || '12345'; + env = { + ...process.env, + SNYK_API: 'http://localhost:' + apiPort + apiPath, + SNYK_TOKEN: '123456789', // replace token from process.env + SNYK_DISABLE_ANALYTICS: '1', + }; + + server = fakeServer(apiPath, env.SNYK_TOKEN); + server.listen(apiPort, () => done()); + }); + + afterEach(() => { + server.restore(); + }); + + afterAll((done) => { + server.close(() => done()); + }); + + it('loads policy file', async () => { + const project = await createProjectFromWorkspace('policy'); + const { code, stdout } = await runSnykCLI('policy', { + cwd: project.path(), + env: env, + }); + + expect(code).toEqual(0); + expect(stdout).toMatch('Current Snyk policy, read from .snyk file'); + }); + + it('fails when policy not found', async () => { + const project = await createProjectFromWorkspace('empty'); + const { code, stdout } = await runSnykCLI('policy', { + cwd: project.path(), + env: env, + }); + + expect(code).toEqual(2); + expect(stdout).toMatch('Could not load policy.'); + }); +}); diff --git a/test/jest/acceptance/snyk-test/json-output.spec.ts b/test/jest/acceptance/snyk-test/json-output.spec.ts new file mode 100644 index 0000000000..86f1209bdc --- /dev/null +++ b/test/jest/acceptance/snyk-test/json-output.spec.ts @@ -0,0 +1,50 @@ +import { fakeServer } from '../../../acceptance/fake-server'; +import { createProjectFromWorkspace } from '../../util/createProject'; +import { runSnykCLI } from '../../util/runSnykCLI'; + +jest.setTimeout(1000 * 60); + +describe('snyk test --json', () => { + let server: ReturnType; + let env: Record; + + beforeAll((done) => { + const apiPath = '/api/v1'; + const apiPort = process.env.PORT || process.env.SNYK_PORT || '12345'; + env = { + ...process.env, + SNYK_API: 'http://localhost:' + apiPort + apiPath, + SNYK_TOKEN: '123456789', // replace token from process.env + SNYK_DISABLE_ANALYTICS: '1', + }; + + server = fakeServer(apiPath, env.SNYK_TOKEN); + server.listen(apiPort, () => done()); + }); + + afterEach(() => { + server.restore(); + }); + + afterAll((done) => { + server.close(() => done()); + }); + + it('includes path errors', async () => { + const project = await createProjectFromWorkspace( + 'no-supported-target-files', + ); + const { code, stdout } = await runSnykCLI(`test --json`, { + cwd: project.path(), + env: env, + }); + + expect(code).toEqual(3); + expect(JSON.parse(stdout)).toMatchObject({ + path: project.path(), + error: expect.stringMatching( + `Could not detect supported target files in ${project.path()}.`, + ), + }); + }); +}); diff --git a/test/jest/acceptance/snyk-test/no-auth.spec.ts b/test/jest/acceptance/snyk-test/no-auth.spec.ts new file mode 100644 index 0000000000..7efa7bd7ad --- /dev/null +++ b/test/jest/acceptance/snyk-test/no-auth.spec.ts @@ -0,0 +1,47 @@ +import { fakeServer } from '../../../acceptance/fake-server'; +import { createProjectFromWorkspace } from '../../util/createProject'; +import { runSnykCLI } from '../../util/runSnykCLI'; +import { removeAuth } from '../../util/removeAuth'; + +jest.setTimeout(1000 * 60); + +describe('snyk test without authentication', () => { + let server: ReturnType; + let env: Record; + + beforeAll((done) => { + const apiPath = '/api/v1'; + const apiPort = process.env.PORT || process.env.SNYK_PORT || '12345'; + env = { + ...process.env, + SNYK_API: 'http://localhost:' + apiPort + apiPath, + SNYK_TOKEN: '123456789', + SNYK_DISABLE_ANALYTICS: '1', + }; + + server = fakeServer(apiPath, env.SNYK_TOKEN); + server.listen(apiPort, () => done()); + }); + + afterEach(() => { + server.restore(); + }); + + afterAll((done) => { + server.close(() => done()); + }); + + it('errors when auth token is not provided', async () => { + const project = await createProjectFromWorkspace('no-vulns'); + + const { code, stdout } = await runSnykCLI(`test`, { + cwd: project.path(), + env: removeAuth(project, env), + }); + + expect(code).toEqual(0); + expect(stdout).toMatch( + '`snyk` requires an authenticated account. Please run `snyk auth` and try again.', + ); + }); +}); diff --git a/test/jest/util/createProject.ts b/test/jest/util/createProject.ts index 36f49503ab..8704d95f93 100644 --- a/test/jest/util/createProject.ts +++ b/test/jest/util/createProject.ts @@ -2,7 +2,7 @@ import * as fse from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; -type TestProject = { +export type TestProject = { path: (filePath?: string) => string; read: (filePath: string) => Promise; readJSON: (filePath: string) => Promise; diff --git a/test/jest/util/removeAuth.ts b/test/jest/util/removeAuth.ts new file mode 100644 index 0000000000..30f5c5fc06 --- /dev/null +++ b/test/jest/util/removeAuth.ts @@ -0,0 +1,18 @@ +import omit = require('lodash.omit'); +import { TestProject } from './createProject'; + +/** + * Removes authentication tokens from environment variables and persisted + * config stores. The latter is done by pointing the config path to a local + * directory within the test project rather than using the user's + * home directory. + */ +export const removeAuth = ( + project: TestProject, + env: Record, +): Record => { + return { + ...omit(env, ['SNYK_TOKEN']), + XDG_CONFIG_HOME: project.path('.config'), + }; +}; diff --git a/test/system/cli.test.ts b/test/system/cli.test.ts index ec18bddf44..5a922fa1c3 100644 --- a/test/system/cli.test.ts +++ b/test/system/cli.test.ts @@ -1,29 +1,13 @@ import * as util from 'util'; -import * as get from 'lodash.get'; -import * as isObject from 'lodash.isobject'; import { test } from 'tap'; import * as ciChecker from '../../src/lib/is-ci'; import * as dockerChecker from '../../src/lib/is-docker'; -import { makeTmpDirectory, silenceLog } from '../utils'; +import { silenceLog } from '../utils'; import * as sinon from 'sinon'; import * as proxyquire from 'proxyquire'; -import * as policy from 'snyk-policy'; -import stripAnsi from 'strip-ansi'; import * as os from 'os'; import * as isDocker from '../../src/lib/is-docker'; -type Ignore = { - [path: string]: { - reason: string; - expires: Date; - created?: Date; - }; -}; - -type Policy = { - [id: string]: Ignore[]; -}; - const port = process.env.PORT || process.env.SNYK_PORT || '12345'; const apiKey = '123456789'; @@ -42,8 +26,6 @@ sinon.stub(util, 'promisify').returns(() => {}); // ensure this is required *after* the demo server, since this will // configure our fake configuration too import * as cli from '../../src/cli/commands'; -import { PolicyNotFoundError } from '../../src/lib/errors'; -import { chdirWorkspaces } from '../acceptance/workspace-helper'; const before = test; const after = test; @@ -67,47 +49,6 @@ before('prime config', async (t) => { t.pass('endpoint removed'); }); -test('test without authentication', async (t) => { - await cli.config('unset', 'api'); - try { - await cli.test('semver@2'); - t.fail('test should not pass if not authenticated'); - } catch (error) { - t.deepEquals(error.strCode, 'NO_API_TOKEN', 'string code is as expected'); - t.match( - error.message, - '`snyk` requires an authenticated account. Please run `snyk auth` and try again.', - 'error message is shown as expected', - ); - } - await cli.config('set', 'api=' + apiKey); -}); - -test('auth via key', async (t) => { - try { - const res = await cli.auth(apiKey); - t.notEqual(res.toLowerCase().indexOf('ready'), -1, 'snyk auth worked'); - } catch (e) { - t.threw(e); - } -}); - -test('auth via invalid key', async (t) => { - const errors = require('../../src/lib/errors/legacy-errors'); - - try { - const res = await cli.auth('_____________'); - t.fail('auth should not succeed: ' + res); - } catch (e) { - const message = stripAnsi(errors.message(e)); - t.equal( - message.toLowerCase().indexOf('authentication failed'), - 0, - 'captured failed auth', - ); - } -}); - test('auth with no args', async (t) => { // stub open so browser window doesn't actually open const open = sinon.stub(); @@ -227,182 +168,6 @@ test('auth with default UTMs', async (t) => { t.threw(e); } }); -test('cli tests error paths', async (t) => { - try { - await cli.test('/', { json: true }); - t.fail('expected exception to be thrown'); - } catch (error) { - const errObj = JSON.parse(error.message); - t.ok(errObj.error.length > 1, 'should display error message'); - t.match(errObj.path, '/', 'path property should be populated'); - t.pass('error json with correct output when one bad project specified'); - } -}); - -test('snyk ignore - all options', async (t) => { - const clock = sinon.useFakeTimers(new Date(2016, 11, 1).getTime()); - const fullPolicy: Policy = { - ID: [ - { - '*': { - reason: 'REASON', - expires: new Date('2017-10-07T00:00:00.000Z'), - }, - }, - ], - }; - try { - fullPolicy.ID[0]['*'].created = new Date(); - const dir = await makeTmpDirectory(); - await cli.ignore({ - id: 'ID', - reason: 'REASON', - expiry: new Date('2017-10-07'), - 'policy-path': dir, - }); - const pol = await policy.load(dir); - t.deepEquals(pol.ignore, fullPolicy, 'policy written correctly'); - clock.restore(); - } catch (err) { - t.throws(err, 'ignore should succeed'); - } -}); - -test('snyk ignore - no ID', async (t) => { - try { - const dir = await makeTmpDirectory(); - await cli.ignore({ - reason: 'REASON', - expiry: new Date('2017-10-07'), - 'policy-path': dir, - }); - t.fail('should not succeed with missing ID'); - } catch (e) { - const errors = require('../../src/lib/errors/legacy-errors'); - const message = stripAnsi(errors.message(e)); - t.equal( - message.toLowerCase().indexOf('id is a required field'), - 0, - 'captured failed ignore (no --id given)', - ); - } -}); - -test('snyk ignore - default options', async (t) => { - const clock = sinon.useFakeTimers(new Date(2016, 11, 1).getTime()); - try { - const dir = await makeTmpDirectory(); - await cli.ignore({ - id: 'ID3', - 'policy-path': dir, - }); - const pol = await policy.load(dir); - t.true(pol.ignore.ID3, 'policy ID written correctly'); - t.is( - pol.ignore.ID3[0]['*'].reason, - 'None Given', - 'policy (default) reason written correctly', - ); - const expiryFromNow = pol.ignore.ID3[0]['*'].expires - Date.now(); - // not more than 30 days ahead, not less than (30 days - 1 minute) - t.true( - expiryFromNow <= 30 * 24 * 60 * 60 * 1000 && - expiryFromNow >= 30 * 24 * 59 * 60 * 1000, - 'policy (default) expiry written correctly', - ); - t.strictEquals( - pol.ignore.ID3[0]['*'].created.getTime(), - new Date().getTime(), - 'created date is the current date', - ); - clock.restore(); - } catch (e) { - t.fail(e, 'ignore should succeed'); - } -}); - -test('snyk ignore - not authorized', async (t) => { - const dir = await makeTmpDirectory(); - try { - await cli.config('set', 'api=' + notAuthorizedApiKey); - await cli.ignore({ - id: 'ID3', - 'policy-path': dir, - }); - } catch (err) { - t.throws(err, 'ignore should succeed'); - } - try { - await policy.load(dir); - } catch (err) { - t.pass('no policy file saved'); - } -}); - -test('snyk policy', async (t) => { - await cli.policy(); - t.pass('policy called'); - - try { - await cli.policy('wrong/path'); - } catch (error) { - t.match(error, PolicyNotFoundError); - } -}); - -test('monitor', async (t) => { - try { - const res = await cli.monitor(); - t.match(res, /Monitoring/, 'monitor captured'); - } catch (error) { - t.fail(error); - } -}); - -test('monitor --json', async (t) => { - try { - const response = await cli.monitor(undefined, { json: true }); - const res = JSON.parse(response); - - if (isObject(res)) { - t.pass('monitor outputted JSON'); - } else { - t.fail('Failed parsing monitor JSON output'); - } - - const keyList = ['packageManager', 'manageUrl']; - - keyList.forEach((k) => { - !get(res, k) ? t.fail(k + 'not found') : t.pass(k + ' found'); - }); - } catch (error) { - t.fail(error); - } -}); - -test('monitor --json no supported target files', async (t) => { - try { - chdirWorkspaces(); - await cli.monitor('no-supported-target-files', { json: true }); - t.fail('should have thrown'); - } catch (error) { - // error.json is a stringified json used for error logging, parse before testing - const jsonResponse = JSON.parse(error.json); - - if (isObject(jsonResponse)) { - t.pass('monitor outputted JSON'); - } else { - t.fail('Failed parsing monitor JSON output'); - } - - const keyList = ['error', 'path']; - t.equals(jsonResponse.ok, false, 'result is an error'); - - keyList.forEach((k) => { - t.ok(get(jsonResponse, k, null), `${k} present`); - }); - } -}); after('teardown', async (t) => { delete process.env.SNYK_API;