Skip to content

Commit

Permalink
chore(testing): add unit tests for checkout (#697)
Browse files Browse the repository at this point in the history
  • Loading branch information
aorinevo authored Mar 9, 2024
1 parent bbe4f5c commit c128006
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 41 deletions.
65 changes: 29 additions & 36 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
"test": "jest --coverage src/"
},
"jest": {
"coveragePathIgnorePatterns": [
"\\.mock\\.ts$"
],
"moduleFileExtensions": [
"ts",
"tsx",
Expand Down
26 changes: 26 additions & 0 deletions src/adapters/adapter.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import IRepoAdapter, { IEnvironmentVariables, IRepo } from './base';

const mockAdapter: IRepoAdapter = {
getCandidateRepos: jest.fn() as unknown as (onRetry: jest.Mock) => Promise<IRepo[]>,
parseRepo: jest.fn() as unknown as (repo: string) => IRepo,
reposEqual: jest.fn() as unknown as (repo1: IRepo, repo2: IRepo) => boolean,
stringifyRepo: jest.fn() as unknown as (repo: IRepo) => string,
mapRepoAfterCheckout: jest.fn() as unknown as (repo: Readonly<IRepo>) => Promise<IRepo>,
checkoutRepo: jest.fn() as unknown as (repo: IRepo) => Promise<void>,
resetChangedFiles: jest.fn() as unknown as (repo: IRepo) => Promise<void>,
resetRepoBeforeApply: jest.fn() as unknown as (repo: IRepo, force: boolean) => Promise<void>,
commitRepo: jest.fn() as unknown as (repo: IRepo) => Promise<void>,
pushRepo: jest.fn() as unknown as (repo: IRepo, force: boolean) => Promise<void>,
createPullRequest: jest.fn() as unknown as (
repo: IRepo,
message: string,
upstreamOwner: string
) => Promise<void>,
getPullRequestStatus: jest.fn() as unknown as (repo: IRepo) => Promise<string[]>,
getRepoDir: jest.fn() as unknown as (repo: IRepo) => string,
getDataDir: jest.fn() as unknown as (repo: IRepo) => string,
getBaseBranch: jest.fn() as unknown as (repo: IRepo) => string,
getEnvironmentVariables: jest.fn() as unknown as (repo: IRepo) => Promise<IEnvironmentVariables>,
};

export default mockAdapter;
111 changes: 111 additions & 0 deletions src/commands/checkout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { IMigrationContext } from '../migration-context';
import checkout from './checkout';
import mockAdapter from '../adapters/adapter.mock';
import mockLogger from '../logger/logger.mock';
import mockSpinner from '../logger/spinner.mock';
import executeSteps from '../util/execute-steps';

jest.mock('fs-extra', () => {
return {
// Mock other methods as needed
mkdirs: jest.fn().mockResolvedValue(undefined),
pathExists: jest.fn().mockResolvedValue(true),
readFile: jest.fn().mockResolvedValue('{"name": "test"}'),
outputFile: jest.fn().mockResolvedValue(undefined),
remove: jest.fn().mockResolvedValue(undefined),
};
});

jest.mock('../util/execute-steps');

describe('checkout command', () => {
const mockContext: IMigrationContext = {
shepherd: {
workingDirectory: 'workingDirectory',
},
migration: {
migrationDirectory: 'migrationDirectory',
spec: {
id: 'id',
title: 'title',
adapter: {
type: 'adapter',
},
hooks: {},
},
workingDirectory: 'workingDirectory',
selectedRepos: [{ name: 'selectedRepos' }],
repos: [{ name: 'selectedRepos' }],
upstreamOwner: 'upstreamOwner',
},
adapter: mockAdapter,
logger: mockLogger,
};

beforeEach(() => {
jest.clearAllMocks();
});

it('clones repos given a specific list of repos', async () => {
(executeSteps as jest.Mock)
.mockResolvedValueOnce({
succeeded: true,
stepResults: [],
})
.mockResolvedValueOnce({
succeeded: true,
stepResults: [],
});
await checkout(mockContext);
expect(mockLogger.info).toHaveBeenCalledWith('Using 1 selected repos');
expect(mockAdapter.checkoutRepo).toHaveBeenCalledWith({ name: 'selectedRepos' });
expect(mockSpinner.succeed).toHaveBeenCalledWith('Checked out repo');
});

it('gets candidate repos when list of repos is not provided', async () => {
mockContext.migration.selectedRepos = undefined;
await checkout(mockContext);
expect(mockAdapter.getCandidateRepos).toHaveBeenCalled();
expect(mockSpinner.succeed).toHaveBeenCalledWith('Loaded 0 repos');
});

it('handles errors when checking out repos', async () => {
mockContext.migration.selectedRepos = [{ name: 'selectedRepos' }];
mockContext.migration.repos = null;
mockAdapter.checkoutRepo = jest.fn().mockImplementationOnce(() => {
throw new Error('Mocked error');
});
await checkout(mockContext);
expect(mockLogger.error).toHaveBeenCalledWith(new Error('Mocked error'));
expect(mockSpinner.fail).toHaveBeenCalledWith('Failed to check out repo; skipping');
});

it('handles errors when running should_migrate steps', async () => {
mockContext.migration.selectedRepos = [{ name: 'selectedRepos' }];
mockContext.migration.repos = null;
(executeSteps as jest.Mock).mockResolvedValueOnce({
succeeded: false,
stepResults: [],
});
await checkout(mockContext);
expect(mockLogger.failIcon).toHaveBeenCalledWith(
'Error running should_migrate steps; skipping'
);
});

it('handles errors when running post_checkout steps', async () => {
mockContext.migration.selectedRepos = [{ name: 'selectedRepos' }];
(executeSteps as jest.Mock)
.mockResolvedValueOnce({
succeeded: true,
stepResults: [],
})
.mockResolvedValueOnce({
succeeded: false,
stepResults: [],
});

await checkout(mockContext);
expect(mockLogger.failIcon).toHaveBeenCalledWith('Error running post_checkout steps; skipping');
});
});
7 changes: 2 additions & 5 deletions src/commands/checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,21 @@ export default async (context: IMigrationContext) => {
adapter,
logger,
} = context;

function onRetry(numSeconds: number) {
logger.info(`Hit rate limit; waiting ${numSeconds} seconds and retrying.`);
}

let repos;

if (selectedRepos) {
logger.info(`Using ${selectedRepos.length} selected repos`);
repos = selectedRepos;
} else {
const spinner = logger.spinner('Loading candidate repos');
repos = await adapter.getCandidateRepos(onRetry);
repos = (await adapter.getCandidateRepos(onRetry)) || [];
spinner.succeed(`Loaded ${repos.length} repos`);
}

context.migration.repos = repos;

const checkedOutRepos: IRepo[] = [];
const discardedRepos: IRepo[] = [];

Expand All @@ -49,7 +47,6 @@ export default async (context: IMigrationContext) => {
spinner.fail('Failed to check out repo; skipping');
return;
}

// We need to create the data directory before running should_migrate
await fs.mkdirs(adapter.getDataDir(repo));

Expand Down
18 changes: 18 additions & 0 deletions src/logger/logger.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ILogger } from './index';
import mockSpinner from './spinner.mock';

const mockLogger: ILogger = {
// Basic logging
debug: jest.fn() as unknown as (message: string) => void,
info: jest.fn() as unknown as (message: string) => void,
warn: jest.fn() as unknown as (message: string) => void,
error: jest.fn() as unknown as (message: string) => void,
fatal: jest.fn() as unknown as (message: string) => void,
succeedIcon: jest.fn() as unknown as (message: string) => void,
failIcon: jest.fn() as unknown as (message: string) => void,
warnIcon: jest.fn() as unknown as (message: string) => void,
infoIcon: jest.fn() as unknown as (message: string) => void,
spinner: jest.fn().mockImplementation(() => mockSpinner),
};

export default mockLogger;
15 changes: 15 additions & 0 deletions src/logger/spinner.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ISpinner } from '.';

const mockSpinner: ISpinner = {
start: jest.fn(),
stop: jest.fn(),
succeed: jest.fn(),
fail: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
clear: jest.fn(),
render: jest.fn(),
destroy: jest.fn(),
};

export default mockSpinner;

0 comments on commit c128006

Please sign in to comment.