Skip to content

Commit b020cc1

Browse files
committed
test(cli): extend git/operations tests with more coverage
Add tests for previously untested functions: - getRepoInfo: Extract owner/repo from remote URL - getRepoName, getRepoOwner: Helpers for repo info - detectDefaultBranch: Check common branch names - gitDeleteRemoteBranch: Delete remote branches - gitLocalBranchExists, gitRemoteBranchExists: Branch checks - gitResetAndClean: Combined reset and clean - gitUnstagedModifiedFiles: Get modified file list Fix import paths to use correct relative depth.
1 parent e6ea051 commit b020cc1

File tree

1 file changed

+246
-3
lines changed

1 file changed

+246
-3
lines changed

packages/cli/test/unit/utils/git/operations.test.mts

Lines changed: 246 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,27 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2424
import { resetEnv, setEnv } from '@socketsecurity/lib/env/rewire'
2525

2626
import {
27+
detectDefaultBranch,
2728
getBaseBranch,
29+
getRepoInfo,
30+
getRepoName,
31+
getRepoOwner,
2832
gitBranch,
2933
gitCheckoutBranch,
3034
gitCleanFdx,
3135
gitCommit,
3236
gitCreateBranch,
3337
gitDeleteBranch,
38+
gitDeleteRemoteBranch,
3439
gitEnsureIdentity,
40+
gitLocalBranchExists,
3541
gitPushBranch,
42+
gitRemoteBranchExists,
43+
gitResetAndClean,
3644
gitResetHard,
45+
gitUnstagedModifiedFiles,
3746
parseGitRemoteUrl,
38-
} from '../../../../../src/utils/git/operations.mts'
47+
} from '../../../../src/utils/git/operations.mts'
3948

