Skip to content

Commit

Permalink
feat: basic html report & tests
Browse files Browse the repository at this point in the history
  • Loading branch information
lili2311 committed Sep 29, 2020
1 parent 4caa901 commit 9884f6a
Show file tree
Hide file tree
Showing 12 changed files with 221 additions and 50 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
32 changes: 25 additions & 7 deletions src/cmds/generate.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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'];
Expand All @@ -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);
}
Expand Down
23 changes: 16 additions & 7 deletions src/lib/generate-output/html/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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;
Expand All @@ -51,7 +58,9 @@ async function registerPeerPartial(
async function compileTemplate(
fileName: string,
): Promise<HandlebarsTemplateDelegate> {
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<string> {
Expand Down
33 changes: 17 additions & 16 deletions src/lib/generate-output/html/templates/licenses-view.hbs
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<meta http-equiv="Content-Language" content="en-us">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Snyk Licenses Report</title>
<link rel="icon" type="image/png" href="https://res.cloudinary.com/snyk/image/upload/v1468845142/favicon/favicon.png"
sizes="194x194">
<link rel="shortcut icon" href="https://res.cloudinary.com/snyk/image/upload/v1468845142/favicon/favicon.ico">
{{!-- {{> inline-css }} --}}
</head>

<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<meta http-equiv="Content-Language" content="en-us">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Snyk Licenses Report</title>
<link rel="icon" type="image/png" href="https://res.cloudinary.com/snyk/image/upload/v1468845142/favicon/favicon.png"
sizes="194x194">
<link rel="shortcut icon" href="https://res.cloudinary.com/snyk/image/upload/v1468845142/favicon/favicon.ico">
{{> inline-css }}
</head>
<body>
<main class="layout-stacked">
<h1>Hello!</h1>
</main>
</body>
</html>

<body class="test-remediation-section-projects">
<main class="layout-stacked">
{{data}}
</main>
</body>
69 changes: 69 additions & 0 deletions src/lib/write-contents-to-file.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
25 changes: 25 additions & 0 deletions test/lib/__snapshots__/generate-html-report.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Generate HTML report License HTML Report is generated as expected 1`] = `
"<!DOCTYPE html>
<html lang=\\"en\\">
<head>
<meta http-equiv=\\"Content-type\\" content=\\"text/html; charset=utf-8\\">
<meta http-equiv=\\"Content-Language\\" content=\\"en-us\\">
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"IE=edge\\">
<title>Snyk Licenses Report</title>
<link rel=\\"icon\\" type=\\"image/png\\" href=\\"https://res.cloudinary.com/snyk/image/upload/v1468845142/favicon/favicon.png\\"
sizes=\\"194x194\\">
<link rel=\\"shortcut icon\\" href=\\"https://res.cloudinary.com/snyk/image/upload/v1468845142/favicon/favicon.ico\\">
</head>
<body>
<main class=\\"layout-stacked\\">
<h1>Hello!</h1>
</main>
</body>
</html>
"
`;
24 changes: 24 additions & 0 deletions test/lib/generate-html-report.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
1 change: 1 addition & 0 deletions test/lib/generate-license-report-data.test.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
4 changes: 3 additions & 1 deletion test/system/__snapshots__/json.test.ts.snap
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"
`;
5 changes: 5 additions & 0 deletions test/system/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
34 changes: 24 additions & 10 deletions test/system/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
19 changes: 11 additions & 8 deletions test/system/json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

0 comments on commit 9884f6a

Please sign in to comment.