Skip to content

Commit c12a467

Browse files
committed
test(cli): achieve 100% command test coverage
Add unit tests for all 57 remaining commands, bringing total coverage to 74/74 commands (100%). Tests follow established patterns with proper mocking, exit handler behavior verification, and meaningful assertions. Key improvements: - Wrapper commands (cargo, bundler, gem) now properly test process.exit() and process.kill() behavior, not just event handler registration - All tests use vi.hoisted() pattern for clean module mocking - Consistent test structure across all command categories Categories covered: - Auth: login, logout, whoami - Scan: scan, create, del, diff, github, list, metadata, reach, report, setup, view - Manifest: manifest, auto, cdxgen, conda, gradle, kotlin, scala, setup - Wrapper: bundler, cargo, gem, go, npm, npx, nuget, pip, pnpm, pycli, raw-npm, raw-npx, sfw, uv, wrapper, yarn - Organization: org, dependencies, list, policy, policy-license, policy-security, quota - Repository: repo, create, del, list, update, view - Config: config, auto, get, list, set, unset - Package: package, score, shallow - Install/Uninstall: install, install-completion, uninstall, uninstall-completion - Other: analytics, ask, audit-log, ci, fix, json, oops, optimize, patch, threat-feed
1 parent 13fca8b commit c12a467

File tree

57 files changed

+19240
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+19240
-0
lines changed

packages/cli/test/unit/commands/analytics/cmd-analytics.test.mts

