npq 3.8.1 → 3.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/npq.mov +0 -0
- package/.github/npq.mp4 +0 -0
- package/.github/npq.png +0 -0
- package/.github/workflows/main.yml +2 -2
- package/README.md +18 -9
- package/__tests__/__fixtures__/test.marshall.js +5 -6
- package/__tests__/cli.test.js +110 -0
- package/__tests__/cliPrompt.test.js +409 -0
- package/__tests__/marshalls.base.test.js +1 -1
- package/__tests__/marshalls.classMethods.test.js +11 -14
- package/__tests__/marshalls.expiredDomains.test.js +2 -2
- package/__tests__/marshalls.provenance.test.js +181 -4
- package/__tests__/marshalls.signatures.test.js +0 -4
- package/__tests__/marshalls.snyk.test.js +294 -0
- package/__tests__/marshalls.tasks.test.js +48 -24
- package/__tests__/marshalls.typosquatting.test.js +16 -9
- package/__tests__/reportResults.test.js +772 -0
- package/__tests__/scripts.test.js +2 -2
- package/bin/npq-hero.js +73 -20
- package/bin/npq.js +113 -21
- package/lib/cli.js +140 -24
- package/lib/helpers/cliPrompt.js +97 -0
- package/lib/helpers/cliSpinner.js +104 -0
- package/lib/helpers/cliSupportHandler.js +50 -7
- package/lib/helpers/packageRepoUtils.js +5 -0
- package/lib/helpers/promiseThrottler.js +96 -0
- package/lib/helpers/reportResults.js +304 -0
- package/lib/helpers/sourcePackages.js +36 -0
- package/lib/marshall.js +42 -65
- package/lib/marshalls/age.marshall.js +3 -2
- package/lib/marshalls/author.marshall.js +36 -7
- package/lib/marshalls/baseMarshall.js +9 -17
- package/lib/marshalls/deprecation.marshall.js +43 -0
- package/lib/marshalls/downloads.marshall.js +1 -1
- package/lib/marshalls/expiredDomains.marshall.js +7 -5
- package/lib/marshalls/index.js +110 -80
- package/lib/marshalls/license.marshall.js +1 -1
- package/lib/marshalls/newbin.marshall.js +1 -2
- package/lib/marshalls/provenance.marshall.js +152 -33
- package/lib/marshalls/repo.marshall.js +6 -5
- package/lib/marshalls/scripts.marshall.js +1 -1
- package/lib/marshalls/signatures.marshall.js +26 -2
- package/lib/marshalls/snyk.marshall.js +61 -15
- package/lib/marshalls/typosquatting.marshall.js +8 -5
- package/lib/marshalls/version-maturity.marshall.js +1 -1
- package/package.json +16 -19
- package/scripts/postinstall.js +10 -11
- package/lib/cliCommons.js +0 -80
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
// Mock environment variables for testing
|
|
2
|
+
const originalEnv = process.env
|
|
3
|
+
const originalIsTTY = process.stdout.isTTY
|
|
4
|
+
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
jest.resetModules()
|
|
7
|
+
process.env = { ...originalEnv }
|
|
8
|
+
// Clear any CI environment variables to ensure clean state
|
|
9
|
+
const ciVars = [
|
|
10
|
+
'CI',
|
|
11
|
+
'CONTINUOUS_INTEGRATION',
|
|
12
|
+
'BUILD_NUMBER',
|
|
13
|
+
'RUN_ID',
|
|
14
|
+
'GITHUB_ACTIONS',
|
|
15
|
+
'GITLAB_CI',
|
|
16
|
+
'TRAVIS',
|
|
17
|
+
'CIRCLECI',
|
|
18
|
+
'JENKINS_URL',
|
|
19
|
+
'TEAMCITY_VERSION',
|
|
20
|
+
'TF_BUILD',
|
|
21
|
+
'BUILDKITE',
|
|
22
|
+
'DRONE'
|
|
23
|
+
]
|
|
24
|
+
ciVars.forEach((envVar) => {
|
|
25
|
+
delete process.env[envVar]
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
process.env = originalEnv
|
|
31
|
+
process.stdout.isTTY = originalIsTTY
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('cliSupportHandler', () => {
|
|
35
|
+
describe('isInteractiveTerminal', () => {
|
|
36
|
+
test('should return false when CI environment variable is set', () => {
|
|
37
|
+
process.env.CI = 'true'
|
|
38
|
+
// Ensure TTY is set to true so we're testing CI detection specifically
|
|
39
|
+
process.stdout.isTTY = true
|
|
40
|
+
|
|
41
|
+
const { isInteractiveTerminal } = require('../lib/helpers/cliSupportHandler')
|
|
42
|
+
const result = isInteractiveTerminal()
|
|
43
|
+
expect(result).toBe(false)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('should return false when GITHUB_ACTIONS environment variable is set', () => {
|
|
47
|
+
process.env.GITHUB_ACTIONS = 'true'
|
|
48
|
+
// Ensure TTY is set to true so we're testing CI detection specifically
|
|
49
|
+
process.stdout.isTTY = true
|
|
50
|
+
|
|
51
|
+
const { isInteractiveTerminal } = require('../lib/helpers/cliSupportHandler')
|
|
52
|
+
const result = isInteractiveTerminal()
|
|
53
|
+
expect(result).toBe(false)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('should return false when GITLAB_CI environment variable is set', () => {
|
|
57
|
+
process.env.GITLAB_CI = 'true'
|
|
58
|
+
// Ensure TTY is set to true so we're testing CI detection specifically
|
|
59
|
+
process.stdout.isTTY = true
|
|
60
|
+
|
|
61
|
+
const { isInteractiveTerminal } = require('../lib/helpers/cliSupportHandler')
|
|
62
|
+
const result = isInteractiveTerminal()
|
|
63
|
+
expect(result).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('should return false when TRAVIS environment variable is set', () => {
|
|
67
|
+
process.env.TRAVIS = 'true'
|
|
68
|
+
// Ensure TTY is set to true so we're testing CI detection specifically
|
|
69
|
+
process.stdout.isTTY = true
|
|
70
|
+
|
|
71
|
+
const { isInteractiveTerminal } = require('../lib/helpers/cliSupportHandler')
|
|
72
|
+
const result = isInteractiveTerminal()
|
|
73
|
+
expect(result).toBe(false)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('should return false when CIRCLECI environment variable is set', () => {
|
|
77
|
+
process.env.CIRCLECI = 'true'
|
|
78
|
+
// Ensure TTY is set to true so we're testing CI detection specifically
|
|
79
|
+
process.stdout.isTTY = true
|
|
80
|
+
|
|
81
|
+
const { isInteractiveTerminal } = require('../lib/helpers/cliSupportHandler')
|
|
82
|
+
const result = isInteractiveTerminal()
|
|
83
|
+
expect(result).toBe(false)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('should return false when multiple CI environment variables are set', () => {
|
|
87
|
+
process.env.CI = 'true'
|
|
88
|
+
process.env.TRAVIS = 'true'
|
|
89
|
+
process.env.CIRCLECI = 'true'
|
|
90
|
+
// Ensure TTY is set to true so we're testing CI detection specifically
|
|
91
|
+
process.stdout.isTTY = true
|
|
92
|
+
|
|
93
|
+
const { isInteractiveTerminal } = require('../lib/helpers/cliSupportHandler')
|
|
94
|
+
const result = isInteractiveTerminal()
|
|
95
|
+
expect(result).toBe(false)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('should return true when no CI environment variables are set and stdout is TTY', () => {
|
|
99
|
+
// Clear all CI environment variables
|
|
100
|
+
delete process.env.CI
|
|
101
|
+
delete process.env.GITHUB_ACTIONS
|
|
102
|
+
delete process.env.GITLAB_CI
|
|
103
|
+
delete process.env.TRAVIS
|
|
104
|
+
delete process.env.CIRCLECI
|
|
105
|
+
delete process.env.JENKINS_URL
|
|
106
|
+
delete process.env.BUILDKITE
|
|
107
|
+
delete process.env.DRONE
|
|
108
|
+
|
|
109
|
+
// Mock process.stdout.isTTY to return true
|
|
110
|
+
const originalIsTTY = process.stdout.isTTY
|
|
111
|
+
process.stdout.isTTY = true
|
|
112
|
+
|
|
113
|
+
const { isInteractiveTerminal } = require('../lib/helpers/cliSupportHandler')
|
|
114
|
+
const result = isInteractiveTerminal()
|
|
115
|
+
|
|
116
|
+
// Restore original value
|
|
117
|
+
process.stdout.isTTY = originalIsTTY
|
|
118
|
+
|
|
119
|
+
expect(result).toBe(true)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('should return false when stdout is not TTY even without CI variables', () => {
|
|
123
|
+
// Clear all CI environment variables
|
|
124
|
+
delete process.env.CI
|
|
125
|
+
delete process.env.GITHUB_ACTIONS
|
|
126
|
+
delete process.env.GITLAB_CI
|
|
127
|
+
delete process.env.TRAVIS
|
|
128
|
+
delete process.env.CIRCLECI
|
|
129
|
+
delete process.env.JENKINS_URL
|
|
130
|
+
delete process.env.BUILDKITE
|
|
131
|
+
delete process.env.DRONE
|
|
132
|
+
|
|
133
|
+
// Mock process.stdout.isTTY to return false
|
|
134
|
+
const originalIsTTY = process.stdout.isTTY
|
|
135
|
+
process.stdout.isTTY = false
|
|
136
|
+
|
|
137
|
+
const { isInteractiveTerminal } = require('../lib/helpers/cliSupportHandler')
|
|
138
|
+
const result = isInteractiveTerminal()
|
|
139
|
+
|
|
140
|
+
// Restore original value
|
|
141
|
+
process.stdout.isTTY = originalIsTTY
|
|
142
|
+
|
|
143
|
+
expect(result).toBe(false)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('isEnvSupport', () => {
|
|
148
|
+
test('should return true for Node.js >= 20.13.0', () => {
|
|
149
|
+
// Mock semver.satisfies to return true for supported version
|
|
150
|
+
const semver = require('semver')
|
|
151
|
+
jest.spyOn(semver, 'satisfies').mockReturnValue(true)
|
|
152
|
+
|
|
153
|
+
jest.resetModules()
|
|
154
|
+
const { isEnvSupport } = require('../lib/helpers/cliSupportHandler')
|
|
155
|
+
|
|
156
|
+
const result = isEnvSupport()
|
|
157
|
+
expect(result).toBe(true)
|
|
158
|
+
|
|
159
|
+
semver.satisfies.mockRestore()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('should return false for Node.js < 20.13.0', () => {
|
|
163
|
+
// Mock semver.satisfies to return false for unsupported version
|
|
164
|
+
const semver = require('semver')
|
|
165
|
+
const mockSatisfies = jest.spyOn(semver, 'satisfies').mockReturnValue(false)
|
|
166
|
+
|
|
167
|
+
const { isEnvSupport } = require('../lib/helpers/cliSupportHandler')
|
|
168
|
+
|
|
169
|
+
const result = isEnvSupport()
|
|
170
|
+
expect(result).toBe(false)
|
|
171
|
+
|
|
172
|
+
mockSatisfies.mockRestore()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('should return true for Node.js > 20.13.0', () => {
|
|
176
|
+
// Mock semver.satisfies to return true for newer supported version
|
|
177
|
+
const semver = require('semver')
|
|
178
|
+
jest.spyOn(semver, 'satisfies').mockReturnValue(true)
|
|
179
|
+
|
|
180
|
+
jest.resetModules()
|
|
181
|
+
const { isEnvSupport } = require('../lib/helpers/cliSupportHandler')
|
|
182
|
+
|
|
183
|
+
const result = isEnvSupport()
|
|
184
|
+
expect(result).toBe(true)
|
|
185
|
+
|
|
186
|
+
semver.satisfies.mockRestore()
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
describe('noSupportError', () => {
|
|
191
|
+
let consoleSpy
|
|
192
|
+
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
afterEach(() => {
|
|
198
|
+
consoleSpy.mockRestore()
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('should output styled error message in interactive terminal', () => {
|
|
202
|
+
// Set up interactive terminal
|
|
203
|
+
process.stdout.isTTY = true
|
|
204
|
+
|
|
205
|
+
const { noSupportError } = require('../lib/helpers/cliSupportHandler')
|
|
206
|
+
const result = noSupportError(false)
|
|
207
|
+
|
|
208
|
+
expect(result).toBe(true)
|
|
209
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
210
|
+
expect.stringMatching(/error:/),
|
|
211
|
+
'npq suppressed due to old node version'
|
|
212
|
+
)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
test('should output plain error message in non-interactive terminal', () => {
|
|
216
|
+
// Set up non-interactive terminal
|
|
217
|
+
process.stdout.isTTY = false
|
|
218
|
+
|
|
219
|
+
const { noSupportError } = require('../lib/helpers/cliSupportHandler')
|
|
220
|
+
const result = noSupportError(false)
|
|
221
|
+
|
|
222
|
+
expect(result).toBe(true)
|
|
223
|
+
expect(consoleSpy).toHaveBeenCalledWith('error: npq suppressed due to old node version')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test('should call process.exit when failFast is true', () => {
|
|
227
|
+
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {})
|
|
228
|
+
process.stdout.isTTY = true
|
|
229
|
+
|
|
230
|
+
const { noSupportError } = require('../lib/helpers/cliSupportHandler')
|
|
231
|
+
noSupportError(true)
|
|
232
|
+
|
|
233
|
+
expect(mockExit).toHaveBeenCalledWith(-1)
|
|
234
|
+
mockExit.mockRestore()
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test('should not call process.exit when failFast is false', () => {
|
|
238
|
+
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {})
|
|
239
|
+
process.stdout.isTTY = true
|
|
240
|
+
|
|
241
|
+
const { noSupportError } = require('../lib/helpers/cliSupportHandler')
|
|
242
|
+
const result = noSupportError(false)
|
|
243
|
+
|
|
244
|
+
expect(result).toBe(true)
|
|
245
|
+
expect(mockExit).not.toHaveBeenCalled()
|
|
246
|
+
mockExit.mockRestore()
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
describe('packageManagerPassthrough', () => {
|
|
251
|
+
test('should spawn package manager with correct arguments and exit with status', () => {
|
|
252
|
+
const mockSpawnSync = jest.fn().mockReturnValue({ status: 0 })
|
|
253
|
+
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {})
|
|
254
|
+
|
|
255
|
+
// Mock child_process
|
|
256
|
+
jest.doMock('child_process', () => ({
|
|
257
|
+
spawnSync: mockSpawnSync
|
|
258
|
+
}))
|
|
259
|
+
|
|
260
|
+
// Mock process.argv
|
|
261
|
+
const originalArgv = process.argv
|
|
262
|
+
process.argv = ['node', 'npq', 'install', 'package-name']
|
|
263
|
+
|
|
264
|
+
const { packageManagerPassthrough } = require('../lib/helpers/cliSupportHandler')
|
|
265
|
+
packageManagerPassthrough()
|
|
266
|
+
|
|
267
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('npm', ['install', 'package-name'], {
|
|
268
|
+
stdio: 'inherit',
|
|
269
|
+
shell: true
|
|
270
|
+
})
|
|
271
|
+
expect(mockExit).toHaveBeenCalledWith(0)
|
|
272
|
+
|
|
273
|
+
// Restore mocks
|
|
274
|
+
process.argv = originalArgv
|
|
275
|
+
mockExit.mockRestore()
|
|
276
|
+
jest.dontMock('child_process')
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
test('should exit with error status when spawn fails', () => {
|
|
280
|
+
const mockSpawnSync = jest.fn().mockReturnValue({ status: 1 })
|
|
281
|
+
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {})
|
|
282
|
+
|
|
283
|
+
// Mock child_process
|
|
284
|
+
jest.doMock('child_process', () => ({
|
|
285
|
+
spawnSync: mockSpawnSync
|
|
286
|
+
}))
|
|
287
|
+
|
|
288
|
+
const originalArgv = process.argv
|
|
289
|
+
process.argv = ['node', 'npq', 'install', 'package-name']
|
|
290
|
+
|
|
291
|
+
const { packageManagerPassthrough } = require('../lib/helpers/cliSupportHandler')
|
|
292
|
+
packageManagerPassthrough()
|
|
293
|
+
|
|
294
|
+
expect(mockExit).toHaveBeenCalledWith(1)
|
|
295
|
+
|
|
296
|
+
// Restore mocks
|
|
297
|
+
process.argv = originalArgv
|
|
298
|
+
mockExit.mockRestore()
|
|
299
|
+
jest.dontMock('child_process')
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
test('should use custom package manager from environment variable', () => {
|
|
303
|
+
const mockSpawnSync = jest.fn().mockReturnValue({ status: 0 })
|
|
304
|
+
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {})
|
|
305
|
+
|
|
306
|
+
// Set custom package manager
|
|
307
|
+
process.env.NPQ_PKG_MGR = 'yarn'
|
|
308
|
+
|
|
309
|
+
// Mock child_process
|
|
310
|
+
jest.doMock('child_process', () => ({
|
|
311
|
+
spawnSync: mockSpawnSync
|
|
312
|
+
}))
|
|
313
|
+
|
|
314
|
+
const originalArgv = process.argv
|
|
315
|
+
process.argv = ['node', 'npq', 'add', 'package-name']
|
|
316
|
+
|
|
317
|
+
const { packageManagerPassthrough } = require('../lib/helpers/cliSupportHandler')
|
|
318
|
+
packageManagerPassthrough()
|
|
319
|
+
|
|
320
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('yarn', ['add', 'package-name'], {
|
|
321
|
+
stdio: 'inherit',
|
|
322
|
+
shell: true
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
// Restore mocks and env
|
|
326
|
+
delete process.env.NPQ_PKG_MGR
|
|
327
|
+
process.argv = originalArgv
|
|
328
|
+
mockExit.mockRestore()
|
|
329
|
+
jest.dontMock('child_process')
|
|
330
|
+
})
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
describe('reportResults', () => {
|
|
335
|
+
const mockMarshallResults = {
|
|
336
|
+
'test-package@1.0.0': [
|
|
337
|
+
{
|
|
338
|
+
'supply-chain-security': {
|
|
339
|
+
marshall: 'supply-chain-security',
|
|
340
|
+
categoryId: 'SupplyChainSecurity',
|
|
341
|
+
errors: [
|
|
342
|
+
{ message: 'Package has suspicious activity in recent versions' },
|
|
343
|
+
{ message: 'Maintainer account shows signs of compromise' }
|
|
344
|
+
],
|
|
345
|
+
warnings: [{ message: 'Package lacks proper security documentation' }]
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
],
|
|
349
|
+
'another-package@2.1.0': [
|
|
350
|
+
{
|
|
351
|
+
'package-health': {
|
|
352
|
+
marshall: 'package-health',
|
|
353
|
+
categoryId: 'PackageHealth',
|
|
354
|
+
errors: [],
|
|
355
|
+
warnings: [
|
|
356
|
+
{ message: 'Package has not been updated in over 2 years' },
|
|
357
|
+
{ message: 'Dependencies are outdated' }
|
|
358
|
+
]
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
]
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
beforeEach(() => {
|
|
365
|
+
// Clear CI environment variables for consistent testing
|
|
366
|
+
delete process.env.CI
|
|
367
|
+
delete process.env.GITHUB_ACTIONS
|
|
368
|
+
delete process.env.GITLAB_CI
|
|
369
|
+
delete process.env.TRAVIS
|
|
370
|
+
delete process.env.CIRCLECI
|
|
371
|
+
delete process.env.JENKINS_URL
|
|
372
|
+
delete process.env.BUILDKITE
|
|
373
|
+
delete process.env.DRONE
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
test('should return undefined for empty results', () => {
|
|
377
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
378
|
+
const result = reportResults({})
|
|
379
|
+
expect(result).toBeUndefined()
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
test('should return undefined for null results', () => {
|
|
383
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
384
|
+
const result = reportResults(null)
|
|
385
|
+
expect(result).toBeUndefined()
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
test('should generate both rich and plain text formats', () => {
|
|
389
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
390
|
+
const result = reportResults(mockMarshallResults)
|
|
391
|
+
|
|
392
|
+
expect(result).toBeDefined()
|
|
393
|
+
expect(result.resultsForPrettyPrint).toBeDefined()
|
|
394
|
+
expect(result.resultsForPlainTextPrint).toBeDefined()
|
|
395
|
+
expect(result.summaryForPrettyPrint).toBeDefined()
|
|
396
|
+
expect(result.summaryForPlainTextPrint).toBeDefined()
|
|
397
|
+
|
|
398
|
+
// Both formats should have content
|
|
399
|
+
expect(result.resultsForPrettyPrint.length).toBeGreaterThan(0)
|
|
400
|
+
expect(result.resultsForPlainTextPrint.length).toBeGreaterThan(0)
|
|
401
|
+
expect(result.summaryForPrettyPrint.length).toBeGreaterThan(0)
|
|
402
|
+
expect(result.summaryForPlainTextPrint.length).toBeGreaterThan(0)
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
test('should count errors and warnings correctly', () => {
|
|
406
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
407
|
+
const result = reportResults(mockMarshallResults)
|
|
408
|
+
|
|
409
|
+
expect(result.countErrors).toBe(2) // 2 errors from test-package
|
|
410
|
+
expect(result.countWarnings).toBe(3) // 1 warning from test-package + 2 from another-package
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
test('should set useRichFormatting to false in CI mode', () => {
|
|
414
|
+
process.env.CI = 'true'
|
|
415
|
+
// Ensure TTY is set to true so we're testing CI detection specifically
|
|
416
|
+
process.stdout.isTTY = true
|
|
417
|
+
|
|
418
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
419
|
+
const result = reportResults(mockMarshallResults)
|
|
420
|
+
|
|
421
|
+
expect(result.useRichFormatting).toBe(false)
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
test('should set useRichFormatting to true in interactive mode', () => {
|
|
425
|
+
// Clear CI environment variables
|
|
426
|
+
delete process.env.CI
|
|
427
|
+
delete process.env.GITHUB_ACTIONS
|
|
428
|
+
delete process.env.GITLAB_CI
|
|
429
|
+
delete process.env.TRAVIS
|
|
430
|
+
delete process.env.CIRCLECI
|
|
431
|
+
delete process.env.JENKINS_URL
|
|
432
|
+
delete process.env.BUILDKITE
|
|
433
|
+
delete process.env.DRONE
|
|
434
|
+
|
|
435
|
+
// Mock process.stdout.isTTY
|
|
436
|
+
const originalIsTTY = process.stdout.isTTY
|
|
437
|
+
process.stdout.isTTY = true
|
|
438
|
+
|
|
439
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
440
|
+
const result = reportResults(mockMarshallResults)
|
|
441
|
+
|
|
442
|
+
// Restore original value
|
|
443
|
+
process.stdout.isTTY = originalIsTTY
|
|
444
|
+
|
|
445
|
+
expect(result.useRichFormatting).toBe(true)
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
test('should include package names in plain text format', () => {
|
|
449
|
+
process.env.CI = 'true'
|
|
450
|
+
// Ensure TTY is set to true so we're testing CI detection specifically
|
|
451
|
+
process.stdout.isTTY = true
|
|
452
|
+
|
|
453
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
454
|
+
const result = reportResults(mockMarshallResults)
|
|
455
|
+
|
|
456
|
+
expect(result.resultsForPlainTextPrint).toContain('test-package@1.0.0')
|
|
457
|
+
expect(result.resultsForPlainTextPrint).toContain('another-package@2.1.0')
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
test('should include error and warning labels in plain text format', () => {
|
|
461
|
+
process.env.CI = 'true'
|
|
462
|
+
// Ensure TTY is set to true so we're testing CI detection specifically
|
|
463
|
+
process.stdout.isTTY = true
|
|
464
|
+
|
|
465
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
466
|
+
const result = reportResults(mockMarshallResults)
|
|
467
|
+
|
|
468
|
+
expect(result.resultsForPlainTextPrint).toContain('ERROR:')
|
|
469
|
+
expect(result.resultsForPlainTextPrint).toContain('WARNING:')
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
test('should include category names in both formats', () => {
|
|
473
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
474
|
+
const result = reportResults(mockMarshallResults)
|
|
475
|
+
|
|
476
|
+
// Rich format should contain category names
|
|
477
|
+
expect(result.resultsForPrettyPrint).toContain('Supply Chain Security')
|
|
478
|
+
expect(result.resultsForPrettyPrint).toContain('Package Health')
|
|
479
|
+
|
|
480
|
+
// Plain text format should also contain category names
|
|
481
|
+
expect(result.resultsForPlainTextPrint).toContain('Supply Chain Security')
|
|
482
|
+
expect(result.resultsForPlainTextPrint).toContain('Package Health')
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
test('should include error and warning messages in both formats', () => {
|
|
486
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
487
|
+
const result = reportResults(mockMarshallResults)
|
|
488
|
+
|
|
489
|
+
const expectedMessages = [
|
|
490
|
+
// NOTE: the first message is cut at the end to not include "versions" as part of the text
|
|
491
|
+
// because it gets cut due to the 80 chars default limit of output to the terminal
|
|
492
|
+
// and fails the test for exact string match
|
|
493
|
+
'Package has suspicious activity in recent',
|
|
494
|
+
'Maintainer account shows signs of compromise',
|
|
495
|
+
'Package lacks proper security documentation',
|
|
496
|
+
'Package has not been updated in over 2 years',
|
|
497
|
+
'Dependencies are outdated'
|
|
498
|
+
]
|
|
499
|
+
|
|
500
|
+
expectedMessages.forEach((message) => {
|
|
501
|
+
expect(result.resultsForPrettyPrint).toContain(message)
|
|
502
|
+
expect(result.resultsForPlainTextPrint).toContain(message)
|
|
503
|
+
})
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
test('should include summary statistics in both formats', () => {
|
|
507
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
508
|
+
const result = reportResults(mockMarshallResults)
|
|
509
|
+
|
|
510
|
+
// Check rich format summary
|
|
511
|
+
expect(result.summaryForPrettyPrint).toContain('Summary:')
|
|
512
|
+
expect(result.summaryForPrettyPrint).toContain('Total packages:')
|
|
513
|
+
expect(result.summaryForPrettyPrint).toContain('2')
|
|
514
|
+
expect(result.summaryForPrettyPrint).toContain('Total errors:')
|
|
515
|
+
expect(result.summaryForPrettyPrint).toContain('Total warnings:')
|
|
516
|
+
expect(result.summaryForPrettyPrint).toContain('3')
|
|
517
|
+
|
|
518
|
+
// Check plain text format summary
|
|
519
|
+
expect(result.summaryForPlainTextPrint).toContain('Summary:')
|
|
520
|
+
expect(result.summaryForPlainTextPrint).toContain('Total packages: 2')
|
|
521
|
+
expect(result.summaryForPlainTextPrint).toContain('Total errors: 2')
|
|
522
|
+
expect(result.summaryForPlainTextPrint).toContain('Total warnings: 3')
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
test('should return all required properties', () => {
|
|
526
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
527
|
+
const result = reportResults(mockMarshallResults)
|
|
528
|
+
|
|
529
|
+
const expectedProperties = [
|
|
530
|
+
'countErrors',
|
|
531
|
+
'countWarnings',
|
|
532
|
+
'resultsForPrettyPrint',
|
|
533
|
+
'resultsForJSON',
|
|
534
|
+
'resultsForPlainTextPrint',
|
|
535
|
+
'summaryForPrettyPrint',
|
|
536
|
+
'summaryForPlainTextPrint',
|
|
537
|
+
'useRichFormatting'
|
|
538
|
+
]
|
|
539
|
+
|
|
540
|
+
expectedProperties.forEach((prop) => {
|
|
541
|
+
expect(result).toHaveProperty(prop)
|
|
542
|
+
})
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
test('should include JSON format with original data structure', () => {
|
|
546
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
547
|
+
const result = reportResults(mockMarshallResults)
|
|
548
|
+
|
|
549
|
+
expect(result.resultsForJSON).toBeDefined()
|
|
550
|
+
expect(Array.isArray(result.resultsForJSON)).toBe(true)
|
|
551
|
+
expect(result.resultsForJSON.length).toBe(2) // Two packages
|
|
552
|
+
|
|
553
|
+
// Check that JSON format contains expected package data
|
|
554
|
+
const packageNames = result.resultsForJSON.map((pkg) => pkg.pkg)
|
|
555
|
+
expect(packageNames).toContain('test-package@1.0.0')
|
|
556
|
+
expect(packageNames).toContain('another-package@2.1.0')
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
test('should set useRichFormatting to false when plain option is true', () => {
|
|
560
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
561
|
+
const result = reportResults(mockMarshallResults, { plain: true })
|
|
562
|
+
|
|
563
|
+
expect(result.useRichFormatting).toBe(false)
|
|
564
|
+
})
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
describe('reportResults helper functions', () => {
|
|
568
|
+
test('should handle getTerminalWidth error cases', () => {
|
|
569
|
+
// First define getWindowSize if it doesn't exist
|
|
570
|
+
const originalGetWindowSize = process.stdout.getWindowSize
|
|
571
|
+
if (!originalGetWindowSize) {
|
|
572
|
+
process.stdout.getWindowSize = () => [80]
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Use jest.spyOn to mock getWindowSize
|
|
576
|
+
const mockGetWindowSize = jest.spyOn(process.stdout, 'getWindowSize')
|
|
577
|
+
mockGetWindowSize.mockImplementation(() => {
|
|
578
|
+
throw new Error('Terminal size detection failed')
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
process.stdout.isTTY = true
|
|
582
|
+
|
|
583
|
+
const testResults = {
|
|
584
|
+
'test-package@1.0.0': [
|
|
585
|
+
{
|
|
586
|
+
'supply-chain-security': {
|
|
587
|
+
categoryId: 'SupplyChainSecurity',
|
|
588
|
+
marshall: 'supply-chain-security',
|
|
589
|
+
errors: [{ message: 'Test error', pkg: 'test-package@1.0.0' }],
|
|
590
|
+
warnings: []
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
]
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
597
|
+
const result = reportResults(testResults)
|
|
598
|
+
expect(result).toBeDefined()
|
|
599
|
+
expect(result.useRichFormatting).toBeDefined()
|
|
600
|
+
|
|
601
|
+
mockGetWindowSize.mockRestore()
|
|
602
|
+
|
|
603
|
+
// Restore original if it existed, or delete if we added it
|
|
604
|
+
if (originalGetWindowSize) {
|
|
605
|
+
process.stdout.getWindowSize = originalGetWindowSize
|
|
606
|
+
} else {
|
|
607
|
+
delete process.stdout.getWindowSize
|
|
608
|
+
}
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
test('should handle stdout without getWindowSize method', () => {
|
|
612
|
+
// Mock process.stdout.isTTY to true but don't define getWindowSize
|
|
613
|
+
const originalGetWindowSize = process.stdout.getWindowSize
|
|
614
|
+
delete process.stdout.getWindowSize
|
|
615
|
+
process.stdout.isTTY = true
|
|
616
|
+
|
|
617
|
+
const testResults = {
|
|
618
|
+
'test-package@1.0.0': [
|
|
619
|
+
{
|
|
620
|
+
'package-health': {
|
|
621
|
+
categoryId: 'PackageHealth',
|
|
622
|
+
marshall: 'package-health',
|
|
623
|
+
errors: [{ message: 'Test error', pkg: 'test-package@1.0.0' }],
|
|
624
|
+
warnings: []
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
]
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
631
|
+
const result = reportResults(testResults)
|
|
632
|
+
expect(result).toBeDefined()
|
|
633
|
+
|
|
634
|
+
// Restore original getWindowSize
|
|
635
|
+
if (originalGetWindowSize) {
|
|
636
|
+
process.stdout.getWindowSize = originalGetWindowSize
|
|
637
|
+
}
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
test('should handle text wrapping for very long words', () => {
|
|
641
|
+
// First define getWindowSize if it doesn't exist
|
|
642
|
+
const originalGetWindowSize = process.stdout.getWindowSize
|
|
643
|
+
if (!originalGetWindowSize) {
|
|
644
|
+
process.stdout.getWindowSize = () => [80]
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Mock getWindowSize to return small width
|
|
648
|
+
const mockGetWindowSize = jest.spyOn(process.stdout, 'getWindowSize')
|
|
649
|
+
mockGetWindowSize.mockReturnValue([40])
|
|
650
|
+
|
|
651
|
+
process.stdout.isTTY = true
|
|
652
|
+
|
|
653
|
+
const testResults = {
|
|
654
|
+
'test-package@1.0.0': [
|
|
655
|
+
{
|
|
656
|
+
'malware-detection': {
|
|
657
|
+
categoryId: 'MalwareDetection',
|
|
658
|
+
marshall: 'malware-detection',
|
|
659
|
+
errors: [
|
|
660
|
+
{
|
|
661
|
+
message:
|
|
662
|
+
'This-is-a-very-long-word-that-should-be-broken-when-wrapping-text-because-it-exceeds-terminal-width',
|
|
663
|
+
pkg: 'test-package@1.0.0'
|
|
664
|
+
}
|
|
665
|
+
],
|
|
666
|
+
warnings: []
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
]
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
673
|
+
const result = reportResults(testResults)
|
|
674
|
+
expect(result).toBeDefined()
|
|
675
|
+
expect(result.resultsForPrettyPrint).toContain('This-is-a-very-long-word')
|
|
676
|
+
|
|
677
|
+
mockGetWindowSize.mockRestore()
|
|
678
|
+
|
|
679
|
+
// Restore original if it existed, or delete if we added it
|
|
680
|
+
if (originalGetWindowSize) {
|
|
681
|
+
process.stdout.getWindowSize = originalGetWindowSize
|
|
682
|
+
} else {
|
|
683
|
+
delete process.stdout.getWindowSize
|
|
684
|
+
}
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
test('should handle text wrapping edge cases', () => {
|
|
688
|
+
process.stdout.isTTY = true
|
|
689
|
+
|
|
690
|
+
const testResults = {
|
|
691
|
+
'test-package@1.0.0': [
|
|
692
|
+
{
|
|
693
|
+
'supply-chain-security': {
|
|
694
|
+
categoryId: 'SupplyChainSecurity',
|
|
695
|
+
marshall: 'supply-chain-security',
|
|
696
|
+
errors: [
|
|
697
|
+
{
|
|
698
|
+
message: 'Word',
|
|
699
|
+
pkg: 'test-package@1.0.0'
|
|
700
|
+
}
|
|
701
|
+
],
|
|
702
|
+
warnings: []
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
]
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
709
|
+
const result = reportResults(testResults)
|
|
710
|
+
expect(result).toBeDefined()
|
|
711
|
+
expect(result.resultsForPrettyPrint).toContain('Word')
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
test('should handle malicious package detection edge cases', () => {
|
|
715
|
+
process.stdout.isTTY = true
|
|
716
|
+
|
|
717
|
+
const testResults = {
|
|
718
|
+
'test-package@1.0.0': [
|
|
719
|
+
{
|
|
720
|
+
'malware-detection': {
|
|
721
|
+
categoryId: 'MalwareDetection',
|
|
722
|
+
marshall: 'malware-detection',
|
|
723
|
+
errors: [{ message: 'Malicious package found in registry', pkg: 'test-package@1.0.0' }],
|
|
724
|
+
warnings: []
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
]
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
731
|
+
const result = reportResults(testResults)
|
|
732
|
+
expect(result).toBeDefined()
|
|
733
|
+
expect(result.countErrors).toBe(1) // Should be 1 for malicious packages
|
|
734
|
+
expect(result.resultsForPrettyPrint).toContain('Malicious package found')
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
test('should handle multiple error arrays with malicious package in later array', () => {
|
|
738
|
+
process.stdout.isTTY = true
|
|
739
|
+
|
|
740
|
+
const testResults = {
|
|
741
|
+
'test-package@1.0.0': [
|
|
742
|
+
{
|
|
743
|
+
'malware-detection': {
|
|
744
|
+
categoryId: 'MalwareDetection',
|
|
745
|
+
marshall: 'malware-detection',
|
|
746
|
+
errors: [
|
|
747
|
+
{ message: 'Regular error 1', pkg: 'test-package@1.0.0' },
|
|
748
|
+
{ message: 'Malicious package found', pkg: 'test-package@1.0.0' }
|
|
749
|
+
],
|
|
750
|
+
warnings: []
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
]
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
757
|
+
const result = reportResults(testResults)
|
|
758
|
+
expect(result).toBeDefined()
|
|
759
|
+
expect(result.countErrors).toBe(1) // Malicious packages count as 1 error regardless of count
|
|
760
|
+
expect(result.resultsForPrettyPrint).toContain('Malicious package found')
|
|
761
|
+
})
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
describe('basic functionality test', () => {
|
|
765
|
+
test('functions can be imported', () => {
|
|
766
|
+
const { reportResults } = require('../lib/helpers/reportResults')
|
|
767
|
+
const { isInteractiveTerminal } = require('../lib/helpers/cliSupportHandler')
|
|
768
|
+
|
|
769
|
+
expect(typeof reportResults).toBe('function')
|
|
770
|
+
expect(typeof isInteractiveTerminal).toBe('function')
|
|
771
|
+
})
|
|
772
|
+
})
|