UNPKG

@interaktiv/mibuilder-core

Version:

Core libraries to interact with MiBuilder projects.

387 lines (335 loc) 12.3 kB
"use strict"; 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;