node-publisher
Version:
A configurable release automation tool inspired by create-react-app and Travis CI.
321 lines (248 loc) • 9.41 kB
JavaScript
const path = require('path');
const childProcess = require('child_process');
const { execSync } = childProcess;
const {
DEFAULT_TEST_RUNNER,
DEFAULT_BRANCH,
DEFAULT_CONFIG_PATH
} = require('./constants');
const {
buildReleaseEnvironment,
loadReleaseConfig,
execCommands,
currentCommitId,
rollbackCommit,
versionTransformer
} = require('./index');
const config = require('./config');
const validations = require('./validations');
const client = require('./client');
jest.mock('fs');
jest.mock('child_process');
jest.mock('./config');
jest.mock('./validations');
jest.mock('./client');
const mockTestRunner = testRunner =>
require('fs').__setReadFileSyncReturnValue(
'package.json',
JSON.stringify({
scripts: {
[testRunner]: 'jest'
}
})
);
describe('buildReleaseEnvironment', () => {
const baseOptions = {};
const throwError = () => {
throw new Error();
};
describe('when branch is passed', () => {
beforeAll(() => mockTestRunner('test'));
it('contains the passed in branch', () => {
const env = buildReleaseEnvironment({ branch: 'my-branch' });
expect(env.branch).toBe('my-branch');
});
});
describe('when branch is not passed', () => {
it('contains the default branch', () => {
const env = buildReleaseEnvironment({});
expect(env.branch).toBe(DEFAULT_BRANCH);
});
});
describe('when configPath is passed', () => {
beforeAll(() => mockTestRunner('test'));
it('contains the passed in config path', () => {
const env = buildReleaseEnvironment({ configPath: '.pre-release.yml' });
expect(env.configPath).toBe('.pre-release.yml');
});
});
describe('when configPath is not passed', () => {
it('contains the default config path', () => {
const env = buildReleaseEnvironment({});
expect(env.configPath).toBe(DEFAULT_CONFIG_PATH);
});
});
describe('when errors are silenced', () => {
const options = Object.assign({}, baseOptions, { quiet: true });
describe('and PWD is not package root', () => {
it('throws an error', () => {
validations.validatePkgRoot.mockImplementation(throwError);
expect(() => buildReleaseEnvironment(options)).toThrow();
validations.validatePkgRoot.mockRestore();
});
});
describe('and the detected test runner is invalid', () => {
beforeAll(() => {
mockTestRunner('test');
validations.validateTestRunner.mockImplementation(throwError);
});
afterAll(() => validations.validateTestRunner.mockRestore());
it('does not throw an error', () => {
expect(() => buildReleaseEnvironment(options)).not.toThrow();
});
it('returns the default test runner', () => {
const env = buildReleaseEnvironment(options);
expect(env.testRunner).toBe(DEFAULT_TEST_RUNNER);
});
});
});
describe('when errors are not silenced', () => {
const options = baseOptions;
describe('and PWD is not package root', () => {
it('throws an error', () => {
validations.validatePkgRoot.mockImplementation(throwError);
expect(() => buildReleaseEnvironment(options)).toThrow();
validations.validatePkgRoot.mockRestore();
});
});
describe('and the detected test runner is invalid', () => {
beforeAll(() => mockTestRunner('test'));
it('throws an error', () => {
validations.validateTestRunner.mockImplementation(throwError);
expect(() => buildReleaseEnvironment(options)).toThrow();
validations.validateTestRunner.mockRestore();
});
});
});
describe('when lerna is the detected client', () => {
const options = baseOptions;
beforeAll(() => client.publishClient.mockReturnValue('lerna'));
describe('and lerna was bootstrapped', () => {
it('does not throw an error', () => {
expect(() => buildReleaseEnvironment(options)).not.toThrow();
});
});
describe('and lerna was not bootstrapped', () => {
it('throws an error', () => {
validations.validateLerna.mockImplementation(throwError);
expect(() => buildReleaseEnvironment(options)).toThrow();
validations.validateLerna.mockRestore();
});
});
});
});
describe('loadReleaseConfig', () => {
describe('when the default configPath is passed', () => {
describe('and .release.yml is present in the root pkg directory', () => {
it('reads the custom release config from the file', () => {
const MOCKED_FILES = ['.release.yml'];
require('fs').__setMockFiles(MOCKED_FILES);
require('fs').__setReadFileSyncReturnValue(
DEFAULT_CONFIG_PATH,
'file contents'
);
config.readReleaseConfig.mockReturnValue('configuration');
loadReleaseConfig({ configPath: DEFAULT_CONFIG_PATH });
expect(config.readReleaseConfig).toHaveBeenCalledWith('file contents');
});
});
describe('and .release.yml is not present in the root pkg directory', () => {
it('loads the default configuration', () => {
require('fs').__setMockFiles([]);
loadReleaseConfig({ configPath: DEFAULT_CONFIG_PATH });
expect(config.buildReleaseConfig).toHaveBeenCalled();
});
});
});
describe('when a non-default configPath is passed', () => {
describe('and the config file is present in the file system', () => {
beforeAll(() => {
const MOCKED_FILES = ['.pre-release.yml', '/path/.release.yml'];
require('fs').__setMockFiles(MOCKED_FILES);
require('fs').__setReadFileSyncReturnValue('.pre-release.yml', 'a');
require('fs').__setReadFileSyncReturnValue('/path/.release.yml', 'b');
});
it('reads the release config from a relative path', () => {
const fileExistsSpy = jest.spyOn(require('fs'), 'existsSync');
loadReleaseConfig({ configPath: '.pre-release.yml' });
expect(config.readReleaseConfig).toHaveBeenCalledWith('a');
expect(fileExistsSpy).toHaveBeenCalledWith(
path.resolve(process.env.PWD, '.pre-release.yml')
);
});
it('reads the release config from an absolute path', () => {
const fileExistsSpy = jest.spyOn(require('fs'), 'existsSync');
loadReleaseConfig({ configPath: '/path/.release.yml' });
expect(config.readReleaseConfig).toHaveBeenCalledWith('b');
expect(fileExistsSpy).toHaveBeenCalledWith('/path/.release.yml');
});
});
describe('and the config file is not present in the file system', () => {
it('throws an error', () => {
require('fs').__setMockFiles([]);
expect(() => {
loadReleaseConfig({ configPath: '.pre-release.yml' });
}).toThrow();
});
});
});
});
describe('execCommands', () => {
childProcess.__permitCommands(['mycommand', 'mySecondCommand']);
afterEach(() => {
execSync.mockClear();
});
describe('when config commands are undefined', () => {
it('execCommand is not called', () => {
execCommands();
expect(execSync).not.toHaveBeenCalled();
});
});
describe('when a single command is being passed', () => {
it('execCommand is called 1 time', () => {
execCommands('mycommand --some-arg');
expect(execSync).toHaveBeenCalledTimes(1);
expect(execSync.mock.calls[0][0]).toBe('mycommand --some-arg');
});
});
describe('when multiple commands are being passed as an array', () => {
it('execCommand is called multiple times', () => {
execCommands(['mycommand --some-arg', 'mySecondCommand --some-arg']);
expect(execSync).toHaveBeenCalledTimes(2);
expect(execSync.mock.calls[0][0]).toBe('mycommand --some-arg');
expect(execSync.mock.calls[1][0]).toBe('mySecondCommand --some-arg');
});
});
});
describe('currentCommitId', () => {
it("returns the HEAD commit's ID", () => {
childProcess.__permitCommands(['git']);
childProcess.__setReturnValues({
'git rev-parse HEAD': Buffer.from(
'f030084d079ba5e07de6546879a84e23de536db1\n',
'utf8'
)
});
const commitId = currentCommitId();
expect(execSync).toHaveBeenCalled();
expect(commitId).toBe('f030084d079ba5e07de6546879a84e23de536db1');
execSync.mockClear();
});
});
describe('rollbackCommit', () => {
it('rolls back to the specified commit', () => {
childProcess.__permitCommands(['git']);
rollbackCommit('f030084d079ba5e07de6546879a84e23de536db1');
expect(execSync).toHaveBeenCalled();
expect(execSync.mock.calls[0][0]).toBe(
'git reset --hard f030084d079ba5e07de6546879a84e23de536db1'
);
execSync.mockClear();
});
});
describe('versionTransformer', () => {
describe('when isFinal flag is true', () => {
const isFinal = true;
it('returns the version with the "v" prefix', () => {
expect(versionTransformer('9.11.1', [], { isFinal })).toBe('v9.11.1');
expect(versionTransformer('v9.11.1', [], { isFinal })).toBe('v9.11.1');
});
});
describe('when isFinal flag is false', () => {
const isFinal = false;
it('returns the version as input by the user', () => {
expect(versionTransformer('9.11.1', [], { isFinal })).toBe('9.11.1');
expect(versionTransformer('v9.11.1', [], { isFinal })).toBe('v9.11.1');
});
});
});