4049
// Mock spawn.
4150
vi.mock('@socketsecurity/lib/spawn', () => ({
@@ -48,12 +57,12 @@ vi.mock('@socketsecurity/lib/bin', () => ({
4857
whichReal: vi.fn().mockResolvedValue('git'),
4958
}))
5059

51-
vi.mock('../../../../../src/constants/cli.mts', () => ({
60+
vi.mock('../../../../src/constants/cli.mts', () => ({
5261
FLAG_QUIET: '--quiet',
5362
}))
5463

5564
// Mock debug.
56-
vi.mock('../../../../../src/utils/debug.mts', () => ({
65+
vi.mock('../../../../src/utils/debug.mts', () => ({
5766
debugGit: vi.fn(),
5867
}))
5968

@@ -331,4 +340,238 @@ describe('git utilities', () => {
331340
)
332341
})
333342
})
343+
344+
describe('getRepoInfo', () => {
345+
it('returns owner and repo from remote URL', async () => {
346+
const { spawn } = vi.mocked(await import('@socketsecurity/lib/spawn'))
347+
spawn.mockResolvedValue({
348+
status: 0,
349+
stdout: 'git@github.com:socketdev/socket-cli.git',
350+
stderr: '',
351+
} as any)
352+
353+
const result = await getRepoInfo('/test/dir')
354+
expect(result).toEqual({ owner: 'socketdev', repo: 'socket-cli' })
355+
})
356+
357+
it('returns undefined when spawn fails', async () => {
358+
const { spawn } = vi.mocked(await import('@socketsecurity/lib/spawn'))
359+
spawn.mockRejectedValue(new Error('Not a git repo'))
360+
361+
const result = await getRepoInfo('/test/dir')
362+
expect(result).toBeUndefined()
363+
})
364+
365+
it('returns undefined when spawn returns null', async () => {
366+
const { spawn } = vi.mocked(await import('@socketsecurity/lib/spawn'))
367+
spawn.mockResolvedValue(null as any)
368+
369+
const result = await getRepoInfo('/test/dir')
370+
expect(result).toBeUndefined()
371+
})
372+
})
373+
374+
describe('getRepoName', () => {
375+
it('returns repo name from remote URL', async () => {
376+
const { spawn } = vi.mocked(await import('@socketsecurity/lib/spawn'))
377+
spawn.mockResolvedValue({
378+
status: 0,
379+
stdout: 'git@github.com:socketdev/socket-cli.git',
380+
stderr: '',
381+
} as any)
382+
383+
const result = await getRepoName('/test/dir')
384+
expect(result).toBe('socket-cli')
385+
})
386+
387+
it('returns default when no repo info', async () => {
388+
const { spawn } = vi.mocked(await import('@socketsecurity/lib/spawn'))
389+
spawn.mockRejectedValue(new Error('Not a git repo'))
390+
391+
const result = await getRepoName('/test/dir')
392+
// Should return the default repository name.
393+
expect(typeof result).toBe('string')
394+
})
395+
})
396+
397+
describe('getRepoOwner', () => {
398+
it('returns owner from remote URL', async () => {
399+
const { spawn } = vi.mocked(await import('@socketsecurity/lib/spawn'))
400+
spawn.mockResolvedValue({
401+
status: 0,
402+
stdout: 'git@github.com:socketdev/socket-cli.git',
403+
stderr: '',
404+
} as any)
405+
406+
const result = await getRepoOwner('/test/dir')
407+
expect(result).toBe('socketdev')
408+
})
409+
410+
it('returns undefined when no repo info', async () => {
411+
const { spawn } = vi.mocked(await import('@socketsecurity/lib/spawn'))
412+
spawn.mockRejectedValue(new Error('Not a git repo'))
413+
414+
const result = await getRepoOwner('/test/dir')
415+
expect(result).toBeUndefined()
416+
})
417+
})
418+
419+
describe('detectDefaultBranch', () => {
420+
it('returns main when it exists locally', async () => {
421+
const { spawn } = vi.mocked(await import('@socketsecurity/lib/spawn'))
422+
spawn.mockResolvedValue({ status: 0, stdout: '', stderr: '' } as any)
423+
424+
const result = await detectDefaultBranch('/test/dir')
425+
expect(result).toBe('main')
426+
})
427+
428+
it('checks common branch names in order', async () => {
429+
const { spawn } = vi.mocked(await import('@socketsecurity/lib/spawn'))
430+
// All local branches fail.
431+
spawn
432+
.mockRejectedValueOnce(new Error('main not found'))
433+
.mockRejectedValueOnce(new Error('master not found'))
434+
.mockRejectedValueOnce(new Error('develop not found'))
435+
.mockRejectedValueOnce(new Error('trunk not found'))
436+
.mockRejectedValueOnce(new Error('default not found'))
437+
// First remote succeeds.
438+
.mockResolvedValueOnce({ status: 0, stdout: 'refs/heads/main', stderr: '' } as any)
439+
440+
const result = await detectDefaultBranch('/test/dir')
441+
expect(result).toBe('main')
442+
})
443+
})
444+
445+
describe('gitDeleteRemoteBranch', () => {
446+
it('deletes a remote branch', async () => {
447+
const { spawn } = vi.mocked(await import('@socketsecurity/lib/spawn'))
448+
spawn.mockResolvedValue({ status: 0, stdout: '', stderr: '' } as any)
449+
450+
const result = await gitDeleteRemoteBranch('old-feature')
451+
expect(result).toBe(true)
452+
expect(spawn).toHaveBeenCalledWith(
453+
'git',
454+
['push', 'origin', '--delete', 'old-feature'],
455+
expect.any(Object),
456+
)
457+
})
458+
459+
it('returns false when branch does not exist', async () => {
460+
const { spawn } = vi.mocked(await import('@socketsecurity/lib/spawn'))
461+
spawn.mockRejectedValue(new Error('Branch not found'))
462+
463+
const result = await gitDeleteRemoteBranch('nonexistent')
464+
expect(result).toBe(false)
465+
})
466+
})
467+
468+
describe('gitLocalBranchExists', () => {
469+
it('returns true when branch exists', async () => {
470+
const { spawn } = vi.mocked(await import('@socketsecurity/lib/spawn'))
471+
spawn.mockResolvedValue({ status: 0, stdout: '', stderr: '' } as any)
472+
473+
const result = await gitLocalBranchExists('main')
474+
expect(result).toBe(true)
475+
expect(spawn).toHaveBeenCalledWith(
476+
'git',
477+
['show-ref', '--quiet', 'refs/heads/main'],
478+
expect.any(Object),
479+
)
480+
})
481+
482+
it('returns false when branch does not exist', async () => {
483+
const { spawn } = vi.mocked(await import('@socketsecurity/lib/spawn'))
484+
spawn.mockRejectedValue(new Error('Branch not found'))
485+
486+
const result = await gitLocalBranchExists('nonexistent')
487+
expect(result).toBe(false)
488+
})
489+
})
490+
491+
describe('gitRemoteBranchExists', () => {
492+
it('returns true when remote branch exists', async () => {
493+
const { spawn } = vi.mocked(await import('@socketsecurity/lib/spawn'))
494+
spawn.mockResolvedValue({
495+
status: 0,
496+
stdout: 'abc123\trefs/heads/main',
497+
stderr: '',
498+
} as any)
499+
500+
const result = await gitRemoteBranchExists('main')
501+
expect(result).toBe(true)
502+
expect(spawn).toHaveBeenCalledWith(
503+
'git',
504+
['ls-remote', '--heads', 'origin', 'main'],
505+
expect.any(Object),
506+
)
507+
})
508+
509+
it('returns false when remote branch does not exist', async () => {
510+
const { spawn } = vi.mocked(await import('@socketsecurity/lib/spawn'))
511+
spawn.mockResolvedValue({
512+
status: 0,
513+
stdout: '',
514+
stderr: '',
515+
} as any)
516+
517+
const result = await gitRemoteBranchExists('nonexistent')
518+
expect(result).toBe(false)
519+
})
520+
521+
it('returns false when spawn fails', async () => {
522+
const { spawn } = vi.mocked(await import('@socketsecurity/lib/spawn'))
523+
spawn.mockRejectedValue(new Error('Network error'))
524+
525+
const result = await gitRemoteBranchExists('main')
526+
expect(result).toBe(false)
527+
})
528+
})
529+
530+
describe('gitResetAndClean', () => {
531+
it('calls gitResetHard and gitCleanFdx', async () => {
532+
const { spawn } = vi.mocked(await import('@socketsecurity/lib/spawn'))
533+
spawn.mockResolvedValue({ status: 0, stdout: '', stderr: '' } as any)
534+
535+
await gitResetAndClean('main', '/test/dir')
536+
expect(spawn).toHaveBeenCalledWith(
537+
'git',
538+
['reset', '--hard', 'main'],
539+
expect.any(Object),
540+
)
541+
expect(spawn).toHaveBeenCalledWith(
542+
'git',
543+
['clean', '-fdx'],
544+
expect.any(Object),
545+
)
546+
})
547+
})
548+
549+
describe('gitUnstagedModifiedFiles', () => {
550+
it('returns list of modified files', async () => {
551+
const { spawn } = vi.mocked(await import('@socketsecurity/lib/spawn'))
552+
spawn.mockResolvedValue({
553+
status: 0,
554+
stdout: 'file1.txt\nfile2.txt\n',
555+
stderr: '',
556+
} as any)
557+
558+
const result = await gitUnstagedModifiedFiles('/test/dir')
559+
expect(result.ok).toBe(true)
560+
if (result.ok) {
561+
expect(result.data).toContain('file1.txt')
562+
expect(result.data).toContain('file2.txt')
563+
}
564+
})
565+
566+
it('returns error when spawn fails', async () => {
567+
const { spawn } = vi.mocked(await import('@socketsecurity/lib/spawn'))
568+
spawn.mockRejectedValue(new Error('Git error'))
569+
570+
const result = await gitUnstagedModifiedFiles('/test/dir')
571+
expect(result.ok).toBe(false)
572+
if (!result.ok) {
573+
expect(result.message).toBe('Git Error')
574+
}
575+
})
576+
})
334577
})

0 commit comments

Comments
 (0)