Lines changed: 463 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
/**
2+
* Unit tests for audit-log command.
3+
*
4+
* Tests the command that displays organization audit logs.
5+
*
6+
* Test Coverage:
7+
* - Command metadata (description, hidden flag)
8+
* - API token requirement validation
9+
* - Organization slug handling
10+
* - Type filter argument parsing
11+
* - Pagination flags: page, per-page
12+
* - Output modes: text, JSON, markdown
13+
* - Dry-run mode
14+
* - Legacy flag detection
15+
*
16+
* Testing Approach:
17+
* - Mock logger to capture output
18+
* - Mock handleAuditLog to verify handler invocation
19+
* - Mock determineOrgSlug for organization handling
20+
* - Mock hasDefaultApiToken for authentication checks
21+
* - Test flag combinations and defaults
22+
*
23+
* Related Files:
24+
* - src/commands/audit-log/cmd-audit-log.mts - Implementation
25+
* - src/commands/audit-log/handle-audit-log.mts - Handler
26+
*/
27+
28+
import { beforeEach, describe, expect, it, vi } from 'vitest'
29+
30+
// Mock the logger.
31+
const mockLogger = vi.hoisted(() => ({
32+
error: vi.fn(),
33+
fail: vi.fn(),
34+
info: vi.fn(),
35+
log: vi.fn(),
36+
success: vi.fn(),
37+
warn: vi.fn(),
38+
}))
39+
40+
vi.mock('@socketsecurity/lib/logger', async importOriginal => {
41+
const actual = await importOriginal<typeof import('@socketsecurity/lib/logger')>()
42+
return {
43+
...actual,
44+
getDefaultLogger: () => mockLogger,
45+
}
46+
})
47+
48+
// Mock dependencies.
49+
const mockHandleAuditLog = vi.hoisted(() => vi.fn())
50+
const mockDetermineOrgSlug = vi.hoisted(() =>
51+
vi.fn().mockResolvedValue(['test-org', 'test-org']),
52+
)
53+
const mockHasDefaultApiToken = vi.hoisted(() => vi.fn().mockReturnValue(true))
54+
55+
vi.mock('../../../../src/commands/audit-log/handle-audit-log.mts', () => ({
56+
handleAuditLog: mockHandleAuditLog,
57+
}))
58+
59+
vi.mock('../../../../src/utils/socket/org-slug.mjs', () => ({
60+
determineOrgSlug: mockDetermineOrgSlug,
61+
}))
62+
63+
vi.mock('../../../../src/utils/socket/sdk.mjs', async importOriginal => {
64+
const actual = await importOriginal<typeof import('../../../../src/utils/socket/sdk.mjs')>()
65+
return {
66+
...actual,
67+
hasDefaultApiToken: mockHasDefaultApiToken,
68+
}
69+
})
70+
71+
// Import after mocks.
72+
const { cmdAuditLog } = await import(
73+
'../../../../src/commands/audit-log/cmd-audit-log.mts'
74+
)
75+
76+
describe('cmd-audit-log', () => {
77+
beforeEach(() => {
78+
vi.clearAllMocks()
79+
process.exitCode = undefined
80+
})
81+
82+
describe('command metadata', () => {
83+
it('should have correct description', () => {
84+
expect(cmdAuditLog.description).toBe(
85+
'Look up the audit log for an organization',
86+
)
87+
})
88+
89+
it('should not be hidden', () => {
90+
expect(cmdAuditLog.hidden).toBe(false)
91+
})
92+
})
93+
94+
describe('run', () => {
95+
const importMeta = { url: 'file:///test/cmd-audit-log.mts' }
96+
const context = { parentName: 'socket' }
97+
98+
it('should support --dry-run flag', async () => {
99+
mockHasDefaultApiToken.mockReturnValueOnce(true)
100+
101+
await cmdAuditLog.run(['--dry-run'], importMeta, context)
102+
103+
expect(mockHandleAuditLog).not.toHaveBeenCalled()
104+
expect(mockLogger.log).toHaveBeenCalledWith(
105+
expect.stringContaining('DryRun'),
106+
)
107+
})
108+
109+
it('should fail without Socket API token', async () => {
110+
mockHasDefaultApiToken.mockReturnValueOnce(false)
111+
112+
await cmdAuditLog.run([], importMeta, context)
113+
114+
// Exit code 2 = invalid usage/validation failure.
115+
expect(process.exitCode).toBe(2)
116+
expect(mockHandleAuditLog).not.toHaveBeenCalled()
117+
})
118+
119+
it('should call handleAuditLog with default values', async () => {
120+
mockHasDefaultApiToken.mockReturnValueOnce(true)
121+
122+
await cmdAuditLog.run([], importMeta, context)
123+
124+
expect(mockHandleAuditLog).toHaveBeenCalledWith({
125+
logType: '',
126+
orgSlug: 'test-org',
127+
outputKind: 'text',
128+
page: 0,
129+
perPage: 30,
130+
})
131+
})
132+
133+
it('should pass type filter as first argument', async () => {
134+
mockHasDefaultApiToken.mockReturnValueOnce(true)
135+
136+
await cmdAuditLog.run(['deleteReport'], importMeta, context)
137+
138+
expect(mockHandleAuditLog).toHaveBeenCalledWith(
139+
expect.objectContaining({
140+
logType: 'DeleteReport',
141+
}),
142+
)
143+
})
144+
145+
it('should capitalize first letter of type filter', async () => {
146+
mockHasDefaultApiToken.mockReturnValueOnce(true)
147+
148+
await cmdAuditLog.run(['createApiToken'], importMeta, context)
149+
150+
expect(mockHandleAuditLog).toHaveBeenCalledWith(
151+
expect.objectContaining({
152+
logType: 'CreateApiToken',
153+
}),
154+
)
155+
})
156+
157+
it('should pass --page flag to handleAuditLog', async () => {
158+
mockHasDefaultApiToken.mockReturnValueOnce(true)
159+
160+
await cmdAuditLog.run(['--page', '2'], importMeta, context)
161+
162+
expect(mockHandleAuditLog).toHaveBeenCalledWith(
163+
expect.objectContaining({
164+
page: 2,
165+
}),
166+
)
167+
})
168+
169+
it('should pass --per-page flag to handleAuditLog', async () => {
170+
mockHasDefaultApiToken.mockReturnValueOnce(true)
171+
172+
await cmdAuditLog.run(['--per-page', '50'], importMeta, context)
173+
174+
expect(mockHandleAuditLog).toHaveBeenCalledWith(
175+
expect.objectContaining({
176+
perPage: 50,
177+
}),
178+
)
179+
})
180+
181+
it('should pass --org flag to determineOrgSlug', async () => {
182+
mockDetermineOrgSlug.mockResolvedValueOnce(['custom-org', 'custom-org'])
183+
mockHasDefaultApiToken.mockReturnValueOnce(true)
184+
185+
await cmdAuditLog.run(['--org', 'custom-org'], importMeta, context)
186+
187+
expect(mockDetermineOrgSlug).toHaveBeenCalledWith('custom-org', true, false)
188+
expect(mockHandleAuditLog).toHaveBeenCalledWith(
189+
expect.objectContaining({
190+
orgSlug: 'custom-org',
191+
}),
192+
)
193+
})
194+
195+
it('should support --json output mode', async () => {
196+
mockHasDefaultApiToken.mockReturnValueOnce(true)
197+
198+
await cmdAuditLog.run(['--json'], importMeta, context)
199+
200+
expect(mockHandleAuditLog).toHaveBeenCalledWith(
201+
expect.objectContaining({
202+
outputKind: 'json',
203+
}),
204+
)
205+
})
206+
207+
it('should support --markdown output mode', async () => {
208+
mockHasDefaultApiToken.mockReturnValueOnce(true)
209+
210+
await cmdAuditLog.run(['--markdown'], importMeta, context)
211+
212+
expect(mockHandleAuditLog).toHaveBeenCalledWith(
213+
expect.objectContaining({
214+
outputKind: 'markdown',
215+
}),
216+
)
217+
})
218+
219+
it('should fail if both --json and --markdown are set', async () => {
220+
mockHasDefaultApiToken.mockReturnValueOnce(true)
221+
222+
await cmdAuditLog.run(
223+
['--json', '--markdown'],
224+
importMeta,
225+
context,
226+
)
227+
228+
// Exit code 2 = invalid usage/validation failure.
229+
expect(process.exitCode).toBe(2)
230+
expect(mockHandleAuditLog).not.toHaveBeenCalled()
231+
})
232+
233+
it('should fail without organization slug', async () => {
234+
mockDetermineOrgSlug.mockResolvedValueOnce(['', ''])
235+
mockHasDefaultApiToken.mockReturnValueOnce(true)
236+
237+
await cmdAuditLog.run([], importMeta, context)
238+
239+
// Exit code 2 = invalid usage/validation failure.
240+
expect(process.exitCode).toBe(2)
241+
expect(mockHandleAuditLog).not.toHaveBeenCalled()
242+
})
243+
244+
it('should handle --no-interactive flag', async () => {
245+
mockHasDefaultApiToken.mockReturnValueOnce(true)
246+
247+
await cmdAuditLog.run(['--no-interactive'], importMeta, context)
248+
249+
expect(mockDetermineOrgSlug).toHaveBeenCalledWith('', false, false)
250+
})
251+
252+
it('should validate type filter is alphabetic', async () => {
253+
mockHasDefaultApiToken.mockReturnValueOnce(true)
254+
255+
await cmdAuditLog.run(['invalid-123'], importMeta, context)
256+
257+
// Exit code 2 = invalid usage/validation failure.
258+
expect(process.exitCode).toBe(2)
259+
expect(mockHandleAuditLog).not.toHaveBeenCalled()
260+
})
261+
262+
it('should allow empty type filter', async () => {
263+
mockHasDefaultApiToken.mockReturnValueOnce(true)
264+
265+
await cmdAuditLog.run([], importMeta, context)
266+
267+
expect(mockHandleAuditLog).toHaveBeenCalledWith(
268+
expect.objectContaining({
269+
logType: '',
270+
}),
271+
)
272+
})
273+
274+
it('should show dry-run output with all parameters', async () => {
275+
mockHasDefaultApiToken.mockReturnValueOnce(true)
276+
277+
await cmdAuditLog.run(
278+
[
279+
'--dry-run',
280+
'deleteReport',
281+
'--page', '2',
282+
'--per-page', '10',
283+
],
284+
importMeta,
285+
context,
286+
)
287+
288+
expect(mockLogger.log).toHaveBeenCalledWith(
289+
expect.stringContaining('audit log entries'),
290+
)
291+
expect(mockLogger.log).toHaveBeenCalledWith(
292+
expect.stringContaining('deleteReport'),
293+
)
294+
})
295+
296+
it('should validate page is numeric', async () => {
297+
mockHasDefaultApiToken.mockReturnValueOnce(true)
298+
299+
await expect(
300+
cmdAuditLog.run(['--page', 'invalid'], importMeta, context),
301+
).rejects.toThrow('Invalid value for --page')
302+
})
303+
304+
it('should validate per-page is numeric', async () => {
305+
mockHasDefaultApiToken.mockReturnValueOnce(true)
306+
307+
await expect(
308+
cmdAuditLog.run(['--per-page', 'invalid'], importMeta, context),
309+
).rejects.toThrow('Invalid value for --per-page')
310+
})
311+
312+
it('should reject negative page numbers', async () => {
313+
mockHasDefaultApiToken.mockReturnValueOnce(true)
314+
315+
await expect(
316+
cmdAuditLog.run(['--page', '-1'], importMeta, context),
317+
).rejects.toThrow('Invalid value for --page')
318+
})
319+
320+
it('should reject negative per-page numbers', async () => {
321+
mockHasDefaultApiToken.mockReturnValueOnce(true)
322+
323+
await expect(
324+
cmdAuditLog.run(['--per-page', '-1'], importMeta, context),
325+
).rejects.toThrow('Invalid value for --per-page')
326+
})
327+
328+
it('should accept zero as page number', async () => {
329+
mockHasDefaultApiToken.mockReturnValueOnce(true)
330+
331+
await cmdAuditLog.run(['--page', '0'], importMeta, context)
332+
333+
expect(mockHandleAuditLog).toHaveBeenCalledWith(
334+
expect.objectContaining({
335+
page: 0,
336+
}),
337+
)
338+
})
339+
340+
it('should combine type filter and pagination flags', async () => {
341+
mockHasDefaultApiToken.mockReturnValueOnce(true)
342+
343+
await cmdAuditLog.run(
344+
['sendInvitation', '--page', '3', '--per-page', '20'],
345+
importMeta,
346+
context,
347+
)
348+
349+
expect(mockHandleAuditLog).toHaveBeenCalledWith(
350+
expect.objectContaining({
351+
logType: 'SendInvitation',
352+
page: 3,
353+
perPage: 20,
354+
}),
355+
)
356+
})
357+
358+
it('should show dry-run with default filter value', async () => {
359+
mockHasDefaultApiToken.mockReturnValueOnce(true)
360+
361+
await cmdAuditLog.run(['--dry-run'], importMeta, context)
362+
363+
expect(mockLogger.log).toHaveBeenCalledWith(
364+
expect.stringContaining('any'),
365+
)
366+
})
367+
368+
it('should handle organization with special characters', async () => {
369+
mockDetermineOrgSlug.mockResolvedValueOnce(['org-with-dash', 'org-with-dash'])
370+
mockHasDefaultApiToken.mockReturnValueOnce(true)
371+
372+
await cmdAuditLog.run(['--org', 'org-with-dash'], importMeta, context)
373+
374+
expect(mockHandleAuditLog).toHaveBeenCalledWith(
375+
expect.objectContaining({
376+
orgSlug: 'org-with-dash',
377+
}),
378+
)
379+
})
380+
})
381+
})

0 commit comments

Comments
 (0)