From c433c6e497ed2e275c4034fdd4a459cb0b5ed2fb Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Tue, 24 Dec 2019 08:21:38 +0000 Subject: [PATCH 1/6] [Lens] Disable saving visualization until there are no changes to the document (#52982) Adding unit test for new functionality Fixing type error Removing unnecessary act statements Removing unnecessary assertion Co-authored-by: Elastic Machine --- .../lens/public/app_plugin/app.test.tsx | 119 +++++++++++++++--- .../plugins/lens/public/app_plugin/app.tsx | 5 +- 2 files changed, 106 insertions(+), 18 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx index b8176cdb14c125..1cdae05833b981 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx @@ -190,6 +190,7 @@ describe('Lens App', () => { (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ id: '1234', title: 'Daaaaaaadaumching!', + expression: 'valid expression', state: { query: 'fake query', datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, @@ -219,6 +220,7 @@ describe('Lens App', () => { args.editorFrame = frame; (args.docStorage.load as jest.Mock).mockResolvedValue({ id: '1234', + expression: 'valid expression', state: { query: 'fake query', datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, @@ -244,6 +246,7 @@ describe('Lens App', () => { expect.objectContaining({ doc: { id: '1234', + expression: 'valid expression', state: { query: 'fake query', datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, @@ -294,6 +297,30 @@ describe('Lens App', () => { newTitle: string; } + let defaultArgs: jest.Mocked<{ + editorFrame: EditorFrameInstance; + data: typeof dataStartMock; + core: typeof core; + dataShim: DataStart; + storage: Storage; + docId?: string; + docStorage: SavedObjectStore; + redirectTo: (id?: string) => void; + }>; + + beforeEach(() => { + defaultArgs = makeDefaultArgs(); + (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ + id: '1234', + title: 'My cool doc', + expression: 'valid expression', + state: { + query: 'kuery', + datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, + }, + } as jest.ResolvedValue); + }); + function getButton(instance: ReactWrapper): TopNavMenuData { return (instance .find('[data-test-subj="lnsApp_topNav"]') @@ -322,12 +349,13 @@ describe('Lens App', () => { initialDocId?: string; }) { const args = { - ...makeDefaultArgs(), + ...defaultArgs, docId: initialDocId, }; args.editorFrame = frame; (args.docStorage.load as jest.Mock).mockResolvedValue({ id: '1234', + expression: 'kibana', state: { query: 'fake query', datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, @@ -335,6 +363,7 @@ describe('Lens App', () => { }); (args.docStorage.save as jest.Mock).mockImplementation(async ({ id }) => ({ id: id || 'aaa', + expression: 'kibana 2', })); const instance = mount(); @@ -350,13 +379,12 @@ describe('Lens App', () => { const onChange = frame.mount.mock.calls[0][1].onChange; onChange({ filterableIndexPatterns: [], - doc: ({ id: initialDocId } as unknown) as Document, + doc: ({ id: initialDocId, expression: 'kibana 3' } as unknown) as Document, }); instance.update(); expect(getButton(instance).disableButton).toEqual(false); - testSave(instance, saveProps); await waitForPromises(); @@ -365,7 +393,7 @@ describe('Lens App', () => { } it('shows a disabled save button when the user does not have permissions', async () => { - const args = makeDefaultArgs(); + const args = defaultArgs; args.core.application = { ...args.core.application, capabilities: { @@ -380,15 +408,40 @@ describe('Lens App', () => { expect(getButton(instance).disableButton).toEqual(true); const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ filterableIndexPatterns: [], doc: ('will save this' as unknown) as Document }); - + onChange({ + filterableIndexPatterns: [], + doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document, + }); instance.update(); + expect(getButton(instance).disableButton).toEqual(true); + }); + it('shows a disabled save button when there are no changes to the document', async () => { + const args = defaultArgs; + (args.docStorage.load as jest.Mock).mockResolvedValue({ + id: '1234', + title: 'My cool doc', + expression: '', + } as jest.ResolvedValue); + args.editorFrame = frame; + + const instance = mount(); expect(getButton(instance).disableButton).toEqual(true); + + const onChange = frame.mount.mock.calls[0][1].onChange; + + act(() => { + onChange({ + filterableIndexPatterns: [], + doc: ({ id: '1234', expression: 'valid expression' } as unknown) as Document, + }); + }); + instance.update(); + expect(getButton(instance).disableButton).toEqual(false); }); it('shows a save button that is enabled when the frame has provided its state', async () => { - const args = makeDefaultArgs(); + const args = defaultArgs; args.editorFrame = frame; const instance = mount(); @@ -396,8 +449,10 @@ describe('Lens App', () => { expect(getButton(instance).disableButton).toEqual(true); const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ filterableIndexPatterns: [], doc: ('will save this' as unknown) as Document }); - + onChange({ + filterableIndexPatterns: [], + doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document, + }); instance.update(); expect(getButton(instance).disableButton).toEqual(false); @@ -413,6 +468,7 @@ describe('Lens App', () => { expect(args.docStorage.save).toHaveBeenCalledWith({ id: undefined, title: 'hello there', + expression: 'kibana 3', }); expect(args.redirectTo).toHaveBeenCalledWith('aaa'); @@ -432,6 +488,7 @@ describe('Lens App', () => { expect(args.docStorage.save).toHaveBeenCalledWith({ id: undefined, title: 'hello there', + expression: 'kibana 3', }); expect(args.redirectTo).toHaveBeenCalledWith('aaa'); @@ -451,6 +508,7 @@ describe('Lens App', () => { expect(args.docStorage.save).toHaveBeenCalledWith({ id: '1234', title: 'hello there', + expression: 'kibana 3', }); expect(args.redirectTo).not.toHaveBeenCalled(); @@ -461,14 +519,17 @@ describe('Lens App', () => { }); it('handles save failure by showing a warning, but still allows another save', async () => { - const args = makeDefaultArgs(); + const args = defaultArgs; args.editorFrame = frame; (args.docStorage.save as jest.Mock).mockRejectedValue({ message: 'failed' }); const instance = mount(); const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ filterableIndexPatterns: [], doc: ({ id: undefined } as unknown) as Document }); + onChange({ + filterableIndexPatterns: [], + doc: ({ id: undefined, expression: 'new expression' } as unknown) as Document, + }); instance.update(); @@ -486,8 +547,32 @@ describe('Lens App', () => { }); describe('query bar state management', () => { + let defaultArgs: jest.Mocked<{ + editorFrame: EditorFrameInstance; + data: typeof dataStartMock; + core: typeof core; + dataShim: DataStart; + storage: Storage; + docId?: string; + docStorage: SavedObjectStore; + redirectTo: (id?: string) => void; + }>; + + beforeEach(() => { + defaultArgs = makeDefaultArgs(); + (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ + id: '1234', + title: 'My cool doc', + expression: 'valid expression', + state: { + query: 'kuery', + datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, + }, + } as jest.ResolvedValue); + }); + it('uses the default time and query language settings', () => { - const args = makeDefaultArgs(); + const args = defaultArgs; args.editorFrame = frame; mount(); @@ -510,7 +595,7 @@ describe('Lens App', () => { }); it('updates the index patterns when the editor frame is changed', async () => { - const args = makeDefaultArgs(); + const args = defaultArgs; args.editorFrame = frame; const instance = mount(); @@ -525,7 +610,7 @@ describe('Lens App', () => { const onChange = frame.mount.mock.calls[0][1].onChange; onChange({ filterableIndexPatterns: [{ id: '1', title: 'newIndex' }], - doc: ({ id: undefined } as unknown) as Document, + doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, }); await waitForPromises(); @@ -541,7 +626,7 @@ describe('Lens App', () => { // Do it again to verify that the dirty checking is done right onChange({ filterableIndexPatterns: [{ id: '2', title: 'second index' }], - doc: ({ id: undefined } as unknown) as Document, + doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, }); await waitForPromises(); @@ -556,7 +641,7 @@ describe('Lens App', () => { }); it('updates the editor frame when the user changes query or time in the search bar', () => { - const args = makeDefaultArgs(); + const args = defaultArgs; args.editorFrame = frame; const instance = mount(); @@ -586,7 +671,7 @@ describe('Lens App', () => { }); it('updates the filters when the user changes them', () => { - const args = makeDefaultArgs(); + const args = defaultArgs; args.editorFrame = frame; const instance = mount(); diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index d3f2ac624a6920..443bcf99a4f09f 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -150,7 +150,10 @@ export function App({ } }, [docId]); - const isSaveable = lastKnownDoc && core.application.capabilities.visualize.save; + const isSaveable = + lastKnownDoc && + lastKnownDoc.expression.length > 0 && + core.application.capabilities.visualize.save; const onError = useCallback( (e: { message: string }) => From 0ded3cb094be322d47c8828e5549eb7d06ca5272 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 24 Dec 2019 12:31:18 +0300 Subject: [PATCH 2/6] Licensing plugin functional tests (#53580) * NP licensing add functional tests (#53002) * fix comment * introduce core provider plugin for integration tests * platform functional tests use core_provider_plugin for testing * add 3 scenario for licensing plugins: server, client, legacy * remove unused code * run all licensing_plugin tests on CI * remove duplicated config * address comments * declare global type for core provider * remove potentially dangerous operation Co-authored-by: Elastic Machine --- test/plugin_functional/config.js | 5 +- .../plugins/core_plugin_b/public/plugin.tsx | 15 +- .../plugins/core_provider_plugin/index.ts | 36 ++++ .../plugins/core_provider_plugin/package.json | 17 ++ .../public/index.ts | 11 +- .../core_provider_plugin/tsconfig.json | 14 ++ .../types.ts} | 28 +-- .../plugins/ui_settings_plugin/kibana.json | 2 +- .../plugins/ui_settings_plugin/tsconfig.json | 3 - .../test_suites/core_plugins/ui_plugins.ts | 45 ++--- .../test_suites/core_plugins/ui_settings.ts | 20 +- x-pack/scripts/functional_tests.js | 2 + x-pack/test/licensing_plugin/apis/changes.ts | 177 ------------------ x-pack/test/licensing_plugin/config.legacy.ts | 15 ++ x-pack/test/licensing_plugin/config.public.ts | 29 +++ x-pack/test/licensing_plugin/config.ts | 4 +- x-pack/test/licensing_plugin/legacy/index.ts | 16 ++ .../test/licensing_plugin/legacy/updates.ts | 66 +++++++ x-pack/test/licensing_plugin/public/index.ts | 16 ++ .../test/licensing_plugin/public/updates.ts | 94 ++++++++++ x-pack/test/licensing_plugin/scenario.ts | 91 +++++++++ .../{apis => server}/header.ts | 1 + .../{apis => server}/index.ts | 5 +- .../licensing_plugin/{apis => server}/info.ts | 1 + .../test/licensing_plugin/server/updates.ts | 66 +++++++ 25 files changed, 544 insertions(+), 235 deletions(-) create mode 100644 test/plugin_functional/plugins/core_provider_plugin/index.ts create mode 100644 test/plugin_functional/plugins/core_provider_plugin/package.json rename test/plugin_functional/plugins/{ui_settings_plugin => core_provider_plugin}/public/index.ts (78%) create mode 100644 test/plugin_functional/plugins/core_provider_plugin/tsconfig.json rename test/plugin_functional/plugins/{ui_settings_plugin/public/plugin.tsx => core_provider_plugin/types.ts} (66%) delete mode 100644 x-pack/test/licensing_plugin/apis/changes.ts create mode 100644 x-pack/test/licensing_plugin/config.legacy.ts create mode 100644 x-pack/test/licensing_plugin/config.public.ts create mode 100644 x-pack/test/licensing_plugin/legacy/index.ts create mode 100644 x-pack/test/licensing_plugin/legacy/updates.ts create mode 100644 x-pack/test/licensing_plugin/public/index.ts create mode 100644 x-pack/test/licensing_plugin/public/updates.ts create mode 100644 x-pack/test/licensing_plugin/scenario.ts rename x-pack/test/licensing_plugin/{apis => server}/header.ts (93%) rename x-pack/test/licensing_plugin/{apis => server}/index.ts (76%) rename x-pack/test/licensing_plugin/{apis => server}/info.ts (95%) create mode 100644 x-pack/test/licensing_plugin/server/updates.ts diff --git a/test/plugin_functional/config.js b/test/plugin_functional/config.js index 172dec9a1d111e..87026ce25d9aa3 100644 --- a/test/plugin_functional/config.js +++ b/test/plugin_functional/config.js @@ -57,11 +57,12 @@ export default async function({ readConfigFile }) { ...functionalConfig.get('kbnTestServer'), serverArgs: [ ...functionalConfig.get('kbnTestServer.serverArgs'), + + // Required to load new platform plugins via `--plugin-path` flag. + '--env.name=development', ...plugins.map( pluginDir => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}` ), - // Required to load new platform plugins via `--plugin-path` flag. - '--env.name=development', ], }, }; diff --git a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx index 5c8e1d03d5a4ac..bda1557bdaf915 100644 --- a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx +++ b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx @@ -22,9 +22,6 @@ import { CorePluginAPluginSetup } from '../../core_plugin_a/public/plugin'; declare global { interface Window { - corePluginB?: string; - hasAccessToInjectedMetadata?: boolean; - receivedStartServices?: boolean; env?: PluginInitializerContext['env']; } } @@ -39,12 +36,6 @@ export class CorePluginBPlugin window.env = pluginContext.env; } public setup(core: CoreSetup, deps: CorePluginBDeps) { - window.corePluginB = `Plugin A said: ${deps.core_plugin_a.getGreeting()}`; - window.hasAccessToInjectedMetadata = 'getInjectedVar' in core.injectedMetadata; - core.getStartServices().then(([coreStart, plugins]) => { - window.receivedStartServices = 'overlays' in coreStart; - }); - core.application.register({ id: 'bar', title: 'Bar', @@ -53,6 +44,12 @@ export class CorePluginBPlugin return renderApp(context, params); }, }); + + return { + sayHi() { + return `Plugin A said: ${deps.core_plugin_a.getGreeting()}`; + }, + }; } public start() {} diff --git a/test/plugin_functional/plugins/core_provider_plugin/index.ts b/test/plugin_functional/plugins/core_provider_plugin/index.ts new file mode 100644 index 00000000000000..01f3a67c6b5541 --- /dev/null +++ b/test/plugin_functional/plugins/core_provider_plugin/index.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { resolve } from 'path'; +import { Legacy } from '../../../../kibana'; + +// eslint-disable-next-line import/no-default-export +export default function CoreProviderPlugin(kibana: any) { + const config: Legacy.PluginSpecOptions = { + id: 'core-provider', + require: [], + publicDir: resolve(__dirname, 'public'), + init: (server: Legacy.Server) => ({}), + uiExports: { + hacks: [resolve(__dirname, 'public/index')], + }, + }; + + return new kibana.Plugin(config); +} diff --git a/test/plugin_functional/plugins/core_provider_plugin/package.json b/test/plugin_functional/plugins/core_provider_plugin/package.json new file mode 100644 index 00000000000000..941503b934cbbd --- /dev/null +++ b/test/plugin_functional/plugins/core_provider_plugin/package.json @@ -0,0 +1,17 @@ +{ + "name": "core_provider_plugin", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/core_provider_plugin", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.5.3" + } +} diff --git a/test/plugin_functional/plugins/ui_settings_plugin/public/index.ts b/test/plugin_functional/plugins/core_provider_plugin/public/index.ts similarity index 78% rename from test/plugin_functional/plugins/ui_settings_plugin/public/index.ts rename to test/plugin_functional/plugins/core_provider_plugin/public/index.ts index 3c5997132d460d..c74928203db56f 100644 --- a/test/plugin_functional/plugins/ui_settings_plugin/public/index.ts +++ b/test/plugin_functional/plugins/core_provider_plugin/public/index.ts @@ -16,6 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { UiSettingsPlugin } from './plugin'; +import { npSetup, npStart } from 'ui/new_platform'; +import '../types'; -export const plugin = () => new UiSettingsPlugin(); +window.__coreProvider = { + setup: npSetup, + start: npStart, + testUtils: { + delay: (ms: number) => new Promise(res => setTimeout(res, ms)), + }, +}; diff --git a/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json b/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json new file mode 100644 index 00000000000000..c29959197958df --- /dev/null +++ b/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "types.ts", + "public/**/*.ts", + "../../../../typings/**/*", + ], + "exclude": [] +} diff --git a/test/plugin_functional/plugins/ui_settings_plugin/public/plugin.tsx b/test/plugin_functional/plugins/core_provider_plugin/types.ts similarity index 66% rename from test/plugin_functional/plugins/ui_settings_plugin/public/plugin.tsx rename to test/plugin_functional/plugins/core_provider_plugin/types.ts index 883d203b4c37ac..bf19578c37baab 100644 --- a/test/plugin_functional/plugins/ui_settings_plugin/public/plugin.tsx +++ b/test/plugin_functional/plugins/core_provider_plugin/types.ts @@ -16,22 +16,22 @@ * specific language governing permissions and limitations * under the License. */ - -import { CoreSetup, Plugin } from 'kibana/public'; +import { LegacyCoreSetup, LegacyCoreStart } from 'kibana/public'; declare global { interface Window { - uiSettingsPlugin?: Record; - uiSettingsPluginValue?: string; + __coreProvider: { + setup: { + core: LegacyCoreSetup; + plugins: Record; + }; + start: { + core: LegacyCoreStart; + plugins: Record; + }; + testUtils: { + delay: (ms: number) => Promise; + }; + }; } } - -export class UiSettingsPlugin implements Plugin { - public setup(core: CoreSetup) { - window.uiSettingsPlugin = core.uiSettings.getAll().ui_settings_plugin; - window.uiSettingsPluginValue = core.uiSettings.get('ui_settings_plugin'); - } - - public start() {} - public stop() {} -} diff --git a/test/plugin_functional/plugins/ui_settings_plugin/kibana.json b/test/plugin_functional/plugins/ui_settings_plugin/kibana.json index 05d2dca0af9377..35e4c35490e2fb 100644 --- a/test/plugin_functional/plugins/ui_settings_plugin/kibana.json +++ b/test/plugin_functional/plugins/ui_settings_plugin/kibana.json @@ -4,5 +4,5 @@ "kibanaVersion": "kibana", "configPath": ["ui_settings_plugin"], "server": true, - "ui": true + "ui": false } diff --git a/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json b/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json index 1ba21f11b7de2a..7c170405bbfc7f 100644 --- a/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json @@ -5,9 +5,6 @@ "skipLibCheck": true }, "include": [ - "index.ts", - "public/**/*.ts", - "public/**/*.tsx", "server/**/*.ts", "../../../../typings/**/*", ], diff --git a/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts b/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts index ff535835464870..b76463ee767393 100644 --- a/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts +++ b/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts @@ -19,6 +19,7 @@ import expect from '@kbn/expect'; import { PluginFunctionalProviderContext } from '../../services'; +import '../../../../test/plugin_functional/plugins/core_provider_plugin/types'; // eslint-disable-next-line import/no-default-export export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) { @@ -31,22 +32,35 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider await PageObjects.common.navigateToApp('settings'); }); - it('should attach string to window.corePluginB', async () => { - const corePluginB = await browser.execute('return window.corePluginB'); - expect(corePluginB).to.equal(`Plugin A said: Hello from Plugin A!`); + it('should run the new platform plugins', async () => { + expect( + await browser.execute(() => { + return window.__coreProvider.setup.plugins.core_plugin_b.sayHi(); + }) + ).to.be('Plugin A said: Hello from Plugin A!'); }); }); - describe('have injectedMetadata service provided', function describeIndexTests() { + describe('should have access to the core services', function describeIndexTests() { before(async () => { - await PageObjects.common.navigateToApp('bar'); + await PageObjects.common.navigateToApp('settings'); + }); + + it('to injectedMetadata service', async () => { + expect( + await browser.execute(() => { + return window.__coreProvider.setup.core.injectedMetadata.getKibanaBuildNumber(); + }) + ).to.be.a('number'); }); - it('should attach boolean to window.hasAccessToInjectedMetadata', async () => { - const hasAccessToInjectedMetadata = await browser.execute( - 'return window.hasAccessToInjectedMetadata' - ); - expect(hasAccessToInjectedMetadata).to.equal(true); + it('to start services via coreSetup.getStartServices', async () => { + expect( + await browser.executeAsync(async cb => { + const [coreStart] = await window.__coreProvider.setup.core.getStartServices(); + cb(Boolean(coreStart.overlays)); + }) + ).to.be(true); }); }); @@ -61,16 +75,5 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider expect(envData.packageInfo.version).to.be.a('string'); }); }); - - describe('have access to start services via coreSetup.getStartServices', function describeIndexTests() { - before(async () => { - await PageObjects.common.navigateToApp('bar'); - }); - - it('should attach boolean to window.receivedStartServices', async () => { - const receivedStartServices = await browser.execute('return window.receivedStartServices'); - expect(receivedStartServices).to.equal(true); - }); - }); }); } diff --git a/test/plugin_functional/test_suites/core_plugins/ui_settings.ts b/test/plugin_functional/test_suites/core_plugins/ui_settings.ts index 2b4227ee798e3f..dec79fd15f4ddd 100644 --- a/test/plugin_functional/test_suites/core_plugins/ui_settings.ts +++ b/test/plugin_functional/test_suites/core_plugins/ui_settings.ts @@ -18,6 +18,7 @@ */ import expect from '@kbn/expect'; import { PluginFunctionalProviderContext } from '../../services'; +import '../../plugins/core_provider_plugin/types'; // eslint-disable-next-line import/no-default-export export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) { @@ -31,15 +32,30 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider }); it('client plugins have access to registered settings', async () => { - const settings = await browser.execute('return window.uiSettingsPlugin'); + const settings = await browser.execute(() => { + return window.__coreProvider.setup.core.uiSettings.getAll().ui_settings_plugin; + }); + expect(settings).to.eql({ category: ['any'], description: 'just for testing', name: 'from_ui_settings_plugin', value: '2', }); - const settingsValue = await browser.execute('return window.uiSettingsPluginValue'); + + const settingsValue = await browser.execute(() => { + return window.__coreProvider.setup.core.uiSettings.get('ui_settings_plugin'); + }); + expect(settingsValue).to.be('2'); + + const settingsValueViaObservables = await browser.executeAsync(async (callback: Function) => { + window.__coreProvider.setup.core.uiSettings + .get$('ui_settings_plugin') + .subscribe(v => callback(v)); + }); + + expect(settingsValueViaObservables).to.be('2'); }); it('server plugins have access to registered settings', async () => { diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index f3e9db0053ad66..2b92e70fb30afa 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -36,4 +36,6 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/ui_capabilities/spaces_only/config'), require.resolve('../test/upgrade_assistant_integration/config'), require.resolve('../test/licensing_plugin/config'), + require.resolve('../test/licensing_plugin/config.public'), + require.resolve('../test/licensing_plugin/config.legacy'), ]); diff --git a/x-pack/test/licensing_plugin/apis/changes.ts b/x-pack/test/licensing_plugin/apis/changes.ts deleted file mode 100644 index cf4fecfa32d949..00000000000000 --- a/x-pack/test/licensing_plugin/apis/changes.ts +++ /dev/null @@ -1,177 +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. - */ -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../services'; -import { PublicLicenseJSON } from '../../../plugins/licensing/server'; - -const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); - -export default function({ getService, getPageObjects }: FtrProviderContext) { - const supertest = getService('supertest'); - const esSupertestWithoutAuth = getService('esSupertestWithoutAuth'); - const security = getService('security'); - const PageObjects = getPageObjects(['common', 'security']); - const testSubjects = getService('testSubjects'); - - const scenario = { - async setup() { - await security.role.create('license_manager-role', { - elasticsearch: { - cluster: ['all'], - }, - kibana: [ - { - base: ['all'], - spaces: ['*'], - }, - ], - }); - - await security.user.create('license_manager_user', { - password: 'license_manager_user-password', - roles: ['license_manager-role'], - full_name: 'license_manager user', - }); - - // ensure we're logged out so we can login as the appropriate users - await PageObjects.security.forceLogout(); - await PageObjects.security.login('license_manager_user', 'license_manager_user-password'); - }, - - async teardown() { - await security.role.delete('license_manager-role'); - }, - - async startBasic() { - const response = await esSupertestWithoutAuth - .post('/_license/start_basic?acknowledge=true') - .auth('license_manager_user', 'license_manager_user-password') - .expect(200); - - expect(response.body.basic_was_started).to.be(true); - }, - - async startTrial() { - const response = await esSupertestWithoutAuth - .post('/_license/start_trial?acknowledge=true') - .auth('license_manager_user', 'license_manager_user-password') - .expect(200); - - expect(response.body.trial_was_started).to.be(true); - }, - - async deleteLicense() { - const response = await esSupertestWithoutAuth - .delete('/_license') - .auth('license_manager_user', 'license_manager_user-password') - .expect(200); - - expect(response.body.acknowledged).to.be(true); - }, - - async getLicense(): Promise { - // > --xpack.licensing.api_polling_frequency set in test config - // to wait for Kibana server to re-fetch the license from Elasticsearch - await delay(1000); - - const { body } = await supertest.get('/api/licensing/info').expect(200); - return body; - }, - }; - - describe('changes in license types', () => { - after(async () => { - await scenario.startBasic(); - }); - - it('provides changes in license types', async () => { - await scenario.setup(); - const initialLicense = await scenario.getLicense(); - expect(initialLicense.license?.type).to.be('basic'); - // security enabled explicitly in test config - expect(initialLicense.features?.security).to.eql({ - isAvailable: true, - isEnabled: true, - }); - - const { - body: legacyInitialLicense, - headers: legacyInitialLicenseHeaders, - } = await supertest.get('/api/xpack/v1/info').expect(200); - - expect(legacyInitialLicense.license?.type).to.be('basic'); - expect(legacyInitialLicense.features).to.have.property('security'); - expect(legacyInitialLicenseHeaders['kbn-xpack-sig']).to.be.a('string'); - - // license hasn't changed - const refetchedLicense = await scenario.getLicense(); - expect(refetchedLicense.license?.type).to.be('basic'); - expect(refetchedLicense.signature).to.be(initialLicense.signature); - - const { - body: legacyRefetchedLicense, - headers: legacyRefetchedLicenseHeaders, - } = await supertest.get('/api/xpack/v1/info').expect(200); - - expect(legacyRefetchedLicense.license?.type).to.be('basic'); - expect(legacyRefetchedLicenseHeaders['kbn-xpack-sig']).to.be( - legacyInitialLicenseHeaders['kbn-xpack-sig'] - ); - - // server allows to request trial only once. - // other attempts will throw 403 - await scenario.startTrial(); - const trialLicense = await scenario.getLicense(); - expect(trialLicense.license?.type).to.be('trial'); - expect(trialLicense.signature).to.not.be(initialLicense.signature); - - expect(trialLicense.features?.security).to.eql({ - isAvailable: true, - isEnabled: true, - }); - - const { body: legacyTrialLicense, headers: legacyTrialLicenseHeaders } = await supertest - .get('/api/xpack/v1/info') - .expect(200); - - expect(legacyTrialLicense.license?.type).to.be('trial'); - expect(legacyTrialLicense.features).to.have.property('security'); - expect(legacyTrialLicenseHeaders['kbn-xpack-sig']).to.not.be( - legacyInitialLicenseHeaders['kbn-xpack-sig'] - ); - - await scenario.startBasic(); - const basicLicense = await scenario.getLicense(); - expect(basicLicense.license?.type).to.be('basic'); - expect(basicLicense.signature).not.to.be(initialLicense.signature); - - expect(basicLicense.features?.security).to.eql({ - isAvailable: true, - isEnabled: true, - }); - - const { body: legacyBasicLicense, headers: legacyBasicLicenseHeaders } = await supertest - .get('/api/xpack/v1/info') - .expect(200); - expect(legacyBasicLicense.license?.type).to.be('basic'); - expect(legacyBasicLicense.features).to.have.property('security'); - expect(legacyBasicLicenseHeaders['kbn-xpack-sig']).to.not.be( - legacyInitialLicenseHeaders['kbn-xpack-sig'] - ); - - await scenario.deleteLicense(); - const inactiveLicense = await scenario.getLicense(); - expect(inactiveLicense.signature).to.not.be(initialLicense.signature); - expect(inactiveLicense).to.not.have.property('license'); - expect(inactiveLicense.features?.security).to.eql({ - isAvailable: false, - isEnabled: true, - }); - // banner shown only when license expired not just deleted - await testSubjects.missingOrFail('licenseExpiredBanner'); - }); - }); -} diff --git a/x-pack/test/licensing_plugin/config.legacy.ts b/x-pack/test/licensing_plugin/config.legacy.ts new file mode 100644 index 00000000000000..27dc3df9944ad8 --- /dev/null +++ b/x-pack/test/licensing_plugin/config.legacy.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function({ readConfigFile }: FtrConfigProviderContext) { + const commonConfig = await readConfigFile(require.resolve('./config')); + + return { + ...commonConfig.getAll(), + testFiles: [require.resolve('./legacy')], + }; +} diff --git a/x-pack/test/licensing_plugin/config.public.ts b/x-pack/test/licensing_plugin/config.public.ts new file mode 100644 index 00000000000000..42209aa49bcb47 --- /dev/null +++ b/x-pack/test/licensing_plugin/config.public.ts @@ -0,0 +1,29 @@ +/* + * 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 path from 'path'; +import { KIBANA_ROOT } from '@kbn/test'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function({ readConfigFile }: FtrConfigProviderContext) { + const commonConfig = await readConfigFile(require.resolve('./config')); + + return { + ...commonConfig.getAll(), + testFiles: [require.resolve('./public')], + kbnTestServer: { + serverArgs: [ + ...commonConfig.get('kbnTestServer.serverArgs'), + + // Required to load new platform plugin provider via `--plugin-path` flag. + '--env.name=development', + `--plugin-path=${path.resolve( + KIBANA_ROOT, + 'test/plugin_functional/plugins/core_provider_plugin' + )}`, + ], + }, + }; +} diff --git a/x-pack/test/licensing_plugin/config.ts b/x-pack/test/licensing_plugin/config.ts index 9a83a6f6b5a0b9..60d44cbd4c47fb 100644 --- a/x-pack/test/licensing_plugin/config.ts +++ b/x-pack/test/licensing_plugin/config.ts @@ -22,7 +22,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { }; return { - testFiles: [require.resolve('./apis')], + testFiles: [require.resolve('./server')], servers, services, pageObjects, @@ -43,7 +43,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { ...functionalTestsConfig.get('kbnTestServer'), serverArgs: [ ...functionalTestsConfig.get('kbnTestServer.serverArgs'), - '--xpack.licensing.api_polling_frequency=300', + '--xpack.licensing.api_polling_frequency=100', ], }, diff --git a/x-pack/test/licensing_plugin/legacy/index.ts b/x-pack/test/licensing_plugin/legacy/index.ts new file mode 100644 index 00000000000000..5c45b8f097baf0 --- /dev/null +++ b/x-pack/test/licensing_plugin/legacy/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { FtrProviderContext } from '../services'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: FtrProviderContext) { + describe('Legacy licensing plugin', function() { + this.tags('ciGroup2'); + // MUST BE LAST! CHANGES LICENSE TYPE! + loadTestFile(require.resolve('./updates')); + }); +} diff --git a/x-pack/test/licensing_plugin/legacy/updates.ts b/x-pack/test/licensing_plugin/legacy/updates.ts new file mode 100644 index 00000000000000..efd5df5d14511f --- /dev/null +++ b/x-pack/test/licensing_plugin/legacy/updates.ts @@ -0,0 +1,66 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../services'; +import { createScenario } from '../scenario'; +import '../../../../test/plugin_functional/plugins/core_provider_plugin/types'; + +// eslint-disable-next-line import/no-default-export +export default function(ftrContext: FtrProviderContext) { + const { getService } = ftrContext; + const supertest = getService('supertest'); + const testSubjects = getService('testSubjects'); + + const scenario = createScenario(ftrContext); + + describe('changes in license types', () => { + after(async () => { + await scenario.teardown(); + }); + + it('provides changes in license types', async () => { + await scenario.setup(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + const { + body: legacyInitialLicense, + headers: legacyInitialLicenseHeaders, + } = await supertest.get('/api/xpack/v1/info').expect(200); + + expect(legacyInitialLicense.license?.type).to.be('basic'); + expect(legacyInitialLicense.features).to.have.property('security'); + expect(legacyInitialLicenseHeaders['kbn-xpack-sig']).to.be.a('string'); + + await scenario.startTrial(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + const { body: legacyTrialLicense, headers: legacyTrialLicenseHeaders } = await supertest + .get('/api/xpack/v1/info') + .expect(200); + + expect(legacyTrialLicense.license?.type).to.be('trial'); + expect(legacyTrialLicense.features).to.have.property('security'); + expect(legacyTrialLicenseHeaders['kbn-xpack-sig']).to.not.be( + legacyInitialLicenseHeaders['kbn-xpack-sig'] + ); + + await scenario.startBasic(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + const { body: legacyBasicLicense, headers: legacyBasicLicenseHeaders } = await supertest + .get('/api/xpack/v1/info') + .expect(200); + expect(legacyBasicLicense.license?.type).to.be('basic'); + expect(legacyBasicLicense.features).to.have.property('security'); + expect(legacyBasicLicenseHeaders['kbn-xpack-sig']).to.not.be( + legacyInitialLicenseHeaders['kbn-xpack-sig'] + ); + + // banner shown only when license expired not just deleted + await testSubjects.missingOrFail('licenseExpiredBanner'); + }); + }); +} diff --git a/x-pack/test/licensing_plugin/public/index.ts b/x-pack/test/licensing_plugin/public/index.ts new file mode 100644 index 00000000000000..3e1445d9a4aab6 --- /dev/null +++ b/x-pack/test/licensing_plugin/public/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { FtrProviderContext } from '../services'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: FtrProviderContext) { + describe('Licensing plugin public client', function() { + this.tags('ciGroup2'); + // MUST BE LAST! CHANGES LICENSE TYPE! + loadTestFile(require.resolve('./updates')); + }); +} diff --git a/x-pack/test/licensing_plugin/public/updates.ts b/x-pack/test/licensing_plugin/public/updates.ts new file mode 100644 index 00000000000000..6d2253fe838689 --- /dev/null +++ b/x-pack/test/licensing_plugin/public/updates.ts @@ -0,0 +1,94 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../services'; +import { LicensingPluginSetup } from '../../../plugins/licensing/public'; +import { createScenario } from '../scenario'; +import '../../../../test/plugin_functional/plugins/core_provider_plugin/types'; + +// eslint-disable-next-line import/no-default-export +export default function(ftrContext: FtrProviderContext) { + const { getService } = ftrContext; + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + + const scenario = createScenario(ftrContext); + + describe('changes in license types', () => { + after(async () => { + await scenario.teardown(); + }); + + it('provides changes in license types', async () => { + await scenario.setup(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + expect( + await browser.executeAsync(async (cb: Function) => { + const { setup, testUtils } = window.__coreProvider; + // this call enforces signature check to detect license update + // and causes license re-fetch + await setup.core.http.get('/'); + await testUtils.delay(100); + + const licensing: LicensingPluginSetup = setup.plugins.licensing; + licensing.license$.subscribe(license => cb(license.type)); + }) + ).to.be('basic'); + + // license hasn't changed + await scenario.waitForPluginToDetectLicenseUpdate(); + + expect( + await browser.executeAsync(async (cb: Function) => { + const { setup, testUtils } = window.__coreProvider; + // this call enforces signature check to detect license update + // and causes license re-fetch + await setup.core.http.get('/'); + await testUtils.delay(100); + + const licensing: LicensingPluginSetup = setup.plugins.licensing; + licensing.license$.subscribe(license => cb(license.type)); + }) + ).to.be('basic'); + + await scenario.startTrial(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + expect( + await browser.executeAsync(async (cb: Function) => { + const { setup, testUtils } = window.__coreProvider; + // this call enforces signature check to detect license update + // and causes license re-fetch + await setup.core.http.get('/'); + await testUtils.delay(100); + + const licensing: LicensingPluginSetup = setup.plugins.licensing; + licensing.license$.subscribe(license => cb(license.type)); + }) + ).to.be('trial'); + + await scenario.startBasic(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + expect( + await browser.executeAsync(async (cb: Function) => { + const { setup, testUtils } = window.__coreProvider; + // this call enforces signature check to detect license update + // and causes license re-fetch + await setup.core.http.get('/'); + await testUtils.delay(100); + + const licensing: LicensingPluginSetup = setup.plugins.licensing; + licensing.license$.subscribe(license => cb(license.type)); + }) + ).to.be('basic'); + + // banner shown only when license expired not just deleted + await testSubjects.missingOrFail('licenseExpiredBanner'); + }); + }); +} diff --git a/x-pack/test/licensing_plugin/scenario.ts b/x-pack/test/licensing_plugin/scenario.ts new file mode 100644 index 00000000000000..46837dfc1be91a --- /dev/null +++ b/x-pack/test/licensing_plugin/scenario.ts @@ -0,0 +1,91 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from './services'; +import { PublicLicenseJSON } from '../../plugins/licensing/server'; +import '../../../test/plugin_functional/plugins/core_provider_plugin/types'; + +const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); + +export function createScenario({ getService, getPageObjects }: FtrProviderContext) { + const supertest = getService('supertest'); + const esSupertestWithoutAuth = getService('esSupertestWithoutAuth'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'security']); + + const scenario = { + async setup() { + await security.role.create('license_manager-role', { + elasticsearch: { + cluster: ['all'], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + await security.user.create('license_manager_user', { + password: 'license_manager_user-password', + roles: ['license_manager-role'], + full_name: 'license_manager user', + }); + + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.logout(); + await PageObjects.security.login('license_manager_user', 'license_manager_user-password'); + }, + + // make sure a license is present, otherwise the security is not available anymore. + async teardown() { + await security.role.delete('license_manager-role'); + await security.user.delete('license_manager_user'); + }, + + // elasticsearch allows to downgrade a license only once. other attempts will throw 403. + async startBasic() { + const response = await esSupertestWithoutAuth + .post('/_license/start_basic?acknowledge=true') + .auth('license_manager_user', 'license_manager_user-password') + .expect(200); + + expect(response.body.basic_was_started).to.be(true); + }, + + // elasticsearch allows to request trial only once. other attempts will throw 403. + async startTrial() { + const response = await esSupertestWithoutAuth + .post('/_license/start_trial?acknowledge=true') + .auth('license_manager_user', 'license_manager_user-password') + .expect(200); + + expect(response.body.trial_was_started).to.be(true); + }, + + async deleteLicense() { + const response = await esSupertestWithoutAuth + .delete('/_license') + .auth('license_manager_user', 'license_manager_user-password') + .expect(200); + + expect(response.body.acknowledged).to.be(true); + }, + + async getLicense(): Promise { + const { body } = await supertest.get('/api/licensing/info').expect(200); + return body; + }, + + async waitForPluginToDetectLicenseUpdate() { + // > --xpack.licensing.api_polling_frequency set in test config + // to wait for Kibana server to re-fetch the license from Elasticsearch + await delay(500); + }, + }; + return scenario; +} diff --git a/x-pack/test/licensing_plugin/apis/header.ts b/x-pack/test/licensing_plugin/server/header.ts similarity index 93% rename from x-pack/test/licensing_plugin/apis/header.ts rename to x-pack/test/licensing_plugin/server/header.ts index 8d95054feaaf23..d2073e8773f185 100644 --- a/x-pack/test/licensing_plugin/apis/header.ts +++ b/x-pack/test/licensing_plugin/server/header.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../services'; +// eslint-disable-next-line import/no-default-export export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/licensing_plugin/apis/index.ts b/x-pack/test/licensing_plugin/server/index.ts similarity index 76% rename from x-pack/test/licensing_plugin/apis/index.ts rename to x-pack/test/licensing_plugin/server/index.ts index fbc0449dcd8fcf..374bfcc0aa6b4a 100644 --- a/x-pack/test/licensing_plugin/apis/index.ts +++ b/x-pack/test/licensing_plugin/server/index.ts @@ -6,13 +6,14 @@ import { FtrProviderContext } from '../services'; +// eslint-disable-next-line import/no-default-export export default function({ loadTestFile }: FtrProviderContext) { - describe('Licensing plugin', function() { + describe('Licensing plugin server client', function() { this.tags('ciGroup2'); loadTestFile(require.resolve('./info')); loadTestFile(require.resolve('./header')); // MUST BE LAST! CHANGES LICENSE TYPE! - loadTestFile(require.resolve('./changes')); + loadTestFile(require.resolve('./updates')); }); } diff --git a/x-pack/test/licensing_plugin/apis/info.ts b/x-pack/test/licensing_plugin/server/info.ts similarity index 95% rename from x-pack/test/licensing_plugin/apis/info.ts rename to x-pack/test/licensing_plugin/server/info.ts index 7ec009d85cd094..cce042c718b7b0 100644 --- a/x-pack/test/licensing_plugin/apis/info.ts +++ b/x-pack/test/licensing_plugin/server/info.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../services'; +// eslint-disable-next-line import/no-default-export export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/licensing_plugin/server/updates.ts b/x-pack/test/licensing_plugin/server/updates.ts new file mode 100644 index 00000000000000..37edb78608a686 --- /dev/null +++ b/x-pack/test/licensing_plugin/server/updates.ts @@ -0,0 +1,66 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../services'; +import { createScenario } from '../scenario'; +import '../../../../test/plugin_functional/plugins/core_provider_plugin/types'; + +// eslint-disable-next-line import/no-default-export +export default function(ftrContext: FtrProviderContext) { + const { getService } = ftrContext; + const testSubjects = getService('testSubjects'); + + const scenario = createScenario(ftrContext); + + describe('changes in license types', () => { + after(async () => { + await scenario.teardown(); + }); + + it('provides changes in license types', async () => { + await scenario.setup(); + await scenario.waitForPluginToDetectLicenseUpdate(); + const initialLicense = await scenario.getLicense(); + expect(initialLicense.license?.type).to.be('basic'); + // security enabled explicitly in test config + expect(initialLicense.features?.security).to.eql({ + isAvailable: true, + isEnabled: true, + }); + + // license hasn't changed + await scenario.waitForPluginToDetectLicenseUpdate(); + const refetchedLicense = await scenario.getLicense(); + expect(refetchedLicense.license?.type).to.be('basic'); + expect(refetchedLicense.signature).to.be(initialLicense.signature); + + await scenario.startTrial(); + await scenario.waitForPluginToDetectLicenseUpdate(); + const trialLicense = await scenario.getLicense(); + expect(trialLicense.license?.type).to.be('trial'); + expect(trialLicense.signature).to.not.be(initialLicense.signature); + + expect(trialLicense.features?.security).to.eql({ + isAvailable: true, + isEnabled: true, + }); + + await scenario.startBasic(); + await scenario.waitForPluginToDetectLicenseUpdate(); + const basicLicense = await scenario.getLicense(); + expect(basicLicense.license?.type).to.be('basic'); + expect(basicLicense.signature).not.to.be(initialLicense.signature); + + expect(basicLicense.features?.security).to.eql({ + isAvailable: true, + isEnabled: true, + }); + + // banner shown only when license expired not just deleted + await testSubjects.missingOrFail('licenseExpiredBanner'); + }); + }); +} From 3b0cce00357bf3858144d414288347c7c74f1a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 24 Dec 2019 10:33:20 +0100 Subject: [PATCH 3/6] [APM] Transaction page throws unhandled exception if transactions doesn't have `http.request` (#53760) * Making http.request optional * changing unit test --- .../DetailView/index.test.tsx | 27 +++++++++++++++++++ .../ErrorGroupDetails/DetailView/index.tsx | 2 +- .../Summary/TransactionSummary.test.tsx | 18 +++++++++++-- .../shared/Summary/TransactionSummary.tsx | 2 +- .../Summary/__fixtures__/transactions.ts | 22 +++++++++++++++ .../apm/typings/es_schemas/raw/fields/Http.ts | 2 +- 6 files changed, 68 insertions(+), 5 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.test.tsx index af4c129d61b60d..7fe1386ba6414a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.test.tsx @@ -126,4 +126,31 @@ describe('DetailView', () => { expect(wrapper.exists()).toBe(true); expect(wrapper).toMatchSnapshot(); }); + + it('should render without http request info', () => { + const errorGroup = { + occurrencesCount: 10, + transaction: undefined, + error: { + timestamp: { + us: 0 + }, + http: { response: { status_code: 404 } }, + url: { full: 'myUrl' }, + service: { name: 'myService' }, + user: { id: 'myUserId' }, + error: { exception: { handled: true } }, + transaction: { id: 'myTransactionId', sampled: true } + } as any + }; + expect(() => + shallow( + + ) + ).not.toThrowError(); + }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx index 879733685c5f78..29c5d3329d16b4 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx @@ -81,7 +81,7 @@ export function DetailView({ errorGroup, urlParams, location }: Props) { const errorUrl = error.error.page?.url || error.url?.full; - const method = error.http?.request.method; + const method = error.http?.request?.method; const status = error.http?.response?.status_code; return ( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx index 19f626187cdd1a..80ecea3cf9b338 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx @@ -7,12 +7,26 @@ import { shallow } from 'enzyme'; import React from 'react'; import { TransactionSummary } from './TransactionSummary'; -import { Transaction } from '../../../../typings/es_schemas/ui/Transaction'; import * as exampleTransactions from './__fixtures__/transactions'; describe('TransactionSummary', () => { describe('render', () => { - const transaction: Transaction = exampleTransactions.httpOk; + const transaction = exampleTransactions.httpOk; + + const props = { + errorCount: 0, + totalDuration: 0, + transaction + }; + + it('renders', () => { + expect(() => + shallow() + ).not.toThrowError(); + }); + }); + describe('renders RUM transaction without request info', () => { + const transaction = exampleTransactions.httpRumOK; const props = { errorCount: 0, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx index c24435d3810083..8b7380a18edc39 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx @@ -28,7 +28,7 @@ const getTransactionResultSummaryItem = (transaction: Transaction) => { : transaction.url?.full; if (url) { - const method = transaction.http?.request.method; + const method = transaction.http?.request?.method; const status = transaction.http?.response?.status_code; return ; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts b/x-pack/legacy/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts index 59496dad165721..f4346e47f23c2f 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts @@ -25,3 +25,25 @@ export const httpOk: Transaction = { duration: { us: 0 } } }; + +export const httpRumOK: Transaction = { + '@timestamp': '0', + agent: { name: 'rum-js', version: '0' }, + http: { + response: { status_code: 200 } + }, + processor: { event: 'transaction', name: 'transaction' }, + service: { name: 'testServiceName' }, + timestamp: { us: 0 }, + trace: { id: 'testTrace' }, + transaction: { + page: { + url: 'elastic.co' + }, + name: 'testTransaction', + id: 'testId', + sampled: false, + type: 'testType', + duration: { us: 0 } + } +}; diff --git a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/fields/Http.ts b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/fields/Http.ts index cb63499a98186e..be54e66c3d01b7 100644 --- a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/fields/Http.ts +++ b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/fields/Http.ts @@ -5,7 +5,7 @@ */ export interface Http { - request: { method: string; [key: string]: unknown }; + request?: { method: string; [key: string]: unknown }; response?: { status_code: number; [key: string]: unknown }; version?: string; } From 53513f6b7b85f1140352a99f65ff53f5cdb1ec79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Tue, 24 Dec 2019 12:39:08 +0100 Subject: [PATCH 4/6] [APM] Add log statements for flaky test (#53775) * [APM] Add log statements for flaky test * Improve logging * Improve logging * Log full index on error --- .../routes/settings/agent_configuration.ts | 25 ++++++++++++-- .../apis/apm/feature_controls.ts | 34 ++++++++++++++----- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts index a6709110790403..ddd6a270251310 100644 --- a/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts @@ -117,10 +117,19 @@ export const createAgentConfigurationRoute = createRoute(() => ({ }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return await createOrUpdateConfiguration({ - configuration: context.params.body, + const configuration = context.params.body; + + // TODO: Remove logger. Only added temporarily to debug flaky test (https://github.com/elastic/kibana/issues/51764) + context.logger.info( + `Hitting: /api/apm/settings/agent-configuration/new with ${configuration.service.name}/${configuration.service.environment}` + ); + const res = await createOrUpdateConfiguration({ + configuration, setup }); + context.logger.info(`Created agent configuration`); + + return res; } })); @@ -161,8 +170,14 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ }) }, handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); const { body } = context.params; + + // TODO: Remove logger. Only added temporarily to debug flaky test (https://github.com/elastic/kibana/issues/51764) + context.logger.info( + `Hitting: /api/apm/settings/agent-configuration/search for ${body.service.name}/${body.service.environment}` + ); + + const setup = await setupRequest(context, request); const config = await searchConfigurations({ serviceName: body.service.name, environment: body.service.environment, @@ -176,6 +191,10 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ throw new Boom('Not found', { statusCode: 404 }); } + context.logger.info( + `Config was found for ${body.service.name}/${body.service.environment}` + ); + // update `applied_by_agent` field if etags match if (body.etag === config._source.etag && !config._source.applied_by_agent) { markAppliedByAgent({ id: config._id, body: config._source, setup }); diff --git a/x-pack/test/api_integration/apis/apm/feature_controls.ts b/x-pack/test/api_integration/apis/apm/feature_controls.ts index 839d4ff420a76c..ec2bbca23ddd28 100644 --- a/x-pack/test/api_integration/apis/apm/feature_controls.ts +++ b/x-pack/test/api_integration/apis/apm/feature_controls.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable no-console */ + import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -13,6 +15,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const security = getService('security'); const spaces = getService('spaces'); const log = getService('log'); + const es = getService('legacyEs'); const start = encodeURIComponent(new Date(Date.now() - 10000).toISOString()); const end = encodeURIComponent(new Date().toISOString()); @@ -35,6 +38,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) }; expectForbidden: (result: any) => void; expectResponse: (result: any) => void; + onExpectationFail?: () => Promise; } const endpoints: Endpoint[] = [ { @@ -139,10 +143,17 @@ export default function featureControlsTests({ getService }: FtrProviderContext) }, expectForbidden: expect404, expectResponse: expect200, + onExpectationFail: async () => { + const res = await es.search({ + index: '.apm-agent-configuration', + }); + + console.warn(JSON.stringify(res, null, 2)); + }, }, ]; - const elasticsearchRole = { + const elasticsearchPrivileges = { indices: [ { names: ['apm-*'], privileges: ['read', 'view_index_metadata'] }, { names: ['.apm-agent-configuration'], privileges: ['read', 'write', 'view_index_metadata'] }, @@ -205,8 +216,10 @@ export default function featureControlsTests({ getService }: FtrProviderContext) spaceId?: string; }) { for (const endpoint of endpoints) { - log.debug(`hitting ${endpoint.req.url}`); + console.log(`Requesting: ${endpoint.req.url}. Expecting: ${expectation}`); const result = await executeAsUser(endpoint.req, username, password, spaceId); + console.log(`Responded: ${endpoint.req.url}`); + try { if (expectation === 'forbidden') { endpoint.expectForbidden(result); @@ -214,6 +227,10 @@ export default function featureControlsTests({ getService }: FtrProviderContext) endpoint.expectResponse(result); } } catch (e) { + if (endpoint.onExpectationFail) { + await endpoint.onExpectationFail(); + } + const { statusCode, body, req } = result.response; throw new Error( `Endpoint: ${req.method} ${req.path} @@ -229,7 +246,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) describe('apm feature controls', () => { let res: any; before(async () => { - log.debug('creating agent configuration'); + console.log(`Creating agent configuration`); res = await executeAsAdmin({ method: 'post', url: '/api/apm/settings/agent-configuration/new', @@ -238,10 +255,11 @@ export default function featureControlsTests({ getService }: FtrProviderContext) settings: { transaction_sample_rate: 0.5 }, }, }); + console.log(`Agent configuration created`); }); after(async () => { - log.debug('deleting agent configuration'); + console.log('deleting agent configuration'); const configurationId = res.body._id; await executeAsAdmin({ method: 'delete', @@ -255,7 +273,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const password = `${username}-password`; try { await security.role.create(roleName, { - elasticsearch: elasticsearchRole, + elasticsearch: elasticsearchPrivileges, }); await security.user.create(username, { @@ -277,7 +295,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const password = `${username}-password`; try { await security.role.create(roleName, { - elasticsearch: elasticsearchRole, + elasticsearch: elasticsearchPrivileges, kibana: [{ base: ['all'], spaces: ['*'] }], }); @@ -301,7 +319,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const password = `${username}-password`; try { await security.role.create(roleName, { - elasticsearch: elasticsearchRole, + elasticsearch: elasticsearchPrivileges, kibana: [{ feature: { dashboard: ['all'] }, spaces: ['*'] }], }); @@ -339,7 +357,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) disabledFeatures: [], }); await security.role.create(roleName, { - elasticsearch: elasticsearchRole, + elasticsearch: elasticsearchPrivileges, kibana: [ { feature: { apm: ['read'] }, spaces: [space1Id] }, { feature: { dashboard: ['all'] }, spaces: [space2Id] }, From 22b833526a9c8177ecd9db59479c6dd6d1242f28 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 25 Dec 2019 06:27:21 -0700 Subject: [PATCH 5/6] remove use of experimental fs.promises api (#53346) * remove use of experimental fs.promises api * remove one more usage of fs.promises * switch to an alternate fs module to maintain testing strategy Co-authored-by: Elastic Machine --- src/core/server/uuid/fs.ts | 24 +++++++++++++++++++++++ src/core/server/uuid/resolve_uuid.test.ts | 19 +++++------------- src/core/server/uuid/resolve_uuid.ts | 4 +--- utilities/visual_regression.js | 6 +++--- 4 files changed, 33 insertions(+), 20 deletions(-) create mode 100644 src/core/server/uuid/fs.ts diff --git a/src/core/server/uuid/fs.ts b/src/core/server/uuid/fs.ts new file mode 100644 index 00000000000000..f10d6370c09d15 --- /dev/null +++ b/src/core/server/uuid/fs.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Fs from 'fs'; +import { promisify } from 'util'; + +export const readFile = promisify(Fs.readFile); +export const writeFile = promisify(Fs.writeFile); diff --git a/src/core/server/uuid/resolve_uuid.test.ts b/src/core/server/uuid/resolve_uuid.test.ts index 1ddd667eacdee0..d1332daa020572 100644 --- a/src/core/server/uuid/resolve_uuid.test.ts +++ b/src/core/server/uuid/resolve_uuid.test.ts @@ -17,31 +17,22 @@ * under the License. */ -import { promises } from 'fs'; import { join } from 'path'; +import { readFile, writeFile } from './fs'; import { resolveInstanceUuid } from './resolve_uuid'; import { configServiceMock } from '../config/config_service.mock'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { BehaviorSubject } from 'rxjs'; import { Logger } from '../logging'; -const { readFile, writeFile } = promises; - jest.mock('uuid', () => ({ v4: () => 'NEW_UUID', })); -jest.mock('fs', () => { - const actual = jest.requireActual('fs'); - return { - ...actual, - promises: { - ...actual.promises, - readFile: jest.fn(() => Promise.resolve('')), - writeFile: jest.fn(() => Promise.resolve('')), - }, - }; -}); +jest.mock('./fs', () => ({ + readFile: jest.fn(() => Promise.resolve('')), + writeFile: jest.fn(() => Promise.resolve('')), +})); const DEFAULT_FILE_UUID = 'FILE_UUID'; const DEFAULT_CONFIG_UUID = 'CONFIG_UUID'; diff --git a/src/core/server/uuid/resolve_uuid.ts b/src/core/server/uuid/resolve_uuid.ts index 17412bfa0544cb..3f5bdc73873928 100644 --- a/src/core/server/uuid/resolve_uuid.ts +++ b/src/core/server/uuid/resolve_uuid.ts @@ -18,16 +18,14 @@ */ import uuid from 'uuid'; -import { promises } from 'fs'; import { join } from 'path'; import { take } from 'rxjs/operators'; +import { readFile, writeFile } from './fs'; import { IConfigService } from '../config'; import { PathConfigType, config as pathConfigDef } from '../path'; import { HttpConfigType, config as httpConfigDef } from '../http'; import { Logger } from '../logging'; -const { readFile, writeFile } = promises; - const FILE_ENCODING = 'utf8'; const FILE_NAME = 'uuid'; diff --git a/utilities/visual_regression.js b/utilities/visual_regression.js index f174dc4764aed0..d95b3018bea962 100644 --- a/utilities/visual_regression.js +++ b/utilities/visual_regression.js @@ -103,8 +103,8 @@ async function compareScreenshots() { const diffImagePath = path.resolve(DIFF_SCREENSHOTS_DIR, screenshot); - const sessionImage = PNG.sync.read(await fs.promises.readFile(sessionImagePath)); - const baselineImage = PNG.sync.read(await fs.promises.readFile(baselineImagePath)); + const sessionImage = PNG.sync.read(await readFileAsync(sessionImagePath)); + const baselineImage = PNG.sync.read(await readFileAsync(baselineImagePath)); const { width, height } = sessionImage; const diff = new PNG({ width, height }); @@ -117,7 +117,7 @@ async function compareScreenshots() { { threshold: 0 } ); - await fs.promises.writeFile(diffImagePath, PNG.sync.write(diff)); + await writeFileAsync(diffImagePath, PNG.sync.write(diff)); const change = numDiffPixels / (width * height); const changePercentage = (change * 100).toFixed(2); From 4b00bada235d4210bf5d60b843ec831351291b7b Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Thu, 26 Dec 2019 11:15:16 -0500 Subject: [PATCH 6/6] [Maps] Only show legend when layer is visible (#53781) --- .../widget_overlay/layer_control/layer_toc/toc_entry/view.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js index b843d2d4758d4c..c9f115c1ba4cce 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js @@ -46,7 +46,10 @@ export class TOCEntry extends React.Component { }; async _loadHasLegendDetails() { - const hasLegendDetails = await this.props.layer.hasLegendDetails(); + const hasLegendDetails = + (await this.props.layer.hasLegendDetails()) && + this.props.layer.isVisible() && + this.props.layer.showAtZoomLevel(this.props.zoom); if (this._isMounted && hasLegendDetails !== this.state.hasLegendDetails) { this.setState({ hasLegendDetails }); }