Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 38 additions & 15 deletions packages/genomic/__tests__/create-gen.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { execSync } from 'child_process';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
Expand All @@ -7,7 +7,7 @@ import { ExtractedVariables, extractVariables, GitCloner,promptUser, replaceVari

jest.mock('child_process', () => {
return {
execSync: jest.fn(),
execFileSync: jest.fn(),
};
});

Expand Down Expand Up @@ -560,35 +560,58 @@ module.exports = {
});

describe('GitCloner', () => {
const execSyncMock = execSync as jest.MockedFunction<typeof execSync>;
const execFileSyncMock = execFileSync as jest.MockedFunction<
typeof execFileSync
>;
let gitCloner: GitCloner;

beforeEach(() => {
gitCloner = new GitCloner();
execSyncMock.mockReset();
execSyncMock.mockImplementation(() => undefined);
execFileSyncMock.mockReset();
execFileSyncMock.mockImplementation(() => undefined);
});

it('clones default branch when no branch provided', () => {
const tempDir = path.join(testTempDir, 'clone-test');
gitCloner.clone('https://github.com/example/repo.git', tempDir);
const command = execSyncMock.mock.calls[0][0] as string;
expect(command).toContain(
'git clone --single-branch --depth 1 https://github.com/example/repo.git'
);
expect(command.trim().endsWith(tempDir)).toBe(true);
const [file, args] = execFileSyncMock.mock.calls[0];
expect(file).toBe('git');
expect(args).toEqual([
'clone',
'--single-branch',
'--depth',
'1',
'--',
'https://github.com/example/repo.git',
tempDir,
]);
});

it('clones a specific branch when provided', () => {
const tempDir = path.join(testTempDir, 'clone-branch-test');
gitCloner.clone('https://github.com/example/repo.git', tempDir, {
branch: 'dev',
});
const command = execSyncMock.mock.calls[0][0] as string;
expect(command).toContain(
'git clone --branch dev --single-branch --depth 1 https://github.com/example/repo.git'
);
expect(command.trim().endsWith(tempDir)).toBe(true);
const [file, args] = execFileSyncMock.mock.calls[0];
expect(file).toBe('git');
expect(args).toEqual([
'clone',
'--branch',
'dev',
'--single-branch',
'--depth',
'1',
'--',
'https://github.com/example/repo.git',
tempDir,
]);
});

it('passes metacharacter-bearing destinations as a single argv entry', () => {
const tempDir = path.join(testTempDir, 'a; touch pwned');
gitCloner.clone('https://github.com/example/repo.git', tempDir);
const [, args] = execFileSyncMock.mock.calls[0];
expect(args?.[args.length - 1]).toBe(tempDir);
});
});

