@interaktiv/mibuilder-core
Version:
Core libraries to interact with MiBuilder projects.
387 lines (335 loc) • 12.3 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.mockContext = mockContext;
exports.restoreContext = restoreContext;
exports.testSetup = exports.createContext = void 0;
var _crypto = require("crypto");
var _path = require("path");
var _dxl = require("@interaktiv/dxl");
var _functional = require("@interaktiv/functional");
var _types = require("@interaktiv/types");
var _jestSandbox = _interopRequireDefault(require("jest-sandbox"));
var _tempy = require("tempy");
var _configFile = require("./config/config-file");
var _constants = require("./config/constants");
var _logger = require("./logger/logger");
/*
* Different hooks into {@link ConfigFile} used for testing instead of doing
* file IO.
*
* export interface ConfigMock {
* // readFn A function that controls all aspect of {@link ConfigFile.read}
* // For example, it won't set the contents
* // unless explicitly done. Only use this if you know what you are
* // doing. Use retrieveContents instead.
* readFn?: () => Promise<ConfigContents>;
*
* // A function that controls all aspects of {@link ConfigFile.write}.
* // For example, it won't read the contents unless
* // explicitly done. Only use this if you know what you are doing. Use
* // updateContents instead.
* writeFn?: (contents: AnyJson) => Promise<void>;
*
* // The contents that are used when @{link ConfigFile.read} unless
* // retrieveContents is set. This will also contain the
* // new config when @{link ConfigFile.write} is called. This will
* // persist through config instances,
* // such as {@link Alias.update} and {@link Alias.fetch}.
* contents?: ConfigContents;
*
* // A function to conditionally read based on the config instance. The
* // `this` value will be the config instance.
* retrieveContents?: () => Promise<JsonMap>;
*
* // A function to conditionally set based on the config instance. The
* // `this` value will be the config instance.
* updateContents?: () => Promise<JsonMap>;
* }
*/
function _uniqid() {
return (0, _crypto.randomBytes)(16).toString('hex');
}
/**
* @param {string} uid The id for the tes directory
* @return {Promise<string>} A promise that resolves with the "local" test path
*/
function getTestLocalPath(uid) {
return Promise.resolve((0, _path.join)(_tempy.root, uid, 'mibuilder_core', 'local'));
}
/**
* @param {string} uid The id for the tes directory
* @return {Promise<string>} A promise that resolves with the "global" test path
*/
function getTestGlobalPath(uid) {
return Promise.resolve((0, _path.join)(_tempy.root, uid, 'mibuilder_core', 'global'));
}
/**
* @param {boolean} isGlobal Is this global or local
* @param {string} [uid=_uniqid()] Optional the id for the test directory
* @return {Promise<string>} A promise that resolves with the root path
*/
function retrieveRootPath(isGlobal, uid = _uniqid()) {
return isGlobal ? getTestGlobalPath(uid) : getTestLocalPath(uid);
}
/**
* @param {string} uid The id for the tes directory
* @return {Promise<string>} A promise that resolves with the "global" test path
*/
function getTestProjectJsonPath(uid) {
return Promise.resolve((0, _path.join)(_tempy.root, uid, 'mibuilder_core', _constants.MIBUILDER_PROJECT_JSON_STATE_FOLDER));
}
function retrieveProjectJsonPath(uid = _uniqid()) {
return getTestProjectJsonPath(uid);
}
/**
* Instantiate a @interaktiv/mibuilder-core test context. This is automatically
* created by `const $$ = testSetup()` but is useful if you don't want to have
* a global mock of @interaktiv/mibuilder-core and you want to isolate it to
* a single describe.
*
* **Note:** Call `mockContext` in your beforeEach to have clean mocks of
* @interaktiv/mibuilder-core every test run.
*
* @example
* ```
* const $$ = createContext();
*
* beforeEach(() => {
* mockContext($$);
* });
*
* afterEach(() => {
* restoreContext($$);
* });
* ```
*
* @return {Object<TestContext>} The new test context
*/
const createContext = () => {
if (!jest) {
throw new Error('The package jest was not found. Add it to your package.json and pass it in to testSetup(jest)');
} // Create a global jest sandbox and a test logger instance for use within
// tests.
const _createSandbox = (0, _types.isFunction)(jest.createSandbox) ? jest.createSandbox : _jestSandbox.default;
const defaultSandbox = _createSandbox();
/*
* The test context should look like this:
*
* export interface TestContext {
* // The default sandbox is cleared out before each test run.
* sandbox: jest-sandbox;
*
* // An object of different sandboxes. Used when
* // needing to restore parts of the system for customized testing.
* sandboxes: collection<jest-sandbox>;
*
* // The test logger that is used when {@link Logger.child} is used
* // anywhere. It uses memory logging.
* testLogger: Logger;
*
* // id A unique id for the test run.
* id: string;
*
* // A function that returns unique strings.
* uniqid: () => string;
*
* // An object used in tests that interact with config files.
* configMocks: {
* [configName: string]: Optional<ConfigMock>;
* MiBuilderProjectJson?: ConfigMock;
* MiBuilderConfig?: ConfigMock;
* };
*
* // A function used when resolving the local path.
* // @param uid Unique id.
* localPathRetriever: (uid: string) => Promise<string>;
*
* // A function used when resolving the global path.
* // @param uid Unique id.
* globalPathRetriever: (uid: string) => Promise<string>;
*
* // A function used for resolving paths. Calls localPathRetriever and
* // globalPathRetriever.
* // @param isGlobal `true` if the config is global.
* // @param uid user id.
* rootPathRetriever: (isGlobal: boolean, uid?: string) => Promise<string>;
*
* // Gets a config mock contents by name.
* // @param name The name of the config.
* // @param group If the config supports groups.
* getConfigMockContents(name: string, group?: string): ConfigContents;
*
* // Sets a config mock contents by name
* // @param name The name of the config mock.
* // @param value The actual mock contents. The Mock data.
* setConfigMockContents(name: string, value: ConfigContents): void;
* }
*/
// The newly created test context
return {
sandbox: defaultSandbox,
sandboxes: {
default: defaultSandbox,
config: _createSandbox(),
connection: _createSandbox()
},
testLogger: new _logger.Logger({
name: 'MiBuilder_Core_Test_Logger'
}).useMemoryLogging(),
id: _uniqid(),
uniqid: _uniqid,
configMocks: {},
localPathRetriever: getTestLocalPath,
globalPathRetriever: getTestGlobalPath,
rootPathRetriever: retrieveRootPath,
projectJsonPathRetriever: retrieveProjectJsonPath,
getConfigMockContents(name, group) {
const mock = this.configMocks[name];
if (!mock || !mock.contents) return {};
if (group && mock.contents[group]) {
return (0, _types.ensureJsonMap)(mock.contents[group]);
}
return mock.contents;
},
setConfigMockContents(name, value) {
(0, _types.ensureString)(name);
if ((0, _types.isJsonMap)(value)) this.configMocks[name] = value;
}
};
};
/**
* Mock a @interaktiv/mibuilder-core test context. This will mock out logging
* to a file, config file reading and writing,
* local and global path resolution, and http request using connection (soon)*.
*
* This is automatically mocked in the global beforeEach created by
* `const $$ = testSetup()` but is useful if you don't want to have a global
* mock of @interaktiv/mibuilder-core and you want to isolate it to a single
* describe.
*
* **Note:** Always call `restoreContext` in your afterEach.
*
* @example
* ```
* const $$ = createContext();
*
* beforeEach(() => {
* mockContext($$);
* });
*
* afterEach(() => {
* restoreContext($$);
* });
* ```
* @param {Object<TestContext>} testContext The test context to mock
*/
exports.createContext = createContext;
function mockContext(testContext) {
// Most core files create a child logger so mock this to return our test
// logger.
testContext.sandbox.spyOn(_logger.Logger, 'child').mockReturnValue(Promise.resolve(testContext.testLogger));
testContext.sandboxes.config.spyOn(_configFile.ConfigFile, 'resolveRootDir').mockImplementation(isGlobal => {
return testContext.rootPathRetriever(isGlobal, testContext.id);
}); // Mock out all config file IO for all tests. They can restore individually
// if they need original functionality.
testContext.sandboxes.config.spyOn(_configFile.ConfigFile.prototype, 'read').mockImplementation(async function () {
const mock = testContext.configMocks[this.constructor.name] || {};
if (mock.readFn) return mock.readFn.call(this);
let contents = mock.contents || {};
if (mock.retrieveContents) {
contents = await mock.retrieveContents.call(this);
}
this.setContentsFromObject(contents);
return Promise.resolve(this.getContents());
});
testContext.sandboxes.config.spyOn(_configFile.ConfigFile.prototype, 'write').mockImplementation(async function (newContents) {
if (!testContext.configMocks[this.constructor.name]) {
testContext.configMocks[this.constructor.name] = {};
}
const mock = testContext.configMocks[this.constructor.name];
if (!mock) return;
if (mock.writeFn) {
mock.writeFn.call(this, newContents);
return;
}
let contents = newContents || this.getContents();
if (mock.updateContents) {
contents = await mock.updateContents.call(this);
}
this.setContents(contents);
mock.contents = this.toObject();
});
}
/**
* Restore a @interaktiv/mibuilder-core test context. This is automatically
* mocked in the global beforeEach created by `const $$ = testSetup()` but is
* useful if you don't want to have a global mock of @interaktiv/mibuilder-core
* and you want to isolate it to a single describe.
*
* @example
* ```
* const $$ = createContext();
*
* beforeEach(() => {
* mockContext($$);
* });
*
* afterEach(() => {
* restoreContext($$);
* });
* ```
*
* @param {Object<TestContext>} testContext The text context to restore
*/
function restoreContext(testContext) {
testContext.sandbox.restore();
(0, _functional.pipe)(_types.definiteValuesOf, (0, _functional.forEach)(theSandbox => theSandbox.restore()))(testContext.sandboxes);
testContext.configMocks = {};
}
function _testSetup(jest) {
const testContext = createContext(jest); // eslint-disable-next-line jest/require-top-level-describe
beforeEach(() => {
mockContext(testContext);
}); // eslint-disable-next-line jest/require-top-level-describe
afterEach(() => {
restoreContext(testContext);
});
return testContext;
}
/**
* Use to mock out different pieces of mibuilder-core to make testing easier.
* This will mock out logging to a file, config file reading and writing, local
* and global path resolution, and http request using connection (soon)*.
*
* **Note:** The setupTest should be outside of the describe. If you need to
* mock per test, use `createContext`, `mockContext`, and `restoreContext`.
* ```
* // In a mocha tests
* import { testSetup } from '@interaktiv/mibuilder-core/dist/test-setup';
*
* const $$ = testSetup();
*
* describe(() => {
* it('test', () => {
* // Mock out your own method
* $$.sandbox.spyOn(MyClass.prototype, 'myMethod')
* .mockImplementation(() => {});
*
* // Set the contents that is used when aliases are read. Same for all
* // config files.
* $$.configMocks.Aliases = {
* contents: { 'myTestAlias': 'user@company.com' }
* };
*
* // Will use the contents set above.
* const username = Aliases.fetch('myTestAlias');
* expect(username).toBe('user@company.com');
* });
* });
* ```
*/
const testSetup = (0, _dxl.once)(_testSetup);
exports.testSetup = testSetup;