diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index e5b3bf67ab1c7..a9df537c41d50 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -18,6 +18,18 @@ export const WHITELISTED_JOB_CONTENT_TYPES = [ 'image/png', ]; +export const KBN_SCREENSHOT_HEADER_BLACKLIST = [ + 'accept-encoding', + 'content-length', + 'content-type', + 'host', + 'referer', + // `Transfer-Encoding` is hop-by-hop header that is meaningful + // only for a single transport-level connection, and shouldn't + // be stored by caches or forwarded by proxies. + 'transfer-encoding', +]; + export const UI_SETTINGS_CUSTOM_PDF_LOGO = 'xpackReporting:customPdfLogo'; /** diff --git a/x-pack/plugins/reporting/export_types/common/execute_job/add_force_now_query_string.test.ts b/x-pack/plugins/reporting/export_types/common/execute_job/add_force_now_query_string.test.ts new file mode 100644 index 0000000000000..815d0169be2af --- /dev/null +++ b/x-pack/plugins/reporting/export_types/common/execute_job/add_force_now_query_string.test.ts @@ -0,0 +1,61 @@ +/* + * 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 { createMockServer } from '../../../test_helpers/create_mock_server'; +import { addForceNowQuerystring } from './index'; + +let mockServer: any; +beforeEach(() => { + mockServer = createMockServer(''); +}); + +test(`fails if no URL is passed`, async () => { + await expect( + addForceNowQuerystring({ + job: {}, + server: mockServer, + }) + ).rejects.toBeDefined(); +}); + +test(`adds forceNow to hash's query, if it exists`, async () => { + const forceNow = '2000-01-01T00:00:00.000Z'; + const { urls } = await addForceNowQuerystring({ + job: { relativeUrl: '/app/kibana#/something', forceNow }, + server: mockServer, + }); + + expect(urls[0]).toEqual( + 'http://localhost:5601/sbp/app/kibana#/something?forceNow=2000-01-01T00%3A00%3A00.000Z' + ); +}); + +test(`appends forceNow to hash's query, if it exists`, async () => { + const forceNow = '2000-01-01T00:00:00.000Z'; + + const { urls } = await addForceNowQuerystring({ + job: { + relativeUrl: '/app/kibana#/something?_g=something', + forceNow, + }, + server: mockServer, + }); + + expect(urls[0]).toEqual( + 'http://localhost:5601/sbp/app/kibana#/something?_g=something&forceNow=2000-01-01T00%3A00%3A00.000Z' + ); +}); + +test(`doesn't append forceNow query to url, if it doesn't exists`, async () => { + const { urls } = await addForceNowQuerystring({ + job: { + relativeUrl: '/app/kibana#/something', + }, + server: mockServer, + }); + + expect(urls[0]).toEqual('http://localhost:5601/sbp/app/kibana#/something'); +}); diff --git a/x-pack/plugins/reporting/export_types/common/execute_job/add_force_now_query_string.ts b/x-pack/plugins/reporting/export_types/common/execute_job/add_force_now_query_string.ts new file mode 100644 index 0000000000000..d68972a1a78f3 --- /dev/null +++ b/x-pack/plugins/reporting/export_types/common/execute_job/add_force_now_query_string.ts @@ -0,0 +1,60 @@ +/* + * 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. + */ +// @ts-ignore +import url from 'url'; +import { ConditionalHeaders, KbnServer, ReportingJob } from '../../../types'; +import { getAbsoluteUrlFactory } from './get_absolute_url'; + +function getSavedObjectAbsoluteUrl(job: ReportingJob, relativeUrl: string, server: KbnServer) { + const getAbsoluteUrl: any = getAbsoluteUrlFactory(server); + + const { pathname: path, hash, search } = url.parse(relativeUrl); + return getAbsoluteUrl({ basePath: job.basePath, path, hash, search }); +} + +export const addForceNowQuerystring = async ({ + job, + conditionalHeaders, + logo, + server, +}: { + job: ReportingJob; + conditionalHeaders?: ConditionalHeaders; + logo?: any; + server: KbnServer; +}) => { + // if no URLS then its from PNG which should only have one so put it in the array and process as PDF does + if (!job.urls) { + if (!job.relativeUrl) { + throw new Error(`Unable to generate report. Url is not defined.`); + } + job.urls = [getSavedObjectAbsoluteUrl(job, job.relativeUrl, server)]; + } + + const urls = job.urls.map(jobUrl => { + if (!job.forceNow) { + return jobUrl; + } + + const parsed: any = url.parse(jobUrl, true); + const hash: any = url.parse(parsed.hash.replace(/^#/, ''), true); + + const transformedHash = url.format({ + pathname: hash.pathname, + query: { + ...hash.query, + forceNow: job.forceNow, + }, + }); + + return url.format({ + ...parsed, + hash: transformedHash, + }); + }); + + return { job, conditionalHeaders, logo, urls, server }; +}; diff --git a/x-pack/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.test.ts b/x-pack/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.test.ts new file mode 100644 index 0000000000000..a289093d2ebaa --- /dev/null +++ b/x-pack/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.test.ts @@ -0,0 +1,45 @@ +/* + * 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. + */ + +// @ts-ignore +import { cryptoFactory } from '../../../server/lib/crypto'; +import { createMockServer } from '../../../test_helpers/create_mock_server'; +import { decryptJobHeaders } from './index'; + +let mockServer: any; +beforeEach(() => { + mockServer = createMockServer(''); +}); + +const encryptHeaders = async (headers: Record) => { + const crypto = cryptoFactory(mockServer); + return await crypto.encrypt(headers); +}; + +describe('headers', () => { + test(`fails if it can't decrypt headers`, async () => { + await expect( + decryptJobHeaders({ + job: { relativeUrl: '/app/kibana#/something', timeRange: {} }, + server: mockServer, + }) + ).rejects.toBeDefined(); + }); + + test(`passes back decrypted headers that were passed in`, async () => { + const headers = { + foo: 'bar', + baz: 'quix', + }; + + const encryptedHeaders = await encryptHeaders(headers); + const { decryptedHeaders } = await decryptJobHeaders({ + job: { relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }, + server: mockServer, + }); + expect(decryptedHeaders).toEqual(headers); + }); +}); diff --git a/x-pack/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.ts b/x-pack/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.ts new file mode 100644 index 0000000000000..d10d8b51f84ec --- /dev/null +++ b/x-pack/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ +// @ts-ignore +import { cryptoFactory } from '../../../server/lib/crypto'; +import { CryptoFactory, KbnServer, ReportingJob } from '../../../types'; + +export const decryptJobHeaders = async ({ + job, + server, +}: { + job: ReportingJob; + server: KbnServer; +}) => { + const crypto: CryptoFactory = cryptoFactory(server); + const decryptedHeaders: string = await crypto.decrypt(job.headers); + return { job, decryptedHeaders, server }; +}; diff --git a/x-pack/plugins/reporting/export_types/common/execute_job/get_absolute_url.test.js b/x-pack/plugins/reporting/export_types/common/execute_job/get_absolute_url.test.ts similarity index 58% rename from x-pack/plugins/reporting/export_types/common/execute_job/get_absolute_url.test.js rename to x-pack/plugins/reporting/export_types/common/execute_job/get_absolute_url.test.ts index 39ca6fd52f51e..0c57408f81e7f 100644 --- a/x-pack/plugins/reporting/export_types/common/execute_job/get_absolute_url.test.js +++ b/x-pack/plugins/reporting/export_types/common/execute_job/get_absolute_url.test.ts @@ -4,118 +4,89 @@ * you may not use this file except in compliance with the Elastic License. */ -import { memoize } from 'lodash'; +import { createMockServer } from '../../../test_helpers/create_mock_server'; import { getAbsoluteUrlFactory } from './get_absolute_url'; -const createMockServer = ({ settings = {} } = {}) => { - const mockServer = { - expose: () => {}, - config: memoize(() => { - return { - get: jest.fn() - }; - }), - info: { - protocol: 'http' - } - }; - - const defaultSettings = { - 'server.host': 'something', - 'server.port': 8080, - 'server.basePath': '/tst', - 'xpack.reporting.kibanaServer': {} - }; - mockServer.config().get.mockImplementation(key => { - return key in settings ? settings[key] : defaultSettings[key]; - }); - - return mockServer; -}; - test(`by default it builds url using information from server.info.protocol and the server.config`, () => { - const mockServer = createMockServer(); - + const mockServer = createMockServer(''); const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); const absoluteUrl = getAbsoluteUrl(); - expect(absoluteUrl).toBe(`http://something:8080/tst/app/kibana`); + expect(absoluteUrl).toBe(`http://localhost:5601/sbp/app/kibana`); }); test(`uses kibanaServer.protocol if specified`, () => { const settings = { - 'xpack.reporting.kibanaServer.protocol': 'https' + 'xpack.reporting.kibanaServer.protocol': 'https', }; const mockServer = createMockServer({ settings }); const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); const absoluteUrl = getAbsoluteUrl(); - expect(absoluteUrl).toBe(`https://something:8080/tst/app/kibana`); + expect(absoluteUrl).toBe(`https://localhost:5601/sbp/app/kibana`); }); test(`uses kibanaServer.hostname if specified`, () => { const settings = { - 'xpack.reporting.kibanaServer.hostname': 'something-else' + 'xpack.reporting.kibanaServer.hostname': 'something-else', }; const mockServer = createMockServer({ settings }); const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); const absoluteUrl = getAbsoluteUrl(); - expect(absoluteUrl).toBe(`http://something-else:8080/tst/app/kibana`); + expect(absoluteUrl).toBe(`http://something-else:5601/sbp/app/kibana`); }); test(`uses kibanaServer.port if specified`, () => { const settings = { - 'xpack.reporting.kibanaServer.port': 8008 + 'xpack.reporting.kibanaServer.port': 8008, }; const mockServer = createMockServer({ settings }); const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); const absoluteUrl = getAbsoluteUrl(); - expect(absoluteUrl).toBe(`http://something:8008/tst/app/kibana`); + expect(absoluteUrl).toBe(`http://localhost:8008/sbp/app/kibana`); }); test(`uses the provided hash`, () => { - const mockServer = createMockServer(); + const mockServer = createMockServer(''); const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); const hash = '/hash'; const absoluteUrl = getAbsoluteUrl({ hash }); - expect(absoluteUrl).toBe(`http://something:8080/tst/app/kibana#${hash}`); + expect(absoluteUrl).toBe(`http://localhost:5601/sbp/app/kibana#${hash}`); }); test(`uses the provided hash with queryString`, () => { - const mockServer = createMockServer(); + const mockServer = createMockServer(''); const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); const hash = '/hash?querystring'; const absoluteUrl = getAbsoluteUrl({ hash }); - expect(absoluteUrl).toBe(`http://something:8080/tst/app/kibana#${hash}`); + expect(absoluteUrl).toBe(`http://localhost:5601/sbp/app/kibana#${hash}`); }); test(`uses the provided basePath`, () => { - const mockServer = createMockServer(); + const mockServer = createMockServer(''); const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); const absoluteUrl = getAbsoluteUrl({ basePath: '/s/marketing' }); - expect(absoluteUrl).toBe(`http://something:8080/s/marketing/app/kibana`); + expect(absoluteUrl).toBe(`http://localhost:5601/s/marketing/app/kibana`); }); test(`uses the path`, () => { - const mockServer = createMockServer(); + const mockServer = createMockServer(''); const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); const path = '/app/canvas'; const absoluteUrl = getAbsoluteUrl({ path }); - expect(absoluteUrl).toBe(`http://something:8080/tst${path}`); + expect(absoluteUrl).toBe(`http://localhost:5601/sbp${path}`); }); test(`uses the search`, () => { - const mockServer = createMockServer(); + const mockServer = createMockServer(''); const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); const search = '_t=123456789'; const absoluteUrl = getAbsoluteUrl({ search }); - expect(absoluteUrl).toBe(`http://something:8080/tst/app/kibana?${search}`); + expect(absoluteUrl).toBe(`http://localhost:5601/sbp/app/kibana?${search}`); }); - - diff --git a/x-pack/plugins/reporting/export_types/common/execute_job/get_absolute_url.js b/x-pack/plugins/reporting/export_types/common/execute_job/get_absolute_url.ts similarity index 79% rename from x-pack/plugins/reporting/export_types/common/execute_job/get_absolute_url.js rename to x-pack/plugins/reporting/export_types/common/execute_job/get_absolute_url.ts index bc3fefe36f049..04bdb2bc70465 100644 --- a/x-pack/plugins/reporting/export_types/common/execute_job/get_absolute_url.js +++ b/x-pack/plugins/reporting/export_types/common/execute_job/get_absolute_url.ts @@ -5,24 +5,26 @@ */ import url from 'url'; +// @ts-ignore import { oncePerServer } from '../../../server/lib/once_per_server'; +import { ConfigObject, KbnServer } from '../../../types'; -function getAbsoluteUrlFn(server) { - const config = server.config(); +function getAbsoluteUrlFn(server: KbnServer) { + const config: ConfigObject = server.config(); return function getAbsoluteUrl({ basePath = config.get('server.basePath'), - hash, + hash = '', path = '/app/kibana', - search + search = '', } = {}) { return url.format({ protocol: config.get('xpack.reporting.kibanaServer.protocol') || server.info.protocol, hostname: config.get('xpack.reporting.kibanaServer.hostname') || config.get('server.host'), port: config.get('xpack.reporting.kibanaServer.port') || config.get('server.port'), pathname: basePath + path, - hash: hash, - search + hash, + search, }); }; } diff --git a/x-pack/plugins/reporting/export_types/common/execute_job/get_conditional_headers.test.ts b/x-pack/plugins/reporting/export_types/common/execute_job/get_conditional_headers.test.ts new file mode 100644 index 0000000000000..65949a5c68e51 --- /dev/null +++ b/x-pack/plugins/reporting/export_types/common/execute_job/get_conditional_headers.test.ts @@ -0,0 +1,199 @@ +/* + * 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 { createMockServer } from '../../../test_helpers/create_mock_server'; +import { getConditionalHeaders, getCustomLogo } from './index'; + +let mockServer: any; +beforeEach(() => { + mockServer = createMockServer(''); +}); + +describe('conditions', () => { + test(`uses hostname from reporting config if set`, async () => { + const settings: any = { + 'xpack.reporting.kibanaServer.hostname': 'custom-hostname', + }; + + mockServer = createMockServer({ settings }); + + const permittedHeaders = { + foo: 'bar', + baz: 'quix', + }; + + const { conditionalHeaders } = await getConditionalHeaders({ + job: {}, + filteredHeaders: permittedHeaders, + server: mockServer, + }); + + expect(conditionalHeaders.conditions.hostname).toEqual( + mockServer.config().get('xpack.reporting.kibanaServer.hostname') + ); + }); + + test(`uses hostname from server.config if reporting config not set`, async () => { + const permittedHeaders = { + foo: 'bar', + baz: 'quix', + }; + + const { conditionalHeaders } = await getConditionalHeaders({ + job: {}, + filteredHeaders: permittedHeaders, + server: mockServer, + }); + + expect(conditionalHeaders.conditions.hostname).toEqual(mockServer.config().get('server.host')); + }); + + test(`uses port from reporting config if set`, async () => { + const settings = { + 'xpack.reporting.kibanaServer.port': 443, + }; + + mockServer = createMockServer({ settings }); + + const permittedHeaders = { + foo: 'bar', + baz: 'quix', + }; + + const { conditionalHeaders } = await getConditionalHeaders({ + job: {}, + filteredHeaders: permittedHeaders, + server: mockServer, + }); + + expect(conditionalHeaders.conditions.port).toEqual( + mockServer.config().get('xpack.reporting.kibanaServer.port') + ); + }); + + test(`uses port from server if reporting config not set`, async () => { + const permittedHeaders = { + foo: 'bar', + baz: 'quix', + }; + + const { conditionalHeaders } = await getConditionalHeaders({ + job: {}, + filteredHeaders: permittedHeaders, + server: mockServer, + }); + + expect(conditionalHeaders.conditions.port).toEqual(mockServer.config().get('server.port')); + }); + + test(`uses basePath from server config`, async () => { + const permittedHeaders = { + foo: 'bar', + baz: 'quix', + }; + + const { conditionalHeaders } = await getConditionalHeaders({ + job: {}, + filteredHeaders: permittedHeaders, + server: mockServer, + }); + + expect(conditionalHeaders.conditions.basePath).toEqual( + mockServer.config().get('server.basePath') + ); + }); + + test(`uses protocol from reporting config if set`, async () => { + const settings = { + 'xpack.reporting.kibanaServer.protocol': 'https', + }; + + mockServer = createMockServer({ settings }); + + const permittedHeaders = { + foo: 'bar', + baz: 'quix', + }; + + const { conditionalHeaders } = await getConditionalHeaders({ + job: {}, + filteredHeaders: permittedHeaders, + server: mockServer, + }); + + expect(conditionalHeaders.conditions.protocol).toEqual( + mockServer.config().get('xpack.reporting.kibanaServer.protocol') + ); + }); + + test(`uses protocol from server.info`, async () => { + const permittedHeaders = { + foo: 'bar', + baz: 'quix', + }; + + const { conditionalHeaders } = await getConditionalHeaders({ + job: {}, + filteredHeaders: permittedHeaders, + server: mockServer, + }); + + expect(conditionalHeaders.conditions.protocol).toEqual(mockServer.info.protocol); + }); +}); + +test('uses basePath from job when creating saved object service', async () => { + const permittedHeaders = { + foo: 'bar', + baz: 'quix', + }; + + const { conditionalHeaders } = await getConditionalHeaders({ + job: {}, + filteredHeaders: permittedHeaders, + server: mockServer, + }); + + const logo = 'custom-logo'; + mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo); + + const jobBasePath = '/sbp/s/marketing'; + await getCustomLogo({ + job: { basePath: jobBasePath }, + conditionalHeaders, + server: mockServer, + }); + + expect(mockServer.savedObjects.getScopedSavedObjectsClient.mock.calls[0][0].getBasePath()).toBe( + jobBasePath + ); +}); + +test(`uses basePath from server if job doesn't have a basePath when creating saved object service`, async () => { + const permittedHeaders = { + foo: 'bar', + baz: 'quix', + }; + + const { conditionalHeaders } = await getConditionalHeaders({ + job: {}, + filteredHeaders: permittedHeaders, + server: mockServer, + }); + + const logo = 'custom-logo'; + mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo); + + await getCustomLogo({ + job: {}, + conditionalHeaders, + server: mockServer, + }); + + expect(mockServer.savedObjects.getScopedSavedObjectsClient.mock.calls[0][0].getBasePath()).toBe( + '/sbp' + ); +}); diff --git a/x-pack/plugins/reporting/export_types/common/execute_job/get_conditional_headers.ts b/x-pack/plugins/reporting/export_types/common/execute_job/get_conditional_headers.ts new file mode 100644 index 0000000000000..155e38fbdff46 --- /dev/null +++ b/x-pack/plugins/reporting/export_types/common/execute_job/get_conditional_headers.ts @@ -0,0 +1,30 @@ +/* + * 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 { ConditionalHeaders, ConfigObject, KbnServer, ReportingJob } from '../../../types'; + +export const getConditionalHeaders = ({ + job, + filteredHeaders, + server, +}: { + job: ReportingJob; + filteredHeaders: Record; + server: KbnServer; +}) => { + const config: ConfigObject = server.config(); + + const conditionalHeaders: ConditionalHeaders = { + headers: filteredHeaders, + conditions: { + hostname: config.get('xpack.reporting.kibanaServer.hostname') || config.get('server.host'), + port: config.get('xpack.reporting.kibanaServer.port') || config.get('server.port'), + basePath: config.get('server.basePath'), + protocol: config.get('xpack.reporting.kibanaServer.protocol') || server.info.protocol, + }, + }; + + return { job, conditionalHeaders, server }; +}; diff --git a/x-pack/plugins/reporting/export_types/common/execute_job/get_custom_logo.test.ts b/x-pack/plugins/reporting/export_types/common/execute_job/get_custom_logo.test.ts new file mode 100644 index 0000000000000..8ab96af979cd8 --- /dev/null +++ b/x-pack/plugins/reporting/export_types/common/execute_job/get_custom_logo.test.ts @@ -0,0 +1,36 @@ +/* + * 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 { createMockServer } from '../../../test_helpers/create_mock_server'; +import { getConditionalHeaders, getCustomLogo } from './index'; + +let mockServer: any; +beforeEach(() => { + mockServer = createMockServer(''); +}); + +test(`gets logo from uiSettings`, async () => { + const permittedHeaders = { + foo: 'bar', + baz: 'quix', + }; + + const { conditionalHeaders } = await getConditionalHeaders({ + job: {}, + filteredHeaders: permittedHeaders, + server: mockServer, + }); + + const { logo } = await getCustomLogo({ + job: {}, + conditionalHeaders, + server: mockServer, + }); + + mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo); + + expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo'); +}); diff --git a/x-pack/plugins/reporting/export_types/common/execute_job/get_custom_logo.ts b/x-pack/plugins/reporting/export_types/common/execute_job/get_custom_logo.ts new file mode 100644 index 0000000000000..0b25187a938ae --- /dev/null +++ b/x-pack/plugins/reporting/export_types/common/execute_job/get_custom_logo.ts @@ -0,0 +1,37 @@ +/* + * 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 { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../common/constants'; +import { ConditionalHeaders, KbnServer, ReportingJob } from '../../../types'; + +export const getCustomLogo = async ({ + job, + conditionalHeaders, + server, +}: { + job: ReportingJob; + conditionalHeaders: ConditionalHeaders; + server: KbnServer; +}) => { + const serverBasePath: string = server.config().get('server.basePath'); + + const fakeRequest: any = { + headers: conditionalHeaders.headers, + // This is used by the spaces SavedObjectClientWrapper to determine the existing space. + // We use the basePath from the saved job, which we'll have post spaces being implemented; + // or we use the server base path, which uses the default space + getBasePath: () => job.basePath || serverBasePath, + }; + + const savedObjects = server.savedObjects; + + const savedObjectsClient = savedObjects.getScopedSavedObjectsClient(fakeRequest); + + const uiSettings = server.uiSettingsServiceFactory({ savedObjectsClient }); + + const logo = await uiSettings.get(UI_SETTINGS_CUSTOM_PDF_LOGO); + + return { job, conditionalHeaders, logo, server }; +}; diff --git a/x-pack/plugins/reporting/export_types/common/execute_job/index.ts b/x-pack/plugins/reporting/export_types/common/execute_job/index.ts new file mode 100644 index 0000000000000..33e61f57e9fe3 --- /dev/null +++ b/x-pack/plugins/reporting/export_types/common/execute_job/index.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +export { addForceNowQuerystring } from './add_force_now_query_string'; +export { decryptJobHeaders } from './decrypt_job_headers'; +export { getConditionalHeaders } from './get_conditional_headers'; +export { getCustomLogo } from './get_custom_logo'; +export { omitBlacklistedHeaders } from './omit_blacklisted_headers'; +export { getAbsoluteUrlFactory } from './get_absolute_url'; diff --git a/x-pack/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.test.ts b/x-pack/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.test.ts new file mode 100644 index 0000000000000..5ee21d89aed56 --- /dev/null +++ b/x-pack/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { createMockServer } from '../../../test_helpers/create_mock_server'; +import { omitBlacklistedHeaders } from './index'; + +let mockServer: any; +beforeEach(() => { + mockServer = createMockServer(''); +}); + +test(`omits blacklisted headers`, async () => { + const permittedHeaders = { + foo: 'bar', + baz: 'quix', + }; + + const blacklistedHeaders = { + 'accept-encoding': '', + 'content-length': '', + 'content-type': '', + host: '', + 'transfer-encoding': '', + }; + + const { filteredHeaders } = await omitBlacklistedHeaders({ + job: {}, + decryptedHeaders: { + ...permittedHeaders, + ...blacklistedHeaders, + }, + server: mockServer, + }); + + expect(filteredHeaders).toEqual(permittedHeaders); +}); diff --git a/x-pack/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.ts b/x-pack/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.ts new file mode 100644 index 0000000000000..a334d57a73f68 --- /dev/null +++ b/x-pack/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.ts @@ -0,0 +1,24 @@ +/* + * 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 { omit } from 'lodash'; +import { KBN_SCREENSHOT_HEADER_BLACKLIST } from '../../../common/constants'; +import { KbnServer, ReportingJob } from '../../../types'; + +export const omitBlacklistedHeaders = ({ + job, + decryptedHeaders, + server, +}: { + job: ReportingJob; + decryptedHeaders: Record; + server: KbnServer; +}) => { + const filteredHeaders: Record = omit( + decryptedHeaders, + KBN_SCREENSHOT_HEADER_BLACKLIST + ); + return { job, filteredHeaders, server }; +}; diff --git a/x-pack/plugins/reporting/export_types/png/server/execute_job/index.js b/x-pack/plugins/reporting/export_types/png/server/execute_job/index.js index 0059f3d4a6359..d9edbd7e0c6de 100644 --- a/x-pack/plugins/reporting/export_types/png/server/execute_job/index.js +++ b/x-pack/plugins/reporting/export_types/png/server/execute_job/index.js @@ -4,101 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import url from 'url'; import * as Rx from 'rxjs'; import { mergeMap, catchError, map, takeUntil } from 'rxjs/operators'; import { oncePerServer } from '../../../../server/lib/once_per_server'; import { generatePngObservableFactory } from '../lib/generate_png'; -import { cryptoFactory } from '../../../../server/lib/crypto'; -import { getAbsoluteUrlFactory } from '../../../common/execute_job/get_absolute_url'; -import { omit } from 'lodash'; - -const KBN_SCREENSHOT_HEADER_BLACKLIST = [ - 'accept-encoding', - 'content-length', - 'content-type', - 'host', - 'referer', - // `Transfer-Encoding` is hop-by-hop header that is meaningful - // only for a single transport-level connection, and shouldn't - // be stored by caches or forwarded by proxies. - 'transfer-encoding', -]; +import { decryptJobHeaders, omitBlacklistedHeaders, getConditionalHeaders, addForceNowQuerystring } from '../../../common/execute_job/'; function executeJobFn(server) { const generatePngObservable = generatePngObservableFactory(server); - const crypto = cryptoFactory(server); - const getAbsoluteUrl = getAbsoluteUrlFactory(server); - const config = server.config(); - - const decryptJobHeaders = async (job) => { - const decryptedHeaders = await crypto.decrypt(job.headers); - return { job, decryptedHeaders }; - }; - - const omitBlacklistedHeaders = ({ job, decryptedHeaders }) => { - const filteredHeaders = omit(decryptedHeaders, KBN_SCREENSHOT_HEADER_BLACKLIST); - return { job, filteredHeaders }; - }; - - const getSavedObjectAbsoluteUrl = (job, relativeUrl) => { - if (relativeUrl) { - const { pathname: path, hash, search } = url.parse(relativeUrl); - return getAbsoluteUrl({ basePath: job.basePath, path, hash, search }); - } - - throw new Error(`Unable to generate report. Url is not defined.`); - }; - - const getConditionalHeaders = ({ job, filteredHeaders }) => { - const conditionalHeaders = { - headers: filteredHeaders, - conditions: { - hostname: config.get('xpack.reporting.kibanaServer.hostname') || config.get('server.host'), - port: config.get('xpack.reporting.kibanaServer.port') || config.get('server.port'), - basePath: config.get('server.basePath'), - protocol: config.get('xpack.reporting.kibanaServer.protocol') || server.info.protocol, - } - }; - - return { job, conditionalHeaders }; - }; - - const addForceNowQuerystring = async ({ job, conditionalHeaders }) => { - - const jobUrl = getSavedObjectAbsoluteUrl(job, job.relativeUrl); - - if (!job.forceNow) { - return { job, conditionalHeaders, hashUrl: jobUrl }; - } - - const parsed = url.parse(jobUrl, true); - const hash = url.parse(parsed.hash.replace(/^#/, ''), true); - - const transformedHash = url.format({ - pathname: hash.pathname, - query: { - ...hash.query, - forceNow: job.forceNow - } - }); - - const hashUrl = url.format({ - ...parsed, - hash: transformedHash - }); - - return { job, conditionalHeaders, hashUrl }; - }; return function executeJob(jobToExecute, cancellationToken) { - const process$ = Rx.of(jobToExecute).pipe( + const process$ = Rx.of({ job: jobToExecute, server }).pipe( mergeMap(decryptJobHeaders), catchError(() => Rx.throwError('Failed to decrypt report job data. Please re-generate this report.')), map(omitBlacklistedHeaders), map(getConditionalHeaders), mergeMap(addForceNowQuerystring), - mergeMap(({ job, conditionalHeaders, hashUrl }) => { + mergeMap(({ job, conditionalHeaders, urls }) => { + const hashUrl = urls[0]; return generatePngObservable(hashUrl, job.browserTimezone, conditionalHeaders, job.layout); }), map(buffer => ({ diff --git a/x-pack/plugins/reporting/export_types/png/server/execute_job/index.test.js b/x-pack/plugins/reporting/export_types/png/server/execute_job/index.test.js index 7594bec96eec4..90a80c3399b17 100644 --- a/x-pack/plugins/reporting/export_types/png/server/execute_job/index.test.js +++ b/x-pack/plugins/reporting/export_types/png/server/execute_job/index.test.js @@ -60,194 +60,6 @@ const encryptHeaders = async (headers) => { return await crypto.encrypt(headers); }; -describe('headers', () => { - test(`fails if no URL is passed`, async () => { - const executeJob = executeJobFactory(mockServer); - await expect(executeJob({ timeRange: {} }, cancellationToken)).rejects.toBeDefined(); - }); - - test(`fails if it can't decrypt headers`, async () => { - const executeJob = executeJobFactory(mockServer); - await expect(executeJob({ relativeUrl: '/app/kibana#/something', timeRange: {} }, cancellationToken)).rejects.toBeDefined(); - }); - - test(`passes in decrypted headers to generatePng`, async () => { - const headers = { - foo: 'bar', - baz: 'quix', - }; - - const generatePngObservable = generatePngObservableFactory(); - generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const encryptedHeaders = await encryptHeaders(headers); - const executeJob = executeJobFactory(mockServer); - await executeJob({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }, cancellationToken); - - expect(generatePngObservable).toBeCalledWith('http://localhost:5601/sbp/app/kibana#/something', undefined, expect.objectContaining({ - headers: headers - }), undefined); - }); - - test(`omits blacklisted headers`, async () => { - const permittedHeaders = { - foo: 'bar', - baz: 'quix', - }; - - const blacklistedHeaders = { - 'accept-encoding': '', - 'content-length': '', - 'content-type': '', - 'host': '', - 'transfer-encoding': '', - }; - - const encryptedHeaders = await encryptHeaders({ - ...permittedHeaders, - ...blacklistedHeaders - }); - - const generatePngObservable = generatePngObservableFactory(); - generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - await executeJob({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }, cancellationToken); - - expect(generatePngObservable).toBeCalledWith('http://localhost:5601/sbp/app/kibana#/something', undefined, expect.objectContaining({ - headers: permittedHeaders - }), undefined); - }); - - describe('conditions', () => { - test(`uses hostname from reporting config if set`, async () => { - config['xpack.reporting.kibanaServer.hostname'] = 'custom-hostname'; - - const encryptedHeaders = await encryptHeaders({}); - - const generatePngObservable = generatePngObservableFactory(); - generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - await executeJob({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }, cancellationToken); - - //expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:custompngLogo'); - expect(generatePngObservable).toBeCalledWith('http://custom-hostname:5601/sbp/app/kibana#/something', undefined, expect.objectContaining({ - headers: expect.anything(), - conditions: expect.objectContaining({ - hostname: config['xpack.reporting.kibanaServer.hostname'] - }) - }), undefined); - }); - - test(`uses hostname from server.config if reporting config not set`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - const generatePngObservable = generatePngObservableFactory(); - generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - await executeJob({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }, cancellationToken); - - expect(generatePngObservable).toBeCalledWith('http://localhost:5601/sbp/app/kibana#/something', undefined, expect.objectContaining({ - headers: expect.anything(), - conditions: expect.objectContaining({ - hostname: config['server.host'] - }) - }), undefined); - }); - - test(`uses port from reporting config if set`, async () => { - config['xpack.reporting.kibanaServer.port'] = 443; - - const encryptedHeaders = await encryptHeaders({}); - - const generatePngObservable = generatePngObservableFactory(); - generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - await executeJob({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }, cancellationToken); - - expect(generatePngObservable).toBeCalledWith('http://localhost:443/sbp/app/kibana#/something', undefined, expect.objectContaining({ - headers: expect.anything(), - conditions: expect.objectContaining({ - port: config['xpack.reporting.kibanaServer.port'] - }) - }), undefined); - }); - - test(`uses port from server if reporting config not set`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - const generatePngObservable = generatePngObservableFactory(); - generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - await executeJob({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }, cancellationToken); - - expect(generatePngObservable).toBeCalledWith('http://localhost:5601/sbp/app/kibana#/something', undefined, expect.objectContaining({ - headers: expect.anything(), - conditions: expect.objectContaining({ - port: config['server.port'] - }) - }), undefined); - }); - - test(`uses basePath from server config`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - const generatePngObservable = generatePngObservableFactory(); - generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - await executeJob({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }, cancellationToken); - - expect(generatePngObservable).toBeCalledWith('http://localhost:5601/sbp/app/kibana#/something', undefined, expect.objectContaining({ - headers: expect.anything(), - conditions: expect.objectContaining({ - basePath: config['server.basePath'] - }) - }), undefined); - }); - - test(`uses protocol from reporting config if set`, async () => { - config['xpack.reporting.kibanaServer.protocol'] = 'https'; - - const encryptedHeaders = await encryptHeaders({}); - - const generatePngObservable = generatePngObservableFactory(); - generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - await executeJob({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }, cancellationToken); - - expect(generatePngObservable).toBeCalledWith('https://localhost:5601/sbp/app/kibana#/something', undefined, expect.objectContaining({ - headers: expect.anything(), - conditions: expect.objectContaining({ - protocol: config['xpack.reporting.kibanaServer.protocol'] - }) - }), undefined); - }); - - test(`uses protocol from server.info`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - const generatePngObservable = generatePngObservableFactory(); - generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - await executeJob({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }, cancellationToken); - - expect(generatePngObservable).toBeCalledWith('http://localhost:5601/sbp/app/kibana#/something', undefined, expect.objectContaining({ - headers: expect.anything(), - conditions: expect.objectContaining({ - protocol: mockServer.info.protocol - }) - }), undefined); - }); - }); -}); - test(`passes browserTimezone to generatePng`, async () => { const encryptedHeaders = await encryptHeaders({}); @@ -261,51 +73,6 @@ test(`passes browserTimezone to generatePng`, async () => { expect(generatePngObservable).toBeCalledWith('http://localhost:5601/sbp/app/kibana#/something', browserTimezone, expect.anything(), undefined); }); -test(`adds forceNow to hash's query, if it exists`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - const generatePngObservable = generatePngObservableFactory(); - generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - const forceNow = '2000-01-01T00:00:00.000Z'; - - await executeJob({ relativeUrl: '/app/kibana#/something', forceNow, headers: encryptedHeaders }, cancellationToken); - - expect(generatePngObservable).toBeCalledWith('http://localhost:5601/sbp/app/kibana#/something?forceNow=2000-01-01T00%3A00%3A00.000Z', undefined, expect.anything(), undefined); -}); - -test(`appends forceNow to hash's query, if it exists`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - const generatePngObservable = generatePngObservableFactory(); - generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - const forceNow = '2000-01-01T00:00:00.000Z'; - - await executeJob({ - relativeUrl: '/app/kibana#/something?_g=something', - forceNow, - headers: encryptedHeaders - }, cancellationToken); - - expect(generatePngObservable).toBeCalledWith('http://localhost:5601/sbp/app/kibana#/something?_g=something&forceNow=2000-01-01T00%3A00%3A00.000Z', undefined, expect.anything(), undefined); -}); - -test(`doesn't append forceNow query to url, if it doesn't exists`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - const generatePngObservable = generatePngObservableFactory(); - generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - - await executeJob({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }, cancellationToken); - - expect(generatePngObservable).toBeCalledWith('http://localhost:5601/sbp/app/kibana#/something', undefined, expect.anything(), undefined); -}); - test(`returns content_type of application/png`, async () => { const executeJob = executeJobFactory(mockServer); const encryptedHeaders = await encryptHeaders({}); diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/index.js b/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/index.js index 16553c1a91e41..996fb179c9e09 100644 --- a/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/index.js +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/index.js @@ -4,107 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import url from 'url'; -import { cryptoFactory } from '../../../../server/lib/crypto'; import * as Rx from 'rxjs'; import { mergeMap, catchError, map, takeUntil } from 'rxjs/operators'; -import { omit } from 'lodash'; -import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../../common/constants'; import { oncePerServer } from '../../../../server/lib/once_per_server'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; import { compatibilityShimFactory } from './compatibility_shim'; +import { decryptJobHeaders, omitBlacklistedHeaders, getConditionalHeaders, + addForceNowQuerystring, getCustomLogo } from '../../../common/execute_job/'; -const KBN_SCREENSHOT_HEADER_BLACKLIST = [ - 'accept-encoding', - 'content-length', - 'content-type', - 'host', - 'referer', - // `Transfer-Encoding` is hop-by-hop header that is meaningful - // only for a single transport-level connection, and shouldn't - // be stored by caches or forwarded by proxies. - 'transfer-encoding', -]; function executeJobFn(server) { const generatePdfObservable = generatePdfObservableFactory(server); - const crypto = cryptoFactory(server); const compatibilityShim = compatibilityShimFactory(server); - const config = server.config(); - - const serverBasePath = server.config().get('server.basePath'); - - const decryptJobHeaders = async (job) => { - const decryptedHeaders = await crypto.decrypt(job.headers); - return { job, decryptedHeaders }; - }; - - const omitBlacklistedHeaders = ({ job, decryptedHeaders }) => { - const filteredHeaders = omit(decryptedHeaders, KBN_SCREENSHOT_HEADER_BLACKLIST); - return { job, filteredHeaders }; - }; - - const getConditionalHeaders = ({ job, filteredHeaders }) => { - const conditionalHeaders = { - headers: filteredHeaders, - conditions: { - hostname: config.get('xpack.reporting.kibanaServer.hostname') || config.get('server.host'), - port: config.get('xpack.reporting.kibanaServer.port') || config.get('server.port'), - basePath: config.get('server.basePath'), - protocol: config.get('xpack.reporting.kibanaServer.protocol') || server.info.protocol, - } - }; - - return { job, conditionalHeaders }; - }; - - const getCustomLogo = async ({ job, conditionalHeaders }) => { - const fakeRequest = { - headers: conditionalHeaders.headers, - // This is used by the spaces SavedObjectClientWrapper to determine the existing space. - // We use the basePath from the saved job, which we'll have post spaces being implemented; - // or we use the server base path, which uses the default space - getBasePath: () => job.basePath || serverBasePath - }; - - const savedObjects = server.savedObjects; - const savedObjectsClient = savedObjects.getScopedSavedObjectsClient(fakeRequest); - const uiSettings = server.uiSettingsServiceFactory({ - savedObjectsClient - }); - - const logo = await uiSettings.get(UI_SETTINGS_CUSTOM_PDF_LOGO); - - return { job, conditionalHeaders, logo }; - }; - - const addForceNowQuerystring = async ({ job, conditionalHeaders, logo }) => { - const urls = job.urls.map(jobUrl => { - if (!job.forceNow) { - return jobUrl; - } - - const parsed = url.parse(jobUrl, true); - const hash = url.parse(parsed.hash.replace(/^#/, ''), true); - - const transformedHash = url.format({ - pathname: hash.pathname, - query: { - ...hash.query, - forceNow: job.forceNow - } - }); - - return url.format({ - ...parsed, - hash: transformedHash - }); - }); - return { job, conditionalHeaders, logo, urls }; - }; return compatibilityShim(function executeJob(jobToExecute, cancellationToken) { - const process$ = Rx.of(jobToExecute).pipe( + const process$ = Rx.of({ job: jobToExecute, server }).pipe( mergeMap(decryptJobHeaders), catchError(() => Rx.throwError('Failed to decrypt report job data. Please re-generate this report.')), map(omitBlacklistedHeaders), diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js b/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js index dccfbe7a5f567..9b2d2418a0637 100644 --- a/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js @@ -60,242 +60,6 @@ const encryptHeaders = async (headers) => { return await crypto.encrypt(headers); }; -describe('headers', () => { - test(`fails if it can't decrypt headers`, async () => { - const executeJob = executeJobFactory(mockServer); - await expect(executeJob({ objects: [], timeRange: {} }, cancellationToken)).rejects.toBeDefined(); - }); - - test(`passes in decrypted headers to generatePdf`, async () => { - const headers = { - foo: 'bar', - baz: 'quix', - }; - - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const encryptedHeaders = await encryptHeaders(headers); - const executeJob = executeJobFactory(mockServer); - await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken); - - expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({ - headers: headers - }), undefined, undefined); - }); - - test(`omits blacklisted headers`, async () => { - const permittedHeaders = { - foo: 'bar', - baz: 'quix', - }; - - const blacklistedHeaders = { - 'accept-encoding': '', - 'content-length': '', - 'content-type': '', - 'host': '', - 'transfer-encoding': '', - }; - - const encryptedHeaders = await encryptHeaders({ - ...permittedHeaders, - ...blacklistedHeaders - }); - - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken); - - expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({ - headers: permittedHeaders - }), undefined, undefined); - }); - - describe('conditions', () => { - test(`uses hostname from reporting config if set`, async () => { - config['xpack.reporting.kibanaServer.hostname'] = 'custom-hostname'; - - const encryptedHeaders = await encryptHeaders({}); - - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken); - - expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo'); - expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({ - headers: expect.anything(), - conditions: expect.objectContaining({ - hostname: config['xpack.reporting.kibanaServer.hostname'] - }) - }), undefined, undefined); - }); - - test(`uses hostname from server.config if reporting config not set`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken); - - expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo'); - expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({ - headers: expect.anything(), - conditions: expect.objectContaining({ - hostname: config['server.host'] - }) - }), undefined, undefined); - }); - - test(`uses port from reporting config if set`, async () => { - config['xpack.reporting.kibanaServer.port'] = 443; - - const encryptedHeaders = await encryptHeaders({}); - - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken); - - expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo'); - expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({ - headers: expect.anything(), - conditions: expect.objectContaining({ - port: config['xpack.reporting.kibanaServer.port'] - }) - }), undefined, undefined); - }); - - test(`uses port from server if reporting config not set`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken); - - expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo'); - expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({ - headers: expect.anything(), - conditions: expect.objectContaining({ - port: config['server.port'] - }) - }), undefined, undefined); - }); - - test(`uses basePath from server config`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken); - - expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo'); - expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({ - headers: expect.anything(), - conditions: expect.objectContaining({ - basePath: config['server.basePath'] - }) - }), undefined, undefined); - }); - - test(`uses protocol from reporting config if set`, async () => { - config['xpack.reporting.kibanaServer.protocol'] = 'https'; - - const encryptedHeaders = await encryptHeaders({}); - - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken); - - expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo'); - expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({ - headers: expect.anything(), - conditions: expect.objectContaining({ - protocol: config['xpack.reporting.kibanaServer.protocol'] - }) - }), undefined, undefined); - }); - - test(`uses protocol from server.info`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken); - - expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo'); - expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({ - headers: expect.anything(), - conditions: expect.objectContaining({ - protocol: mockServer.info.protocol - }) - }), undefined, undefined); - }); - }); -}); - -test('uses basePath from job when creating saved object service', async () => { - const encryptedHeaders = await encryptHeaders({}); - - const logo = 'custom-logo'; - mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo); - - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - const jobBasePath = '/sbp/s/marketing'; - await executeJob({ objects: [], headers: encryptedHeaders, basePath: jobBasePath }, cancellationToken); - - expect(mockServer.savedObjects.getScopedSavedObjectsClient.mock.calls[0][0].getBasePath()).toBe(jobBasePath); -}); - -test(`uses basePath from server if job doesn't have a basePath when creating saved object service`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - const logo = 'custom-logo'; - mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo); - - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken); - - expect(mockServer.savedObjects.getScopedSavedObjectsClient.mock.calls[0][0].getBasePath()).toBe('/sbp'); -}); - -test(`gets logo from uiSettings`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - const logo = 'custom-logo'; - mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo); - - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken); - - expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo'); - expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.anything(), undefined, logo); -}); - test(`passes browserTimezone to generatePdf`, async () => { const encryptedHeaders = await encryptHeaders({}); @@ -310,51 +74,6 @@ test(`passes browserTimezone to generatePdf`, async () => { expect(generatePdfObservable).toBeCalledWith(undefined, [], browserTimezone, expect.anything(), undefined, undefined); }); -test(`adds forceNow to hash's query, if it exists`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - const forceNow = '2000-01-01T00:00:00.000Z'; - - await executeJob({ objects: [{ relativeUrl: '/app/kibana#/something' }], forceNow, headers: encryptedHeaders }, cancellationToken); - - expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/sbp/app/kibana#/something?forceNow=2000-01-01T00%3A00%3A00.000Z'], undefined, expect.anything(), undefined, undefined); -}); - -test(`appends forceNow to hash's query, if it exists`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - const forceNow = '2000-01-01T00:00:00.000Z'; - - await executeJob({ - objects: [{ relativeUrl: '/app/kibana#/something?_g=something' }], - forceNow, - headers: encryptedHeaders - }, cancellationToken); - - expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/sbp/app/kibana#/something?_g=something&forceNow=2000-01-01T00%3A00%3A00.000Z'], undefined, expect.anything(), undefined, undefined); -}); - -test(`doesn't append forceNow query to url, if it doesn't exists`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = executeJobFactory(mockServer); - - await executeJob({ objects: [{ relativeUrl: '/app/kibana#/something' }], headers: encryptedHeaders }, cancellationToken); - - expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/sbp/app/kibana#/something'], undefined, expect.anything(), undefined, undefined); -}); - test(`returns content_type of application/pdf`, async () => { const executeJob = executeJobFactory(mockServer); const encryptedHeaders = await encryptHeaders({}); diff --git a/x-pack/plugins/reporting/test_helpers/create_mock_server.ts b/x-pack/plugins/reporting/test_helpers/create_mock_server.ts new file mode 100644 index 0000000000000..953ecd1e7eef5 --- /dev/null +++ b/x-pack/plugins/reporting/test_helpers/create_mock_server.ts @@ -0,0 +1,44 @@ +/* + * 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 { memoize } from 'lodash'; + +export const createMockServer = ({ settings = {} }: any) => { + const mockServer = { + expose: () => { + ' '; + }, + config: memoize(() => ({ get: jest.fn() })), + info: { + protocol: 'http', + }, + plugins: { + elasticsearch: { + getCluster: memoize(() => { + return { + callWithRequest: jest.fn(), + }; + }), + }, + }, + savedObjects: { + getScopedSavedObjectsClient: jest.fn(), + }, + uiSettingsServiceFactory: jest.fn().mockReturnValue({ get: jest.fn() }), + }; + + const defaultSettings: any = { + 'xpack.reporting.encryptionKey': 'testencryptionkey', + 'server.basePath': '/sbp', + 'server.host': 'localhost', + 'server.port': 5601, + 'xpack.reporting.kibanaServer': {}, + }; + mockServer.config().get.mockImplementation((key: any) => { + return key in settings ? settings[key] : defaultSettings[key]; + }); + + return mockServer; +}; diff --git a/x-pack/plugins/reporting/types.d.ts b/x-pack/plugins/reporting/types.d.ts index cca035d6d5e52..c12d03dc2990e 100644 --- a/x-pack/plugins/reporting/types.d.ts +++ b/x-pack/plugins/reporting/types.d.ts @@ -3,12 +3,29 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +interface UiSettings { + get: (value: string) => string; +} + +type SavedObjectClient = any; + +// these types shoud be in core kibana and are only here temporarily export interface KbnServer { + info: { protocol: string }; config: () => ConfigObject; + savedObjects: { + getScopedSavedObjectsClient: ( + fakeRequest: { headers: object; getBasePath: () => string } + ) => SavedObjectClient; + }; + uiSettingsServiceFactory: ( + { savedObjectsClient }: { savedObjectsClient: SavedObjectClient } + ) => UiSettings; } export interface ConfigObject { - get: (path: string) => any; + get: (path?: string) => any; } export interface Size { @@ -65,3 +82,16 @@ export interface ConditionalHeadersConditions { port: number; basePath: string; } + +export interface CryptoFactory { + decrypt: (headers?: Record) => string; +} +export interface ReportingJob { + headers?: Record; + basePath?: string; + urls?: string[]; + relativeUrl?: string; + forceNow?: string; + timeRange?: any; + objects?: [any]; +} diff --git a/x-pack/test/reporting/configs/functional.js b/x-pack/test/reporting/configs/functional.js index fdc33d8cf2cd9..82d38ccde3119 100644 --- a/x-pack/test/reporting/configs/functional.js +++ b/x-pack/test/reporting/configs/functional.js @@ -22,5 +22,8 @@ export async function getFunctionalConfig({ readConfigFile }) { reportName: 'X-Pack Reporting Functional Tests', }, kbnTestServer: xPackFunctionalTestsConfig.get('kbnTestServer'), + testFiles: [require.resolve('../functional')], }; } + +export default getFunctionalConfig; \ No newline at end of file diff --git a/x-pack/test/reporting/functional/lib/common.js b/x-pack/test/reporting/functional/lib/common.js deleted file mode 100644 index b21c41d9c4109..0000000000000 --- a/x-pack/test/reporting/functional/lib/common.js +++ /dev/null @@ -1,46 +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 fs from 'fs'; -import pixelmatch from 'pixelmatch'; -import { PNG } from 'pngjs'; - -export async function comparePngs(actualPath, expectedPath, diffPath, log) { - log.debug(`comparePngs: ${actualPath} vs ${expectedPath}`); - return new Promise(resolve => { - const actual = fs.createReadStream(actualPath).pipe(new PNG()).on('parsed', doneReading); - const expected = fs.createReadStream(expectedPath).pipe(new PNG()).on('parsed', doneReading); - let filesRead = 0; - - // Note that this threshold value only affects color comparison from pixel to pixel. It won't have - // any affect when comparing neighboring pixels - so slight shifts, font variations, or "blurry-ness" - // will still show up as diffs, but upping this will not help that. Instead we keep the threshold low, and expect - // some the diffCount to be lower than our own threshold value. - const THRESHOLD = .1; - - function doneReading() { - if (++filesRead < 2) return; - const diffPng = new PNG({ width: actual.width, height: actual.height }); - log.debug(`calculating diff pixels...`); - const diffPixels = pixelmatch( - actual.data, - expected.data, - diffPng.data, - actual.width, - actual.height, - { - threshold: THRESHOLD, - // Adding this doesn't seem to make a difference at all, but ideally we want to avoid picking up anti aliasing - // differences from fonts on different OSs. - includeAA: true - } - ); - log.debug(`diff pixels: ${diffPixels}`); - diffPng.pack().pipe(fs.createWriteStream(diffPath)); - resolve(diffPixels); - } - }); -} \ No newline at end of file diff --git a/x-pack/test/reporting/functional/lib/compare_pdfs.js b/x-pack/test/reporting/functional/lib/compare_pdfs.js index 9609e483442e6..f7fbfe051b844 100644 --- a/x-pack/test/reporting/functional/lib/compare_pdfs.js +++ b/x-pack/test/reporting/functional/lib/compare_pdfs.js @@ -10,7 +10,7 @@ import { promisify } from 'bluebird'; import mkdirp from 'mkdirp'; import { PDFImage } from 'pdf-image'; import PDFJS from 'pdfjs-dist'; -import { comparePngs } from './common'; +import { comparePngs } from '../../../../../test/functional/services/lib/compare_pngs'; const mkdirAsync = promisify(mkdirp); @@ -44,8 +44,8 @@ export async function checkIfPdfsMatch(actualPdfPath, baselinePdfPath, screensho fs.writeFileSync(actualCopyPath, fs.readFileSync(actualPdfPath)); const convertOptions = { - '-density': '300', }; + const actualPdfImage = new PDFImage(actualCopyPath, { convertOptions }); const expectedPdfImage = new PDFImage(baselineCopyPath, { convertOptions }); @@ -70,7 +70,7 @@ export async function checkIfPdfsMatch(actualPdfPath, baselinePdfPath, screensho log.debug(`Converting actual pdf page ${pageNum} to png`); const actualPagePng = await actualPdfImage.convertPage(pageNum); const diffPngPath = path.resolve(failureDirectoryPath, `${baselinePdfFileName}-${pageNum}.png`); - diffTotal += await comparePngs(actualPagePng, expectedPagePng, diffPngPath, log); + diffTotal += await comparePngs(actualPagePng, expectedPagePng, diffPngPath, sessionDirectoryPath, log); pageNum++; } diff --git a/x-pack/test/reporting/functional/lib/compare_pngs.js b/x-pack/test/reporting/functional/lib/compare_pngs.js index 105be520a1607..afbebc03406fa 100644 --- a/x-pack/test/reporting/functional/lib/compare_pngs.js +++ b/x-pack/test/reporting/functional/lib/compare_pngs.js @@ -8,7 +8,7 @@ import path from 'path'; import fs from 'fs'; import { promisify } from 'bluebird'; import mkdirp from 'mkdirp'; -import { comparePngs } from './common'; +import { comparePngs } from '../../../../../test/functional/services/lib/compare_pngs'; const mkdirAsync = promisify(mkdirp); @@ -44,7 +44,7 @@ export async function checkIfPngsMatch(actualpngPath, baselinepngPath, screensho let diffTotal = 0; const diffPngPath = path.resolve(failureDirectoryPath, `${baselinepngFileName}-${1}.png`); - diffTotal += await comparePngs(actualCopyPath, baselineCopyPath, diffPngPath, log); + diffTotal += await comparePngs(actualCopyPath, baselineCopyPath, diffPngPath, sessionDirectoryPath, log); return diffTotal; diff --git a/x-pack/test/reporting/functional/reporting.js b/x-pack/test/reporting/functional/reporting.js index 196b5ffeafd83..76acff0dde8b2 100644 --- a/x-pack/test/reporting/functional/reporting.js +++ b/x-pack/test/reporting/functional/reporting.js @@ -106,19 +106,21 @@ export default function ({ getService, getPageObjects }) { await PageObjects.reporting.checkUsePrintLayout(); await PageObjects.reporting.clickGenerateReportButton(); await PageObjects.reporting.clickDownloadReportButton(60000); + PageObjects.reporting.clearToastNotifications(); const url = await PageObjects.reporting.getUrlOfTab(1); await PageObjects.reporting.closeTab(1); const reportData = await PageObjects.reporting.getRawPdfReportData(url); const reportFileName = 'dashboard_print'; const sessionReportPath = await writeSessionReport(reportFileName, reportData); - const diffCount = await checkIfPdfsMatch( + const percentSimilar = await checkIfPdfsMatch( sessionReportPath, getBaselineReportPath(reportFileName), config.get('screenshots.directory'), log ); - expect(diffCount).to.be(0); + // After expected OS differences, the diff count came to be around 128k + expect(percentSimilar).to.be.lessThan(0.05); }); it('matches same baseline report with margins turned on', async function () { @@ -134,6 +136,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.reporting.checkUsePrintLayout(); await PageObjects.reporting.clickGenerateReportButton(); await PageObjects.reporting.clickDownloadReportButton(60000); + PageObjects.reporting.clearToastNotifications(); const url = await PageObjects.reporting.getUrlOfTab(1); const reportData = await PageObjects.reporting.getRawPdfReportData(url); @@ -141,13 +144,15 @@ export default function ({ getService, getPageObjects }) { await PageObjects.reporting.closeTab(1); const reportFileName = 'dashboard_print'; const sessionReportPath = await writeSessionReport(reportFileName, reportData); - const diffCount = await checkIfPdfsMatch( + const percentSimilar = await checkIfPdfsMatch( sessionReportPath, getBaselineReportPath(reportFileName), config.get('screenshots.directory'), log ); - expect(diffCount).to.be(0); + // After expected OS differences, the diff count came to be around 128k + expect(percentSimilar).to.be.lessThan(0.05); + }); }); @@ -165,13 +170,16 @@ export default function ({ getService, getPageObjects }) { await PageObjects.reporting.removeForceSharedItemsContainerSize(); await PageObjects.reporting.clickDownloadReportButton(60000); + PageObjects.reporting.clearToastNotifications(); + const url = await PageObjects.reporting.getUrlOfTab(1); await PageObjects.reporting.closeTab(1); const reportData = await PageObjects.reporting.getRawPdfReportData(url); const reportFileName = 'dashboard_preserve_layout'; const sessionReportPath = await writeSessionReport(reportFileName, reportData); - const diffCount = await checkIfPdfsMatch( + + const percentSimilar = await checkIfPdfsMatch( sessionReportPath, getBaselineReportPath(reportFileName), config.get('screenshots.directory'), @@ -180,7 +188,7 @@ export default function ({ getService, getPageObjects }) { // After expected OS differences, the diff count came to be around 350k. Due to // https://github.com/elastic/kibana/issues/21485 this jumped up to something like 368 when // comparing the same baseline for chromium and phantom. - expect(diffCount).to.be.lessThan(400000); + expect(percentSimilar).to.be.lessThan(0.05); }); @@ -229,13 +237,15 @@ export default function ({ getService, getPageObjects }) { await PageObjects.reporting.removeForceSharedItemsContainerSize(); await PageObjects.reporting.clickDownloadReportButton(60000); + PageObjects.reporting.clearToastNotifications(); + const url = await PageObjects.reporting.getUrlOfTab(1); await PageObjects.reporting.closeTab(1); const reportData = await PageObjects.reporting.getRawPdfReportData(url); const reportFileName = 'dashboard_preserve_layout'; const sessionReportPath = await writeSessionReport(reportFileName, reportData, 'png'); - const diffCount = await checkIfPngsMatch( + const percentSimilar = await checkIfPngsMatch( sessionReportPath, getBaselineReportPath(reportFileName, 'png'), config.get('screenshots.directory'), @@ -244,7 +254,7 @@ export default function ({ getService, getPageObjects }) { // After expected OS differences, the diff count came to be around 350k. Due to // https://github.com/elastic/kibana/issues/21485 this jumped up to something like 368 when // comparing the same baseline for chromium and phantom. - expect(diffCount).to.be.lessThan(400000); + expect(percentSimilar).to.be.lessThan(0.05); }); }); @@ -307,6 +317,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.reporting.openPdfReportingPanel(); await PageObjects.reporting.clickGenerateReportButton(); await PageObjects.reporting.clickDownloadReportButton(60000); + PageObjects.reporting.clearToastNotifications(); const url = await PageObjects.reporting.getUrlOfTab(1); const reportData = await PageObjects.reporting.getRawPdfReportData(url); @@ -314,7 +325,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.reporting.closeTab(1); const reportFileName = 'visualize_print'; const sessionReportPath = await writeSessionReport(reportFileName, reportData); - const diffCount = await checkIfPdfsMatch( + const percentSimilar = await checkIfPdfsMatch( sessionReportPath, getBaselineReportPath(reportFileName), config.get('screenshots.directory'), @@ -325,7 +336,7 @@ export default function ({ getService, getPageObjects }) { // which will be much easier when we only support one browser type (chromium instead of phantom). // The reason this is so high currently is because of a phantom bug: // https://github.com/elastic/kibana/issues/21485 - expect(diffCount).to.be.lessThan(810000); + expect(percentSimilar).to.be.lessThan(0.05); }); }); });