diff --git a/src/dispatcher/__tests__/TestPlugin.js b/src/dispatcher/__tests__/TestPlugin.js new file mode 100644 index 000000000..5ae68bcc1 --- /dev/null +++ b/src/dispatcher/__tests__/TestPlugin.js @@ -0,0 +1,10 @@ +/** + * Copyright 2018-present Facebook. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @format + */ + +import {FlipperPlugin} from '../../plugin.js'; + +export default class extends FlipperPlugin {} diff --git a/src/dispatcher/__tests__/plugins.electron.js b/src/dispatcher/__tests__/plugins.electron.js new file mode 100644 index 000000000..78a1c4879 --- /dev/null +++ b/src/dispatcher/__tests__/plugins.electron.js @@ -0,0 +1,121 @@ +/** + * Copyright 2018-present Facebook. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @format + */ + +import dispatcher, { + getDynamicPlugins, + checkDisabled, + checkGK, + requirePlugin, +} from '../plugins'; +import path from 'path'; +import {remote} from 'electron'; +import {FlipperPlugin} from '../../plugin'; +import reducers from '../../reducers/index.js'; +import Logger from '../../fb-stubs/Logger.js'; +import configureStore from 'redux-mock-store'; +import {TEST_PASSING_GK, TEST_FAILING_GK} from '../../fb-stubs/GK'; + +const mockStore = configureStore([])(reducers(undefined, {type: 'INIT'})); +const logger = new Logger(); + +test('dispatcher dispatches REGISTER_PLUGINS', () => { + dispatcher(mockStore, logger); + const actions = mockStore.getActions(); + expect(actions[0].type).toBe('REGISTER_PLUGINS'); +}); + +test('getDynamicPlugins returns empty array', () => { + // $FlowFixMe process.env not defined in electron API spec + remote.process.env.PLUGINS = null; + const res = getDynamicPlugins(); + expect(res).toEqual([]); +}); + +test('getDynamicPlugins returns empty array for invalid JSON', () => { + // $FlowFixMe process.env not defined in electron API spec + remote.process.env.PLUGINS = 'invalid JOSN }}[]'; + const res = getDynamicPlugins(); + expect(res).toEqual([]); +}); + +test('getDynamicPlugins from env', () => { + const plugins = [{name: 'test'}]; + // $FlowFixMe process.env not defined in electron API spec + remote.process.env.PLUGINS = JSON.stringify(plugins); + const res = getDynamicPlugins(); + expect(res).toEqual(plugins); +}); + +test('checkDisabled', () => { + const disabledPlugin = 'pluginName'; + const config = {disabledPlugins: [disabledPlugin]}; + // $FlowFixMe process.env not defined in electron API spec + remote.process.env.CONFIG = JSON.stringify(config); + const disabled = checkDisabled(); + + expect( + disabled({ + name: 'other Name', + out: './test/index.js', + }), + ).toBeTruthy(); + + expect( + disabled({ + name: disabledPlugin, + out: './test/index.js', + }), + ).toBeFalsy(); +}); + +test('checkGK for plugin without GK', () => { + expect( + checkGK({ + name: 'pluginID', + out: './test/index.js', + }), + ).toBeTruthy(); +}); + +test('checkGK for passing plugin', () => { + expect( + checkGK({ + name: 'pluginID', + gatekeeper: TEST_PASSING_GK, + out: './test/index.js', + }), + ).toBeTruthy(); +}); + +test('checkGK for failing plugin', () => { + expect( + checkGK({ + name: 'pluginID', + gatekeeper: TEST_FAILING_GK, + out: './test/index.js', + }), + ).toBeFalsy(); +}); + +test('requirePlugin returns null for invalid requires', () => { + const plugin = requirePlugin(require)({ + name: 'pluginID', + out: 'this/path/does not/exist', + }); + + expect(plugin).toBeNull(); +}); + +test('requirePlugin loads plugin', () => { + const plugin = requirePlugin(require)({ + name: 'pluginID', + out: path.join(__dirname, 'TestPlugin.js'), + // $FlowFixMe Electron require returns default exports wrapped in an object + }).default; + + expect(plugin.prototype).toBeInstanceOf(FlipperPlugin); +}); diff --git a/src/dispatcher/plugins.js b/src/dispatcher/plugins.js index 9b305607c..6ef933df0 100644 --- a/src/dispatcher/plugins.js +++ b/src/dispatcher/plugins.js @@ -37,7 +37,7 @@ export default (store: Store, logger: Logger) => { > = [...getBundledPlugins(), ...getDynamicPlugins()] .filter(disabled) .filter(checkGK) - .map(requirePlugin) + .map(requirePlugin()) .filter(Boolean); store.dispatch(registerPlugins(initialPlugins)); @@ -69,7 +69,7 @@ function getBundledPlugins(): Array { })); } -function getDynamicPlugins() { +export function getDynamicPlugins() { let dynamicPlugins: Array = []; try { // $FlowFixMe process.env not defined in electron API spec @@ -80,7 +80,7 @@ function getDynamicPlugins() { return dynamicPlugins; } -function checkGK(plugin: PluginDefinition): boolean { +export function checkGK(plugin: PluginDefinition): boolean { const result = plugin.gatekeeper && !GK.get(plugin.gatekeeper); if (!result) { console.warn( @@ -92,7 +92,7 @@ function checkGK(plugin: PluginDefinition): boolean { return !result; } -function checkDisabled(): (plugin: PluginDefinition) => boolean { +export function checkDisabled(): (plugin: PluginDefinition) => boolean { let disabledPlugins: Set = new Set(); try { disabledPlugins = new Set( @@ -106,17 +106,21 @@ function checkDisabled(): (plugin: PluginDefinition) => boolean { return (plugin: PluginDefinition) => !disabledPlugins.has(plugin.name); } -function requirePlugin( - pluginDefinition: PluginDefinition, -): ?Class | FlipperDevicePlugin<>> { - try { - const plugin = window.electronRequire(pluginDefinition.out); - if (!plugin.prototype instanceof FlipperBasePlugin) { - throw new Error(`Plugin ${plugin.name} is not a FlipperBasePlugin`); +export function requirePlugin( + requireFunction: Function = window.electronRequire, +) { + return ( + pluginDefinition: PluginDefinition, + ): ?Class | FlipperDevicePlugin<>> => { + try { + const plugin = requireFunction(pluginDefinition.out); + if (!plugin.prototype instanceof FlipperBasePlugin) { + throw new Error(`Plugin ${plugin.name} is not a FlipperBasePlugin`); + } + return plugin; + } catch (e) { + console.error(pluginDefinition, e); + return null; } - return plugin; - } catch (e) { - console.error(pluginDefinition, e); - return null; - } + }; } diff --git a/src/reducers/__tests__/plugins.node.js b/src/reducers/__tests__/plugins.node.js new file mode 100644 index 000000000..221898603 --- /dev/null +++ b/src/reducers/__tests__/plugins.node.js @@ -0,0 +1,71 @@ +/** + * Copyright 2018-present Facebook. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @format + */ + +import {default as reducer, registerPlugins} from '../plugins'; +import { + FlipperBasePlugin, + FlipperPlugin, + FlipperDevicePlugin, +} from '../../plugin.js'; + +const testBasePlugin = class extends FlipperBasePlugin { + static id = 'TestPlugin'; +}; + +const testPlugin = class extends FlipperPlugin { + static id = 'TestPlugin'; +}; + +const testDevicePlugin = class extends FlipperDevicePlugin { + static id = 'TestDevicePlugin'; +}; + +test('add clientPlugin', () => { + const res = reducer( + { + devicePlugins: new Map(), + clientPlugins: new Map(), + }, + registerPlugins([testPlugin]), + ); + expect(res.clientPlugins.get(testPlugin.id)).toBe(testPlugin); +}); + +test('add devicePlugin', () => { + const res = reducer( + { + devicePlugins: new Map(), + clientPlugins: new Map(), + }, + registerPlugins([testDevicePlugin]), + ); + expect(res.devicePlugins.get(testDevicePlugin.id)).toBe(testDevicePlugin); +}); + +test('do not add plugin twice', () => { + const res = reducer( + { + devicePlugins: new Map(), + clientPlugins: new Map(), + }, + registerPlugins([testPlugin, testPlugin]), + ); + expect(res.clientPlugins.size).toEqual(1); +}); + +test('do not add other classes', () => { + const res = reducer( + { + devicePlugins: new Map(), + clientPlugins: new Map(), + }, + // $FlowFixMe testing wrong classes on purpose here + registerPlugins([testBasePlugin]), + ); + expect(res.devicePlugins.size).toEqual(0); + expect(res.devicePlugins.size).toEqual(0); +});