Skip to content

Commit

Permalink
test: mock maven and npm providers in unitests (#67)
Browse files Browse the repository at this point in the history
* test: mock npm interactions in UT

* test: mock UT of java maven provider

* test: adapt mocha and c8 to test rewired modules

---------

Signed-off-by: Zvi Grinberg <[email protected]>
  • Loading branch information
zvigrinberg authored Oct 30, 2023
1 parent 4e15a50 commit d91b678
Show file tree
Hide file tree
Showing 36 changed files with 35,817 additions and 169 deletions.
1,440 changes: 1,319 additions & 121 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 11 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"lint:fix": "eslint src test --ext js --fix",
"test": "c8 npm run tests",
"localtest": "EXHORT_PIP3_PATH=/home/zgrinber/python3.9/bin/pip3 EXHORT_PYTHON3_PATH=/home/zgrinber/python3.9/bin/python3 c8 npm run tests",
"postlocaltest": " git status | grep src/providers/ | grep rewire | xargs -i git clean -f {}",
"tests": "mocha --grep \"Integration Tests|.*analysis module.*\" --invert",
"tests:rep": "mocha --reporter-option maxDiffSize=0 --reporter json > unit-tests-result.json",
"integration-tests": "mocha --grep \"Integration Tests\"",
Expand All @@ -47,13 +48,18 @@
},
"dependencies": {
"@cyclonedx/cyclonedx-library": "^4.0.0",
"babel-core": "^6.26.3",
"fast-xml-parser": "^4.2.4",
"node-hook": "^1.0.0",
"packageurl-js": "^1.0.2",
"yargs": "^17.7.2"
},
"devDependencies": {
"@babel/cli": "^7.23.0",
"@babel/core": "^7.23.2",
"@openapitools/openapi-generator-cli": "^2.6.0",
"@types/node": "^20.3.1",
"babel-plugin-rewire": "^1.2.0",
"c8": "^8.0.0",
"chai": "^4.3.7",
"eslint": "^8.42.0",
Expand All @@ -65,7 +71,7 @@
"typescript": "^5.1.3"
},
"mocha": {
"check-leaks": true,
"check-leaks": false,
"color": true,
"extension": "js",
"fail-zero": true,
Expand All @@ -82,9 +88,11 @@
"exclude": [
"src/cli.js",
"src/index.js",
"src/analysis.js"
"src/analysis.js",
"src/providers/java_maven.js",
"src/providers/javascript_npm.js"
],
"lines": 85,
"lines": 83,
"reporter": [
"html",
"json",
Expand Down
50 changes: 34 additions & 16 deletions src/providers/javascript_npm.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,31 @@ import path from 'node:path'
import Sbom from '../sbom.js'
import {PackageURL} from 'packageurl-js'

export default { isSupported, provideComponent, provideStack }
export var npmInteractions = {
listing: function runNpmListing(npmListing) {
let npmOutput = execSync(npmListing, err => {
if (err) {
throw new Error('failed to get npmOutput json from npm')
}
});
return npmOutput;
},
version: function checkNpmVersion(npm) {
execSync(`${npm} --version`, err => {
if (err) {
throw new Error('npm is not accessible')
}
})
},
createPackageLock: function createPackageLock(npm, manifestDir) {
execSync(`${npm} i --package-lock-only --prefix ${manifestDir}`, err => {
if (err) {
throw new Error('failed to create npmOutput list')
}
})
}
}
export default { isSupported, provideComponent, provideStack, npmInteractions }

/** @typedef {import('../provider').Provider} */

Expand Down Expand Up @@ -73,6 +97,12 @@ function getNpmListing(npm, allFilter, manifestDir) {
return `${npm} ls${allFilter} --omit=dev --package-lock-only --json --prefix ${manifestDir}`;
}







/**
* Create SBOM json string for npm Package.
* @param {string} manifest - path for package.json
Expand All @@ -84,24 +114,12 @@ function getSBOM(manifest, opts = {}, includeTransitive) {
// get custom npm path
let npm = getCustomPath('npm', opts)
// verify npm is accessible
execSync(`${npm} --version`, err => {
if (err) {
throw new Error('npm is not accessible')
}
})
npmInteractions.version(npm);
let manifestDir = path.dirname(manifest)
execSync(`${npm} i --package-lock-only --prefix ${manifestDir}`, err => {
if (err) {
throw new Error('failed to create npmOutput list')
}
})
npmInteractions.createPackageLock(npm, manifestDir);
let allFilter = includeTransitive? " --all" : ""
let npmListing = getNpmListing(npm, allFilter, manifestDir)
let npmOutput = execSync(npmListing, err => {
if (err) {
throw new Error('failed to get npmOutput json from npm')
}
});
let npmOutput = npmInteractions.listing(npmListing);
let depsObject = JSON.parse(npmOutput);
let rootName = depsObject["name"]
let rootVersion = depsObject["version"]
Expand Down
8 changes: 5 additions & 3 deletions src/providers/python_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default class Python_controller {
pathToPythonBin
realEnvironment
pathToRequirements
options

/**
* Constructor to create new python controller instance to interact with pip package manager
Expand All @@ -25,12 +26,13 @@ export default class Python_controller {
* @param {string} pathToRequirements
* @
*/
constructor(realEnvironment,pathToPip,pathToPython,pathToRequirements) {
constructor(realEnvironment,pathToPip,pathToPython,pathToRequirements,options={}) {
this.pathToPythonBin = pathToPython
this.pathToPipBin = pathToPip
this.realEnvironment= realEnvironment
this.prepareEnvironment()
this.pathToRequirements = pathToRequirements
this.options = options
}
prepareEnvironment()
{
Expand Down Expand Up @@ -82,7 +84,7 @@ export default class Python_controller {
console.log("Starting time to get requirements.txt dependency tree = " + startingTime)
}
if(!this.realEnvironment) {
let installBestEfforts = getCustom("EXHORT_PYTHON_INSTALL_BEST_EFFORTS","false");
let installBestEfforts = getCustom("EXHORT_PYTHON_INSTALL_BEST_EFFORTS","false",this.options);
if(installBestEfforts === "false")
{
execSync(`${this.pathToPipBin} install -r ${this.pathToRequirements}`, err =>{
Expand All @@ -95,7 +97,7 @@ export default class Python_controller {
// that means that it will install the packages without referring to the versions, but will let pip choose the version
// tailored for version of the python environment( and of pip package manager) for each package.
else {
let matchManifestVersions = getCustom("MATCH_MANIFEST_VERSIONS","true");
let matchManifestVersions = getCustom("MATCH_MANIFEST_VERSIONS","true",this.options);
if(matchManifestVersions === "true")
{
throw new Error("Conflicting settings, EXHORT_PYTHON_INSTALL_BEST_EFFORTS=true can only work with MATCH_MANIFEST_VERSIONS=false")
Expand Down
6 changes: 4 additions & 2 deletions test/it/end-to-end.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,12 @@ suite('Integration Tests', () => {
// // process.env["RHDA_TOKEN"] = "34JKLDS-4234809-66666666666"
// // process.env["RHDA_SOURCE"] = "Zvika Client"
// // let result = await index.stackAnalysis("/tmp/rajan-0410/go.mod", false, opts);
//
// let opts = {
// MATCH_MANIFEST_VERSIONS: 'false'
// }
//
// let pomPath = `/tmp/231023/requirements.txt`
// let providedDataForStack = await index.stackAnalysis(pomPath)
// let providedDataForStack = await index.stackAnalysis(pomPath,opts)
// console.log(JSON.stringify(providedDataForStack.summary,null , 4))
// expect(providedDataForStack.summary.dependencies.scanned).greaterThan(0)
// }).timeout(15000);
Expand Down
46 changes: 41 additions & 5 deletions test/providers/java_maven.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,30 @@ import { expect } from 'chai'
import fs from 'fs'
import sinon from "sinon";
// import exhort from "../../dist/src/index.js"
import javaMvnProvider from '../../src/providers/java_maven.js'
import {rewireProvider} from "./test-utils.js";
let clock

let javaMvnProviderRewire = await rewireProvider("src/providers/java_maven")

/** this function is parsing the outputfile path from the given command, and write that file the providerContent supplied.
*
* @param {string}command - the command string to be executed
* @param {string}providerContent - the content of the mocked data to replace original content in intercepted temp file
* @param {string} outputFileParameter - name of the parameter indicating the output file of the command invocation, including '='.
* @private
*/
function interceptAndOverwriteDataWithMock(command, providerContent, outputFileParameter) {
let length = outputFileParameter.length;
let indexOf = command.indexOf(outputFileParameter);
let outputFileTokenPlusRest = command.substring(indexOf + length);
let endOfOutputFile = outputFileTokenPlusRest.indexOf("-f");
let interceptedFilePath = outputFileTokenPlusRest.substring(0, endOfOutputFile).trim()
fs.writeFileSync(interceptedFilePath, providerContent)
}

import javaMvnProvider from '../../src/providers/java_maven.js'
let clock
suite('testing the java-maven data provider', () => {

[
{name: 'pom.xml', expected: true},
{name: 'some_other.file', expected: false}
Expand All @@ -17,7 +35,8 @@ suite('testing the java-maven data provider', () => {
)
});

[ "poms_deps_with_2_ignore_long",
[
"poms_deps_with_2_ignore_long",
"pom_deps_with_ignore_on_artifact",
"pom_deps_with_ignore_on_dependency",
"pom_deps_with_ignore_on_group",
Expand Down Expand Up @@ -48,15 +67,24 @@ suite('testing the java-maven data provider', () => {
test(`verify maven data provided for stack analysis with scenario ${scenario}`, async () => {
// load the expected graph for the scenario
let expectedSbom = fs.readFileSync(`test/providers/tst_manifests/maven/${testCase}/stack_analysis_expected_sbom.json`,).toString()
let dependencyTreeTextContent = fs.readFileSync(`test/providers/tst_manifests/maven/${testCase}/dep-tree.txt`,).toString()
expectedSbom = JSON.stringify(JSON.parse(expectedSbom))
let mockedExecFunction = function(command){
if(command.includes(":tree")){
interceptAndOverwriteDataWithMock(command,dependencyTreeTextContent,"DoutputFile=")
}
}
javaMvnProviderRewire.__set__('execSync',mockedExecFunction)
// invoke sut stack analysis for scenario manifest
let providedDataForStack = await javaMvnProvider.provideStack(`test/providers/tst_manifests/maven/${testCase}/pom.xml`)
let providedDataForStack = await javaMvnProviderRewire.__get__("provideStack")(`test/providers/tst_manifests/maven/${testCase}/pom.xml`)
javaMvnProviderRewire.__ResetDependency__()
// verify returned data matches expectation
expect(providedDataForStack).to.deep.equal({
ecosystem: 'maven',
contentType: 'application/vnd.cyclonedx+json',
content: expectedSbom
})

// these test cases takes ~2500-2700 ms each pr >10000 in CI (for the first test-case)
}).timeout(process.env.GITHUB_ACTIONS ? 40000 : 10000)

Expand All @@ -65,15 +93,23 @@ suite('testing the java-maven data provider', () => {
let expectedSbom = fs.readFileSync(`test/providers/tst_manifests/maven/${testCase}/component_analysis_expected_sbom.json`,).toString().trim()
// read target manifest file
expectedSbom = JSON.stringify(JSON.parse(expectedSbom))
let effectivePomContent = fs.readFileSync(`test/providers/tst_manifests/maven/${testCase}/effective-pom.xml`,).toString()
let manifestContent = fs.readFileSync(`test/providers/tst_manifests/maven/${testCase}/pom.xml`).toString()
let mockedExecFunction = function(command){
if(command.includes(":effective-pom")){
interceptAndOverwriteDataWithMock(command, effectivePomContent,"Doutput=");
}
}
javaMvnProviderRewire.__set__('execSync',mockedExecFunction)
// invoke sut component analysis for scenario manifest
let providedDataForStack = await javaMvnProvider.provideComponent(manifestContent)
let providedDataForStack = await javaMvnProviderRewire.__get__("provideComponent")(manifestContent)
// verify returned data matches expectation
expect(providedDataForStack).to.deep.equal({
ecosystem: 'maven',
contentType: 'application/vnd.cyclonedx+json',
content: expectedSbom
})
javaMvnProviderRewire.__ResetDependency__()
// these test cases takes ~1400-2000 ms each pr >10000 in CI (for the first test-case)
}).timeout(process.env.GITHUB_ACTIONS ? 15000 : 5000)
// these test cases takes ~1400-2000 ms each pr >10000 in CI (for the first test-case)
Expand Down
49 changes: 40 additions & 9 deletions test/providers/javascript_npm.test.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,52 @@
import { expect } from 'chai'
import fs from 'fs'
import sinon from "sinon";
// import babelCore from 'babel-core'
import javascriptNpmProvider from "../../src/providers/javascript_npm.js"
import {rewireProvider} from "./test-utils.js";


let javascriptNpmProviderRewire = await rewireProvider("src/providers/javascript_npm")

let clock
suite('testing the javascript-npm data provider', () => {


// async function rewireProvider()
// {
// let jsNpmProvider = fs.readFileSync("src/providers/javascript_npm.js")
// let javascriptNpmProviderSource = babelCore.transform(jsNpmProvider, {plugins: ["babel-plugin-rewire"]}).code;
// fs.writeFileSync("src/providers/javascript_npm_rewire.js",javascriptNpmProviderSource)
// javascriptNpmProviderRewire = await import("../../src/providers/javascript_npm_rewire.js")
// }

suite('testing the javascript-npm data provider', async() => {
[

{name: 'package.json', expected: true},
{name: 'some_other.file', expected: false}
].forEach(testCase => {
test(`verify isSupported returns ${testCase.expected} for ${testCase.name}`, () =>
expect(javascriptNpmProvider.isSupported(testCase.name)).to.equal(testCase.expected)
)
});

[
"package_json_deps_without_exhortignore_object",
"package_json_deps_with_exhortignore_object"
].forEach(testCase => {
let scenario = testCase.replace('package_json_deps_', '').replaceAll('_', ' ')
test(`verify package.json data provided for stack analysis with scenario ${scenario}`, async () => {
// javascriptNpmProviderRewire = await rewireProvider("src/providers/javascript_npm")
// load the expected graph for the scenario
let expectedSbom = fs.readFileSync(`test/providers/tst_manifests/npm/${testCase}/stack_expected_sbom.json`,).toString()
let npmListing = fs.readFileSync(`test/providers/tst_manifests/npm/${testCase}/npm_listing_stack.json`,).toString()
expectedSbom = JSON.stringify(JSON.parse(expectedSbom))
let mpmMockedInteractions = {
listing: () => npmListing,
version: () => void (0),
createPackageLock: () => void (0)
}
javascriptNpmProviderRewire.__set__('npmInteractions', mpmMockedInteractions)
// invoke sut stack analysis for scenario manifest

let providedDataForStack = await javascriptNpmProvider.provideStack(`test/providers/tst_manifests/npm/${testCase}/package.json`)
let providedDataForStack = javascriptNpmProviderRewire.__get__("provideStack")(`test/providers/tst_manifests/npm/${testCase}/package.json`)
// new(year: number, month: number, date?: number, hours?: number, minutes?: number, seconds?: number, ms?: number): Date

// providedDataForStack.content = providedDataForStack.content.replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\"","")
Expand All @@ -37,23 +56,35 @@ suite('testing the javascript-npm data provider', () => {
contentType: 'application/vnd.cyclonedx+json',
content: expectedSbom
})
// these test cases takes ~2500-2700 ms each pr >10000 in CI (for the first test-case)
}).timeout(process.env.GITHUB_ACTIONS ? 30000 : 10000)

javascriptNpmProviderRewire.__ResetDependency__()
// javascriptNpmProviderSource.runNpmListing.restore()
// these test cases takes ~2500-2700 ms each pr >10000 in CI (for the first test-case)
}).timeout(process.env.GITHUB_ACTIONS ? 30000 : 10000);
test(`verify package.json data provided for component analysis with scenario ${scenario}`, async () => {
// javascriptNpmProviderRewire = await rewireProvider("src/providers/javascript_npm")
// load the expected list for the scenario
let expectedSbom = fs.readFileSync(`test/providers/tst_manifests/npm/${testCase}/component_expected_sbom.json`,).toString().trim()
expectedSbom = JSON.stringify(JSON.parse(expectedSbom))
let npmListing = fs.readFileSync(`test/providers/tst_manifests/npm/${testCase}/npm_listing_component.json`,).toString()
// read target manifest file
let manifestContent = fs.readFileSync(`test/providers/tst_manifests/npm/${testCase}/package.json`).toString()
// sinon.stub(javascriptNpmProviderSource,'runNpmListing').callsFake(() => npmListing)
let mpmMockedInteractions = {
listing: () => npmListing,
version: () => void (0),
createPackageLock: () => void (0)
}
javascriptNpmProviderRewire.__set__('npmInteractions', mpmMockedInteractions)
// invoke sut stack analysis for scenario manifest
let providedDataForStack = await javascriptNpmProvider.provideComponent(manifestContent)
let providedDataForStack = await javascriptNpmProviderRewire.__get__("provideComponent")(manifestContent)
// verify returned data matches expectation
expect(providedDataForStack).to.deep.equal({
ecosystem: 'npm',
contentType: 'application/vnd.cyclonedx+json',
content: expectedSbom
})
javascriptNpmProviderRewire.__ResetDependency__()
// javascriptNpmProviderSource.runNpmListing.restore()
// these test cases takes ~1400-2000 ms each pr >10000 in CI (for the first test-case)
}).timeout(process.env.GITHUB_ACTIONS ? 15000 : 10000)

Expand Down
19 changes: 19 additions & 0 deletions test/providers/test-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import babelCore from "babel-core";
import fs from "fs";

async function dynamicImportProvider(path) {
return await import(path)
}

/**
*
* @param path
* @return providerInstance - provider instance that exposing private method/functions/properties to be mocked/stubbed
*/
export function rewireProvider(path)
{
let providerBuffeer = fs.readFileSync(path + ".js")
let providerSource = babelCore.transform(providerBuffeer, {plugins: ["babel-plugin-rewire"]}).code;
fs.writeFileSync(path + "_rewire.js",providerSource)
return dynamicImportProvider("../../" + path + "_rewire.js" )
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pom-with-deps-and-ignore:pom-with-dependency-not-ignored-for-tests:jar:0.0.1
Loading

0 comments on commit d91b678

Please sign in to comment.