Skip to content

Commit fa74c74

Browse files
committed
test(cli): add unit tests for config and command output modules
Add comprehensive test coverage for: - Config commands: output-config-get, output-config-set, output-config-unset, output-config-list, output-config-auto - Install/uninstall: output-install-completion, output-uninstall-completion - Manifest: output-manifest, output-manifest-setup Coverage increased from 68.43% to ~70.1%
1 parent aab2970 commit fa74c74

File tree

9 files changed

+1601
-0
lines changed

9 files changed

+1601
-0
lines changed
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
/**
2+
* Unit tests for config auto output formatting.
3+
*
4+
* Purpose:
5+
* Tests the output formatting for config auto-discovery results.
6+
*
7+
* Test Coverage:
8+
* - outputConfigAuto function
9+
* - JSON output format
10+
* - Text output format
11+
* - Markdown output format
12+
* - Interactive prompts for defaultOrg
13+
* - Read-only mode handling
14+
*
15+
* Related Files:
16+
* - src/commands/config/output-config-auto.mts (implementation)
17+
*/
18+
19+
import { beforeEach, describe, expect, it, vi } from 'vitest'
20+
21+
// Mock logger.
22+
const mockLogger = vi.hoisted(() => ({
23+
log: vi.fn(),
24+
error: vi.fn(),
25+
warn: vi.fn(),
26+
fail: vi.fn(),
27+
success: vi.fn(),
28+
info: vi.fn(),
29+
}))
30+
vi.mock('@socketsecurity/lib/logger', () => ({
31+
getDefaultLogger: () => mockLogger,
32+
}))
33+
34+
// Mock prompts.
35+
const mockSelect = vi.hoisted(() => vi.fn())
36+
vi.mock('@socketsecurity/lib/stdio/prompts', () => ({
37+
select: mockSelect,
38+
}))
39+
40+
// Mock config utilities.
41+
const mockIsConfigFromFlag = vi.hoisted(() => vi.fn(() => false))
42+
const mockUpdateConfigValue = vi.hoisted(() =>
43+
vi.fn(() => ({ ok: true, message: 'Updated' })),
44+
)
45+
46+
vi.mock('../../../../src/utils/config.mts', () => ({
47+
isConfigFromFlag: mockIsConfigFromFlag,
48+
updateConfigValue: mockUpdateConfigValue,
49+
}))
50+
51+
vi.mock('../../../../src/utils/error/fail-msg-with-badge.mts', () => ({
52+
failMsgWithBadge: (msg: string, cause?: string) =>
53+
cause ? `${msg}: ${cause}` : msg,
54+
}))
55+
56+
vi.mock('../../../../src/utils/output/markdown.mts', () => ({
57+
mdHeader: (text: string) => `# ${text}`,
58+
}))
59+
60+
vi.mock('../../../../src/utils/output/result-json.mjs', () => ({
61+
serializeResultJson: (result: unknown) => JSON.stringify(result, null, 2),
62+
}))
63+
64+
import { outputConfigAuto } from '../../../../src/commands/config/output-config-auto.mts'
65+
66+
import type { CResult } from '../../../../src/types.mts'
67+
68+
describe('output-config-auto', () => {
69+
beforeEach(() => {
70+
vi.clearAllMocks()
71+
process.exitCode = undefined
72+
mockIsConfigFromFlag.mockReturnValue(false)
73+
mockUpdateConfigValue.mockReturnValue({ ok: true, message: 'Updated' })
74+
})
75+
76+
describe('outputConfigAuto', () => {
77+
describe('JSON output', () => {
78+
it('outputs success result as JSON', async () => {
79+
const result: CResult<string> = {
80+
ok: true,
81+
data: 'my-org',
82+
}
83+
84+
await outputConfigAuto('defaultOrg', result, 'json')
85+
86+
expect(mockLogger.log).toHaveBeenCalledWith(
87+
expect.stringContaining('"ok": true'),
88+
)
89+
})
90+
91+
it('outputs error result as JSON', async () => {
92+
const result: CResult<string> = {
93+
ok: false,
94+
message: 'Auto-discovery failed',
95+
}
96+
97+
await outputConfigAuto('defaultOrg', result, 'json')
98+
99+
expect(mockLogger.log).toHaveBeenCalledWith(
100+
expect.stringContaining('"ok": false'),
101+
)
102+
expect(process.exitCode).toBe(1)
103+
})
104+
105+
it('uses custom exit code when provided', async () => {
106+
const result: CResult<string> = {
107+
ok: false,
108+
message: 'Failed',
109+
code: 5,
110+
}
111+
112+
await outputConfigAuto('defaultOrg', result, 'json')
113+
114+
expect(process.exitCode).toBe(5)
115+
})
116+
})
117+
118+
describe('Markdown output', () => {
119+
it('outputs auto-discovery header and value', async () => {
120+
const result: CResult<string> = {
121+
ok: true,
122+
data: 'discovered-org',
123+
}
124+
125+
await outputConfigAuto('defaultOrg', result, 'markdown')
126+
127+
const logs = mockLogger.log.mock.calls.map(c => c[0]).join('\n')
128+
expect(logs).toContain('# Auto discover config value')
129+
expect(logs).toContain('defaultOrg')
130+
expect(logs).toContain('discovered-org')
131+
})
132+
133+
it('includes message when provided', async () => {
134+
const result: CResult<string> = {
135+
ok: true,
136+
data: 'my-org',
137+
message: 'Found via GitHub API',
138+
}
139+
140+
await outputConfigAuto('defaultOrg', result, 'markdown')
141+
142+
const logs = mockLogger.log.mock.calls.map(c => c[0]).join('\n')
143+
expect(logs).toContain('Found via GitHub API')
144+
})
145+
})
146+
147+
describe('Text output', () => {
148+
it('outputs discovered value', async () => {
149+
// Mock select to return empty (No)
150+
mockSelect.mockResolvedValue('')
151+
152+
const result: CResult<string> = {
153+
ok: true,
154+
data: 'auto-org',
155+
}
156+
157+
await outputConfigAuto('defaultOrg', result, 'text')
158+
159+
const logs = mockLogger.log.mock.calls.map(c => c[0]).join('\n')
160+
expect(logs).toContain('defaultOrg: auto-org')
161+
})
162+
163+
it('shows read-only message when config from flag', async () => {
164+
mockIsConfigFromFlag.mockReturnValue(true)
165+
const result: CResult<string> = {
166+
ok: true,
167+
data: 'test-org',
168+
}
169+
170+
await outputConfigAuto('defaultOrg', result, 'text')
171+
172+
const logs = mockLogger.log.mock.calls.map(c => c[0]).join('\n')
173+
expect(logs).toContain('read-only')
174+
})
175+
176+
it('updates config when user confirms for defaultOrg', async () => {
177+
mockSelect.mockResolvedValue('my-org')
178+
179+
const result: CResult<string> = {
180+
ok: true,
181+
data: 'my-org',
182+
}
183+
184+
await outputConfigAuto('defaultOrg', result, 'text')
185+
186+
expect(mockUpdateConfigValue).toHaveBeenCalledWith(
187+
'defaultOrg',
188+
'my-org',
189+
)
190+
const logs = mockLogger.log.mock.calls.map(c => c[0]).join('\n')
191+
expect(logs).toContain('Updated defaultOrg')
192+
})
193+
194+
it('shows no changes message when user declines', async () => {
195+
mockSelect.mockResolvedValue('')
196+
197+
const result: CResult<string> = {
198+
ok: true,
199+
data: 'my-org',
200+
}
201+
202+
await outputConfigAuto('defaultOrg', result, 'text')
203+
204+
const logs = mockLogger.log.mock.calls.map(c => c[0]).join('\n')
205+
expect(logs).toContain('No changes made')
206+
})
207+
208+
it('handles update failure', async () => {
209+
mockSelect.mockResolvedValue('my-org')
210+
mockUpdateConfigValue.mockReturnValue({
211+
ok: false,
212+
message: 'Write failed',
213+
cause: 'Permission denied',
214+
})
215+
216+
const result: CResult<string> = {
217+
ok: true,
218+
data: 'my-org',
219+
}
220+
221+
await outputConfigAuto('defaultOrg', result, 'text')
222+
223+
const logs = mockLogger.log.mock.calls.map(c => c[0]).join('\n')
224+
expect(logs).toContain('Write failed')
225+
})
226+
227+
it('outputs error with fail message', async () => {
228+
const result: CResult<string> = {
229+
ok: false,
230+
message: 'Could not auto-discover',
231+
cause: 'No orgs found',
232+
}
233+
234+
await outputConfigAuto('defaultOrg', result, 'text')
235+
236+
expect(mockLogger.fail).toHaveBeenCalledWith(
237+
expect.stringContaining('Could not auto-discover'),
238+
)
239+
})
240+
241+
it('handles enforcedOrgs key with prompt', async () => {
242+
mockSelect.mockResolvedValue('enforced-org')
243+
244+
const result: CResult<string> = {
245+
ok: true,
246+
data: 'enforced-org',
247+
}
248+
249+
await outputConfigAuto('enforcedOrgs', result, 'text')
250+
251+
// Select should be called for enforcedOrgs.
252+
expect(mockSelect).toHaveBeenCalled()
253+
})
254+
})
255+
})
256+
})

0 commit comments

Comments
 (0)