Expand Down
17 changes: 8 additions & 9 deletions packages/genomic/src/git/git-cloner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { execSync } from 'child_process';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import { createSpinner } from 'inquirerer';
import * as os from 'os';
Expand Down Expand Up @@ -101,20 +101,19 @@ export class GitCloner {
const singleBranch = options?.singleBranch ?? true;
const silent = options?.silent ?? true;

const branchArgs = branch ? ` --branch ${branch}` : '';
const singleBranchArgs = singleBranch ? ' --single-branch' : '';
const depthArgs = ` --depth ${depth}`;

const command = `git clone${branchArgs}${singleBranchArgs}${depthArgs} ${url} ${destination}`;
const args = ['clone'];
if (branch) args.push('--branch', branch);
if (singleBranch) args.push('--single-branch');
args.push('--depth', String(depth), '--', url, destination);

const spinner = silent ? createSpinner(`Cloning ${url}...`) : null;

try {
if (spinner) {
spinner.start();
}
execSync(command, {

execFileSync('git', args, {
stdio: silent ? 'pipe' : 'inherit',
encoding: 'utf-8'
});
Expand Down
7 changes: 4 additions & 3 deletions packages/genomic/src/utils/npm-version-check.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { execSync } from 'child_process';
import { execFileSync } from 'child_process';

import { VersionCheckResult } from './types';

Expand All @@ -13,8 +13,9 @@ export async function checkNpmVersion(
currentVersion: string
): Promise<VersionCheckResult> {
try {
const latestVersion = execSync(
`npm view ${packageName} version`,
const latestVersion = execFileSync(
'npm',
['view', packageName, 'version'],
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
).trim();

Expand Down
35 changes: 20 additions & 15 deletions packages/inquirerer/__tests__/defaultFrom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import { Question } from '../src/question';

jest.mock('readline');
jest.mock('child_process', () => ({
execSync: jest.fn()
execSync: jest.fn(),
execFileSync: jest.fn()
}));

import { execSync } from 'child_process';
const mockedExecSync = execSync as jest.MockedFunction<typeof execSync>;
import { execFileSync } from 'child_process';
const mockedExecFileSync = execFileSync as jest.MockedFunction<
typeof execFileSync
>;

function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
Expand Down Expand Up @@ -99,7 +102,7 @@ describe('Inquirerer - defaultFrom feature', () => {

describe('git resolvers', () => {
it('should use git.user.name as default', async () => {
mockedExecSync.mockReturnValue('John Doe\n' as any);
mockedExecFileSync.mockReturnValue('John Doe\n' as any);

const prompter = new Inquirerer({
input: mockInput,
Expand All @@ -118,14 +121,15 @@ describe('Inquirerer - defaultFrom feature', () => {
const result = await prompter.prompt({}, questions);

expect(result).toEqual({ authorName: 'John Doe' });
expect(mockedExecSync).toHaveBeenCalledWith(
'git config --global user.name',
expect(mockedExecFileSync).toHaveBeenCalledWith(
'git',
['config', '--global', '--', 'user.name'],
expect.any(Object)
);
});

it('should use git.user.email as default', async () => {
mockedExecSync.mockReturnValue('john@example.com\n' as any);
mockedExecFileSync.mockReturnValue('john@example.com\n' as any);

const prompter = new Inquirerer({
input: mockInput,
Expand All @@ -144,14 +148,15 @@ describe('Inquirerer - defaultFrom feature', () => {
const result = await prompter.prompt({}, questions);

expect(result).toEqual({ authorEmail: 'john@example.com' });
expect(mockedExecSync).toHaveBeenCalledWith(
'git config --global user.email',
expect(mockedExecFileSync).toHaveBeenCalledWith(
'git',
['config', '--global', '--', 'user.email'],
expect.any(Object)
);
});

it('should fallback to static default when git config fails', async () => {
mockedExecSync.mockImplementation(() => {
mockedExecFileSync.mockImplementation(() => {
throw new Error('Git not configured');
});

Expand All @@ -176,7 +181,7 @@ describe('Inquirerer - defaultFrom feature', () => {
});

it('should resolve multiple git fields', async () => {
mockedExecSync
mockedExecFileSync
.mockReturnValueOnce('Jane Smith\n' as any)
.mockReturnValueOnce('jane@example.com\n' as any);

Expand Down Expand Up @@ -339,7 +344,7 @@ describe('Inquirerer - defaultFrom feature', () => {

describe('priority and fallbacks', () => {
it('should prioritize argv over defaultFrom', async () => {
mockedExecSync.mockReturnValue('Git User\n' as any);
mockedExecFileSync.mockReturnValue('Git User\n' as any);

const prompter = new Inquirerer({
input: mockInput,
Expand All @@ -362,7 +367,7 @@ describe('Inquirerer - defaultFrom feature', () => {
});

it('should use undefined when resolver returns undefined and no static default', async () => {
mockedExecSync.mockImplementation(() => {
mockedExecFileSync.mockImplementation(() => {
throw new Error('Git not configured');
});

Expand All @@ -386,7 +391,7 @@ describe('Inquirerer - defaultFrom feature', () => {
});

it('should handle mixed defaultFrom and static defaults', async () => {
mockedExecSync.mockReturnValue('Jane Doe\n' as any);
mockedExecFileSync.mockReturnValue('Jane Doe\n' as any);

const prompter = new Inquirerer({
input: mockInput,
Expand Down Expand Up @@ -527,7 +532,7 @@ describe('Inquirerer - defaultFrom feature', () => {
});

it('should not override when defaultFrom resolver fails and field is required', async () => {
mockedExecSync.mockImplementation(() => {
mockedExecFileSync.mockImplementation(() => {
throw new Error('Git not configured');
});

Expand Down
27 changes: 16 additions & 11 deletions packages/inquirerer/__tests__/resolvers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ import {
// Mock child_process.execSync for git config tests
jest.mock('child_process', () => ({
execSync: jest.fn(),
execFileSync: jest.fn(),
}));

import { execSync } from 'child_process';
import { execFileSync,execSync } from 'child_process';
const mockedExecSync = execSync as jest.MockedFunction<typeof execSync>;
const mockedExecFileSync = execFileSync as jest.MockedFunction<
typeof execFileSync
>;

describe('DefaultResolverRegistry', () => {
let registry: DefaultResolverRegistry;
Expand Down Expand Up @@ -166,13 +170,14 @@ describe('Git Resolvers', () => {

describe('getGitConfig', () => {
it('should return git config value when successful', () => {
mockedExecSync.mockReturnValue('John Doe\n' as any);
mockedExecFileSync.mockReturnValue('John Doe\n' as any);

const result = getGitConfig('user.name');

expect(result).toBe('John Doe');
expect(mockedExecSync).toHaveBeenCalledWith(
'git config --global user.name',
expect(mockedExecFileSync).toHaveBeenCalledWith(
'git',
['config', '--global', '--', 'user.name'],
expect.objectContaining({
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
Expand All @@ -181,15 +186,15 @@ describe('Git Resolvers', () => {
});

it('should trim whitespace from git config value', () => {
mockedExecSync.mockReturnValue(' test@example.com \n' as any);
mockedExecFileSync.mockReturnValue(' test@example.com \n' as any);

const result = getGitConfig('user.email');

expect(result).toBe('test@example.com');
});

it('should return undefined when git config fails', () => {
mockedExecSync.mockImplementation(() => {
mockedExecFileSync.mockImplementation(() => {
throw new Error('Git config not found');
});

Expand All @@ -199,7 +204,7 @@ describe('Git Resolvers', () => {
});

it('should return undefined when git config returns empty string', () => {
mockedExecSync.mockReturnValue('' as any);
mockedExecFileSync.mockReturnValue('' as any);

const result = getGitConfig('user.name');

Expand All @@ -209,15 +214,15 @@ describe('Git Resolvers', () => {

describe('git.user.name resolver', () => {
it('should resolve git user name', async () => {
mockedExecSync.mockReturnValue('Jane Smith\n' as any);
mockedExecFileSync.mockReturnValue('Jane Smith\n' as any);

const result = await globalResolverRegistry.resolve('git.user.name');

expect(result).toBe('Jane Smith');
});

it('should return undefined when git config fails', async () => {
mockedExecSync.mockImplementation(() => {
mockedExecFileSync.mockImplementation(() => {
throw new Error('Git not configured');
});

Expand All @@ -229,15 +234,15 @@ describe('Git Resolvers', () => {

describe('git.user.email resolver', () => {
it('should resolve git user email', async () => {
mockedExecSync.mockReturnValue('jane@example.com\n' as any);
mockedExecFileSync.mockReturnValue('jane@example.com\n' as any);

const result = await globalResolverRegistry.resolve('git.user.email');

expect(result).toBe('jane@example.com');
});

it('should return undefined when git config fails', async () => {
mockedExecSync.mockImplementation(() => {
mockedExecFileSync.mockImplementation(() => {
throw new Error('Git not configured');
});

Expand Down
18 changes: 11 additions & 7 deletions packages/inquirerer/__tests__/setFrom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import { Question } from '../src/question';

jest.mock('readline');
jest.mock('child_process', () => ({
execSync: jest.fn()
execSync: jest.fn(),
execFileSync: jest.fn()
}));

import { execSync } from 'child_process';
const mockedExecSync = execSync as jest.MockedFunction<typeof execSync>;
import { execFileSync } from 'child_process';
const mockedExecFileSync = execFileSync as jest.MockedFunction<
typeof execFileSync
>;

describe('Inquirerer - setFrom feature', () => {
let mockWrite: jest.Mock;
Expand Down Expand Up @@ -106,7 +109,7 @@ describe('Inquirerer - setFrom feature', () => {
});

it('should use git.user.name with setFrom', async () => {
mockedExecSync.mockReturnValue('John Doe\n' as any);
mockedExecFileSync.mockReturnValue('John Doe\n' as any);

const prompter = new Inquirerer({
input: mockInput,
Expand All @@ -125,8 +128,9 @@ describe('Inquirerer - setFrom feature', () => {
const result = await prompter.prompt({}, questions);

expect(result).toEqual({ authorName: 'John Doe' });
expect(mockedExecSync).toHaveBeenCalledWith(
'git config --global user.name',
expect(mockedExecFileSync).toHaveBeenCalledWith(
'git',
['config', '--global', '--', 'user.name'],
expect.any(Object)
);
});
Expand Down Expand Up @@ -216,7 +220,7 @@ describe('Inquirerer - setFrom feature', () => {
});

it('should allow both setFrom and defaultFrom on different questions', async () => {
mockedExecSync.mockReturnValue('Git User\n' as any);
mockedExecFileSync.mockReturnValue('Git User\n' as any);

const prompter = new Inquirerer({
input: mockInput,
Expand Down
Loading
Loading