src/lib/core/installer.test.js
import test from 'ava';
import sinon from 'sinon';
import path from 'path';
import Subject from '../../__mocks__/rxjs-subject';
import fs, {
alreadyExistsFilename,
alreadyExistsPath,
throwableMkdirPath,
} from '../../__mocks__/fs';
import memFs, { store } from '../../__mocks__/mem-fs';
import memFsEditor, {
fs as memFsEditorFs,
} from '../../__mocks__/mem-fs-editor';
import { defaultUserGeneratorConfig, systemSeederTemplate } from '../constants';
import { mockImports, resetImports } from '../utils/test-helpers';
import InstallerError from './installer-error';
import Installer, { __RewireAPI__ as moduleRewireAPI } from './installer';
const helpData = {
seedersFolder: 'seeders-folder',
customSeederTemplate: 'some-template.js',
};
const defaultConfig = {
projectRoot: '/project/root',
userConfigExists: true,
userConfigFilename: 'config-filename.js',
userConfigFilepath: '/project/root/config-filename.js',
configTemplate: '/template/folder/config-template.js',
};
test.beforeEach('mock imports', t => {
const mocks = {
Subject,
fs,
memFs,
editor: memFsEditor,
config: { ...defaultConfig },
getObjectWithSelectedKeys: sinon.stub(),
normalizeSeederName: sinon.stub().returnsArg(0),
};
t.context = { mocks };
mockImports({ moduleRewireAPI, mocks });
});
test.afterEach.always('unmock imports', t => {
const imports = Object.keys(t.context.mocks);
resetImports({ moduleRewireAPI, imports });
});
test('Should create a installer instance', t => {
sinon.stub(Installer.prototype, '_initConfig');
sinon.stub(Installer.prototype, '_initMemFs');
const installer = new Installer();
t.truthy(installer._subject);
t.is(typeof installer.install, 'function');
t.true(
installer._initConfig.calledWith({
...defaultUserGeneratorConfig,
customSeederTemplate: undefined,
})
);
t.true(installer._initMemFs.called);
Installer.prototype._initConfig.restore();
Installer.prototype._initMemFs.restore();
});
test('Should create a installer instance with args', t => {
sinon.stub(Installer.prototype, '_initConfig');
sinon.stub(Installer.prototype, '_initMemFs');
const installer = new Installer({ ...helpData });
t.truthy(installer._subject);
t.is(typeof installer.install, 'function');
t.true(installer._initConfig.calledWith({ ...helpData }));
t.true(installer._initMemFs.called);
Installer.prototype._initConfig.restore();
Installer.prototype._initMemFs.restore();
});
test('Should _initConfig', t => {
const context = {};
const _initConfig = Installer.prototype._initConfig.bind(context);
_initConfig({ ...helpData });
t.snapshot(context);
});
test('Should _initConfig without customSeederTemplate', t => {
const context = {};
const _initConfig = Installer.prototype._initConfig.bind(context);
const config = { ...helpData };
delete config.customSeederTemplate;
_initConfig(config);
t.snapshot(context);
});
test('Should _initMemFs', t => {
const { mocks } = t.context;
const context = {};
const _initMemFs = Installer.prototype._initMemFs.bind(context);
_initMemFs();
t.true(mocks.memFs.create.called);
t.true(mocks.editor.create.calledWith(store));
t.is(context._memFsEditor, memFsEditorFs);
});
test('Should install', t => {
const context = {
_install: sinon.stub().resolves(),
_subject: { asObservable: () => 'observable' },
};
const install = Installer.prototype.install.bind(context);
const results = install();
t.is(results, 'observable');
t.true(context._install.called);
});
test('Should getGeneratorConfig', t => {
const context = {
config: {
userSeedersFolderName: 'foldername',
},
};
const getGeneratorConfig = Installer.prototype.getGeneratorConfig.bind(
context
);
const expectedResults = {
seedersFolder: context.config.userSeedersFolderName,
};
const results = getGeneratorConfig();
t.deepEqual(results, expectedResults);
});
test('Should getGeneratorConfig with customSeederTemplate', t => {
const context = {
config: {
userSeedersFolderName: 'foldername',
customSeederTemplateFilename: 'template-filename.js',
},
};
const getGeneratorConfig = Installer.prototype.getGeneratorConfig.bind(
context
);
const expectedResults = {
seedersFolder: context.config.userSeedersFolderName,
customSeederTemplate: context.config.customSeederTemplateFilename,
};
const results = getGeneratorConfig();
t.deepEqual(results, expectedResults);
});
test('Should _install and success', async t => {
const context = {
_createCustomSeederTemplate: sinon.stub().resolves(),
_writeUserGeneratorConfigToPackageJson: sinon.stub().resolves(),
_createSeedersFolder: sinon.stub().resolves(),
_writeUserConfig: sinon.stub().resolves(),
_subject: {
next: sinon.stub(),
complete: sinon.stub(),
error: sinon.stub(),
},
};
const _install = Installer.prototype._install.bind(context);
await _install();
t.true(context._createCustomSeederTemplate.called);
t.true(context._writeUserGeneratorConfigToPackageJson.called);
t.true(context._createSeedersFolder.called);
t.true(context._writeUserConfig.called);
t.true(context._subject.next.calledWith({ type: 'START' }));
t.true(context._subject.next.calledWith({ type: 'SUCCESS' }));
t.true(context._subject.complete.called);
t.false(context._subject.error.called);
});
test('Should _install and fail', async t => {
const error = new Error('some-error');
const context = {
_createCustomSeederTemplate: sinon.stub().resolves(),
_writeUserGeneratorConfigToPackageJson: sinon.stub().resolves(),
_createSeedersFolder: sinon.stub().rejects(error),
_writeUserConfig: sinon.stub().resolves(),
_subject: {
next: sinon.stub(),
complete: sinon.stub(),
error: sinon.stub(),
},
};
const _install = Installer.prototype._install.bind(context);
await t.notThrowsAsync(() => _install());
t.true(context._createCustomSeederTemplate.called);
t.true(context._writeUserGeneratorConfigToPackageJson.called);
t.true(context._createSeedersFolder.called);
t.false(context._writeUserConfig.called);
t.true(context._subject.next.calledWith({ type: 'START' }));
t.false(context._subject.next.calledWith({ type: 'SUCCESS' }));
t.false(context._subject.complete.called);
t.true(
context._subject.error.calledWith({ type: 'ERROR', payload: { error } })
);
});
test('Should _install and fail with InstallerError', async t => {
const type = 'CREATE_SEEDERS_FOLDER_ERROR';
const payload = { some: 'data' };
const baseError = new Error('some-base-error');
const error = new InstallerError({ type, payload, error: baseError });
const context = {
_createCustomSeederTemplate: sinon.stub().resolves(),
_writeUserGeneratorConfigToPackageJson: sinon.stub().resolves(),
_createSeedersFolder: sinon.stub().rejects(error),
_writeUserConfig: sinon.stub().resolves(),
_subject: {
next: sinon.stub(),
complete: sinon.stub(),
error: sinon.stub(),
},
};
const _install = Installer.prototype._install.bind(context);
await _install();
t.true(context._createCustomSeederTemplate.called);
t.true(context._writeUserGeneratorConfigToPackageJson.called);
t.true(context._createSeedersFolder.called);
t.false(context._writeUserConfig.called);
t.true(context._subject.next.calledWith({ type: 'START' }));
t.false(context._subject.next.calledWith({ type: 'SUCCESS' }));
t.false(context._subject.complete.called);
t.true(
context._subject.error.calledWith({
type,
payload: { ...payload, error: baseError },
})
);
});
test('Should _commitMemFsChanges', async t => {
const context = {
_memFsEditor: { commit: sinon.stub().callsArg(0) },
};
const _commitMemFsChanges = Installer.prototype._commitMemFsChanges.bind(
context
);
await _commitMemFsChanges();
t.true(context._memFsEditor.commit.called);
});
test('Should _createCustomSeederTemplate and success', async t => {
const customSeederTemplateFilename = 'seeder-template.js';
const customSeederTemplatePath = `/some/${customSeederTemplateFilename}`;
const config = { customSeederTemplateFilename, customSeederTemplatePath };
const payload = { customSeederTemplateFilename, customSeederTemplatePath };
const subject = new Subject();
const context = {
_subject: subject,
config,
_commitMemFsChanges: sinon.stub().resolves(),
_memFsEditor: { copy: sinon.stub() },
};
const _createCustomSeederTemplate = Installer.prototype._createCustomSeederTemplate.bind(
context
);
await t.notThrowsAsync(() => _createCustomSeederTemplate());
t.true(
subject.next.calledWith({
type: 'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_START',
payload,
})
);
t.false(
subject.next.calledWith({
type: 'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SKIP_NO_CUSTOM',
payload,
})
);
t.false(
subject.next.calledWith({
type: 'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SKIP_FILE_EXISTS',
payload,
})
);
t.true(
subject.next.calledWith({
type: 'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SUCCESS',
payload,
})
);
t.true(
context._memFsEditor.copy.calledWith(
systemSeederTemplate,
customSeederTemplatePath
)
);
t.true(context._commitMemFsChanges.called);
});
test('Should _createCustomSeederTemplate and skip because no custom seeder choosed', async t => {
const customSeederTemplateFilename = undefined;
const customSeederTemplatePath = undefined;
const config = { customSeederTemplateFilename, customSeederTemplatePath };
const payload = { customSeederTemplateFilename, customSeederTemplatePath };
const subject = new Subject();
const context = {
_subject: subject,
config,
_commitMemFsChanges: sinon.stub().resolves(),
_memFsEditor: { copy: sinon.stub() },
};
const _createCustomSeederTemplate = Installer.prototype._createCustomSeederTemplate.bind(
context
);
await t.notThrows(() => _createCustomSeederTemplate());
t.true(
subject.next.calledWith({
type: 'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_START',
payload,
})
);
t.true(
subject.next.calledWith({
type: 'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SKIP_NO_CUSTOM',
payload,
})
);
t.false(
subject.next.calledWith({
type: 'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SKIP_FILE_EXISTS',
payload,
})
);
t.false(
subject.next.calledWith({
type: 'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SUCCESS',
payload,
})
);
t.false(context._memFsEditor.copy.called);
t.false(context._commitMemFsChanges.called);
});
test('Should _createCustomSeederTemplate and skip because no the seeder template already exists', async t => {
const customSeederTemplateFilename = alreadyExistsFilename;
const customSeederTemplatePath = alreadyExistsPath;
const config = { customSeederTemplateFilename, customSeederTemplatePath };
const payload = { customSeederTemplateFilename, customSeederTemplatePath };
const subject = new Subject();
const context = {
_subject: subject,
config,
_commitMemFsChanges: sinon.stub().resolves(),
_memFsEditor: { copy: sinon.stub() },
};
const _createCustomSeederTemplate = Installer.prototype._createCustomSeederTemplate.bind(
context
);
await t.notThrows(() => _createCustomSeederTemplate());
t.true(
subject.next.calledWith({
type: 'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_START',
payload,
})
);
t.false(
subject.next.calledWith({
type: 'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SKIP_NO_CUSTOM',
payload,
})
);
t.true(
subject.next.calledWith({
type: 'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SKIP_FILE_EXISTS',
payload,
})
);
t.false(
subject.next.calledWith({
type: 'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SUCCESS',
payload,
})
);
t.false(context._memFsEditor.copy.called);
t.false(context._commitMemFsChanges.called);
});
test('Should _createCustomSeederTemplate and fail', async t => {
const customSeederTemplateFilename = 'seeder-template.js';
const customSeederTemplatePath = `/some/${customSeederTemplateFilename}`;
const config = { customSeederTemplateFilename, customSeederTemplatePath };
const payload = { customSeederTemplateFilename, customSeederTemplatePath };
const subject = new Subject();
const error = new Error('some-error');
const context = {
_subject: subject,
config,
_commitMemFsChanges: sinon.stub().rejects(error),
_memFsEditor: { copy: sinon.stub() },
};
const _createCustomSeederTemplate = Installer.prototype._createCustomSeederTemplate.bind(
context
);
const rejectionError = await t.throwsAsync(() =>
_createCustomSeederTemplate()
);
t.is(rejectionError.type, 'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_ERROR');
t.deepEqual(rejectionError.payload, { ...payload, error });
t.true(
subject.next.calledWith({
type: 'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_START',
payload,
})
);
t.false(
subject.next.calledWith({
type: 'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SKIP_NO_CUSTOM',
payload,
})
);
t.false(
subject.next.calledWith({
type: 'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SKIP_FILE_EXISTS',
payload,
})
);
t.false(
subject.next.calledWith({
type: 'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SUCCESS',
payload,
})
);
t.true(context._memFsEditor.copy.called);
t.true(context._commitMemFsChanges.called);
});
test('Should _writeUserGeneratorConfigToPackageJson and success', async t => {
const config = {
userPackageJsonPath: path.join(__dirname, './__mocks__/package.json'),
};
const generatorConfig = {
seedersFolder: '/some/folder',
customSeederTemplateFilename: 'some-filename.js',
};
const payload = {
packageJsonPath: config.userPackageJsonPath,
};
const subject = new Subject();
const context = {
_subject: subject,
config,
_commitMemFsChanges: sinon.stub().resolves(),
_memFsEditor: { writeJSON: sinon.stub() },
getGeneratorConfig: () => generatorConfig,
};
const _writeUserGeneratorConfigToPackageJson = Installer.prototype._writeUserGeneratorConfigToPackageJson.bind(
context
);
await t.notThrowsAsync(() => _writeUserGeneratorConfigToPackageJson());
t.true(
subject.next.calledWith({
type: 'WRITE_USER_GENERETOR_CONFIG_START',
payload,
})
);
t.true(
subject.next.calledWith({
type: 'WRITE_USER_GENERETOR_CONFIG_SUCCESS',
payload,
})
);
t.true(
context._memFsEditor.writeJSON.calledWith(payload.packageJsonPath, {
mdSeed: generatorConfig,
})
);
t.true(context._commitMemFsChanges.called);
});
test('Should _writeUserGeneratorConfigToPackageJson and fail', async t => {
const config = {
userPackageJsonPath: path.join(__dirname, './__mocks__/package.json'),
};
const generatorConfig = {
seedersFolder: '/some/folder',
customSeederTemplateFilename: 'some-filename.js',
};
const payload = {
packageJsonPath: config.userPackageJsonPath,
};
const subject = new Subject();
const error = new Error('some-error');
const context = {
_subject: subject,
config,
_commitMemFsChanges: sinon.stub().rejects(error),
_memFsEditor: { writeJSON: sinon.stub() },
getGeneratorConfig: () => generatorConfig,
};
const _writeUserGeneratorConfigToPackageJson = Installer.prototype._writeUserGeneratorConfigToPackageJson.bind(
context
);
const rejectionError = await t.throwsAsync(
_writeUserGeneratorConfigToPackageJson()
);
t.is(rejectionError.type, 'WRITE_USER_GENERETOR_CONFIG_ERROR');
t.deepEqual(rejectionError.payload, { ...payload, error });
t.true(
subject.next.calledWith({
type: 'WRITE_USER_GENERETOR_CONFIG_START',
payload,
})
);
t.false(
subject.next.calledWith({
type: 'WRITE_USER_GENERETOR_CONFIG_SUCCESS',
payload,
})
);
t.true(context._memFsEditor.writeJSON.called);
t.true(context._commitMemFsChanges.called);
});
test('Should _createSeedersFolder and success', async t => {
const { mocks } = t.context;
const folderpath = '/some/folder/path/folder';
const foldername = folderpath.split('/').pop();
const payload = { folderpath, foldername };
const config = {
userSeedersFolderPath: folderpath,
userSeedersFolderName: foldername,
};
const subject = new Subject();
const context = { _subject: subject, config };
const _createSeedersFolder = Installer.prototype._createSeedersFolder.bind(
context
);
await t.notThrows(() => _createSeedersFolder());
t.true(
subject.next.calledWith({
type: 'CREATE_SEEDERS_FOLDER_START',
payload,
})
);
t.false(
subject.next.calledWith({
type: 'CREATE_SEEDERS_FOLDER_SKIP_FOLDER_EXISTS',
payload,
})
);
t.true(
subject.next.calledWith({
type: 'CREATE_SEEDERS_FOLDER_SUCCESS',
payload,
})
);
t.true(mocks.fs.existsSync.calledWith(folderpath));
t.true(mocks.fs.mkdirSync.calledWith(folderpath));
});
test('Should _createSeedersFolder and skip', async t => {
const { mocks } = t.context;
const folderpath = alreadyExistsPath;
const foldername = alreadyExistsPath.split('/').pop();
const payload = { folderpath, foldername };
const config = {
userSeedersFolderPath: folderpath,
userSeedersFolderName: foldername,
};
const subject = new Subject();
const context = { _subject: subject, config };
const _createSeedersFolder = Installer.prototype._createSeedersFolder.bind(
context
);
await t.notThrows(() => _createSeedersFolder());
t.true(
subject.next.calledWith({
type: 'CREATE_SEEDERS_FOLDER_START',
payload,
})
);
t.true(
subject.next.calledWith({
type: 'CREATE_SEEDERS_FOLDER_SKIP_FOLDER_EXISTS',
payload,
})
);
t.false(
subject.next.calledWith({
type: 'CREATE_SEEDERS_FOLDER_SUCCESS',
payload,
})
);
t.true(mocks.fs.existsSync.calledWith(folderpath));
t.false(mocks.fs.mkdirSync.calledWith(folderpath));
});
test('Should _createSeedersFolder and fail', async t => {
const { mocks } = t.context;
const folderpath = throwableMkdirPath;
const foldername = throwableMkdirPath.split('/').pop();
const payload = { folderpath, foldername };
const config = {
userSeedersFolderPath: folderpath,
userSeedersFolderName: foldername,
};
const subject = new Subject();
const context = { _subject: subject, config };
const _createSeedersFolder = Installer.prototype._createSeedersFolder.bind(
context
);
const rejectionError = await t.throwsAsync(() => _createSeedersFolder());
t.is(rejectionError.type, 'CREATE_SEEDERS_FOLDER_ERROR');
t.deepEqual(rejectionError.payload, {
...payload,
error: new Error('some-error'),
});
t.true(
subject.next.calledWith({
type: 'CREATE_SEEDERS_FOLDER_START',
payload,
})
);
t.false(
subject.next.calledWith({
type: 'CREATE_SEEDERS_FOLDER_SKIP_FOLDER_EXISTS',
payload,
})
);
t.false(
subject.next.calledWith({
type: 'CREATE_SEEDERS_FOLDER_SUCCESS',
payload,
})
);
t.true(mocks.fs.existsSync.calledWith(folderpath));
t.true(mocks.fs.mkdirSync.calledWith(folderpath));
});
test('Should _writeUserConfig and success', async t => {
const config = {
userConfigExists: false,
userConfigFilename: 'filename.js',
userConfigFilepath: '/some/path/filename.js',
configTemplatePath: '/some/template.js',
};
const payload = {
fileExists: config.userConfigExists,
filename: config.userConfigFilename,
filepath: config.userConfigFilepath,
};
const subject = new Subject();
const context = {
_subject: subject,
config,
_commitMemFsChanges: sinon.stub().resolves(),
_memFsEditor: { copy: sinon.stub() },
};
const _writeUserConfig = Installer.prototype._writeUserConfig.bind(context);
await t.notThrowsAsync(() => _writeUserConfig());
t.true(
subject.next.calledWith({
type: 'WRITE_USER_CONFIG_START',
payload,
})
);
t.false(
subject.next.calledWith({
type: 'WRITE_USER_CONFIG_SKIP_FILE_EXISTS',
payload,
})
);
t.true(
subject.next.calledWith({
type: 'WRITE_USER_CONFIG_SUCCESS',
payload,
})
);
t.true(
context._memFsEditor.copy.calledWith(
config.configTemplatePath,
config.userConfigFilepath
)
);
t.true(context._commitMemFsChanges.called);
});
test('Should _writeUserConfig and skip', async t => {
const config = {
userConfigExists: true,
userConfigFilename: 'filename.js',
userConfigFilepath: '/some/path/filename.js',
configTemplatePath: '/some/template.js',
};
const payload = {
fileExists: config.userConfigExists,
filename: config.userConfigFilename,
filepath: config.userConfigFilepath,
};
const subject = new Subject();
const context = {
_subject: subject,
config,
_commitMemFsChanges: sinon.stub().resolves(),
_memFsEditor: { copy: sinon.stub() },
};
const _writeUserConfig = Installer.prototype._writeUserConfig.bind(context);
await t.notThrows(() => _writeUserConfig());
t.true(
subject.next.calledWith({
type: 'WRITE_USER_CONFIG_START',
payload,
})
);
t.true(
subject.next.calledWith({
type: 'WRITE_USER_CONFIG_SKIP_FILE_EXISTS',
payload,
})
);
t.false(
subject.next.calledWith({
type: 'WRITE_USER_CONFIG_SUCCESS',
payload,
})
);
t.false(context._memFsEditor.copy.called);
t.false(context._commitMemFsChanges.called);
});
test('Should _writeUserConfig and fail', async t => {
const config = {
userConfigExists: false,
userConfigFilename: 'filename.js',
userConfigFilepath: '/some/path/filename.js',
configTemplatePath: '/some/template.js',
};
const payload = {
fileExists: config.userConfigExists,
filename: config.userConfigFilename,
filepath: config.userConfigFilepath,
};
const subject = new Subject();
const error = new Error('some-error');
const context = {
_subject: subject,
config,
_commitMemFsChanges: sinon.stub().rejects(error),
_memFsEditor: { copy: sinon.stub() },
};
const _writeUserConfig = Installer.prototype._writeUserConfig.bind(context);
const rejectionError = await t.throwsAsync(() => _writeUserConfig());
t.is(rejectionError.type, 'WRITE_USER_CONFIG_ERROR');
t.deepEqual(rejectionError.payload, { ...payload, error });
t.true(
subject.next.calledWith({
type: 'WRITE_USER_CONFIG_START',
payload,
})
);
t.false(
subject.next.calledWith({
type: 'WRITE_USER_CONFIG_SKIP_FILE_EXISTS',
payload,
})
);
t.false(
subject.next.calledWith({
type: 'WRITE_USER_CONFIG_SUCCESS',
payload,
})
);
t.true(
context._memFsEditor.copy.calledWith(
config.configTemplatePath,
config.userConfigFilepath
)
);
t.true(context._commitMemFsChanges.called);
});