diff --git a/.gitignore b/.gitignore index 7d6fa97..eac580a 100644 --- a/.gitignore +++ b/.gitignore @@ -65,5 +65,9 @@ json/ reports/ preview/ dist/ -.nodesecurerc +/.nodesecurerc .DS_Store + +# IDE +.vscode +jsconfig.json \ No newline at end of file diff --git a/package.json b/package.json index a7de9b9..a3329ca 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "lint": "eslint src test bin scripts", "test-only": "glob -c \"tsx --test-reporter=spec --test\" \"./test/**/*.spec.ts\"", "test": "c8 --all --src ./src -r html npm run test-only", + "test:e2e": "glob -c \"tsx --test-reporter=spec --test\" \"./test/commands/*.spec.ts\"", "preview:light": "tsx --no-warnings ./scripts/preview.js --theme light", "preview:dark": "tsx --no-warnings ./scripts/preview.js --theme dark", "prepublishOnly": "npm run build" diff --git a/test/commands/execute.spec.ts b/test/commands/execute.spec.ts new file mode 100644 index 0000000..84b122f --- /dev/null +++ b/test/commands/execute.spec.ts @@ -0,0 +1,78 @@ +import dotenv from "dotenv"; +dotenv.config(); + +// Import Node.js Dependencies +import { fileURLToPath } from "node:url"; +import path from "node:path"; +import fs from "node:fs/promises"; +import { afterEach, describe, it } from "node:test"; +import assert from "node:assert"; + +// Import Third-party Dependencies +import stripAnsi from "strip-ansi"; + +// Import Internal Dependencies +import { filterProcessStdout } from "../helpers/reportCommandRunner.js"; +import * as CONSTANTS from "../../src/constants.js"; + +// CONSTANTS +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const processDir = path.join(__dirname, "../.."); + +describe("Report execute command", async () => { + afterEach(async() => await fs.rm(CONSTANTS.DIRS.CLONES, { + recursive: true, force: true + })); + + it("should execute command on fixture '.nodesecurerc'", async() => { + const options = { + cmd: "node", + args: ["dist/bin/index.js", "execute"], + cwd: processDir + }; + + function byMessage(buffer) { + const message = `.*`; + const afterNonAlphaNum = String.raw`?<=[^a-zA-Z\d\s:]\s`; + const beforeTime = String.raw`?=\s\d{1,5}.\d{1,4}ms`; + const withoutDuplicates = String.raw`(?![\s\S]*\1)`; + + const matchMessage = `(${afterNonAlphaNum})(${message})(${beforeTime})|(${afterNonAlphaNum})(${message})`; + const reg = new RegExp(`(${matchMessage})${withoutDuplicates}`, "g"); + + const matchedMessages = stripAnsi(buffer.toString()).match(reg); + + return matchedMessages ?? [""]; + } + + const expectedLines = [ + "Executing nreport at: C:\\PERSO\\dev\\report", + "title: Default report title", + "reporters: html,pdf", + "[Fetcher: NPM] - Fetching NPM packages metadata on the NPM Registry", + "", + "[Fetcher: NPM] - successfully executed in", + "[Fetcher: GIT] - Cloning GIT repositories", + "[Fetcher: GIT] - Fetching repositories metadata on the NPM Registry", + "[Fetcher: GIT] - successfully executed in", + "[Reporter: HTML] - Building template and assets", + "[Reporter: HTML] - successfully executed in", + "[Reporter: PDF] - Using puppeteer to convert HTML content to PDF", + "[Reporter: PDF] - successfully executed in", + "Security report successfully generated! Enjoy 🚀." + ]; + + let actualLines: string[]= []; + + try { + actualLines = await filterProcessStdout(options, byMessage); + } + catch (error) { + console.log(error); + + assert.fail("Execute command should not throw"); + } + + assert.deepEqual(actualLines, expectedLines, "we are expecting these lines"); + }); +}); diff --git a/test/commands/initialize.spec.ts b/test/commands/initialize.spec.ts new file mode 100644 index 0000000..ad13212 --- /dev/null +++ b/test/commands/initialize.spec.ts @@ -0,0 +1,48 @@ +import dotenv from "dotenv"; +dotenv.config(); + +// Import Node.js Dependencies +import { fileURLToPath } from "node:url"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { beforeEach, describe, it } from "node:test"; +import assert from "node:assert"; + +// Import Third-party Dependencies +import stripAnsi from "strip-ansi"; + +// Import Internal Dependencies +import { runProcess } from "../helpers/reportCommandRunner.ts"; + +// CONSTANTS +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const processDir = path.join(__dirname, "..", "fixtures"); +const configFilePath = path.join(processDir, ".nodesecurerc"); + +describe("Report init command", async() => { + beforeEach(async () => await fs.unlink(configFilePath)); + it("should create config if not exists", async() => { + const lines = [ + /.*/, + / > Executing nreport at: .*$/, + /.*/, + /Successfully generated NodeSecure runtime configuration at current location/, + /.*/ + ]; + + const processOptions = { + cmd: "node", + args: ["dist/bin/index.js", "initialize"], + cwd: processDir + }; + + for await (const line of runProcess(processOptions)) { + const regexp = lines.shift(); + assert.ok(regexp, "we are expecting this line"); + assert.ok(regexp.test(stripAnsi(line)), `line (${line}) matches ${regexp}`); + } + + // to avoid false positive if no lines have been emitted from process + assert.equal(lines.length, 0); + }); +}); diff --git a/test/fixtures/.nodesecurerc b/test/fixtures/.nodesecurerc new file mode 100644 index 0000000..04fe099 --- /dev/null +++ b/test/fixtures/.nodesecurerc @@ -0,0 +1,42 @@ +{ + "version": "1.0.0", + "i18n": "english", + "strategy": "github-advisory", + "registry": "https://registry.npmjs.org", + "report": { + "theme": "light", + "includeTransitiveInternal": false, + "reporters": [ + "html", + "pdf" + ], + "charts": [ + { + "name": "Extensions", + "display": true, + "interpolation": "d3.interpolateRainbow", + "type": "bar" + }, + { + "name": "Licenses", + "display": true, + "interpolation": "d3.interpolateCool", + "type": "bar" + }, + { + "name": "Warnings", + "display": true, + "type": "horizontalBar", + "interpolation": "d3.interpolateInferno" + }, + { + "name": "Flags", + "display": true, + "type": "horizontalBar", + "interpolation": "d3.interpolateSinebow" + } + ], + "title": "Default report title", + "showFlags": true + } +} \ No newline at end of file diff --git a/test/helpers/reportCommandRunner.ts b/test/helpers/reportCommandRunner.ts new file mode 100644 index 0000000..6cc82db --- /dev/null +++ b/test/helpers/reportCommandRunner.ts @@ -0,0 +1,54 @@ +// Import Node.js Dependencies +import { ChildProcess, spawn } from "node:child_process"; +import { createInterface } from "node:readline"; + +// Import Third-party Dependencies +import stripAnsi from "strip-ansi"; + +export async function* runProcess(options) { + const childProcess = spawnedProcess(options); + try { + if(!childProcess.stdout){ + return + } + + const rStream = createInterface(childProcess.stdout); + + for await (const line of rStream) { + yield stripAnsi(line); + } + } + finally { + childProcess.kill(); + } +} + +export function filterProcessStdout(options, filter): Promise { + return new Promise((resolve, reject) => { + const childProcess = spawnedProcess(options); + const output = new Set(); + + childProcess.stdout?.on("data", (buffer) => { + filter(buffer).forEach((filteredData) => { + output.add(filteredData); + }); + }); + + childProcess.on("close", (code) => { + resolve(Array.from(output)); + }); + + childProcess.on("error", (err) => { + reject(err); + }); + }); +} + +function spawnedProcess(options): ChildProcess { + const { cmd, args = [], cwd = process.cwd() } = options; + + return spawn(cmd, args, { + stdio: ["ignore", "pipe", "pipe", "ipc"], + cwd + }); +}