diff --git a/x-pack/plugins/security/index.js b/x-pack/plugins/security/index.js index cd6b35a797170..0c60bd282b9e4 100644 --- a/x-pack/plugins/security/index.js +++ b/x-pack/plugins/security/index.js @@ -16,13 +16,13 @@ import { validateConfig } from './server/lib/validate_config'; import { authenticateFactory } from './server/lib/auth_redirect'; import { checkLicense } from './server/lib/check_license'; import { initAuthenticator } from './server/lib/authentication/authenticator'; -import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; -import { registerPrivilegesIfNecessary } from './server/lib/privileges'; import { initPrivilegesApi } from './server/routes/api/v1/privileges'; import { hasPrivilegesWithServer } from './server/lib/authorization/has_privileges'; import { SecurityAuditLogger } from './server/lib/audit_logger'; import { AuditLogger } from '../../server/lib/audit_logger'; import { SecureSavedObjectsClient } from './server/lib/saved_objects_client/secure_saved_objects_client'; +import { registerPrivilegesWithCluster } from './server/lib/privileges'; +import { watchStatusAndLicenseToInitialize } from './server/lib/watch_status_and_license_to_initialize'; export const security = (kibana) => new kibana.Plugin({ id: 'security', @@ -88,25 +88,22 @@ export const security = (kibana) => new kibana.Plugin({ }, async init(server) { - const pluginId = 'security'; + const plugin = this; const config = server.config(); const xpackMainPlugin = server.plugins.xpack_main; const xpackInfo = xpackMainPlugin.info; - const plugin = this; - - mirrorPluginStatus(xpackMainPlugin, plugin); - - const xpackInfoFeature = xpackInfo.feature(this.id); + const xpackInfoFeature = xpackInfo.feature(plugin.id); // Register a function that is called whenever the xpack info changes, // to re-compute the license check results for this plugin xpackInfoFeature.registerLicenseCheckResultsGenerator(checkLicense); - // Register a function to respond to xpack license changes - xpackInfoFeature.registerLicenseChangeCallback(() => { - registerPrivilegesIfNecessary(server, plugin, xpackInfo); + watchStatusAndLicenseToInitialize(xpackMainPlugin, plugin, async (license) => { + if (license.allowRbac) { + await registerPrivilegesWithCluster(server); + } }); validateConfig(config, message => server.log(['security', 'warning'], message)); @@ -131,7 +128,7 @@ export const security = (kibana) => new kibana.Plugin({ }) => { const adminCluster = server.plugins.elasticsearch.getCluster('admin'); - if (!xpackInfo.feature(pluginId).getLicenseCheckResults().allowRbac) { + if (!xpackInfoFeature.getLicenseCheckResults().allowRbac) { const { callWithRequest } = adminCluster; const callCluster = (...args) => callWithRequest(request, ...args); @@ -176,7 +173,7 @@ export const security = (kibana) => new kibana.Plugin({ initLogoutView(server); server.injectUiAppVars('login', () => { - const { showLogin, loginMessage, allowLogin } = xpackInfo.feature(pluginId).getLicenseCheckResults() || {}; + const { showLogin, loginMessage, allowLogin } = xpackInfo.feature(plugin.id).getLicenseCheckResults() || {}; return { loginState: { diff --git a/x-pack/plugins/security/server/lib/mirror_status_and_initialize.js b/x-pack/plugins/security/server/lib/mirror_status_and_initialize.js deleted file mode 100644 index af9ad91db70fb..0000000000000 --- a/x-pack/plugins/security/server/lib/mirror_status_and_initialize.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements. - * Licensed under the Elastic License; you may not use this file except in compliance with the Elastic License. */ -import { Observable } from 'rxjs'; - -export function mirrorStatusAndInitialize(upstreamStatus, downstreamStatus, onGreen) { - const currentState$ = Observable - .of({ - state: upstreamStatus.state, - message: upstreamStatus.message, - }); - - const newState$ = Observable - .fromEvent(upstreamStatus, 'change', null, (previousState, previousMsg, state, message) => { - return { - state, - message, - }; - }); - - const state$ = Observable.merge(currentState$, newState$); - - let onGreenPromise; - const onGreen$ = Observable.create(observer => { - if (!onGreenPromise) { - onGreenPromise = onGreen(); - } - - onGreenPromise - .then(() => { - observer.next({ - state: 'green', - message: 'Ready', - }); - }) - .catch((err) => { - onGreenPromise = null; - observer.next({ - state: 'red', - message: err.message - }); - }); - }); - - - state$ - .switchMap(({ state, message }) => { - if (state !== 'green') { - return Observable.of({ state, message }); - } - - return onGreen$; - }) - .do(({ state, message }) => { - downstreamStatus[state](message); - }) - .subscribe(); -} diff --git a/x-pack/plugins/security/server/lib/mirror_status_and_initialize.test.js b/x-pack/plugins/security/server/lib/mirror_status_and_initialize.test.js deleted file mode 100644 index 7c1cb52576130..0000000000000 --- a/x-pack/plugins/security/server/lib/mirror_status_and_initialize.test.js +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements. - * Licensed under the Elastic License; you may not use this file except in compliance with the Elastic License. */ -import { EventEmitter } from 'events'; -import { once } from 'lodash'; -import { mirrorStatusAndInitialize } from './mirror_status_and_initialize'; - -['red', 'yellow', 'disabled' ].forEach(state => { - test(`mirrors ${state} immediately`, () => { - const message = `${state} is the status`; - const upstreamStatus = new EventEmitter(); - upstreamStatus.state = state; - upstreamStatus.message = message; - - const downstreamStatus = { - [state]: jest.fn() - }; - - mirrorStatusAndInitialize(upstreamStatus, downstreamStatus); - expect(downstreamStatus[state]).toHaveBeenCalledTimes(1); - expect(downstreamStatus[state]).toHaveBeenCalledWith(message); - }); -}); - -test(`calls onGreen and doesn't immediately set downstream status when the initial status is green`, () => { - const upstreamStatus = new EventEmitter(); - upstreamStatus.state = 'green'; - upstreamStatus.message = ''; - - const downstreamStatus = { - green: jest.fn() - }; - - const onGreenMock = jest.fn().mockImplementation(() => new Promise(() => {})); - mirrorStatusAndInitialize(upstreamStatus, downstreamStatus, onGreenMock); - expect(onGreenMock).toHaveBeenCalledTimes(1); - expect(downstreamStatus.green).toHaveBeenCalledTimes(0); -}); - -test(`only calls onGreen once if it resolves immediately`, () => { - const upstreamStatus = new EventEmitter(); - upstreamStatus.state = 'green'; - upstreamStatus.message = ''; - - const downstreamStatus = { - green: () => {} - }; - - const onGreenMock = jest.fn().mockImplementation(() => Promise.resolve()); - - mirrorStatusAndInitialize(upstreamStatus, downstreamStatus, onGreenMock); - upstreamStatus.emit('change', '', '', 'green', ''); - expect(onGreenMock).toHaveBeenCalledTimes(1); -}); - -test(`calls onGreen twice if it rejects`, (done) => { - const upstreamStatus = new EventEmitter(); - upstreamStatus.state = 'green'; - upstreamStatus.message = ''; - - const downstreamStatus = { - red: once(() => { - // once we see this red, we immediately trigger the upstream status again - // to have it retrigger the onGreen function - upstreamStatus.emit('change', '', '', 'green', ''); - }), - }; - - let count = 0; - const onGreenMock = jest.fn().mockImplementation(() => { - if (++count === 2) { - done(); - } - - return Promise.reject(new Error()); - }); - - mirrorStatusAndInitialize(upstreamStatus, downstreamStatus, onGreenMock); -}); - -test(`sets downstream status to green when onGreen promise resolves`, (done) => { - const state = 'green'; - const message = `${state} is the status`; - const upstreamStatus = new EventEmitter(); - upstreamStatus.state = state; - upstreamStatus.message = message; - - const downstreamStatus = { - green: () => { - done(); - } - }; - - const onGreenMock = jest.fn().mockImplementation(() => Promise.resolve()); - mirrorStatusAndInitialize(upstreamStatus, downstreamStatus, onGreenMock); -}); - -test(`sets downstream status to red when onGreen promise rejects`, (done) => { - const upstreamStatus = new EventEmitter(); - upstreamStatus.state = 'green'; - upstreamStatus.message = ''; - - const errorMessage = 'something went real wrong'; - - const downstreamStatus = { - red: (msg) => { - expect(msg).toBe(errorMessage); - done(); - } - }; - - const onGreenMock = jest.fn().mockImplementation(() => Promise.reject(new Error(errorMessage))); - mirrorStatusAndInitialize(upstreamStatus, downstreamStatus, onGreenMock); -}); - -['red', 'yellow', 'disabled' ].forEach(state => { - test(`switches from uninitialized to ${state} on event`, () => { - const message = `${state} is the status`; - const upstreamStatus = new EventEmitter(); - upstreamStatus.state = 'uninitialized'; - upstreamStatus.message = 'uninitialized'; - - const downstreamStatus = { - uninitialized: jest.fn(), - [state]: jest.fn(), - }; - - mirrorStatusAndInitialize(upstreamStatus, downstreamStatus); - upstreamStatus.emit('change', '', '', state, message); - expect(downstreamStatus[state]).toHaveBeenCalledTimes(1); - expect(downstreamStatus[state]).toHaveBeenCalledWith(message); - }); -}); diff --git a/x-pack/plugins/security/server/lib/privileges/index.js b/x-pack/plugins/security/server/lib/privileges/index.js index 65c6f92c7f597..a270b8a60331e 100644 --- a/x-pack/plugins/security/server/lib/privileges/index.js +++ b/x-pack/plugins/security/server/lib/privileges/index.js @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { registerPrivilegesIfNecessary } from './privilege_action_registry'; +export { registerPrivilegesWithCluster } from './privilege_action_registry'; export { getLoginPrivilege, getVersionPrivilege } from './privileges'; diff --git a/x-pack/plugins/security/server/lib/privileges/privilege_action_registry.js b/x-pack/plugins/security/server/lib/privileges/privilege_action_registry.js index 167eea9f0ebe7..7d31b287fe664 100644 --- a/x-pack/plugins/security/server/lib/privileges/privilege_action_registry.js +++ b/x-pack/plugins/security/server/lib/privileges/privilege_action_registry.js @@ -11,19 +11,6 @@ import { buildPrivilegeMap } from './privileges'; import { getClient } from '../../../../../server/lib/get_client_shield'; import { equivalentPrivileges } from './equivalent_privileges'; -export async function registerPrivilegesIfNecessary(server, plugin, xpackInfo) { - const { allowRbac } = xpackInfo.feature('security').getLicenseCheckResults(); - - if (allowRbac) { - try { - await registerPrivilegesWithCluster(server); - } catch (e) { - plugin.status.red(`Unable to register privileges`); - server.log(['security', 'error'], `Unable to register privileges with cluster: ${e}`); - } - } -} - export async function registerPrivilegesWithCluster(server) { const config = server.config(); const kibanaVersion = config.get('pkg.version'); diff --git a/x-pack/plugins/security/server/lib/privileges/privilege_action_registry.test.js b/x-pack/plugins/security/server/lib/privileges/privilege_action_registry.test.js index 6f476bc3180bf..398a68a097ed9 100644 --- a/x-pack/plugins/security/server/lib/privileges/privilege_action_registry.test.js +++ b/x-pack/plugins/security/server/lib/privileges/privilege_action_registry.test.js @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { registerPrivilegesWithCluster, registerPrivilegesIfNecessary } from './privilege_action_registry'; +import { registerPrivilegesWithCluster } from './privilege_action_registry'; import { getClient } from '../../../../../server/lib/get_client_shield'; import { buildPrivilegeMap } from './privileges'; -import { XPackInfo } from '../../../../xpack_main/server/lib/xpack_info'; -import { checkLicense } from '../check_license'; jest.mock('../../../../../server/lib/get_client_shield', () => ({ getClient: jest.fn(), })); @@ -16,90 +14,6 @@ jest.mock('./privileges', () => ({ buildPrivilegeMap: jest.fn(), })); -const registerPrivilegesIfNecessaryTest = (description, { settings = {}, simulateFailure = false, allowRbac, assert }) => { - const registerMockCallWithInternalUser = () => { - const callWithInternalUser = jest.fn(() => { - if (simulateFailure) { - throw new Error('Something happened'); - } - }); - - getClient.mockReturnValue({ - callWithInternalUser, - }); - return callWithInternalUser; - }; - - const defaultVersion = 'default-version'; - const defaultApplication = 'default-application'; - - const createMockServer = () => { - const mockServer = { - config: jest.fn().mockReturnValue({ - get: jest.fn(), - }), - log: jest.fn(), - plugins: { - // Only needed/used for XPackInfo constructor - elasticsearch: { - getCluster: jest.fn().mockReturnValue({ - callWithInternalUser: jest.fn().mockReturnValue({ - features: { - security: { - enabled: true - } - }, - license: { - mode: allowRbac ? 'platinum' : 'basic', - status: 'active' - } - }) - }) - } - } - }; - - const defaultSettings = { - 'pkg.version': defaultVersion, - 'xpack.security.rbac.application': defaultApplication, - }; - - mockServer.config().get.mockImplementation(key => { - return key in settings ? settings[key] : defaultSettings[key]; - }); - - return mockServer; - }; - - const createMockPlugin = () => { - return { - status: { - red: jest.fn() - } - }; - }; - - test(description, async () => { - const mockServer = createMockServer(); - const mockPlugin = createMockPlugin(); - const mockCallWithInternalUser = registerMockCallWithInternalUser(); - - const xpackInfo = new XPackInfo(mockServer, {}); - xpackInfo.feature('security').registerLicenseCheckResultsGenerator(checkLicense); - - await xpackInfo.refreshNow(); - - await registerPrivilegesIfNecessary(mockServer, mockPlugin, xpackInfo); - - assert({ - mocks: { - plugin: mockPlugin, - callWithInternalUser: mockCallWithInternalUser - } - }); - }); -}; - const registerPrivilegesWithClusterTest = (description, { settings = {}, expectedPrivileges, existingPrivileges, assert }) => { const registerMockCallWithInternalUser = () => { const callWithInternalUser = jest.fn(); @@ -194,130 +108,102 @@ const registerPrivilegesWithClusterTest = (description, { settings = {}, expecte }); }; -describe('registerPrivilegesIfNecessary', () => { - registerPrivilegesIfNecessaryTest('does not register privileges if rbac is disabled', { - allowRbac: false, - assert: ({ mocks }) => { - expect(mocks.callWithInternalUser).not.toHaveBeenCalled(); - expect(mocks.plugin.status.red).not.toHaveBeenCalled(); - } - }); - - registerPrivilegesIfNecessaryTest('registers privileges if rbac is enabled', { - allowRbac: true, - assert: ({ mocks }) => { - expect(mocks.callWithInternalUser).toHaveBeenCalledWith('shield.getPrivilege', expect.anything()); - expect(mocks.plugin.status.red).not.toHaveBeenCalled(); - } - }); - - registerPrivilegesIfNecessaryTest('sets plugin status to red on error', { - allowRbac: true, - simulateFailure: true, - assert: ({ mocks }) => { - expect(mocks.plugin.status.red).toHaveBeenCalledWith('Unable to register privileges'); - } - }); +registerPrivilegesWithClusterTest(`passes application and kibanaVersion to buildPrivilegeMap`, { + settings: { + 'pkg.version': 'foo-version', + 'xpack.security.rbac.application': 'foo-application', + }, + assert: ({ mocks }) => { + expect(mocks.buildPrivilegeMap).toHaveBeenCalledWith('foo-application', 'foo-version'); + }, }); -describe('registerPrivilegesWithCluster', () => { - registerPrivilegesWithClusterTest(`passes application and kibanaVersion to buildPrivilegeMap`, { - settings: { - 'pkg.version': 'foo-version', - 'xpack.security.rbac.application': 'foo-application', - }, - assert: ({ mocks }) => { - expect(mocks.buildPrivilegeMap).toHaveBeenCalledWith('foo-application', 'foo-version'); - }, - }); +registerPrivilegesWithClusterTest(`updates privileges when simple top-level privileges don't match`, { + expectedPrivileges: { + expected: true + }, + existingPrivileges: { + expected: false + }, + assert: ({ expectUpdatedPrivileges }) => { + expectUpdatedPrivileges(); + } +}); - registerPrivilegesWithClusterTest(`updates privileges when simple top-level privileges don't match`, { - expectedPrivileges: { +registerPrivilegesWithClusterTest(`updates privileges when nested privileges don't match`, { + expectedPrivileges: { + kibana: { expected: true - }, - existingPrivileges: { + } + }, + existingPrivileges: { + kibana: { expected: false - }, - assert: ({ expectUpdatedPrivileges }) => { - expectUpdatedPrivileges(); } - }); + }, + assert: ({ expectUpdatedPrivileges }) => { + expectUpdatedPrivileges(); + } +}); - registerPrivilegesWithClusterTest(`updates privileges when nested privileges don't match`, { - expectedPrivileges: { - kibana: { - expected: true - } - }, - existingPrivileges: { - kibana: { - expected: false - } - }, - assert: ({ expectUpdatedPrivileges }) => { - expectUpdatedPrivileges(); +registerPrivilegesWithClusterTest(`updates privileges when nested privileges arrays don't match`, { + expectedPrivileges: { + kibana: { + expected: ['one', 'two'] } - }); - - registerPrivilegesWithClusterTest(`updates privileges when nested privileges arrays don't match`, { - expectedPrivileges: { - kibana: { - expected: ['one', 'two'] - } - }, - existingPrivileges: { - kibana: { - expected: ['one'] - } - }, - assert: ({ expectUpdatedPrivileges }) => { - expectUpdatedPrivileges(); + }, + existingPrivileges: { + kibana: { + expected: ['one'] } - }); + }, + assert: ({ expectUpdatedPrivileges }) => { + expectUpdatedPrivileges(); + } +}); - registerPrivilegesWithClusterTest(`updates privileges when nested property array values are reordered`, { - expectedPrivileges: { - kibana: { - foo: ['one', 'two'] - } - }, - existingPrivileges: { - kibana: { - foo: ['two', 'one'] - } - }, - assert: ({ expectUpdatedPrivileges }) => { - expectUpdatedPrivileges(); +registerPrivilegesWithClusterTest(`updates privileges when nested property array values are reordered`, { + expectedPrivileges: { + kibana: { + foo: ['one', 'two'] } - }); - - registerPrivilegesWithClusterTest(`doesn't update privileges when simple top-level privileges match`, { - expectedPrivileges: { - expected: true - }, - existingPrivileges: { - expected: true - }, - assert: ({ expectDidntUpdatePrivileges }) => { - expectDidntUpdatePrivileges(); + }, + existingPrivileges: { + kibana: { + foo: ['two', 'one'] } - }); + }, + assert: ({ expectUpdatedPrivileges }) => { + expectUpdatedPrivileges(); + } +}); - registerPrivilegesWithClusterTest(`doesn't update privileges when nested properties are reordered`, { - expectedPrivileges: { - kibana: { - foo: true, - bar: false - } - }, - existingPrivileges: { - kibana: { - bar: false, - foo: true - } - }, - assert: ({ expectDidntUpdatePrivileges }) => { - expectDidntUpdatePrivileges(); +registerPrivilegesWithClusterTest(`doesn't update privileges when simple top-level privileges match`, { + expectedPrivileges: { + expected: true + }, + existingPrivileges: { + expected: true + }, + assert: ({ expectDidntUpdatePrivileges }) => { + expectDidntUpdatePrivileges(); + } +}); + +registerPrivilegesWithClusterTest(`doesn't update privileges when nested properties are reordered`, { + expectedPrivileges: { + kibana: { + foo: true, + bar: false } - }); + }, + existingPrivileges: { + kibana: { + bar: false, + foo: true + } + }, + assert: ({ expectDidntUpdatePrivileges }) => { + expectDidntUpdatePrivileges(); + } }); diff --git a/x-pack/plugins/security/server/lib/watch_status_and_license_to_initialize.js b/x-pack/plugins/security/server/lib/watch_status_and_license_to_initialize.js new file mode 100644 index 0000000000000..6e68a4e404705 --- /dev/null +++ b/x-pack/plugins/security/server/lib/watch_status_and_license_to_initialize.js @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Observable } from 'rxjs'; + +export function watchStatusAndLicenseToInitialize(xpackMainPlugin, downstreamPlugin, initialize) { + const xpackInfo = xpackMainPlugin.info; + const xpackInfoFeature = xpackInfo.feature(downstreamPlugin.id); + + const upstreamStatus = xpackMainPlugin.status; + const currentStatus$ = Observable + .of({ + state: upstreamStatus.state, + message: upstreamStatus.message, + }); + const newStatus$ = Observable + .fromEvent(upstreamStatus, 'change', null, (previousState, previousMsg, state, message) => { + return { + state, + message, + }; + }); + const status$ = Observable.merge(currentStatus$, newStatus$); + + const currentLicense$ = Observable + .of(xpackInfoFeature.getLicenseCheckResults()); + const newLicense$ = Observable + .fromEventPattern(xpackInfoFeature.registerLicenseChangeCallback) + .map(() => xpackInfoFeature.getLicenseCheckResults()); + const license$ = Observable.merge(currentLicense$, newLicense$); + + Observable.combineLatest(status$, license$) + .map(([status, license]) => ({ status, license })) + .switchMap(({ status, license }) => { + if (status.state !== 'green') { + return Observable.of({ state: status.state, message: status.message }); + } + + return initialize(license) + .then(() => ({ + state: 'green', + message: 'Ready', + })) + .catch((err) => ({ + state: 'red', + message: err.message + })); + }) + .do(({ state, message }) => { + downstreamPlugin.status[state](message); + }) + .subscribe(); +} diff --git a/x-pack/plugins/security/server/lib/watch_status_and_license_to_initialize.test.js b/x-pack/plugins/security/server/lib/watch_status_and_license_to_initialize.test.js new file mode 100644 index 0000000000000..3826edf676761 --- /dev/null +++ b/x-pack/plugins/security/server/lib/watch_status_and_license_to_initialize.test.js @@ -0,0 +1,223 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements. + * Licensed under the Elastic License; you may not use this file except in compliance with the Elastic License. */ +import { EventEmitter } from 'events'; +import { watchStatusAndLicenseToInitialize } from './watch_status_and_license_to_initialize'; + +const createMockXpackMainPluginAndFeature = (featureId) => { + const licenseChangeCallbacks = []; + + const mockFeature = { + getLicenseCheckResults: jest.fn(), + registerLicenseChangeCallback: (callback) => { + licenseChangeCallbacks.push(callback); + }, + mock: { + triggerLicenseChange: () => { + for (const callback of licenseChangeCallbacks) { + callback(); + } + }, + setLicenseCheckResults: (value) => { + mockFeature.getLicenseCheckResults.mockReturnValue(value); + } + } + }; + + const mockXpackMainPlugin = { + info: { + feature: (id) => { + if (id === featureId) { + return mockFeature; + } + throw new Error('Unexpected feature'); + } + }, + status: new EventEmitter(), + mock: { + setStatus: (state, message) => { + mockXpackMainPlugin.status.state = state; + mockXpackMainPlugin.status.message = message; + mockXpackMainPlugin.status.emit('change', null, null, state, message); + } + } + }; + + return { mockXpackMainPlugin, mockFeature }; +}; + +const createMockDownstreamPlugin = (id) => { + const defaultImplementation = () => { throw new Error('Not implemented'); }; + return { + id, + status: { + disabled: jest.fn().mockImplementation(defaultImplementation), + yellow: jest.fn().mockImplementation(defaultImplementation), + green: jest.fn().mockImplementation(defaultImplementation), + red: jest.fn().mockImplementation(defaultImplementation), + }, + }; +}; + +['red', 'yellow', 'disabled' ].forEach(state => { + test(`mirrors ${state} immediately`, () => { + const pluginId = 'foo-plugin'; + const message = `${state} is now the state`; + const { mockXpackMainPlugin } = createMockXpackMainPluginAndFeature(pluginId); + mockXpackMainPlugin.mock.setStatus(state, message); + const downstreamPlugin = createMockDownstreamPlugin(pluginId); + const initializeMock = jest.fn(); + downstreamPlugin.status[state].mockImplementation(() => {}); + + watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); + + expect(initializeMock).not.toHaveBeenCalled(); + expect(downstreamPlugin.status[state]).toHaveBeenCalledTimes(1); + expect(downstreamPlugin.status[state]).toHaveBeenCalledWith(message); + }); +}); + +test(`calls initialize and doesn't immediately set downstream status when the initial status is green`, () => { + const pluginId = 'foo-plugin'; + const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); + mockXpackMainPlugin.mock.setStatus('green', 'green is now the state'); + const licenseCheckResults = Symbol(); + mockFeature.mock.setLicenseCheckResults(licenseCheckResults); + const downstreamPlugin = createMockDownstreamPlugin(pluginId); + const initializeMock = jest.fn().mockImplementation(() => new Promise(() => {})); + + watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); + + expect(initializeMock).toHaveBeenCalledTimes(1); + expect(initializeMock).toHaveBeenCalledWith(licenseCheckResults); + expect(downstreamPlugin.status.green).toHaveBeenCalledTimes(0); +}); + +test(`sets downstream plugin's status to green when initialize resolves`, (done) => { + const pluginId = 'foo-plugin'; + const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); + mockXpackMainPlugin.mock.setStatus('green', 'green is now the state'); + const licenseCheckResults = Symbol(); + mockFeature.mock.setLicenseCheckResults(licenseCheckResults); + const downstreamPlugin = createMockDownstreamPlugin(pluginId); + const initializeMock = jest.fn().mockImplementation(() => Promise.resolve()); + + watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); + + expect(initializeMock).toHaveBeenCalledTimes(1); + expect(initializeMock).toHaveBeenCalledWith(licenseCheckResults); + downstreamPlugin.status.green.mockImplementation(actualMessage => + { + expect(actualMessage).toBe('Ready'); + done(); + }); +}); + +test(`sets downstream plugin's status to red when initialize rejects`, (done) => { + const pluginId = 'foo-plugin'; + const errorMessage = 'the error message'; + const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); + mockXpackMainPlugin.mock.setStatus('green'); + const licenseCheckResults = Symbol(); + mockFeature.mock.setLicenseCheckResults(licenseCheckResults); + const downstreamPlugin = createMockDownstreamPlugin(pluginId); + const initializeMock = jest.fn().mockImplementation(() => Promise.reject(new Error(errorMessage))); + + watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); + + expect(initializeMock).toHaveBeenCalledTimes(1); + expect(initializeMock).toHaveBeenCalledWith(licenseCheckResults); + downstreamPlugin.status.red.mockImplementation(message => { + expect(message).toBe(errorMessage); + done(); + }); +}); + +test(`calls initialize twice when it gets a new license and the status is green`, (done) => { + const pluginId = 'foo-plugin'; + const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); + mockXpackMainPlugin.mock.setStatus('green'); + const firstLicenseCheckResults = Symbol(); + const secondLicenseCheckResults = Symbol(); + mockFeature.mock.setLicenseCheckResults(firstLicenseCheckResults); + const downstreamPlugin = createMockDownstreamPlugin(pluginId); + const initializeMock = jest.fn().mockImplementation(() => Promise.resolve()); + + let count = 0; + downstreamPlugin.status.green.mockImplementation(message => { + expect(message).toBe('Ready'); + ++count; + if (count === 1) { + mockFeature.mock.setLicenseCheckResults(secondLicenseCheckResults); + mockFeature.mock.triggerLicenseChange(); + } + if (count === 2) { + expect(initializeMock).toHaveBeenCalledWith(firstLicenseCheckResults); + expect(initializeMock).toHaveBeenCalledWith(secondLicenseCheckResults); + expect(initializeMock).toHaveBeenCalledTimes(2); + done(); + } + }); + + watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); +}); + +test(`doesn't call initialize twice when it gets a new license when the status isn't green`, (done) => { + const pluginId = 'foo-plugin'; + const redMessage = 'the red message'; + const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); + mockXpackMainPlugin.mock.setStatus('green'); + const firstLicenseCheckResults = Symbol(); + const secondLicenseCheckResults = Symbol(); + mockFeature.mock.setLicenseCheckResults(firstLicenseCheckResults); + const downstreamPlugin = createMockDownstreamPlugin(pluginId); + const initializeMock = jest.fn().mockImplementation(() => Promise.resolve()); + + downstreamPlugin.status.green.mockImplementation(message => { + expect(message).toBe('Ready'); + mockXpackMainPlugin.mock.setStatus('red', redMessage); + mockFeature.mock.setLicenseCheckResults(secondLicenseCheckResults); + mockFeature.mock.triggerLicenseChange(); + }); + + downstreamPlugin.status.red.mockImplementation(message => { + expect(message).toBe(redMessage); + expect(initializeMock).toHaveBeenCalledTimes(1); + expect(initializeMock).toHaveBeenCalledWith(firstLicenseCheckResults); + done(); + }); + + watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); +}); + +test(`calls initialize twice when the status changes to green twice`, (done) => { + const pluginId = 'foo-plugin'; + const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); + mockXpackMainPlugin.mock.setStatus('green'); + const licenseCheckResults = Symbol(); + mockFeature.mock.setLicenseCheckResults(licenseCheckResults); + const downstreamPlugin = createMockDownstreamPlugin(pluginId); + const initializeMock = jest.fn().mockImplementation(() => Promise.resolve()); + + let count = 0; + downstreamPlugin.status.green.mockImplementation(message => { + expect(message).toBe('Ready'); + ++count; + if (count === 1) { + mockXpackMainPlugin.mock.setStatus('green'); + } + if (count === 2) { + expect(initializeMock).toHaveBeenCalledWith(licenseCheckResults); + expect(initializeMock).toHaveBeenCalledTimes(2); + done(); + } + }); + + watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); +}); +