diff --git a/package.json b/package.json index 32c45ea..88e2f73 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "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", diff --git a/src/cmds/generate.ts b/src/cmds/generate.ts index 56c3903..3f4c618 100644 --- a/src/cmds/generate.ts +++ b/src/cmds/generate.ts @@ -1,7 +1,16 @@ 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 { + 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 = { @@ -20,22 +29,24 @@ export const builder = { orgPublicId: { required: true, default: undefined, - desc: 'Public id of the organization in Snyk (available on organization settings)' + desc: + 'Public id of the organization in Snyk (available on organization settings)', }, template: { default: undefined, - desc: 'Path to custom Handelbars.js template file (*.hbs)' + 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] + 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.', + desc: + 'How should the data be represented. Defaults to a license based view.', }, }; export const aliases = ['g']; @@ -57,7 +68,14 @@ export async function handler(argv: { orgPublicId, ); const generateReportFunc = outputHandlers[outputFormat]; - return await generateReportFunc(licenseData, template, view); + 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/lib/generate-output/html/index.ts b/src/lib/generate-output/html/index.ts index 05e32e8..838c673 100644 --- a/src/lib/generate-output/html/index.ts +++ b/src/lib/generate-output/html/index.ts @@ -6,6 +6,7 @@ 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', @@ -15,19 +16,25 @@ export const enum SupportedViews { export async function generateHtmlReport( data: LicenseReportData, - templateOverridePath: string, - view: SupportedViews, + 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); - return htmlTemplate({hello: 1}); + debug(`✅ Compiled template ${hbsTemplate}`); + return htmlTemplate(data); } -function selectTemplate(view, templateOverride): string { - const DEFAULT_TEMPLATE = './templates/licenses-view.hbs'; +function selectTemplate(view: SupportedViews, templateOverride?): string { switch (view) { case SupportedViews.ORG_LICENSES: return templateOverride || DEFAULT_TEMPLATE; @@ -51,7 +58,9 @@ async function registerPeerPartial( async function compileTemplate( fileName: string, ): Promise { - return readFile(path.resolve(__dirname, fileName), 'utf8').then(Handlebars.compile); + return readFile(path.resolve(__dirname, fileName), 'utf8').then( + Handlebars.compile, + ); } function readFile(filePath: string, encoding: string): Promise { diff --git a/src/lib/generate-output/html/templates/licenses-view.hbs b/src/lib/generate-output/html/templates/licenses-view.hbs index 2e9c2e6..b17e16d 100644 --- a/src/lib/generate-output/html/templates/licenses-view.hbs +++ b/src/lib/generate-output/html/templates/licenses-view.hbs @@ -1,20 +1,21 @@ + + + + + + Snyk Licenses Report + + + {{!-- {{> inline-css }} --}} + - - - - - - Snyk Licenses Report - - - {{> inline-css }} - + +
+

Hello!

+
+ + - -
- {{data}} -
- 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 6322b0f..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; diff --git a/test/system/__snapshots__/json.test.ts.snap b/test/system/__snapshots__/json.test.ts.snap index 6270fd1..19ebe42 100644 --- a/test/system/__snapshots__/json.test.ts.snap +++ b/test/system/__snapshots__/json.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`\`snyk-licenses-report json <...>\` Shows error when missing --orgPublicId 1`] = ` -"Command failed: node ./dist/index.js json +"Command failed: DEBUG=* node ./dist/index.js json index.js json Generate org licenses & dependencies data in JSON format @@ -10,6 +10,8 @@ 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 index 06ec6a7..19d9c9b 100644 --- a/test/system/basic.test.ts +++ b/test/system/basic.test.ts @@ -3,6 +3,11 @@ 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) { diff --git a/test/system/generate.test.ts b/test/system/generate.test.ts index ea7d3d3..cb67486 100644 --- a/test/system/generate.test.ts +++ b/test/system/generate.test.ts @@ -6,17 +6,31 @@ 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`, (err, stdout) => { - expect(stdout).toBe(""); - expect(err.message.trim()).toMatchSnapshot(); - 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.todo("generated the report successfully with default params"); - it.todo("generated the report successfully with custom template") - it.todo("generated the report successfully with custom template") + 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"); + 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 index 6664d97..d9032b3 100644 --- a/test/system/json.test.ts +++ b/test/system/json.test.ts @@ -4,18 +4,21 @@ 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(`node ${main} json`, (err, stdout) => { - expect(stdout).toBe(""); + 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(`DEBUG=snyk-license* node ${main} json --orgPublicId=${ORG_ID}`, (err, stdout) => { - expect(err).toBeNull(); - console.log({err, stdout, ORG_ID}) - expect(stdout.trim()).toMatch("BSD-2-Clause"); - 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); });