Skip to content

Commit 96f1046

Browse files
authored
tests: restructure integration tests (#1231)
- tests `from-setup` were disabled due to inconsistencies between npm6,7,8,9 they were a backup an actually not needed, as they are backed up by the demo data that was built from them. - tests `edge-cases`, `dogfooding` and `args-pass-through` were refactored to own files - `args-pass-through` was enhanced --------- Signed-off-by: Jan Kowalleck <[email protected]>
1 parent a0882cc commit 96f1046

13 files changed

+469
-313
lines changed

.github/workflows/nodejs.yml

+2
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ jobs:
102102
with:
103103
node-version: ${{ matrix.node-version }}
104104
# cache: 'npm'
105+
- name: npm version
106+
run: npm --version
105107
- name: setup subject
106108
shell: bash
107109
run: |

.github/workflows/npm-ls_demo-results.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ jobs:
5151
- macos-latest
5252
include:
5353
- subject: local-workspaces
54-
additional_npm-ls_args: '-w my-local-e'
54+
additional_npm-ls_args: '--workspace==my-local-e'
5555
npm-version: '10' # Current
5656
node-version: '22' # Current
5757
os: ubuntu-latest
5858
- subject: local-workspaces
59-
additional_npm-ls_args: '-w my-local -w my-local-e'
59+
additional_npm-ls_args: '--workspace==my-local --workspace==my-local-e'
6060
npm-version: '10' # Current
6161
node-version: '22' # Current
6262
os: ubuntu-latest
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
!/.gitignore
3-
!/package.json
43
!/.npmrc
4+
!/package.json
55
!/README.md
66
!/packages/
77
!/packages/**
+2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1+
; see the docs: https://docs.npmjs.com/cli/v7/using-npm/config
2+
13
# mitigate https://github.com/npm/cli/issues/5733
24
install-links=false

jest.config.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,12 @@ module.exports = {
186186
],
187187

188188
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
189-
// testPathIgnorePatterns: [
190-
// "/node_modules/"
191-
// ],
189+
testPathIgnorePatterns: [
190+
'/node_modules/',
191+
'/_data/',
192+
'/_helper/',
193+
'/_tmp/'
194+
],
192195

193196
// The regexp pattern or array of patterns that Jest uses to detect test files
194197
// testRegex: [],

tests/_helper/index.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,13 @@ function makeXmlReproducible (xml) {
146146
* @return {number[]}
147147
*/
148148
function getNpmVersion () {
149-
return spawnSync('npm', ['--version'], {
149+
const v = spawnSync('npm', ['--version'], {
150150
stdio: ['ignore', 'pipe', 'ignore'],
151151
encoding: 'utf8',
152152
shell: process.platform.startsWith('win')
153153
}).stdout.split('.').map(Number)
154+
process.stderr.write(`\ndetected npm version: ${JSON.stringify(v)}\n`)
155+
return v
154156
}
155157

156158
module.exports = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*!
2+
This file is part of CycloneDX generator for NPM projects.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
16+
SPDX-License-Identifier: Apache-2.0
17+
Copyright (c) OWASP Foundation. All Rights Reserved.
18+
*/
19+
20+
const { join } = require('path')
21+
const { mkdirSync, readFileSync } = require('fs')
22+
23+
const { describe, expect, test } = require('@jest/globals')
24+
25+
const { mkTemp, runCLI, dummyProjectsRoot, npmLsReplacement } = require('./')
26+
27+
describe('integration.cli.args-pass-through', () => {
28+
const cliRunTestTimeout = 15000
29+
30+
const tmpRoot = mkTemp('cli.args-pass-through')
31+
32+
describe('npm-version depending npm-args', () => {
33+
const tmpRootRun = join(tmpRoot, 'npmVersion-depending-npmArgs')
34+
mkdirSync(tmpRootRun)
35+
36+
const rMinor = Math.round(99 * Math.random())
37+
const rPatch = Math.round(99 * Math.random())
38+
const le6 = Math.round(6 * Math.random())
39+
const ge7 = 7 + Math.round(92 * Math.random())
40+
41+
const npmArgsGeneral = ['--json', '--long']
42+
const npm6ArgsGeneral = [...npmArgsGeneral, '--depth=255']
43+
const npm7ArgsGeneral = [...npmArgsGeneral, '--all']
44+
const npm8ArgsGeneral = [...npmArgsGeneral, '--all']
45+
const npm9ArgsGeneral = [...npmArgsGeneral, '--all']
46+
const npm10ArgsGeneral = [...npmArgsGeneral, '--all']
47+
48+
test.each([
49+
// region basic
50+
['basic npm 6', `6.${rMinor}.${rPatch}`, [], npm6ArgsGeneral],
51+
['basic npm 7', `7.${rMinor}.${rPatch}`, [], npm7ArgsGeneral],
52+
['basic npm 8', `8.${rMinor}.${rPatch}`, [], npm8ArgsGeneral],
53+
['basic npm 9', `9.${rMinor}.${rPatch}`, [], npm9ArgsGeneral],
54+
['basic npm 10', `10.${rMinor}.${rPatch}`, [], npm10ArgsGeneral],
55+
// endregion basic
56+
// region omit
57+
['omit everything npm 6', `6.${rMinor}.${rPatch}`, ['--omit', 'dev', 'optional', 'peer'], [...npm6ArgsGeneral, '--production']],
58+
['omit everything npm 7', `7.${rMinor}.${rPatch}`, ['--omit', 'dev', 'optional', 'peer'], [...npm7ArgsGeneral, '--production']],
59+
['omit everything npm lower 8.7', `8.${le6}.${rPatch}`, ['--omit', 'dev', 'optional', 'peer'], [...npm8ArgsGeneral, '--production']],
60+
['omit everything npm greater-equal 8.7', `8.${ge7}.${rPatch}`, ['--omit', 'dev', 'optional', 'peer'], [...npm8ArgsGeneral, '--omit=dev', '--omit=optional', '--omit=peer']],
61+
['omit everything npm 9', `9.${rMinor}.${rPatch}`, ['--omit', 'dev', 'optional', 'peer'], [...npm9ArgsGeneral, '--omit=dev', '--omit=optional', '--omit=peer']],
62+
['omit everything npm 10', `10.${rMinor}.${rPatch}`, ['--omit', 'dev', 'optional', 'peer'], [...npm10ArgsGeneral, '--omit=dev', '--omit=optional', '--omit=peer']],
63+
// endregion omit
64+
// region package-lock-only
65+
['package-lock-only not supported npm 6 ', `6.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm6ArgsGeneral]],
66+
['package-lock-only npm 7', `7.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm7ArgsGeneral, '--package-lock-only']],
67+
['package-lock-only npm 8', `8.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm8ArgsGeneral, '--package-lock-only']],
68+
['package-lock-only npm 9', `9.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm9ArgsGeneral, '--package-lock-only']],
69+
['package-lock-only npm 10', `10.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm10ArgsGeneral, '--package-lock-only']]
70+
// endregion package-lock-only
71+
])('%s', async (purpose, npmVersion, cdxArgs, expectedArgs) => {
72+
const logFileBase = join(tmpRootRun, purpose.replace(/\W/g, '_'))
73+
const cwd = dummyProjectsRoot
74+
75+
const { res, errFile } = runCLI([
76+
...cdxArgs,
77+
'--',
78+
join('with-lockfile', 'package.json')
79+
], logFileBase, cwd, {
80+
CT_VERSION: npmVersion,
81+
CT_EXPECTED_ARGS: expectedArgs.join(' '),
82+
npm_execpath: npmLsReplacement.checkArgs
83+
})
84+
85+
try {
86+
await expect(res).resolves.toBe(0)
87+
} catch (err) {
88+
process.stderr.write(readFileSync(errFile))
89+
throw err
90+
}
91+
}, cliRunTestTimeout)
92+
})
93+
})
+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*!
2+
This file is part of CycloneDX generator for NPM projects.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
16+
SPDX-License-Identifier: Apache-2.0
17+
Copyright (c) OWASP Foundation. All Rights Reserved.
18+
*/
19+
20+
const { spawnSync } = require('child_process')
21+
22+
const { describe, expect, test } = require('@jest/globals')
23+
24+
const { projectRootPath, cliWrapper } = require('./')
25+
26+
describe('integration.cli.dogfooding', () => {
27+
const cliRunTestTimeout = 15000
28+
29+
test.each(['JSON', 'XML'])('dogfooding %s', (format) => {
30+
const res = spawnSync(
31+
process.execPath,
32+
['--', cliWrapper, '--output-format', format, '--ignore-npm-errors'],
33+
{
34+
cwd: projectRootPath,
35+
stdio: ['ignore', 'inherit', 'pipe'],
36+
encoding: 'utf8'
37+
}
38+
)
39+
try {
40+
expect(res.error).toBeUndefined()
41+
expect(res.status).toBe(0)
42+
} catch (err) {
43+
process.stderr.write('\n')
44+
process.stderr.write(res.stderr)
45+
process.stderr.write('\n')
46+
throw err
47+
}
48+
}, cliRunTestTimeout)
49+
})
+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/*!
2+
This file is part of CycloneDX generator for NPM projects.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
16+
SPDX-License-Identifier: Apache-2.0
17+
Copyright (c) OWASP Foundation. All Rights Reserved.
18+
*/
19+
20+
const { join } = require('path')
21+
const { mkdirSync, writeFileSync, readFileSync } = require('fs')
22+
23+
const { describe, expect, test } = require('@jest/globals')
24+
25+
const { makeReproducible } = require('../_helper')
26+
const { UPDATE_SNAPSHOTS, mkTemp, runCLI, latestCdxSpecVersion, dummyProjectsRoot, npmLsReplacement, demoResultsRoot } = require('./')
27+
28+
describe('integration.cli.edge-cases', () => {
29+
const cliRunTestTimeout = 15000
30+
31+
const tmpRoot = mkTemp('cli.edge_cases')
32+
33+
describe('broken project', () => {
34+
const tmpRootRun = join(tmpRoot, 'broken-project')
35+
mkdirSync(tmpRootRun)
36+
37+
test.each([
38+
['no-lockfile', /missing evidence/i],
39+
['no-manifest', /missing .*manifest file/i]
40+
])('%s', async (folderName, expectedError) => {
41+
const logFileBase = join(tmpRootRun, folderName)
42+
const cwd = join(dummyProjectsRoot, folderName)
43+
44+
const { res, errFile } = runCLI([], logFileBase, cwd, { npm_execpath: undefined })
45+
46+
try {
47+
await expect(res).rejects.toThrow(expectedError)
48+
} catch (err) {
49+
process.stderr.write(readFileSync(errFile))
50+
throw err
51+
}
52+
}, cliRunTestTimeout)
53+
})
54+
55+
describe('with broken npm-ls', () => {
56+
const tmpRootRun = join(tmpRoot, 'with-broken')
57+
mkdirSync(tmpRootRun)
58+
59+
test('error on non-existing binary', async () => {
60+
const logFileBase = join(tmpRootRun, 'non-existing')
61+
const cwd = join(dummyProjectsRoot, 'with-lockfile')
62+
63+
const { res, errFile } = runCLI([], logFileBase, cwd, {
64+
npm_execpath: npmLsReplacement.nonExistingBinary
65+
})
66+
67+
try {
68+
await expect(res).rejects.toThrow(/^unexpected npm execpath/i)
69+
} catch (err) {
70+
process.stderr.write(readFileSync(errFile))
71+
throw err
72+
}
73+
}, cliRunTestTimeout)
74+
75+
test('error on non-zero exit', async () => {
76+
const logFileBase = join(tmpRootRun, 'error-exit-nonzero')
77+
const cwd = join(dummyProjectsRoot, 'with-lockfile')
78+
79+
const expectedExitCode = 1 + Math.floor(254 * Math.random())
80+
81+
const { res, errFile } = runCLI([], logFileBase, cwd, {
82+
CT_VERSION: '8.99.0',
83+
// non-zero exit code
84+
CT_EXIT_CODE: `${expectedExitCode}`,
85+
npm_execpath: npmLsReplacement.justExit
86+
})
87+
88+
try {
89+
await expect(res).rejects.toThrow(`npm-ls exited with errors: ${expectedExitCode} noSignal`)
90+
} catch (err) {
91+
process.stderr.write(readFileSync(errFile))
92+
throw err
93+
}
94+
}, cliRunTestTimeout)
95+
96+
test('error on broken json response', async () => {
97+
const logFileBase = join(tmpRootRun, 'error-json-broken')
98+
const cwd = join(dummyProjectsRoot, 'with-lockfile')
99+
100+
const { res, errFile } = runCLI([], logFileBase, cwd, {
101+
CT_VERSION: '8.99.0',
102+
// abuse the npm-ls replacement, as it can be caused to crash under control.
103+
npm_execpath: npmLsReplacement.brokenJson
104+
})
105+
106+
try {
107+
await expect(res).rejects.toThrow(/failed to parse npm-ls response/i)
108+
} catch (err) {
109+
process.stderr.write(readFileSync(errFile))
110+
throw err
111+
}
112+
}, cliRunTestTimeout)
113+
})
114+
115+
test('suppressed error on non-zero exit', async () => {
116+
const dd = { subject: 'dev-dependencies', npm: '8', node: '14', os: 'ubuntu-latest' }
117+
118+
mkdirSync(join(tmpRoot, 'suppressed-error-on-non-zero-exit'))
119+
const expectedOutSnap = join(demoResultsRoot, 'suppressed-error-on-non-zero-exit', `${dd.subject}_npm${dd.npm}_node${dd.node}_${dd.os}.snap.json`)
120+
const logFileBase = join(tmpRoot, 'suppressed-error-on-non-zero-exit', `${dd.subject}_npm${dd.npm}_node${dd.node}_${dd.os}`)
121+
const cwd = dummyProjectsRoot
122+
123+
const expectedExitCode = 1 + Math.floor(254 * Math.random())
124+
125+
const { res, outFile, errFile } = runCLI([
126+
'-vvv',
127+
'--ignore-npm-errors',
128+
'--output-reproducible',
129+
// no intention to test all the spec-versions nor all the output-formats - this would be not our scope.
130+
'--spec-version', `${latestCdxSpecVersion}`,
131+
'--output-format', 'JSON',
132+
// prevent file interaction in this synthetic scenario - they would not exist anyway
133+
'--package-lock-only',
134+
'--',
135+
join('with-lockfile', 'package.json')
136+
], logFileBase, cwd, {
137+
CT_VERSION: `${dd.npm}.99.0`,
138+
// non-zero exit code
139+
CT_EXIT_CODE: expectedExitCode,
140+
CT_SUBJECT: dd.subject,
141+
CT_NPM: dd.npm,
142+
CT_NODE: dd.node,
143+
CT_OS: dd.os,
144+
npm_execpath: npmLsReplacement.demoResults
145+
})
146+
147+
try {
148+
await expect(res).resolves.toBe(0)
149+
} catch (err) {
150+
process.stderr.write(readFileSync(errFile))
151+
throw err
152+
}
153+
154+
const actualOutput = makeReproducible('json', readFileSync(outFile, 'utf8'))
155+
156+
if (UPDATE_SNAPSHOTS) {
157+
writeFileSync(expectedOutSnap, actualOutput, 'utf8')
158+
}
159+
160+
expect(actualOutput).toEqual(
161+
readFileSync(expectedOutSnap, 'utf8'),
162+
`${outFile} should equal ${expectedOutSnap}`
163+
)
164+
}, cliRunTestTimeout)
165+
})

0 commit comments

Comments
 (0)