Skip to content

Commit a9b7fc0

Browse files
committed
test(cli): add unit tests for fix command and error display utilities
1 parent 97b2843 commit a9b7fc0

File tree

3 files changed

+657
-0
lines changed

3 files changed

+657
-0
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* Unit tests for branch cleanup utilities.
3+
*
4+
* Purpose:
5+
* Tests the branch lifecycle management for the fix command.
6+
*
7+
* Test Coverage:
8+
* - cleanupStaleBranch function
9+
* - cleanupFailedPrBranches function
10+
* - cleanupSuccessfulPrLocalBranch function
11+
* - cleanupErrorBranches function
12+
*
13+
* Related Files:
14+
* - src/commands/fix/branch-cleanup.mts (implementation)
15+
*/
16+
17+
import { beforeEach, describe, expect, it, vi } from 'vitest'
18+
19+
// Mock logger.
20+
const mockLogger = vi.hoisted(() => ({
21+
log: vi.fn(),
22+
error: vi.fn(),
23+
warn: vi.fn(),
24+
fail: vi.fn(),
25+
success: vi.fn(),
26+
}))
27+
vi.mock('@socketsecurity/lib/logger', () => ({
28+
getDefaultLogger: () => mockLogger,
29+
}))
30+
31+
// Mock git operations.
32+
const mockGitDeleteBranch = vi.hoisted(() => vi.fn())
33+
const mockGitDeleteRemoteBranch = vi.hoisted(() => vi.fn())
34+
vi.mock('../../../../src/utils/git/operations.mjs', () => ({
35+
gitDeleteBranch: mockGitDeleteBranch,
36+
gitDeleteRemoteBranch: mockGitDeleteRemoteBranch,
37+
}))
38+
39+
import {
40+
cleanupErrorBranches,
41+
cleanupFailedPrBranches,
42+
cleanupStaleBranch,
43+
cleanupSuccessfulPrLocalBranch,
44+
} from '../../../../src/commands/fix/branch-cleanup.mts'
45+
46+
describe('branch-cleanup', () => {
47+
const cwd = '/test/repo'
48+
const branch = 'socket/fix/GHSA-1234-5678-90ab'
49+
const ghsaId = 'GHSA-1234-5678-90ab'
50+
51+
beforeEach(() => {
52+
vi.clearAllMocks()
53+
})
54+
55+
describe('cleanupStaleBranch', () => {
56+
it('deletes remote and local branches when remote deletion succeeds', async () => {
57+
mockGitDeleteRemoteBranch.mockResolvedValue(true)
58+
mockGitDeleteBranch.mockResolvedValue(undefined)
59+
60+
const result = await cleanupStaleBranch(branch, ghsaId, cwd)
61+
62+
expect(result).toBe(true)
63+
expect(mockGitDeleteRemoteBranch).toHaveBeenCalledWith(branch, cwd)
64+
expect(mockGitDeleteBranch).toHaveBeenCalledWith(branch, cwd)
65+
expect(mockLogger.warn).toHaveBeenCalledWith(
66+
expect.stringContaining('Stale branch'),
67+
)
68+
})
69+
70+
it('returns false when remote deletion fails', async () => {
71+
mockGitDeleteRemoteBranch.mockResolvedValue(false)
72+
73+
const result = await cleanupStaleBranch(branch, ghsaId, cwd)
74+
75+
expect(result).toBe(false)
76+
expect(mockGitDeleteRemoteBranch).toHaveBeenCalledWith(branch, cwd)
77+
expect(mockGitDeleteBranch).not.toHaveBeenCalled()
78+
expect(mockLogger.error).toHaveBeenCalledWith(
79+
expect.stringContaining('Failed to delete stale remote branch'),
80+
)
81+
})
82+
83+
it('logs warning about stale branch', async () => {
84+
mockGitDeleteRemoteBranch.mockResolvedValue(true)
85+
86+
await cleanupStaleBranch(branch, ghsaId, cwd)
87+
88+
expect(mockLogger.warn).toHaveBeenCalledWith(
89+
expect.stringContaining(branch),
90+
)
91+
})
92+
})
93+
94+
describe('cleanupFailedPrBranches', () => {
95+
it('deletes both remote and local branches', async () => {
96+
mockGitDeleteRemoteBranch.mockResolvedValue(true)
97+
mockGitDeleteBranch.mockResolvedValue(undefined)
98+
99+
await cleanupFailedPrBranches(branch, cwd)
100+
101+
expect(mockGitDeleteRemoteBranch).toHaveBeenCalledWith(branch, cwd)
102+
expect(mockGitDeleteBranch).toHaveBeenCalledWith(branch, cwd)
103+
})
104+
105+
it('continues to delete local branch even if remote fails', async () => {
106+
mockGitDeleteRemoteBranch.mockResolvedValue(false)
107+
mockGitDeleteBranch.mockResolvedValue(undefined)
108+
109+
await cleanupFailedPrBranches(branch, cwd)
110+
111+
expect(mockGitDeleteRemoteBranch).toHaveBeenCalledWith(branch, cwd)
112+
expect(mockGitDeleteBranch).toHaveBeenCalledWith(branch, cwd)
113+
})
114+
})
115+
116+
describe('cleanupSuccessfulPrLocalBranch', () => {
117+
it('deletes only local branch, keeping remote for PR', async () => {
118+
mockGitDeleteBranch.mockResolvedValue(undefined)
119+
120+
await cleanupSuccessfulPrLocalBranch(branch, cwd)
121+
122+
expect(mockGitDeleteBranch).toHaveBeenCalledWith(branch, cwd)
123+
expect(mockGitDeleteRemoteBranch).not.toHaveBeenCalled()
124+
})
125+
})
126+
127+
describe('cleanupErrorBranches', () => {
128+
it('deletes both remote and local when remote exists', async () => {
129+
mockGitDeleteRemoteBranch.mockResolvedValue(true)
130+
mockGitDeleteBranch.mockResolvedValue(undefined)
131+
132+
await cleanupErrorBranches(branch, cwd, true)
133+
134+
expect(mockGitDeleteRemoteBranch).toHaveBeenCalledWith(branch, cwd)
135+
expect(mockGitDeleteBranch).toHaveBeenCalledWith(branch, cwd)
136+
})
137+
138+
it('deletes only local branch when remote does not exist', async () => {
139+
mockGitDeleteBranch.mockResolvedValue(undefined)
140+
141+
await cleanupErrorBranches(branch, cwd, false)
142+
143+
expect(mockGitDeleteRemoteBranch).not.toHaveBeenCalled()
144+
expect(mockGitDeleteBranch).toHaveBeenCalledWith(branch, cwd)
145+
})
146+
})
147+
})
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/**
2+
* Unit tests for fix result output formatting.
3+
*
4+
* Purpose:
5+
* Tests the output formatting for fix command results.
6+
*
7+
* Test Coverage:
8+
* - outputFixResult function
9+
* - JSON output format
10+
* - Markdown output format
11+
* - Text output format
12+
* - Success and error handling
13+
*
14+
* Related Files:
15+
* - src/commands/fix/output-fix-result.mts (implementation)
16+
*/
17+
18+
import { beforeEach, describe, expect, it, vi } from 'vitest'
19+
20+
// Mock logger.
21+
const mockLogger = vi.hoisted(() => ({
22+
log: vi.fn(),
23+
error: vi.fn(),
24+
warn: vi.fn(),
25+
fail: vi.fn(),
26+
success: vi.fn(),
27+
}))
28+
vi.mock('@socketsecurity/lib/logger', () => ({
29+
getDefaultLogger: () => mockLogger,
30+
}))
31+
32+
// Mock utilities.
33+
vi.mock('../../../../src/utils/error/fail-msg-with-badge.mts', () => ({
34+
failMsgWithBadge: (msg: string, cause?: string) =>
35+
cause ? `${msg}: ${cause}` : msg,
36+
}))
37+
38+
vi.mock('../../../../src/utils/output/markdown.mts', () => ({
39+
mdError: (msg: string, cause?: string) =>
40+
`## Error: ${msg}${cause ? `\n${cause}` : ''}`,
41+
mdHeader: (text: string) => `# ${text}`,
42+
}))
43+
44+
vi.mock('../../../../src/utils/output/result-json.mjs', () => ({
45+
serializeResultJson: (result: unknown) => JSON.stringify(result, null, 2),
46+
}))
47+
48+
import { outputFixResult } from '../../../../src/commands/fix/output-fix-result.mts'
49+
50+
import type { CResult } from '../../../../src/types.mts'
51+
52+
describe('output-fix-result', () => {
53+
beforeEach(() => {
54+
vi.clearAllMocks()
55+
process.exitCode = undefined
56+
})
57+
58+
describe('outputFixResult', () => {
59+
describe('JSON output', () => {
60+
it('outputs success result as JSON', async () => {
61+
const result: CResult<{ fixed: number }> = {
62+
ok: true,
63+
data: { fixed: 5 },
64+
}
65+
66+
await outputFixResult(result, 'json')
67+
68+
expect(mockLogger.log).toHaveBeenCalledWith(
69+
expect.stringContaining('"ok": true'),
70+
)
71+
expect(process.exitCode).toBeUndefined()
72+
})
73+
74+
it('outputs error result as JSON', async () => {
75+
const result: CResult<unknown> = {
76+
ok: false,
77+
message: 'Fix failed',
78+
cause: 'No vulnerabilities found',
79+
}
80+
81+
await outputFixResult(result, 'json')
82+
83+
expect(mockLogger.log).toHaveBeenCalledWith(
84+
expect.stringContaining('"ok": false'),
85+
)
86+
expect(process.exitCode).toBe(1)
87+
})
88+
89+
it('uses custom exit code when provided', async () => {
90+
const result: CResult<unknown> = {
91+
ok: false,
92+
message: 'Fix failed',
93+
code: 2,
94+
}
95+
96+
await outputFixResult(result, 'json')
97+
98+
expect(process.exitCode).toBe(2)
99+
})
100+
})
101+
102+
describe('Markdown output', () => {
103+
it('outputs success as markdown header', async () => {
104+
const result: CResult<unknown> = {
105+
ok: true,
106+
data: {},
107+
}
108+
109+
await outputFixResult(result, 'markdown')
110+
111+
expect(mockLogger.log).toHaveBeenCalledWith('# Fix Completed')
112+
expect(mockLogger.log).toHaveBeenCalledWith('✓ Finished!')
113+
expect(process.exitCode).toBeUndefined()
114+
})
115+
116+
it('outputs error as markdown error', async () => {
117+
const result: CResult<unknown> = {
118+
ok: false,
119+
message: 'Fix failed',
120+
cause: 'No packages to fix',
121+
}
122+
123+
await outputFixResult(result, 'markdown')
124+
125+
expect(mockLogger.log).toHaveBeenCalledWith(
126+
expect.stringContaining('## Error: Fix failed'),
127+
)
128+
expect(process.exitCode).toBe(1)
129+
})
130+
})
131+
132+
describe('Text output', () => {
133+
it('outputs success message', async () => {
134+
const result: CResult<unknown> = {
135+
ok: true,
136+
data: {},
137+
}
138+
139+
await outputFixResult(result, 'text')
140+
141+
expect(mockLogger.success).toHaveBeenCalledWith('Finished!')
142+
expect(mockLogger.log).toHaveBeenCalledWith('')
143+
expect(process.exitCode).toBeUndefined()
144+
})
145+
146+
it('outputs error with fail message', async () => {
147+
const result: CResult<unknown> = {
148+
ok: false,
149+
message: 'Fix failed',
150+
cause: 'API error',
151+
}
152+
153+
await outputFixResult(result, 'text')
154+
155+
expect(mockLogger.fail).toHaveBeenCalledWith(
156+
expect.stringContaining('Fix failed'),
157+
)
158+
expect(process.exitCode).toBe(1)
159+
})
160+
161+
it('handles error without cause', async () => {
162+
const result: CResult<unknown> = {
163+
ok: false,
164+
message: 'Something went wrong',
165+
}
166+
167+
await outputFixResult(result, 'text')
168+
169+
expect(mockLogger.fail).toHaveBeenCalledWith(
170+
expect.stringContaining('Something went wrong'),
171+
)
172+
})
173+
})
174+
})
175+
})

0 commit comments

Comments
 (0)