diff --git a/.nvmrc b/.nvmrc index 91d73a1..7a3707e 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1,2 +1,2 @@ -12 +14 package-lock=false diff --git a/README.md b/README.md index d15e23c..cb4815b 100644 --- a/README.md +++ b/README.md @@ -7,4 +7,23 @@ Snyk helps you find, fix and monitor for known vulnerabilities in your dependencies, both on an ad hoc basis and as part of your CI (Build) system. ## Snyk snyk-licenses-texts -Snyk Licenses Texts +Snyk Licenses Report that provides Organization level licenses used, copyrights & dependencies data (including license texts & their urls). + +## Usage +Ensure `SNYK_TOKEN` is set and has access to the Organization you want to generate the report for. + +### Basic CLI commands +- `help` - show help & all available commands and their options +- `json` - generate the raw JSON licenses & dependencies data +- `generate` - generates an HTML report of licenses & dependencies data + +Example usage: +`snyk-licenses-report help` +`snyk-licenses-report json --orgPublicId=` +`snyk-licenses-report generate --orgPublicId=` + + +## Development setup +- `npm i` +- `npm run test` +- `DEBUG=snyk-license* node dist/index.js generate --orgPublicId=` diff --git a/package.json b/package.json index de7f220..88e2f73 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,15 @@ "test:unit": "jest", "test:coverage": "npm run test:unit -- --coverage", "test:watch": "tsc-watch --onSuccess 'npm run test:unit'", - "build": "tsc", - "build-watch": "tsc -w", + "build": "tsc && npm run copy:templates", + "build-watch": "tsc -w && npm run copy:templates", "prepare": "npm run build", "snyk-test": "snyk test", - "pkg-binaries": "npx pkg . -t node12-linux-x64,node12-macos-x64,node12-win-x64 --out-path ./dist/binaries" + "pkg-binaries": "npx pkg . -t node12-linux-x64,node12-macos-x64,node12-win-x64 --out-path ./dist/binaries", + "copy:templates": "cpx 'src/**/*.hbs' ./dist" + }, + "bin": { + "snyk-licenses-report": "dist/index.js" }, "types": "./dist/index.d.ts", "repository": { @@ -25,11 +29,12 @@ "author": "Snyk Tech Services", "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=14" }, "files": [ "bin", - "dist" + "dist", + "src/lib/generate-output/html/templates" ], "homepage": "https://github.com/snyk-tech-services/snyk-licenses-texts#readme", "dependencies": { @@ -39,14 +44,16 @@ "debug": "4.1.1", "handlebars": "4.7.6", "lodash": "4.17.20", - "node-fetch": "2.6.0", + "node-fetch": "2.6.1", "snyk-api-ts-client": "1.5.0", "snyk-config": "^3.0.0", "source-map-support": "^0.5.16", "tslib": "2.0.1", - "yargs": "16.0.2" + "yargs": "16.0.2", + "yargs-command-config": "1.0.5" }, "devDependencies": { + "cpx": "1.5.0", "@types/jest": "26.0.13", "@types/lodash": "4.14.161", "@types/node": "14.6.3", @@ -64,6 +71,9 @@ "pkg": { "scripts": [ "dist/**/*.js" + ], + "assets": [ + "src/**/templates/*.hbs" ] }, "release": { diff --git a/src/cmds/generate.ts b/src/cmds/generate.ts index e69de29..3f4c618 100644 --- a/src/cmds/generate.ts +++ b/src/cmds/generate.ts @@ -0,0 +1,82 @@ +import * as debugLib from 'debug'; +import * as pathLib from 'path'; +import { getApiToken } from '../lib/get-api-token'; +import { + LicenseReportData, + generateLicenseData, +} from '../lib/generate-org-license-report'; +import { + generateHtmlReport, + generatePdfReport, + SupportedViews, +} from '../lib/generate-output'; +import { writeContentsToFile } from '../lib/write-contents-to-file'; +const debug = debugLib('snyk-licenses:generate'); + +const outputHandlers = { + [OutputFormat.HTML]: generateHtmlReport, + // [OutputFormat.PDF]: generatePdfReport +}; +const enum OutputFormat { + HTML = 'html', + // TODO: support later + // PDF = 'pdf', +} + +export const desc = + 'Generate org licenses & dependencies report in HTML format'; +export const builder = { + orgPublicId: { + required: true, + default: undefined, + desc: + 'Public id of the organization in Snyk (available on organization settings)', + }, + template: { + default: undefined, + desc: 'Path to custom Handelbars.js template file (*.hbs)', + }, + outputFormat: { + default: OutputFormat.HTML, + desc: 'Report format', + // TODO: add also PDF when ready + options: [OutputFormat.HTML], + }, + view: { + // TODO: add also dependency based view when ready + default: SupportedViews.ORG_LICENSES, + desc: + 'How should the data be represented. Defaults to a license based view.', + }, +}; +export const aliases = ['g']; + +export async function handler(argv: { + orgPublicId: string; + outputFormat: OutputFormat; + template: string; + view: SupportedViews; +}) { + try { + const { orgPublicId, outputFormat, template, view } = argv; + debug( + 'ℹ️ Options: ' + + JSON.stringify({ orgPublicId, outputFormat, template, view }), + ); + getApiToken(); + const licenseData: LicenseReportData = await generateLicenseData( + orgPublicId, + ); + const generateReportFunc = outputHandlers[outputFormat]; + const res = await generateReportFunc(licenseData, template, view); + if (res) { + const outputFileName = `${orgPublicId}-${view}.html`; + const outputFile = pathLib.resolve(__dirname, outputFileName); + debug(`ℹ️ Saving generated report to ${outputFile}`); + writeContentsToFile(res, outputFile); + console.log('License report saved at ' + outputFile); + } + } catch (e) { + console.error(e); + } +} diff --git a/src/cmds/json.ts b/src/cmds/json.ts index d222446..fe64052 100644 --- a/src/cmds/json.ts +++ b/src/cmds/json.ts @@ -1,5 +1,10 @@ +import * as debugLib from 'debug'; + import { getApiToken } from '../lib/get-api-token'; -import { generateOrgLicensesReport } from '../lib/generate-org-license-report'; +import { generateLicenseData } from '../lib/generate-org-license-report'; +import { SupportedViews } from '../lib/generate-output'; + +const debug = debugLib('snyk-licenses:json'); export const command = 'json'; export const desc = 'Generate org licenses & dependencies data in JSON format'; @@ -8,16 +13,27 @@ export const builder = { required: true, default: undefined, }, + view: { + // TODO: add also dependency based view when ready + default: SupportedViews.ORG_LICENSES, + desc: + 'How should the data be represented. Defaults to a license based view.', + }, }; export const aliases = ['j']; -export async function handler(argv: { orgPublicId: string }) { +export async function handler(argv: { + orgPublicId: string; + view: SupportedViews; +}) { try { + const { orgPublicId, view } = argv; + debug('ℹ️ Options: ' + JSON.stringify({ orgPublicId, view })); // check SNYK_TOKEN is set as the sdk uses it getApiToken(); // TODO: define and pass options to help filter the response // based on filters available in API - const data = await generateOrgLicensesReport(argv.orgPublicId, {}); + const data = await generateLicenseData(orgPublicId, {}); console.log(JSON.stringify(data)); } catch (e) { console.error(e); diff --git a/src/lib/api/org/dependencies.ts b/src/lib/api/org/dependencies.ts index 5d33445..ecf4da2 100644 --- a/src/lib/api/org/dependencies.ts +++ b/src/lib/api/org/dependencies.ts @@ -4,7 +4,7 @@ import * as snykApiSdk from 'snyk-api-ts-client'; import { getApiToken } from '../../get-api-token'; import { SortBy, Order } from './types'; -const debug = debugLib('getDependenciesDataForOrg'); +const debug = debugLib('snyk-licenses:getDependenciesDataForOrg'); interface GetDependenciesDataOptions { sortBy: SortBy; @@ -32,7 +32,7 @@ export async function getDependenciesDataForOrg( ); return dependenciesData; } catch (e) { - debug('Failed to fetch dependencies' + e); + debug('❌ Failed to fetch dependencies' + e); throw e; } } diff --git a/src/lib/api/org/licenses.ts b/src/lib/api/org/licenses.ts index e945a6c..9e747a7 100644 --- a/src/lib/api/org/licenses.ts +++ b/src/lib/api/org/licenses.ts @@ -4,7 +4,7 @@ import * as snykApiSdk from 'snyk-api-ts-client'; import { getApiToken } from '../../get-api-token'; import { SortBy, Order } from './types'; -const debug = debugLib('getLicenseDataForOrg'); +const debug = debugLib('snyk-licenses:getLicenseDataForOrg'); interface GetLicenseDataOptions { sortBy: SortBy; @@ -27,7 +27,7 @@ export async function getLicenseDataForOrg( const licenseData = await getAllLicensesData(snykApiClient, body, sortBy, order); return licenseData; } catch (e) { - debug('Failed to fetch licenses' + e); + debug('❌ Failed to fetch licenses' + e); throw e; } } diff --git a/src/lib/generate-org-license-report.ts b/src/lib/generate-org-license-report.ts index e9365ad..0bfe51a 100644 --- a/src/lib/generate-org-license-report.ts +++ b/src/lib/generate-org-license-report.ts @@ -7,43 +7,30 @@ import { getLicenseDataForOrg, getDependenciesDataForOrg } from './api/org'; import { fetchSpdxLicenseTextAndUrl, fetchNonSpdxLicenseTextAndUrl } from './license-text'; import { LicenseReportDataEntry, EnrichedDependency, Dependency } from './types'; -const debug = debugLib('generateOrgLicensesReport'); +const debug = debugLib('snyk-licenses:generateOrgLicensesReport'); -interface LicenseReportData { +export interface LicenseReportData { [licenseID: string]: LicenseReportDataEntry; } -export async function generateOrgLicensesReport( - orgPublicId: string, - options, -): Promise { - try { - const licenseData: LicenseReportData = await generateLicenseData( - orgPublicId, - options, - ); - const report = licenseData; - return report as any; - } catch (e) { - debug('Failed to generate report data', e); - throw e; - } -} - export async function generateLicenseData( orgPublicId: string, - options, + options?, ): Promise { + debug(`ℹ️ Generating license data for Org:${orgPublicId}`); + try { const licenseData = await getLicenseDataForOrg(orgPublicId, options); + debug(`✅ Got license API data for Org:${orgPublicId}`); const dependenciesDataRaw = await getDependenciesDataForOrg( orgPublicId, options, ); + debug(`✅ Got dependencies API data for Org:${orgPublicId}`); const licenseReportData: LicenseReportData = {}; const dependenciesData = _.groupBy(dependenciesDataRaw.results, 'id'); // TODO: what if 0? - debug(`Processing ${licenseData.total} licenses`); + debug(`⏳ Processing ${licenseData.total} licenses`); const dependenciesAll = []; for (const license of licenseData.results) { @@ -68,9 +55,11 @@ export async function generateLicenseData( licenseUrl: licenseData?.licenseUrl, }; } + debug(`✅ Done processing ${licenseData.total} licenses`); + return licenseReportData; } catch (e) { - debug('Failed to generate report data', e); + debug('❌ Failed to generate report data', e); throw e; } } @@ -102,12 +91,12 @@ async function getLicenseTextAndUrl( try { return await fetchSpdxLicenseTextAndUrl(id); } catch (e) { - debug('Failed to get license data as SPDX, trying non-SPDX'); + debug(`❌ Failed to get license data for as SPDX, trying non-SPDX: ${id}`); } try { return await fetchNonSpdxLicenseTextAndUrl(id); } catch (e) { - debug('Failed to get license data as non-SPDX'); + debug(`❌ Failed to get license data as non-SPDX: ${id}`); } return undefined; diff --git a/src/lib/generate-output/html/index.ts b/src/lib/generate-output/html/index.ts new file mode 100644 index 0000000..838c673 --- /dev/null +++ b/src/lib/generate-output/html/index.ts @@ -0,0 +1,75 @@ +import * as Handlebars from 'handlebars'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as debugLib from 'debug'; + +import { LicenseReportData } from '../../generate-org-license-report'; + +const debug = debugLib('snyk-licenses:generateHtmlReport'); +const DEFAULT_TEMPLATE = './templates/licenses-view.hbs'; + +export const enum SupportedViews { + ORG_LICENSES = 'org-licenses', + // TODO: support later + // PROJECT_DEPENDENCIES = 'project-dependencies', +} + +export async function generateHtmlReport( + data: LicenseReportData, + templateOverridePath: string | undefined = undefined, + view: SupportedViews = SupportedViews.ORG_LICENSES, +) { + // TODO: add any helpers & data transformations that are useful here + debug('ℹ️ Generating HTML report'); + const hbsTemplate = selectTemplate(view, templateOverridePath); + debug( + `✅ Using template ${ + hbsTemplate === DEFAULT_TEMPLATE ? 'default template' : hbsTemplate + }`, + ); + await registerPeerPartial(hbsTemplate, 'inline-css'); + debug(`✅ Registered Handlebars.js partials`); + const htmlTemplate = await compileTemplate(hbsTemplate); + debug(`✅ Compiled template ${hbsTemplate}`); + return htmlTemplate(data); +} + +function selectTemplate(view: SupportedViews, templateOverride?): string { + switch (view) { + case SupportedViews.ORG_LICENSES: + return templateOverride || DEFAULT_TEMPLATE; + // TODO: support later + // case SupportedViews.PROJECT_DEPENDENCIES: + // return templateOverride || '../templates/project-dependencies-view.hbs'; + default: + return DEFAULT_TEMPLATE; + } +} + +async function registerPeerPartial( + templatePath: string, + name: string, +): Promise { + const file = path.join(__dirname, templatePath); + const template = await compileTemplate(file); + Handlebars.registerPartial(name, template); +} + +async function compileTemplate( + fileName: string, +): Promise { + return readFile(path.resolve(__dirname, fileName), 'utf8').then( + Handlebars.compile, + ); +} + +function readFile(filePath: string, encoding: string): Promise { + return new Promise((resolve, reject) => { + fs.readFile(filePath, encoding, (err, data) => { + if (err) { + reject(err); + } + resolve(data); + }); + }); +} diff --git a/src/lib/generate-output/html/templates/inline-css.hbs b/src/lib/generate-output/html/templates/inline-css.hbs new file mode 100644 index 0000000..d498387 --- /dev/null +++ b/src/lib/generate-output/html/templates/inline-css.hbs @@ -0,0 +1,2 @@ + diff --git a/src/lib/generate-output/html/templates/licenses-view.hbs b/src/lib/generate-output/html/templates/licenses-view.hbs new file mode 100644 index 0000000..b17e16d --- /dev/null +++ b/src/lib/generate-output/html/templates/licenses-view.hbs @@ -0,0 +1,21 @@ + + + + + + + + Snyk Licenses Report + + + {{!-- {{> inline-css }} --}} + + + +
+

