Skip to content

Commit 7806eba

Browse files
authored
test(e2e): add nominal tests for exec & init CLI commands (#434)
no breaking changes use childprocess to collect CLI stdout missing limit case tests fix #395
1 parent 44ac795 commit 7806eba

File tree

6 files changed

+291
-77
lines changed

6 files changed

+291
-77
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,9 @@ json/
6565
reports/
6666
preview/
6767
dist/
68-
.nodesecurerc
68+
/.nodesecurerc
6969
.DS_Store
70+
71+
# IDE
72+
.vscode
73+
jsconfig.json

package.json

Lines changed: 77 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,77 @@
1-
{
2-
"name": "@nodesecure/report",
3-
"version": "3.0.0",
4-
"description": "NodeSecure HTML & PDF graphic security report",
5-
"main": "./dist/src/index.js",
6-
"type": "module",
7-
"bin": {
8-
"nreport": "./dist/bin/index.js"
9-
},
10-
"exports": {
11-
".": {
12-
"import": "./dist/src/index.js"
13-
}
14-
},
15-
"scripts": {
16-
"build": "tsc && npm run build:views && npm run build:public",
17-
"build:views": "rimraf dist/views && cp -r views dist/views",
18-
"build:public": "rimraf dist/public && cp -r public dist/public",
19-
"lint": "eslint src test bin scripts",
20-
"test-only": "glob -c \"tsx --test-reporter=spec --test\" \"./test/**/*.spec.ts\"",
21-
"test": "c8 --all --src ./src -r html npm run test-only",
22-
"preview:light": "tsx --no-warnings ./scripts/preview.js --theme light",
23-
"preview:dark": "tsx --no-warnings ./scripts/preview.js --theme dark",
24-
"prepublishOnly": "npm run build"
25-
},
26-
"files": [
27-
"dist"
28-
],
29-
"repository": {
30-
"type": "git",
31-
"url": "git+https://github.com/NodeSecure/report.git"
32-
},
33-
"keywords": [
34-
"security",
35-
"report",
36-
"nodesecure",
37-
"pdf",
38-
"html",
39-
"chart"
40-
],
41-
"author": "NodeSecure",
42-
"license": "MIT",
43-
"bugs": {
44-
"url": "https://github.com/NodeSecure/report/issues"
45-
},
46-
"homepage": "https://github.com/NodeSecure/report#readme",
47-
"dependencies": {
48-
"@nodesecure/flags": "^2.4.0",
49-
"@nodesecure/ossf-scorecard-sdk": "^3.2.1",
50-
"@nodesecure/rc": "^4.0.0",
51-
"@nodesecure/scanner": "^6.0.2",
52-
"@nodesecure/utils": "^2.2.0",
53-
"@openally/mutex": "^1.0.0",
54-
"@topcli/spinner": "^2.1.2",
55-
"esbuild": "^0.25.0",
56-
"filenamify": "^6.0.0",
57-
"kleur": "^4.1.5",
58-
"puppeteer": "24.3.1",
59-
"sade": "^1.8.1",
60-
"zup": "0.0.2"
61-
},
62-
"devDependencies": {
63-
"@openally/config.eslint": "^2.1.0",
64-
"@openally/config.typescript": "^1.0.3",
65-
"@types/node": "^22.2.0",
66-
"c8": "^10.1.2",
67-
"glob": "^11.0.0",
68-
"open": "^10.1.0",
69-
"rimraf": "^6.0.1",
70-
"tsx": "^4.19.2",
71-
"typescript": "^5.7.2"
72-
},
73-
"engines": {
74-
"node": ">=20"
75-
}
76-
}
1+
{
2+
"name": "@nodesecure/report",
3+
"version": "3.0.0",
4+
"description": "NodeSecure HTML & PDF graphic security report",
5+
"main": "./dist/src/index.js",
6+
"type": "module",
7+
"bin": {
8+
"nreport": "./dist/bin/index.js"
9+
},
10+
"exports": {
11+
".": {
12+
"import": "./dist/src/index.js"
13+
}
14+
},
15+
"scripts": {
16+
"build": "tsc && npm run build:views && npm run build:public",
17+
"build:views": "rimraf dist/views && cp -r views dist/views",
18+
"build:public": "rimraf dist/public && cp -r public dist/public",
19+
"lint": "eslint src test bin scripts",
20+
"test-only": "glob -c \"tsx --test-reporter=spec --test\" \"./test/**/*.spec.ts\"",
21+
"test": "c8 --all --src ./src -r html npm run test-only",
22+
"test:e2e": "glob -c \"tsx -r dotenv/config --test-reporter=spec --test\" \"./test/**/*.e2e-spec.ts\"",
23+
"preview:light": "tsx --no-warnings ./scripts/preview.js --theme light",
24+
"preview:dark": "tsx --no-warnings ./scripts/preview.js --theme dark",
25+
"prepublishOnly": "npm run build"
26+
},
27+
"files": [
28+
"dist"
29+
],
30+
"repository": {
31+
"type": "git",
32+
"url": "git+https://github.com/NodeSecure/report.git"
33+
},
34+
"keywords": [
35+
"security",
36+
"report",
37+
"nodesecure",
38+
"pdf",
39+
"html",
40+
"chart"
41+
],
42+
"author": "NodeSecure",
43+
"license": "MIT",
44+
"bugs": {
45+
"url": "https://github.com/NodeSecure/report/issues"
46+
},
47+
"homepage": "https://github.com/NodeSecure/report#readme",
48+
"dependencies": {
49+
"@nodesecure/flags": "^2.4.0",
50+
"@nodesecure/ossf-scorecard-sdk": "^3.2.1",
51+
"@nodesecure/rc": "^4.0.0",
52+
"@nodesecure/scanner": "^6.0.2",
53+
"@nodesecure/utils": "^2.2.0",
54+
"@openally/mutex": "^1.0.0",
55+
"@topcli/spinner": "^2.1.2",
56+
"esbuild": "^0.25.0",
57+
"filenamify": "^6.0.0",
58+
"kleur": "^4.1.5",
59+
"puppeteer": "24.3.1",
60+
"sade": "^1.8.1",
61+
"zup": "0.0.2"
62+
},
63+
"devDependencies": {
64+
"@openally/config.eslint": "^2.1.0",
65+
"@openally/config.typescript": "^1.0.3",
66+
"@types/node": "^22.2.0",
67+
"c8": "^10.1.2",
68+
"glob": "^11.0.0",
69+
"open": "^10.1.0",
70+
"rimraf": "^6.0.1",
71+
"tsx": "^4.19.2",
72+
"typescript": "^5.7.2"
73+
},
74+
"engines": {
75+
"node": ">=20"
76+
}
77+
}

test/commands/execute.e2e-spec.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Import Node.js Dependencies
2+
import { fileURLToPath } from "node:url";
3+
import path from "node:path";
4+
import fs from "node:fs/promises";
5+
import { afterEach, describe, it } from "node:test";
6+
import assert from "node:assert";
7+
import { stripVTControlCharacters } from "node:util";
8+
9+
// Import Internal Dependencies
10+
import { filterProcessStdout } from "../helpers/reportCommandRunner.js";
11+
import * as CONSTANTS from "../../src/constants.js";
12+
13+
// CONSTANTS
14+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
15+
const kProcessDir = path.join(__dirname, "../..");
16+
17+
describe("Report execute command", () => {
18+
afterEach(async() => await fs.rm(CONSTANTS.DIRS.CLONES, {
19+
recursive: true, force: true
20+
}));
21+
22+
it("should execute command on fixture '.nodesecurerc'", async() => {
23+
const options = {
24+
cmd: "node",
25+
args: ["dist/bin/index.js", "execute"],
26+
cwd: kProcessDir
27+
};
28+
29+
function byMessage(buffer) {
30+
const message = ".*";
31+
const afterNonAlphaNum = String.raw`?<=[^a-zA-Z\d\s:]\s`;
32+
const beforeTime = String.raw`?=\s\d{1,5}.\d{1,4}ms`;
33+
const withoutDuplicates = String.raw`(?![\s\S]*\1)`;
34+
35+
const matchMessage = `(${afterNonAlphaNum})(${message})(${beforeTime})|(${afterNonAlphaNum})(${message})`;
36+
const reg = new RegExp(`(${matchMessage})${withoutDuplicates}`, "g");
37+
38+
const matchedMessages = stripVTControlCharacters(buffer.toString()).match(reg);
39+
40+
return matchedMessages ?? [""];
41+
}
42+
43+
const expectedLines = [
44+
`Executing nreport at: ${kProcessDir}`,
45+
"title: Default report title",
46+
"reporters: html,pdf",
47+
"[Fetcher: NPM] - Fetching NPM packages metadata on the NPM Registry",
48+
"",
49+
"[Fetcher: NPM] - successfully executed in",
50+
"[Fetcher: GIT] - Cloning GIT repositories",
51+
"[Fetcher: GIT] - Fetching repositories metadata on the NPM Registry",
52+
"[Fetcher: GIT] - successfully executed in",
53+
"[Reporter: HTML] - Building template and assets",
54+
"[Reporter: HTML] - successfully executed in",
55+
"[Reporter: PDF] - Using puppeteer to convert HTML content to PDF",
56+
"[Reporter: PDF] - successfully executed in",
57+
"Security report successfully generated! Enjoy 🚀."
58+
];
59+
60+
const actualLines = await filterProcessStdout(options, byMessage);
61+
assert.deepEqual(actualLines, expectedLines, "we are expecting these lines");
62+
});
63+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Import Node.js Dependencies
2+
import { fileURLToPath } from "node:url";
3+
import fs from "node:fs";
4+
import os from "node:os";
5+
import path from "node:path";
6+
import { before, describe, it } from "node:test";
7+
import assert from "node:assert";
8+
import { stripVTControlCharacters } from "node:util";
9+
10+
// Import Internal Dependencies
11+
import { runProcess } from "../helpers/reportCommandRunner.js";
12+
13+
// CONSTANTS
14+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
15+
const kBinDir = path.join(__dirname, "../..", "dist/bin/index.js");
16+
const kProcessDir = os.tmpdir();
17+
const kConfigFilePath = path.join(kProcessDir, ".nodesecurerc");
18+
19+
describe("Report init command if config does not exists", () => {
20+
before(() => {
21+
if (fs.existsSync(kConfigFilePath)) {
22+
fs.unlinkSync(kConfigFilePath);
23+
}
24+
});
25+
26+
it("should create config if not exists", async() => {
27+
const lines = [
28+
/.*/,
29+
/ > Executing nreport at: .*$/,
30+
/.*/,
31+
/Successfully generated NodeSecure runtime configuration at current location/,
32+
/.*/
33+
];
34+
35+
const processOptions = {
36+
cmd: "node",
37+
args: [kBinDir, "initialize"],
38+
cwd: kProcessDir
39+
};
40+
41+
for await (const line of runProcess(processOptions)) {
42+
const regexp = lines.shift();
43+
assert.ok(regexp, "we are expecting this line");
44+
assert.ok(regexp.test(stripVTControlCharacters(line)), `line (${line}) matches ${regexp}`);
45+
}
46+
47+
// to prevent false positive if no lines have been emitted from process
48+
assert.equal(lines.length, 0);
49+
});
50+
});

test/fixtures/.nodesecurerc

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"version": "1.0.0",
3+
"i18n": "english",
4+
"strategy": "github-advisory",
5+
"registry": "https://registry.npmjs.org",
6+
"report": {
7+
"theme": "light",
8+
"includeTransitiveInternal": false,
9+
"reporters": [
10+
"html",
11+
"pdf"
12+
],
13+
"charts": [
14+
{
15+
"name": "Extensions",
16+
"display": true,
17+
"interpolation": "d3.interpolateRainbow",
18+
"type": "bar"
19+
},
20+
{
21+
"name": "Licenses",
22+
"display": true,
23+
"interpolation": "d3.interpolateCool",
24+
"type": "bar"
25+
},
26+
{
27+
"name": "Warnings",
28+
"display": true,
29+
"type": "horizontalBar",
30+
"interpolation": "d3.interpolateInferno"
31+
},
32+
{
33+
"name": "Flags",
34+
"display": true,
35+
"type": "horizontalBar",
36+
"interpolation": "d3.interpolateSinebow"
37+
}
38+
],
39+
"title": "Default report title",
40+
"showFlags": true
41+
}
42+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Import Node.js Dependencies
2+
import { ChildProcess, spawn } from "node:child_process";
3+
import { createInterface } from "node:readline";
4+
import { stripVTControlCharacters } from "node:util";
5+
6+
export async function* runProcess(options) {
7+
const childProcess = spawnedProcess(options);
8+
try {
9+
if (!childProcess.stdout) {
10+
return;
11+
}
12+
13+
const rStream = createInterface(childProcess.stdout);
14+
15+
for await (const line of rStream) {
16+
yield stripVTControlCharacters(line);
17+
}
18+
}
19+
finally {
20+
childProcess.kill();
21+
}
22+
}
23+
24+
export function filterProcessStdout(options, filter): Promise<string[]> {
25+
const { resolve, reject, promise } = Promise.withResolvers<string[]>();
26+
27+
const childProcess = spawnedProcess(options);
28+
const output = new Set<string>();
29+
30+
childProcess.stdout?.on("data", (buffer) => {
31+
filter(buffer).forEach((filteredData) => {
32+
output.add(filteredData);
33+
});
34+
});
35+
36+
childProcess.on("close", () => {
37+
resolve(Array.from(output));
38+
});
39+
40+
childProcess.on("error", (err) => {
41+
reject(err);
42+
});
43+
44+
return promise;
45+
}
46+
47+
function spawnedProcess(options): ChildProcess {
48+
const { cmd, args = [], cwd = process.cwd() } = options;
49+
50+
return spawn(cmd, args, {
51+
stdio: ["ignore", "pipe", "pipe", "ipc"],
52+
cwd
53+
});
54+
}

0 commit comments

Comments
 (0)