Skip to content

Commit 70a3114

Browse files
committed
test(hooks): improve test coverage
1 parent 08c8c62 commit 70a3114

File tree

11 files changed

+1361
-280
lines changed

11 files changed

+1361
-280
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ tests/*/
1010
node_modules
1111
/.husky
1212
/.eslintcache
13+
coverage/
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2+
import {
3+
log,
4+
initEnvironment,
5+
formatDuration,
6+
isSkipped,
7+
isDdevProject,
8+
exec,
9+
ensureMutagenSync
10+
} from '../../shared/core'
11+
import { $, fs, which } from 'zx'
12+
13+
// Mock zx
14+
vi.mock('zx', () => {
15+
// $ needs to be a function that returns a function (for template literal)
16+
const $ = vi.fn().mockImplementation(() => {
17+
// Return a function that handles the template literal
18+
return vi.fn().mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 })
19+
})
20+
// @ts-ignore
21+
$.verbose = false
22+
return {
23+
$: $,
24+
chalk: {
25+
blue: (s: string) => s,
26+
green: (s: string) => s,
27+
red: (s: string) => s,
28+
yellow: (s: string) => s,
29+
cyan: (s: string) => s,
30+
gray: (s: string) => s,
31+
level: 0
32+
},
33+
fs: {
34+
pathExists: vi.fn(),
35+
readFile: vi.fn(),
36+
},
37+
which: vi.fn()
38+
}
39+
})
40+
41+
describe('core.ts', () => {
42+
beforeEach(() => {
43+
vi.clearAllMocks()
44+
vi.resetModules()
45+
process.env = { ...process.env } // Clone env
46+
delete process.env.DDEV_PHP // Ensure this doesn't interfere
47+
})
48+
49+
describe('log', () => {
50+
it('should log messages with correct prefixes', () => {
51+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
52+
53+
log.info('test info')
54+
expect(consoleSpy).toHaveBeenCalledWith('💡 test info')
55+
56+
log.success('test success')
57+
expect(consoleSpy).toHaveBeenCalledWith('✅ test success')
58+
59+
log.error('test error')
60+
expect(consoleSpy).toHaveBeenCalledWith('❌ test error')
61+
62+
log.warn('test warn')
63+
expect(consoleSpy).toHaveBeenCalledWith('⚠️ test warn')
64+
65+
log.step('test step')
66+
expect(consoleSpy).toHaveBeenCalledWith('📋 test step')
67+
68+
log.tool('tool', 'test tool')
69+
expect(consoleSpy).toHaveBeenCalledWith('🔧 Running tool: test tool')
70+
71+
log.celebrate('test celebrate')
72+
expect(consoleSpy).toHaveBeenCalledWith('🎉 test celebrate')
73+
74+
log.skip('test skip')
75+
expect(consoleSpy).toHaveBeenCalledWith('🚫 test skip')
76+
})
77+
})
78+
79+
describe('formatDuration', () => {
80+
it('should format milliseconds correctly', () => {
81+
expect(formatDuration(500)).toBe('500ms')
82+
expect(formatDuration(1500)).toBe('1.5s')
83+
expect(formatDuration(1000)).toBe('1.0s')
84+
})
85+
})
86+
87+
describe('isSkipped', () => {
88+
it('should return true if SKIP_ env var is set', () => {
89+
process.env.SKIP_TEST_TOOL = '1'
90+
expect(isSkipped('test-tool')).toBe(true)
91+
92+
process.env.SKIP_ANOTHER_TOOL = 'true'
93+
expect(isSkipped('another_tool')).toBe(true)
94+
})
95+
96+
it('should return false if SKIP_ env var is not set', () => {
97+
expect(isSkipped('unknown-tool')).toBe(false)
98+
})
99+
100+
it('should normalize tool names', () => {
101+
process.env.SKIP_MY_COOL_TOOL = '1'
102+
expect(isSkipped('my-cool-tool')).toBe(true)
103+
expect(isSkipped('my_cool_tool')).toBe(true)
104+
})
105+
})
106+
107+
describe('isDdevProject', () => {
108+
it('should return false if DDEV_PHP is false', async () => {
109+
process.env.DDEV_PHP = 'false'
110+
expect(await isDdevProject()).toBe(false)
111+
})
112+
113+
it('should return false if .ddev/config.yaml does not exist', async () => {
114+
vi.mocked(fs.pathExists).mockResolvedValue(false)
115+
expect(await isDdevProject()).toBe(false)
116+
})
117+
118+
it('should return false if ddev binary is missing', async () => {
119+
vi.mocked(fs.pathExists).mockResolvedValue(true)
120+
vi.mocked(which).mockRejectedValue(new Error('not found'))
121+
expect(await isDdevProject()).toBe(false)
122+
})
123+
124+
it('should return true if config exists and ddev is found', async () => {
125+
vi.mocked(fs.pathExists).mockResolvedValue(true)
126+
vi.mocked(which).mockResolvedValue('/usr/local/bin/ddev')
127+
expect(await isDdevProject()).toBe(true)
128+
})
129+
})
130+
131+
describe('exec', () => {
132+
it('should execute command directly for non-php type', async () => {
133+
await exec(['ls', '-la'], { type: 'system' })
134+
// @ts-ignore
135+
expect($).toHaveBeenCalled()
136+
// Check the template literal call
137+
// The mock implementation of $ needs to handle template literals if we want to inspect arguments precisely
138+
// But for now, just checking it was called is a start.
139+
})
140+
141+
it('should wrap command in docker exec for php type in ddev project', async () => {
142+
// Setup DDEV environment
143+
vi.mocked(fs.pathExists).mockResolvedValue(true)
144+
vi.mocked(which).mockResolvedValue('/bin/ddev')
145+
vi.mocked(fs.readFile).mockResolvedValue('name: my-project\n')
146+
147+
// Mock platform to be linux for predictable UID/GID
148+
Object.defineProperty(process, 'platform', { value: 'linux' })
149+
vi.spyOn(process, 'getuid').mockReturnValue(1000)
150+
vi.spyOn(process, 'getgid').mockReturnValue(1000)
151+
152+
// Mock $ to capture the command
153+
let capturedCommand: any[] = []
154+
// @ts-ignore
155+
$.mockImplementation((options) => {
156+
return (pieces, ...args) => {
157+
capturedCommand = pieces
158+
return Promise.resolve({ stdout: '', stderr: '', exitCode: 0 })
159+
}
160+
})
161+
162+
await exec(['php', '-v'], { type: 'php' })
163+
164+
// Verify docker exec command construction
165+
// We can't easily inspect the template literal reconstruction without a more complex mock
166+
// But we can verify that fs.readFile was called to get the project name
167+
expect(fs.readFile).toHaveBeenCalledWith('.ddev/config.yaml', 'utf-8')
168+
})
169+
170+
it('should throw error if ddev project name cannot be determined', async () => {
171+
// Setup DDEV environment
172+
vi.mocked(fs.pathExists).mockResolvedValue(true)
173+
vi.mocked(which).mockResolvedValue('/bin/ddev')
174+
// Return empty config or config without name
175+
vi.mocked(fs.readFile).mockResolvedValue('invalid: config\n')
176+
177+
await expect(exec(['php', '-v'], { type: 'php' })).rejects.toThrow('Could not determine DDEV project name')
178+
})
179+
180+
it('should throw error if ddev config read fails', async () => {
181+
// Setup DDEV environment
182+
vi.mocked(fs.pathExists).mockResolvedValue(true)
183+
vi.mocked(which).mockResolvedValue('/bin/ddev')
184+
// fs.readFile throws
185+
vi.mocked(fs.readFile).mockRejectedValue(new Error('read failed'))
186+
187+
await expect(exec(['php', '-v'], { type: 'php' })).rejects.toThrow('Could not determine DDEV project name')
188+
})
189+
})
190+
191+
describe('initEnvironment', () => {
192+
it('should load .env if it exists', async () => {
193+
vi.mocked(fs.pathExists).mockImplementation(async (path) => path === '.env')
194+
195+
// Mock process.loadEnvFile (Node 20+)
196+
process.loadEnvFile = vi.fn()
197+
198+
await initEnvironment()
199+
expect(process.loadEnvFile).toHaveBeenCalledWith('.env')
200+
})
201+
202+
it('should load custom env file from GIT_HOOKS_ENV_FILE', async () => {
203+
process.env.GIT_HOOKS_ENV_FILE = '.custom.env'
204+
vi.mocked(fs.pathExists).mockImplementation(async (path) => path === '.custom.env')
205+
process.loadEnvFile = vi.fn()
206+
207+
await initEnvironment()
208+
expect(process.loadEnvFile).toHaveBeenCalledWith('.custom.env')
209+
})
210+
211+
it('should load .git-hooks.env if .env missing', async () => {
212+
vi.mocked(fs.pathExists).mockImplementation(async (path) => path === '.git-hooks.env')
213+
process.loadEnvFile = vi.fn()
214+
215+
await initEnvironment()
216+
expect(process.loadEnvFile).toHaveBeenCalledWith('.git-hooks.env')
217+
})
218+
219+
it('should log success in verbose mode', async () => {
220+
process.env.GIT_HOOKS_VERBOSE = '1'
221+
vi.mocked(fs.pathExists).mockImplementation(async (path) => path === '.env')
222+
process.loadEnvFile = vi.fn()
223+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
224+
225+
await initEnvironment()
226+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Successfully loaded'))
227+
})
228+
229+
it('should handle errors gracefully', async () => {
230+
vi.mocked(fs.pathExists).mockResolvedValue(true)
231+
process.loadEnvFile = vi.fn().mockImplementation(() => {
232+
throw new Error('Failed to load')
233+
})
234+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
235+
236+
await initEnvironment()
237+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to load'))
238+
})
239+
})
240+
241+
describe('ensureMutagenSync', () => {
242+
it('should sync mutagen if ddev project', async () => {
243+
// Mock isDdevProject true
244+
vi.mocked(fs.pathExists).mockResolvedValue(true)
245+
vi.mocked(which).mockResolvedValue('/bin/ddev')
246+
247+
// Mock exec to succeed
248+
// We need to mock exec since it's imported from the same module
249+
// But wait, we are testing the module itself.
250+
// The exec function calls getExecCommand which calls $
251+
// So we just need $ to succeed.
252+
253+
await ensureMutagenSync()
254+
// @ts-ignore
255+
expect($).toHaveBeenCalled()
256+
})
257+
258+
it('should ignore errors during sync', async () => {
259+
// Mock isDdevProject true
260+
vi.mocked(fs.pathExists).mockResolvedValue(true)
261+
vi.mocked(which).mockResolvedValue('/bin/ddev')
262+
263+
// Mock $ to fail
264+
// @ts-ignore
265+
$.mockImplementation(() => () => Promise.reject(new Error('sync failed')))
266+
267+
await expect(ensureMutagenSync()).resolves.not.toThrow()
268+
})
269+
})
270+
})

0 commit comments

Comments
 (0)