pnpm-catalog-updates 1.0.2 → 1.1.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.
- package/README.md +15 -0
- package/dist/index.js +22031 -10684
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
- package/src/cli/__tests__/commandRegistrar.test.ts +248 -0
- package/src/cli/commandRegistrar.ts +785 -0
- package/src/cli/commands/__tests__/aiCommand.test.ts +161 -0
- package/src/cli/commands/__tests__/analyzeCommand.test.ts +283 -0
- package/src/cli/commands/__tests__/checkCommand.test.ts +435 -0
- package/src/cli/commands/__tests__/graphCommand.test.ts +312 -0
- package/src/cli/commands/__tests__/initCommand.test.ts +317 -0
- package/src/cli/commands/__tests__/rollbackCommand.test.ts +400 -0
- package/src/cli/commands/__tests__/securityCommand.test.ts +467 -0
- package/src/cli/commands/__tests__/themeCommand.test.ts +166 -0
- package/src/cli/commands/__tests__/updateCommand.test.ts +720 -0
- package/src/cli/commands/__tests__/workspaceCommand.test.ts +286 -0
- package/src/cli/commands/aiCommand.ts +163 -0
- package/src/cli/commands/analyzeCommand.ts +219 -0
- package/src/cli/commands/checkCommand.ts +91 -98
- package/src/cli/commands/graphCommand.ts +475 -0
- package/src/cli/commands/initCommand.ts +64 -54
- package/src/cli/commands/rollbackCommand.ts +334 -0
- package/src/cli/commands/securityCommand.ts +165 -100
- package/src/cli/commands/themeCommand.ts +148 -0
- package/src/cli/commands/updateCommand.ts +215 -263
- package/src/cli/commands/workspaceCommand.ts +73 -0
- package/src/cli/constants/cliChoices.ts +93 -0
- package/src/cli/formatters/__tests__/__snapshots__/outputFormatter.test.ts.snap +557 -0
- package/src/cli/formatters/__tests__/ciFormatter.test.ts +526 -0
- package/src/cli/formatters/__tests__/outputFormatter.test.ts +448 -0
- package/src/cli/formatters/__tests__/progressBar.test.ts +709 -0
- package/src/cli/formatters/ciFormatter.ts +964 -0
- package/src/cli/formatters/colorUtils.ts +145 -0
- package/src/cli/formatters/outputFormatter.ts +615 -332
- package/src/cli/formatters/progressBar.ts +43 -52
- package/src/cli/formatters/versionFormatter.ts +132 -0
- package/src/cli/handlers/aiAnalysisHandler.ts +205 -0
- package/src/cli/handlers/changelogHandler.ts +113 -0
- package/src/cli/handlers/index.ts +9 -0
- package/src/cli/handlers/installHandler.ts +130 -0
- package/src/cli/index.ts +175 -726
- package/src/cli/interactive/InteractiveOptionsCollector.ts +387 -0
- package/src/cli/interactive/interactivePrompts.ts +189 -83
- package/src/cli/interactive/optionUtils.ts +89 -0
- package/src/cli/themes/colorTheme.ts +43 -16
- package/src/cli/utils/cliOutput.ts +118 -0
- package/src/cli/utils/commandHelpers.ts +249 -0
- package/src/cli/validators/commandValidator.ts +321 -336
- package/src/cli/validators/index.ts +37 -2
- package/src/cli/options/globalOptions.ts +0 -437
- package/src/cli/options/index.ts +0 -5
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Command Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
import type { OutputFormatter } from '../../formatters/outputFormatter.js'
|
|
7
|
+
|
|
8
|
+
// Use vi.hoisted to ensure mocks are available during vi.mock hoisting
|
|
9
|
+
const mocks = vi.hoisted(() => {
|
|
10
|
+
// Create a mock CommandExitError class for instanceof checks
|
|
11
|
+
class MockCommandExitError extends Error {
|
|
12
|
+
public readonly exitCode: number
|
|
13
|
+
public readonly silent: boolean
|
|
14
|
+
|
|
15
|
+
constructor(exitCode: number, message?: string, silent = false) {
|
|
16
|
+
super(message || (exitCode === 0 ? 'Command completed successfully' : 'Command failed'))
|
|
17
|
+
this.name = 'CommandExitError'
|
|
18
|
+
this.exitCode = exitCode
|
|
19
|
+
this.silent = silent
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static success(message?: string): MockCommandExitError {
|
|
23
|
+
return new MockCommandExitError(0, message, true)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static failure(message?: string): MockCommandExitError {
|
|
27
|
+
return new MockCommandExitError(1, message)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static withCode(code: number, message?: string): MockCommandExitError {
|
|
31
|
+
return new MockCommandExitError(code, message)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Create a chainable chalk mock that supports all color combinations
|
|
36
|
+
const createChalkMock = () => {
|
|
37
|
+
const createColorFn = (text: string) => text
|
|
38
|
+
const colorFn = Object.assign(createColorFn, {
|
|
39
|
+
bold: Object.assign((text: string) => text, {
|
|
40
|
+
cyan: (text: string) => text,
|
|
41
|
+
white: (text: string) => text,
|
|
42
|
+
}),
|
|
43
|
+
dim: Object.assign((text: string) => text, {
|
|
44
|
+
white: (text: string) => text,
|
|
45
|
+
}),
|
|
46
|
+
red: Object.assign((text: string) => text, {
|
|
47
|
+
bold: (text: string) => text,
|
|
48
|
+
}),
|
|
49
|
+
green: (text: string) => text,
|
|
50
|
+
yellow: (text: string) => text,
|
|
51
|
+
blue: (text: string) => text,
|
|
52
|
+
gray: (text: string) => text,
|
|
53
|
+
cyan: (text: string) => text,
|
|
54
|
+
white: (text: string) => text,
|
|
55
|
+
})
|
|
56
|
+
return colorFn
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
spawnSync: vi.fn(),
|
|
61
|
+
exec: vi.fn(
|
|
62
|
+
(
|
|
63
|
+
_cmd: string,
|
|
64
|
+
callback: (error: Error | null, result: { stdout: string; stderr: string }) => void
|
|
65
|
+
) => {
|
|
66
|
+
callback(null, { stdout: '', stderr: '' })
|
|
67
|
+
}
|
|
68
|
+
),
|
|
69
|
+
pathExists: vi.fn(),
|
|
70
|
+
formatSecurityReport: vi.fn().mockReturnValue('Formatted security report'),
|
|
71
|
+
CommandExitError: MockCommandExitError,
|
|
72
|
+
createChalkMock,
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Mock @pcu/utils - include CommandExitError for instanceof checks
|
|
77
|
+
vi.mock('@pcu/utils', () => ({
|
|
78
|
+
CommandExitError: mocks.CommandExitError,
|
|
79
|
+
logger: {
|
|
80
|
+
error: vi.fn(),
|
|
81
|
+
warn: vi.fn(),
|
|
82
|
+
info: vi.fn(),
|
|
83
|
+
debug: vi.fn(),
|
|
84
|
+
},
|
|
85
|
+
t: (key: string) => key,
|
|
86
|
+
// Include async utilities that are used by the code
|
|
87
|
+
timeout: vi.fn().mockImplementation((promise: Promise<unknown>) => promise),
|
|
88
|
+
delay: vi.fn().mockResolvedValue(undefined),
|
|
89
|
+
retry: vi.fn().mockImplementation((fn: () => Promise<unknown>) => fn()),
|
|
90
|
+
// Include validation utilities
|
|
91
|
+
createValidationResult: (isValid = true, errors: string[] = [], warnings: string[] = []) => ({
|
|
92
|
+
isValid,
|
|
93
|
+
errors,
|
|
94
|
+
warnings,
|
|
95
|
+
}),
|
|
96
|
+
// ERR-003: Include error handling utilities
|
|
97
|
+
getErrorCode: (error: unknown) => {
|
|
98
|
+
if (error instanceof Error && 'code' in error) {
|
|
99
|
+
return (error as NodeJS.ErrnoException).code
|
|
100
|
+
}
|
|
101
|
+
return undefined
|
|
102
|
+
},
|
|
103
|
+
toError: (error: unknown) => (error instanceof Error ? error : new Error(String(error))),
|
|
104
|
+
}))
|
|
105
|
+
|
|
106
|
+
// Mock child_process
|
|
107
|
+
vi.mock('node:child_process', () => ({
|
|
108
|
+
spawnSync: mocks.spawnSync,
|
|
109
|
+
exec: mocks.exec,
|
|
110
|
+
}))
|
|
111
|
+
|
|
112
|
+
// Mock fs-extra
|
|
113
|
+
vi.mock('fs-extra', () => ({
|
|
114
|
+
default: {
|
|
115
|
+
pathExists: mocks.pathExists,
|
|
116
|
+
},
|
|
117
|
+
pathExists: mocks.pathExists,
|
|
118
|
+
}))
|
|
119
|
+
|
|
120
|
+
// Mock ProgressBar - needs to be a proper class constructor
|
|
121
|
+
vi.mock('../../formatters/progressBar.js', () => ({
|
|
122
|
+
ProgressBar: class MockProgressBar {
|
|
123
|
+
start = vi.fn()
|
|
124
|
+
succeed = vi.fn()
|
|
125
|
+
fail = vi.fn()
|
|
126
|
+
stop = vi.fn()
|
|
127
|
+
update = vi.fn()
|
|
128
|
+
},
|
|
129
|
+
}))
|
|
130
|
+
|
|
131
|
+
// Mock ThemeManager and StyledText - QUAL-011: Updated for unified output helpers
|
|
132
|
+
vi.mock('../../themes/colorTheme.js', () => ({
|
|
133
|
+
ThemeManager: {
|
|
134
|
+
setTheme: vi.fn(),
|
|
135
|
+
getTheme: vi.fn().mockReturnValue({
|
|
136
|
+
major: (text: string) => text,
|
|
137
|
+
minor: (text: string) => text,
|
|
138
|
+
patch: (text: string) => text,
|
|
139
|
+
}),
|
|
140
|
+
},
|
|
141
|
+
StyledText: {
|
|
142
|
+
iconAnalysis: (text?: string) => `[analysis]${text ?? ''}`,
|
|
143
|
+
iconSuccess: (text?: string) => `[success]${text ?? ''}`,
|
|
144
|
+
iconInfo: (text?: string) => `[info]${text ?? ''}`,
|
|
145
|
+
iconError: (text?: string) => `[error]${text ?? ''}`,
|
|
146
|
+
iconSecurity: (text?: string) => `[security]${text ?? ''}`,
|
|
147
|
+
iconUpdate: (text?: string) => `[update]${text ?? ''}`,
|
|
148
|
+
iconWarning: (text?: string) => `[warning]${text ?? ''}`,
|
|
149
|
+
muted: (text: string) => `[muted]${text}`,
|
|
150
|
+
error: (text: string) => `[error]${text}`,
|
|
151
|
+
},
|
|
152
|
+
}))
|
|
153
|
+
|
|
154
|
+
// Mock cliOutput - QUAL-011: Added for unified output helpers
|
|
155
|
+
vi.mock('../../utils/cliOutput.js', () => ({
|
|
156
|
+
cliOutput: {
|
|
157
|
+
print: vi.fn((...args: unknown[]) => console.log(...args)),
|
|
158
|
+
error: vi.fn((...args: unknown[]) => console.error(...args)),
|
|
159
|
+
warn: vi.fn((...args: unknown[]) => console.warn(...args)),
|
|
160
|
+
},
|
|
161
|
+
}))
|
|
162
|
+
|
|
163
|
+
// Mock chalk with chainable functions from hoisted mock
|
|
164
|
+
vi.mock('chalk', () => ({
|
|
165
|
+
default: mocks.createChalkMock(),
|
|
166
|
+
}))
|
|
167
|
+
|
|
168
|
+
// Import after mock setup
|
|
169
|
+
const { SecurityCommand } = await import('../securityCommand.js')
|
|
170
|
+
|
|
171
|
+
describe('SecurityCommand', () => {
|
|
172
|
+
let command: InstanceType<typeof SecurityCommand>
|
|
173
|
+
let mockOutputFormatter: OutputFormatter
|
|
174
|
+
let consoleSpy: ReturnType<typeof vi.spyOn>
|
|
175
|
+
let consoleErrorSpy: ReturnType<typeof vi.spyOn>
|
|
176
|
+
let consoleWarnSpy: ReturnType<typeof vi.spyOn>
|
|
177
|
+
let processExitSpy: ReturnType<typeof vi.spyOn>
|
|
178
|
+
|
|
179
|
+
const mockNpmAuditResult = {
|
|
180
|
+
vulnerabilities: {
|
|
181
|
+
'example-vuln-1': {
|
|
182
|
+
name: 'vulnerable-package',
|
|
183
|
+
severity: 'high',
|
|
184
|
+
title: 'Remote Code Execution',
|
|
185
|
+
url: 'https://npmjs.com/advisories/1234',
|
|
186
|
+
range: '>=1.0.0 <2.0.0',
|
|
187
|
+
fixAvailable: true,
|
|
188
|
+
via: [{ source: '1234', name: 'vulnerable-package' }],
|
|
189
|
+
cwe: ['CWE-94'],
|
|
190
|
+
cve: ['CVE-2023-1234'],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
beforeEach(() => {
|
|
196
|
+
vi.clearAllMocks()
|
|
197
|
+
|
|
198
|
+
// Set up default mock return values
|
|
199
|
+
mocks.pathExists.mockResolvedValue(true)
|
|
200
|
+
mocks.spawnSync.mockReturnValue({
|
|
201
|
+
status: 1,
|
|
202
|
+
stdout: JSON.stringify(mockNpmAuditResult),
|
|
203
|
+
stderr: '',
|
|
204
|
+
error: undefined,
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
208
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
209
|
+
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
210
|
+
processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never)
|
|
211
|
+
|
|
212
|
+
// Create mock output formatter using hoisted mock
|
|
213
|
+
mockOutputFormatter = {
|
|
214
|
+
formatSecurityReport: mocks.formatSecurityReport,
|
|
215
|
+
formatOutdatedReport: vi.fn(),
|
|
216
|
+
formatUpdateResult: vi.fn(),
|
|
217
|
+
formatUpdatePlan: vi.fn(),
|
|
218
|
+
formatImpactAnalysis: vi.fn(),
|
|
219
|
+
formatJSON: vi.fn(),
|
|
220
|
+
formatYAML: vi.fn(),
|
|
221
|
+
formatTable: vi.fn(),
|
|
222
|
+
formatMinimal: vi.fn(),
|
|
223
|
+
} as unknown as OutputFormatter
|
|
224
|
+
|
|
225
|
+
command = new SecurityCommand(mockOutputFormatter)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
afterEach(() => {
|
|
229
|
+
consoleSpy.mockRestore()
|
|
230
|
+
consoleErrorSpy.mockRestore()
|
|
231
|
+
consoleWarnSpy.mockRestore()
|
|
232
|
+
processExitSpy.mockRestore()
|
|
233
|
+
vi.resetAllMocks()
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
describe('execute', () => {
|
|
237
|
+
it('should perform security scan', async () => {
|
|
238
|
+
try {
|
|
239
|
+
await command.execute({})
|
|
240
|
+
} catch {
|
|
241
|
+
// Expected to throw CommandExitError
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
expect(mocks.spawnSync).toHaveBeenCalled()
|
|
245
|
+
expect(mockOutputFormatter.formatSecurityReport).toHaveBeenCalled()
|
|
246
|
+
expect(consoleSpy).toHaveBeenCalled()
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('should use specified workspace path', async () => {
|
|
250
|
+
try {
|
|
251
|
+
await command.execute({ workspace: '/custom/workspace' })
|
|
252
|
+
} catch {
|
|
253
|
+
// Expected to throw CommandExitError
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
expect(mocks.spawnSync).toHaveBeenCalledWith(
|
|
257
|
+
'pnpm',
|
|
258
|
+
expect.arrayContaining(['audit']),
|
|
259
|
+
expect.objectContaining({
|
|
260
|
+
cwd: '/custom/workspace',
|
|
261
|
+
})
|
|
262
|
+
)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('should show verbose output when specified', async () => {
|
|
266
|
+
try {
|
|
267
|
+
await command.execute({ verbose: true })
|
|
268
|
+
} catch {
|
|
269
|
+
// Expected to throw CommandExitError
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
expect(consoleSpy).toHaveBeenCalled()
|
|
273
|
+
const calls = consoleSpy.mock.calls.flat().join(' ')
|
|
274
|
+
// The mock t() returns the translation key, not the actual translated value
|
|
275
|
+
expect(calls).toContain('command.workspace.title')
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('should exit with code 1 when critical vulnerabilities found', async () => {
|
|
279
|
+
mocks.spawnSync.mockReturnValue({
|
|
280
|
+
status: 1,
|
|
281
|
+
stdout: JSON.stringify({
|
|
282
|
+
vulnerabilities: {
|
|
283
|
+
'critical-vuln': {
|
|
284
|
+
name: 'critical-package',
|
|
285
|
+
severity: 'critical',
|
|
286
|
+
title: 'Critical Issue',
|
|
287
|
+
url: '',
|
|
288
|
+
range: '*',
|
|
289
|
+
fixAvailable: true,
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
}),
|
|
293
|
+
stderr: '',
|
|
294
|
+
error: undefined,
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
await command.execute({})
|
|
299
|
+
expect.fail('Should have thrown CommandExitError')
|
|
300
|
+
} catch (error) {
|
|
301
|
+
expect((error as { exitCode: number }).exitCode).toBe(1)
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('should exit with code 0 when no critical vulnerabilities found', async () => {
|
|
306
|
+
mocks.spawnSync.mockReturnValue({
|
|
307
|
+
status: 0,
|
|
308
|
+
stdout: JSON.stringify({ vulnerabilities: {} }),
|
|
309
|
+
stderr: '',
|
|
310
|
+
error: undefined,
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
await command.execute({})
|
|
315
|
+
} catch (error) {
|
|
316
|
+
expect((error as { exitCode: number }).exitCode).toBe(0)
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('should handle error when package.json not found', async () => {
|
|
321
|
+
mocks.pathExists.mockResolvedValue(false)
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
await command.execute({})
|
|
325
|
+
expect.fail('Should have thrown CommandExitError')
|
|
326
|
+
} catch (error) {
|
|
327
|
+
expect((error as { exitCode: number }).exitCode).toBe(1)
|
|
328
|
+
}
|
|
329
|
+
expect(consoleErrorSpy).toHaveBeenCalled()
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('should handle npm audit failure', async () => {
|
|
333
|
+
mocks.spawnSync.mockReturnValue({
|
|
334
|
+
status: 2,
|
|
335
|
+
stdout: '',
|
|
336
|
+
stderr: 'npm audit failed',
|
|
337
|
+
error: undefined,
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
await command.execute({})
|
|
342
|
+
expect.fail('Should have thrown CommandExitError')
|
|
343
|
+
} catch (error) {
|
|
344
|
+
expect((error as { exitCode: number }).exitCode).toBe(1)
|
|
345
|
+
}
|
|
346
|
+
expect(consoleErrorSpy).toHaveBeenCalled()
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('should filter vulnerabilities by severity', async () => {
|
|
350
|
+
mocks.spawnSync.mockReturnValue({
|
|
351
|
+
status: 1,
|
|
352
|
+
stdout: JSON.stringify({
|
|
353
|
+
vulnerabilities: {
|
|
354
|
+
'low-vuln': {
|
|
355
|
+
name: 'low-package',
|
|
356
|
+
severity: 'low',
|
|
357
|
+
title: 'Low Issue',
|
|
358
|
+
range: '*',
|
|
359
|
+
fixAvailable: false,
|
|
360
|
+
},
|
|
361
|
+
'high-vuln': {
|
|
362
|
+
name: 'high-package',
|
|
363
|
+
severity: 'high',
|
|
364
|
+
title: 'High Issue',
|
|
365
|
+
range: '*',
|
|
366
|
+
fixAvailable: true,
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
}),
|
|
370
|
+
stderr: '',
|
|
371
|
+
error: undefined,
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
await command.execute({ severity: 'high' })
|
|
376
|
+
} catch {
|
|
377
|
+
// Expected to throw CommandExitError
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
expect(mockOutputFormatter.formatSecurityReport).toHaveBeenCalledWith(
|
|
381
|
+
expect.objectContaining({
|
|
382
|
+
vulnerabilities: expect.arrayContaining([expect.objectContaining({ severity: 'high' })]),
|
|
383
|
+
})
|
|
384
|
+
)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('should include dev dependencies when specified', async () => {
|
|
388
|
+
try {
|
|
389
|
+
await command.execute({ includeDev: true })
|
|
390
|
+
} catch {
|
|
391
|
+
// Expected to throw CommandExitError
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
expect(mocks.spawnSync).toHaveBeenCalledWith(
|
|
395
|
+
'pnpm',
|
|
396
|
+
expect.not.arrayContaining(['--omit=dev']),
|
|
397
|
+
expect.any(Object)
|
|
398
|
+
)
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('should warn when snyk not found', async () => {
|
|
402
|
+
mocks.spawnSync.mockImplementation((cmd) => {
|
|
403
|
+
if (cmd === 'snyk') {
|
|
404
|
+
const error = new Error('ENOENT') as NodeJS.ErrnoException
|
|
405
|
+
error.code = 'ENOENT'
|
|
406
|
+
return { error, status: null, stdout: '', stderr: '' }
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
status: 0,
|
|
410
|
+
stdout: JSON.stringify({ vulnerabilities: {} }),
|
|
411
|
+
stderr: '',
|
|
412
|
+
error: undefined,
|
|
413
|
+
}
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
try {
|
|
417
|
+
await command.execute({ snyk: true })
|
|
418
|
+
} catch {
|
|
419
|
+
// Expected to throw CommandExitError
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
expect(consoleWarnSpy).toHaveBeenCalled()
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
describe('validateOptions', () => {
|
|
427
|
+
it('should return no errors for valid options', () => {
|
|
428
|
+
const errors = SecurityCommand.validateOptions({
|
|
429
|
+
format: 'json',
|
|
430
|
+
severity: 'high',
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
expect(errors).toHaveLength(0)
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
it('should return error for invalid format', () => {
|
|
437
|
+
const errors = SecurityCommand.validateOptions({
|
|
438
|
+
format: 'invalid' as never,
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
expect(errors).toContain('validation.invalidFormat')
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
it('should return error for invalid severity', () => {
|
|
445
|
+
const errors = SecurityCommand.validateOptions({
|
|
446
|
+
severity: 'invalid' as never,
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
expect(errors).toContain('validation.invalidSeverity')
|
|
450
|
+
})
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
describe('getHelpText', () => {
|
|
454
|
+
it('should return help text with all options', () => {
|
|
455
|
+
const helpText = SecurityCommand.getHelpText()
|
|
456
|
+
|
|
457
|
+
expect(helpText).toContain('Security vulnerability scanning')
|
|
458
|
+
expect(helpText).toContain('--workspace')
|
|
459
|
+
expect(helpText).toContain('--format')
|
|
460
|
+
expect(helpText).toContain('--audit')
|
|
461
|
+
expect(helpText).toContain('--fix-vulns')
|
|
462
|
+
expect(helpText).toContain('--severity')
|
|
463
|
+
expect(helpText).toContain('--include-dev')
|
|
464
|
+
expect(helpText).toContain('--snyk')
|
|
465
|
+
})
|
|
466
|
+
})
|
|
467
|
+
})
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Command Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
|
|
7
|
+
// Mock @pcu/utils
|
|
8
|
+
vi.mock('@pcu/utils', () => ({
|
|
9
|
+
logger: {
|
|
10
|
+
error: vi.fn(),
|
|
11
|
+
warn: vi.fn(),
|
|
12
|
+
info: vi.fn(),
|
|
13
|
+
debug: vi.fn(),
|
|
14
|
+
},
|
|
15
|
+
t: (key: string, params?: Record<string, unknown>) => {
|
|
16
|
+
if (params) {
|
|
17
|
+
let result = key
|
|
18
|
+
for (const [k, v] of Object.entries(params)) {
|
|
19
|
+
result = result.replace(`{{${k}}}`, String(v))
|
|
20
|
+
}
|
|
21
|
+
return result
|
|
22
|
+
}
|
|
23
|
+
return key
|
|
24
|
+
},
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
// Use vi.hoisted to ensure mocks are available during vi.mock hoisting
|
|
28
|
+
const mocks = vi.hoisted(() => ({
|
|
29
|
+
listThemes: vi.fn(),
|
|
30
|
+
setTheme: vi.fn(),
|
|
31
|
+
getTheme: vi.fn(),
|
|
32
|
+
configurationWizard: vi.fn(),
|
|
33
|
+
selectTheme: vi.fn(),
|
|
34
|
+
}))
|
|
35
|
+
|
|
36
|
+
// Mock the theme module
|
|
37
|
+
vi.mock('../../themes/colorTheme.js', () => ({
|
|
38
|
+
ThemeManager: {
|
|
39
|
+
listThemes: mocks.listThemes,
|
|
40
|
+
setTheme: mocks.setTheme,
|
|
41
|
+
getTheme: mocks.getTheme,
|
|
42
|
+
themes: {
|
|
43
|
+
default: {},
|
|
44
|
+
modern: {},
|
|
45
|
+
minimal: {},
|
|
46
|
+
neon: {},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
StyledText: {
|
|
50
|
+
iconInfo: (text: string) => `[info]${text}`,
|
|
51
|
+
iconSuccess: (text: string) => `[success]${text}`,
|
|
52
|
+
iconError: (text: string) => `[error]${text}`,
|
|
53
|
+
muted: (text: string) => `[muted]${text}`,
|
|
54
|
+
},
|
|
55
|
+
}))
|
|
56
|
+
|
|
57
|
+
// Mock interactive prompts - use class syntax for proper constructor
|
|
58
|
+
vi.mock('../../interactive/interactivePrompts.js', () => {
|
|
59
|
+
const InteractivePromptsMock = vi.fn(function (this: Record<string, unknown>) {
|
|
60
|
+
this.configurationWizard = mocks.configurationWizard
|
|
61
|
+
this.selectTheme = mocks.selectTheme
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
InteractivePrompts: InteractivePromptsMock,
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Import after mock setup
|
|
70
|
+
const { ThemeCommand } = await import('../themeCommand.js')
|
|
71
|
+
|
|
72
|
+
describe('ThemeCommand', () => {
|
|
73
|
+
let command: InstanceType<typeof ThemeCommand>
|
|
74
|
+
let consoleSpy: ReturnType<typeof vi.spyOn>
|
|
75
|
+
let consoleErrorSpy: ReturnType<typeof vi.spyOn>
|
|
76
|
+
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
vi.clearAllMocks()
|
|
79
|
+
|
|
80
|
+
// Set up default mock return values
|
|
81
|
+
mocks.listThemes.mockReturnValue(['default', 'modern', 'minimal', 'neon'])
|
|
82
|
+
mocks.getTheme.mockReturnValue({
|
|
83
|
+
success: (text: string) => `[success]${text}`,
|
|
84
|
+
warning: (text: string) => `[warning]${text}`,
|
|
85
|
+
error: (text: string) => `[error]${text}`,
|
|
86
|
+
info: (text: string) => `[info]${text}`,
|
|
87
|
+
major: (text: string) => `[major]${text}`,
|
|
88
|
+
minor: (text: string) => `[minor]${text}`,
|
|
89
|
+
patch: (text: string) => `[patch]${text}`,
|
|
90
|
+
primary: (text: string) => `[primary]${text}`,
|
|
91
|
+
text: (text: string) => `[text]${text}`,
|
|
92
|
+
muted: (text: string) => `[muted]${text}`,
|
|
93
|
+
prerelease: (text: string) => `[prerelease]${text}`,
|
|
94
|
+
})
|
|
95
|
+
mocks.configurationWizard.mockResolvedValue({ theme: 'modern' })
|
|
96
|
+
mocks.selectTheme.mockResolvedValue('modern')
|
|
97
|
+
|
|
98
|
+
consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
99
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
100
|
+
|
|
101
|
+
command = new ThemeCommand()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
afterEach(() => {
|
|
105
|
+
consoleSpy.mockRestore()
|
|
106
|
+
consoleErrorSpy.mockRestore()
|
|
107
|
+
vi.resetAllMocks()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('execute', () => {
|
|
111
|
+
it('should list themes when --list flag is set', async () => {
|
|
112
|
+
await command.execute({ list: true })
|
|
113
|
+
|
|
114
|
+
expect(consoleSpy).toHaveBeenCalled()
|
|
115
|
+
const calls = consoleSpy.mock.calls.flat().join(' ')
|
|
116
|
+
expect(calls).toContain('default')
|
|
117
|
+
expect(calls).toContain('modern')
|
|
118
|
+
expect(calls).toContain('minimal')
|
|
119
|
+
expect(calls).toContain('neon')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should set theme when --set flag is used with valid theme', async () => {
|
|
123
|
+
await command.execute({ set: 'modern' })
|
|
124
|
+
|
|
125
|
+
expect(mocks.setTheme).toHaveBeenCalledWith('modern')
|
|
126
|
+
expect(consoleSpy).toHaveBeenCalled()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should throw error when --set flag is used with invalid theme', async () => {
|
|
130
|
+
await expect(command.execute({ set: 'invalid-theme' })).rejects.toThrow(
|
|
131
|
+
'command.theme.invalidTheme'
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
expect(consoleErrorSpy).toHaveBeenCalled()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should run interactive mode when --interactive flag is set', async () => {
|
|
138
|
+
await command.execute({ interactive: true })
|
|
139
|
+
|
|
140
|
+
expect(mocks.selectTheme).toHaveBeenCalled()
|
|
141
|
+
expect(mocks.setTheme).toHaveBeenCalledWith('modern')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('should show current theme and list by default', async () => {
|
|
145
|
+
await command.execute({})
|
|
146
|
+
|
|
147
|
+
expect(consoleSpy).toHaveBeenCalled()
|
|
148
|
+
expect(mocks.listThemes).toHaveBeenCalled()
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('getHelpText', () => {
|
|
153
|
+
it('should return help text with all options', () => {
|
|
154
|
+
const helpText = ThemeCommand.getHelpText()
|
|
155
|
+
|
|
156
|
+
expect(helpText).toContain('Configure color theme')
|
|
157
|
+
expect(helpText).toContain('--set')
|
|
158
|
+
expect(helpText).toContain('--list')
|
|
159
|
+
expect(helpText).toContain('--interactive')
|
|
160
|
+
expect(helpText).toContain('default')
|
|
161
|
+
expect(helpText).toContain('modern')
|
|
162
|
+
expect(helpText).toContain('minimal')
|
|
163
|
+
expect(helpText).toContain('neon')
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
})
|