Hello!

+
+ + + diff --git a/src/lib/generate-output/html/templates/project-dependencies-view.hbs b/src/lib/generate-output/html/templates/project-dependencies-view.hbs new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/generate-output/index.ts b/src/lib/generate-output/index.ts new file mode 100644 index 0000000..0c174fb --- /dev/null +++ b/src/lib/generate-output/index.ts @@ -0,0 +1,2 @@ +export * from './html'; +export * from './pdf'; diff --git a/src/lib/generate-output/pdf/index.ts b/src/lib/generate-output/pdf/index.ts new file mode 100644 index 0000000..b490a00 --- /dev/null +++ b/src/lib/generate-output/pdf/index.ts @@ -0,0 +1,5 @@ +export function generatePdfReport() { + // TODO: find a package that could convert the HTML to pdf + // or where we could use the same handlebars template & css + // to generate the pdf +} diff --git a/src/lib/license-text/non-spdx.ts b/src/lib/license-text/non-spdx.ts index d92a3f0..eb7bac2 100644 --- a/src/lib/license-text/non-spdx.ts +++ b/src/lib/license-text/non-spdx.ts @@ -6,7 +6,7 @@ import * as path from 'path'; export async function fetchNonSpdxLicenseTextAndUrl( licenseId: string, ): Promise<{ licenseText: string; licenseUrl: string }> { - const debug = debugLib('fetchNonSpdxLicenseText'); + const debug = debugLib('snyk-licenses:fetchNonSpdxLicenseText'); const fileName = `licenses/${licenseId}.html`; try { const licenseText = await fs.readFileSync( diff --git a/src/lib/license-text/spdx.ts b/src/lib/license-text/spdx.ts index c35369e..4d511de 100644 --- a/src/lib/license-text/spdx.ts +++ b/src/lib/license-text/spdx.ts @@ -6,7 +6,7 @@ import * as cheerio from 'cheerio'; export async function fetchSpdxLicenseTextAndUrl( licenseId: string, ): Promise<{ licenseText: string; licenseUrl: string }> { - const debug = debugLib('fetchSpdxLicenseText'); + const debug = debugLib('snyk-licenses:fetchSpdxLicenseText'); const licenseUrl = `https://spdx.org/licenses/${licenseId}.html`; try { const res = await fetch(licenseUrl); diff --git a/src/lib/write-contents-to-file.ts b/src/lib/write-contents-to-file.ts new file mode 100644 index 0000000..838bfd5 --- /dev/null +++ b/src/lib/write-contents-to-file.ts @@ -0,0 +1,69 @@ +import { gte } from 'semver'; +import * as pathLib from 'path'; +import * as debugLib from 'debug'; +import { existsSync, mkdirSync, createWriteStream } from 'fs'; +export const MIN_VERSION_FOR_MKDIR_RECURSIVE = '10.12.0'; + +const debug = debugLib('snyk-licenses:writeContentsToFile'); + +function writeContentsToFileSwallowingErrors( + outputFile: string, + contents: string, +) { + try { + const ws = createWriteStream(outputFile, { flags: 'w' }); + ws.on('error', (err) => { + console.error(err); + }); + ws.write(contents); + ws.end('\n'); + } catch (err) { + console.error(err); + } +} + +export function writeContentsToFile(contents: string, outputFile: string) { + if (!outputFile) { + return; + } + + if (outputFile.constructor.name !== String.name) { + console.error('--json-output-file should be a filename path'); + return; + } + + // create the directory if it doesn't exist + const dirPath = pathLib.dirname(outputFile); + const createDirSuccess = createDirectory(dirPath); + if (createDirSuccess) { + writeContentsToFileSwallowingErrors(outputFile, contents); + } +} + +function createDirectory(newDirectoryFullPath: string): boolean { + // if the path already exists, true + // if we successfully create the directory, return true + // if we can't successfully create the directory, either because node < 10 and recursive or some other failure, catch the error and return false + + if (existsSync(newDirectoryFullPath)) { + return true; + } + + const nodeVersion = process.version; + + try { + if (gte(nodeVersion, MIN_VERSION_FOR_MKDIR_RECURSIVE)) { + // nodeVersion is >= 10.12.0 - required for mkdirsync recursive + const options: any = { recursive: true }; // TODO: remove this after we drop support for node v8 + mkdirSync(newDirectoryFullPath, options); + return true; + } else { + // nodeVersion is < 10.12.0 + mkdirSync(newDirectoryFullPath); + return true; + } + } catch (err) { + debug(`Could not create directory ${newDirectoryFullPath}: ${err}`); + return false; + } +} diff --git a/test/lib/__snapshots__/generate-html-report.test.ts.snap b/test/lib/__snapshots__/generate-html-report.test.ts.snap new file mode 100644 index 0000000..f845253 --- /dev/null +++ b/test/lib/__snapshots__/generate-html-report.test.ts.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Generate HTML report License HTML Report is generated as expected 1`] = ` +" + + + + + + + Snyk Licenses Report + + + + + +
+

Hello!

+
+ + + +" +`; diff --git a/test/lib/generate-html-report.test.ts b/test/lib/generate-html-report.test.ts new file mode 100644 index 0000000..6e5ef76 --- /dev/null +++ b/test/lib/generate-html-report.test.ts @@ -0,0 +1,24 @@ +import { + generateHtmlReport, +} from '../../src/lib/generate-output'; +import { generateLicenseData } from '../../src/lib/generate-org-license-report'; +describe('Generate HTML report', () => { + const OLD_ENV = process.env; + process.env.SNYK_TOKEN = process.env.SNYK_TEST_TOKEN; + const ORG_ID = process.env.TEST_ORG_ID as string; + + afterAll(async () => { + process.env = { ...OLD_ENV }; + }); + test('SNYK_TOKEN & ORG_ID are set', async () => { + expect(process.env.SNYK_TOKEN).not.toBeNull(); + expect(process.env.ORG_ID).not.toBeNull(); + }); + test('License HTML Report is generated as expected', async () => { + const licenseRes = await generateLicenseData(ORG_ID, {}); + const htmlData = await generateHtmlReport(licenseRes); + expect(htmlData).toMatchSnapshot(); + }, 50000); + + test.todo('Test for when API fails aka bad org id provided'); +}); diff --git a/test/lib/generate-license-report-data.test.ts b/test/lib/generate-license-report-data.test.ts index d514bbf..5c7a0a3 100644 --- a/test/lib/generate-license-report-data.test.ts +++ b/test/lib/generate-license-report-data.test.ts @@ -1,4 +1,5 @@ import { generateLicenseData } from '../../src/lib/generate-org-license-report'; +import { generateHtmlReport } from '../../src/lib/generate-output'; describe('Get org licenses', () => { const OLD_ENV = process.env; @@ -24,4 +25,6 @@ describe('Get org licenses', () => { expect(licenseRes['Unlicense'].dependencies[0].issuesMedium).not.toBeNull(); expect(licenseRes['Unlicense'].dependencies[0].latestVersion).not.toBeNull(); }, 50000); + + test.todo('Test for when API fails aka bad org id provided'); }); diff --git a/test/lib/get-dependencies-data.test.ts b/test/lib/get-dependencies-data.test.ts index 5fd79f5..a6c5787 100644 --- a/test/lib/get-dependencies-data.test.ts +++ b/test/lib/get-dependencies-data.test.ts @@ -16,4 +16,5 @@ describe('Get org dependencies', () => { const licenseRes = await getDependenciesDataForOrg(ORG_ID); expect(licenseRes.results.length > 0).toBeTruthy(); }, 50000); + test.todo('Test for when API fails aka bad org id provided'); }); diff --git a/test/lib/get-license-data.test.ts b/test/lib/get-license-data.test.ts index ea03912..a6fcf1c 100644 --- a/test/lib/get-license-data.test.ts +++ b/test/lib/get-license-data.test.ts @@ -16,4 +16,6 @@ describe('Get org licenses', () => { const licenseRes = await getDependenciesDataForOrg(ORG_ID); expect(licenseRes.results.length > 0).toBeTruthy(); }, 50000); + + test.todo('Test for when API fails aka bad org id provided'); }); diff --git a/test/system/__snapshots__/basic.test.ts.snap b/test/system/__snapshots__/basic.test.ts.snap new file mode 100644 index 0000000..aa28c42 --- /dev/null +++ b/test/system/__snapshots__/basic.test.ts.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`\`snyk-licenses-report help <...>\` Shows help text as expected 1`] = ` +"index.js + +Commands: + index.js generate Generate org licenses & dependencies report in HTML format + [aliases: g] + index.js json Generate org licenses & dependencies data in JSON format + [aliases: j] + +Options: + --version Show version number [boolean] + --help Show help [boolean]" +`; diff --git a/test/system/__snapshots__/generate.test.ts.snap b/test/system/__snapshots__/generate.test.ts.snap new file mode 100644 index 0000000..367ca9a --- /dev/null +++ b/test/system/__snapshots__/generate.test.ts.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`\`snyk-licenses-report generate <...>\` Shows error when missing --orgPublicId 1`] = ` +"Command failed: node ./dist/index.js generate +index.js generate + +Generate org licenses & dependencies report in HTML format + +Options: + --version Show version number [boolean] + --help Show help [boolean] + --orgPublicId Public id of the organization in Snyk (available on + organization settings) [required] + --template Path to custom Handelbars.js template file (*.hbs) + --outputFormat Report format [default: \\"html\\"] + --view How should the data be represented. Defaults to a license + based view. [default: \\"org-licenses\\"] + +Missing required argument: orgPublicId" +`; diff --git a/test/system/__snapshots__/json.test.ts.snap b/test/system/__snapshots__/json.test.ts.snap new file mode 100644 index 0000000..19ebe42 --- /dev/null +++ b/test/system/__snapshots__/json.test.ts.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`\`snyk-licenses-report json <...>\` Shows error when missing --orgPublicId 1`] = ` +"Command failed: DEBUG=* node ./dist/index.js json +index.js json + +Generate org licenses & dependencies data in JSON format + +Options: + --version Show version number [boolean] + --help Show help [boolean] + --orgPublicId [required] + --view How should the data be represented. Defaults to a license based + view. [default: \\"org-licenses\\"] + +Missing required argument: orgPublicId" +`; diff --git a/test/system/basic.test.ts b/test/system/basic.test.ts new file mode 100644 index 0000000..19d9c9b --- /dev/null +++ b/test/system/basic.test.ts @@ -0,0 +1,21 @@ +import { exec } from 'child_process'; +import { sep } from 'path'; +const main = './dist/index.js'.replace(/\//g, sep); + +describe('`snyk-licenses-report help <...>`', () => { + const OLD_ENV = process.env; + process.env.SNYK_TOKEN = process.env.SNYK_TEST_TOKEN; + afterAll(async () => { + process.env = { ...OLD_ENV }; + }); + it('Shows help text as expected', async (done) => { + return exec(`node ${main} help`, (err, stdout) => { + if (err) { + throw err; + } + expect(err).toBeNull(); + expect(stdout.trim()).toMatchSnapshot(); + done(); + }); + }); +}); diff --git a/test/system/generate.test.ts b/test/system/generate.test.ts new file mode 100644 index 0000000..cb67486 --- /dev/null +++ b/test/system/generate.test.ts @@ -0,0 +1,36 @@ +import { exec } from 'child_process'; +import { sep } from 'path'; +const main = './dist/index.js'.replace(/\//g, sep); + +const ORG_ID = process.env.TEST_ORG_ID as string; + +describe('`snyk-licenses-report generate <...>`', () => { + it('Shows error when missing --orgPublicId', async (done) => { + exec( + `node ${main} generate`, + { env: { SNYK_TOKEN: process.env.SNYK_TEST_TOKEN } }, + (err, stdout) => { + expect(stdout).toBe(''); + expect(err.message.trim()).toMatchSnapshot(); + done(); + }, + ); + }); + + it('generated the report successfully with default params', (done) => { + exec( + `node ${main} generate --orgPublicId=${ORG_ID}`, + { env: { SNYK_TOKEN: process.env.SNYK_TEST_TOKEN } }, + (err, stdout) => { + expect(stdout).toMatch('License report saved at'); + expect(err).toBeNull(); + done(); + }, + ); + }, 50000); + it.todo('generated the report successfully with custom template'); + it.todo('generated the report successfully with custom template'); + + it.todo('API is down'); + it.todo('Requested org has no licenses policy'); +}); diff --git a/test/system/json.test.ts b/test/system/json.test.ts new file mode 100644 index 0000000..d9032b3 --- /dev/null +++ b/test/system/json.test.ts @@ -0,0 +1,24 @@ +import { exec } from 'child_process'; +import { sep } from 'path'; +const main = './dist/index.js'.replace(/\//g, sep); +const ORG_ID = process.env.TEST_ORG_ID as string; +describe('`snyk-licenses-report json <...>`', () => { + it('Shows error when missing --orgPublicId', async (done) => { + exec(`DEBUG=* node ${main} json`, (err, stdout) => { + expect(stdout).toBe(''); + expect(err.message.trim()).toMatchSnapshot(); + done(); + }); + }); + it('Generated JSON data with correct --orgPublicId', async (done) => { + exec( + `node ${main} json --orgPublicId=${ORG_ID}`, + { env: { SNYK_TOKEN: process.env.SNYK_TEST_TOKEN } }, + (err, stdout, stderr) => { + expect(err).toBeNull(); + expect(stdout.trim()).toMatch('BSD-2-Clause'); + done(); + }, + ); + }, 30000); +}); diff --git a/tsconfig.json b/tsconfig.json index 55bc439..d749e57 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,5 +6,5 @@ "sourceMap": true, "declaration": true }, - "include": ["./src/lib/**/*", "./src/**/*", "./src/cmds"] + "include": ["./src/**/**/*"] }