npq 3.8.1 → 3.9.0

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.
Files changed (48) hide show
  1. package/.github/npq.mov +0 -0
  2. package/.github/npq.mp4 +0 -0
  3. package/.github/npq.png +0 -0
  4. package/.github/workflows/main.yml +2 -2
  5. package/README.md +18 -9
  6. package/__tests__/__fixtures__/test.marshall.js +5 -6
  7. package/__tests__/cli.test.js +110 -0
  8. package/__tests__/cliPrompt.test.js +409 -0
  9. package/__tests__/marshalls.base.test.js +1 -1
  10. package/__tests__/marshalls.classMethods.test.js +11 -14
  11. package/__tests__/marshalls.expiredDomains.test.js +2 -2
  12. package/__tests__/marshalls.provenance.test.js +181 -4
  13. package/__tests__/marshalls.signatures.test.js +0 -4
  14. package/__tests__/marshalls.snyk.test.js +294 -0
  15. package/__tests__/marshalls.tasks.test.js +48 -24
  16. package/__tests__/marshalls.typosquatting.test.js +16 -9
  17. package/__tests__/reportResults.test.js +772 -0
  18. package/__tests__/scripts.test.js +2 -2
  19. package/bin/npq-hero.js +70 -20
  20. package/bin/npq.js +113 -21
  21. package/lib/cli.js +127 -24
  22. package/lib/helpers/cliPrompt.js +97 -0
  23. package/lib/helpers/cliSpinner.js +104 -0
  24. package/lib/helpers/cliSupportHandler.js +50 -7
  25. package/lib/helpers/packageRepoUtils.js +5 -0
  26. package/lib/helpers/promiseThrottler.js +96 -0
  27. package/lib/helpers/reportResults.js +304 -0
  28. package/lib/helpers/sourcePackages.js +36 -0
  29. package/lib/marshall.js +42 -65
  30. package/lib/marshalls/age.marshall.js +3 -2
  31. package/lib/marshalls/author.marshall.js +36 -7
  32. package/lib/marshalls/baseMarshall.js +9 -17
  33. package/lib/marshalls/deprecation.marshall.js +43 -0
  34. package/lib/marshalls/downloads.marshall.js +1 -1
  35. package/lib/marshalls/expiredDomains.marshall.js +7 -5
  36. package/lib/marshalls/index.js +110 -80
  37. package/lib/marshalls/license.marshall.js +1 -1
  38. package/lib/marshalls/newbin.marshall.js +1 -2
  39. package/lib/marshalls/provenance.marshall.js +152 -33
  40. package/lib/marshalls/repo.marshall.js +6 -5
  41. package/lib/marshalls/scripts.marshall.js +1 -1
  42. package/lib/marshalls/signatures.marshall.js +26 -2
  43. package/lib/marshalls/snyk.marshall.js +61 -15
  44. package/lib/marshalls/typosquatting.marshall.js +8 -5
  45. package/lib/marshalls/version-maturity.marshall.js +1 -1
  46. package/package.json +16 -19
  47. package/scripts/postinstall.js +10 -11
  48. 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
+ })