From d22c47d47b8ea0d1a2a7cf122b59f2e472d75431 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Thu, 17 Sep 2020 12:04:01 -0700 Subject: [PATCH] [Reporting/Config] allow string representations of duration settings (#74202) * [Reporting/Config] use better schema methods * add createMockConfig * update documentation * fix observable.test * add docs links to common options page * make the schema match the docs * wording edits per feedback * self edits * todo comment * fix tests * feedback change 1 * schema utils * fix goof * use objects for the defaults when available * fix pollInterval * fix snapshots * Update report_listing.tsx * call new ByteSizeValue on server side only * revert xpack.reporting.poll * fix ts * fix snapshot * use correct input for duration * revert reorganize imports Co-authored-by: Elastic Machine --- docs/settings/reporting-settings.asciidoc | 54 ++- .../plugins/reporting/common/schema_utils.ts | 28 ++ .../public/components/report_listing.tsx | 11 +- x-pack/plugins/reporting/public/plugin.tsx | 4 +- .../browsers/chromium/driver_factory/index.ts | 3 +- .../plugins/reporting/server/config/index.ts | 2 + .../reporting/server/config/schema.test.ts | 313 +++++++++++++----- .../plugins/reporting/server/config/schema.ts | 50 +-- .../common/get_conditional_headers.test.ts | 50 +-- .../common/get_custom_logo.test.ts | 16 +- .../export_types/common/get_full_urls.test.ts | 12 +- .../export_types/csv/execute_job.test.ts | 2 + .../export_types/csv/generate_csv/index.ts | 5 +- .../printable_pdf/execute_job/index.test.ts | 22 +- .../server/lib/create_worker.test.ts | 30 +- .../reporting/server/lib/create_worker.ts | 3 +- .../lib/screenshots/get_number_of_items.ts | 7 +- .../server/lib/screenshots/inject_css.ts | 1 + .../server/lib/screenshots/observable.test.ts | 42 ++- .../server/lib/screenshots/open_url.ts | 11 +- .../server/lib/screenshots/wait_for_render.ts | 3 +- .../screenshots/wait_for_visualizations.ts | 20 +- .../server/lib/store/index_timestamp.ts | 1 - .../reporting/server/lib/store/store.test.ts | 38 ++- .../reporting/server/lib/store/store.ts | 3 +- .../plugins/reporting/server/plugin.test.ts | 4 +- .../server/routes/diagnostic/browser.test.ts | 10 +- .../server/routes/diagnostic/browser.ts | 37 ++- .../server/routes/diagnostic/config.test.ts | 10 +- .../server/routes/diagnostic/config.ts | 22 +- .../routes/diagnostic/screenshot.test.ts | 6 +- .../reporting/server/routes/jobs.test.ts | 11 +- .../lib/authorized_user_pre_routing.test.ts | 23 +- .../create_mock_browserdriverfactory.ts | 20 +- .../create_mock_reportingplugin.ts | 62 +++- .../reporting/server/test_helpers/index.ts | 8 +- .../usage/reporting_usage_collector.test.ts | 12 +- 37 files changed, 607 insertions(+), 349 deletions(-) create mode 100644 x-pack/plugins/reporting/common/schema_utils.ts diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 9c8d753a2d668..3489dcd018293 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -104,15 +104,14 @@ security is enabled, `xpack.security.encryptionKey`. [cols="2*<"] |=== | `xpack.reporting.queue.pollInterval` - | Specifies the number of milliseconds that the reporting poller waits between polling the - index for any pending Reporting jobs. Defaults to `3000` (3 seconds). + | Specify the {ref}/common-options.html#time-units[time] that the reporting poller waits between polling the index for any + pending Reporting jobs. Can be specified as number of milliseconds. Defaults to `3s`. | [[xpack-reporting-q-timeout]] `xpack.reporting.queue.timeout` {ess-icon} - | How long each worker has to produce a report. If your machine is slow or under - heavy load, you might need to increase this timeout. Specified in milliseconds. - If a Reporting job execution time goes over this time limit, the job will be - marked as a failure and there will not be a download available. - Defaults to `120000` (two minutes). + | {ref}/common-options.html#time-units[How long] each worker has to produce a report. If your machine is slow or under heavy + load, you might need to increase this timeout. If a Reporting job execution goes over this time limit, the job is marked as a + failure and no download will be available. Can be specified as number of milliseconds. + Defaults to `2m`. |=== @@ -127,24 +126,24 @@ control the capturing process. |=== a| `xpack.reporting.capture.timeouts` `.openUrl` {ess-icon} - | Specify how long to allow the Reporting browser to wait for the "Loading..." screen - to dismiss and find the initial data for the Kibana page. If the time is - exceeded, a page screenshot is captured showing the current state, and the download link shows a warning message. - Defaults to `60000` (1 minute). + | Specify the {ref}/common-options.html#time-units[time] to allow the Reporting browser to wait for the "Loading..." screen + to dismiss and find the initial data for the page. If the time is exceeded, a screenshot is captured showing the current + page, and the download link shows a warning message. Can be specified as number of milliseconds. + Defaults to `1m`. a| `xpack.reporting.capture.timeouts` `.waitForElements` {ess-icon} - | Specify how long to allow the Reporting browser to wait for all visualization - panels to load on the Kibana page. If the time is exceeded, a page screenshot - is captured showing the current state, and the download link shows a warning message. Defaults to `30000` (30 - seconds). + | Specify the {ref}/common-options.html#time-units[time] to allow the Reporting browser to wait for all visualization panels + to load on the page. If the time is exceeded, a screenshot is captured showing the current page, and the download link shows + a warning message. Can be specified as number of milliseconds. + Defaults to `30s`. a| `xpack.reporting.capture.timeouts` `.renderComplete` {ess-icon} - | Specify how long to allow the Reporting browser to wait for all visualizations to - fetch and render the data. If the time is exceeded, a - page screenshot is captured showing the current state, and the download link shows a warning message. Defaults to - `30000` (30 seconds). + | Specify the {ref}/common-options.html#time-units[time] to allow the Reporting browser to wait for all visualizations to + fetch and render the data. If the time is exceeded, a screenshot is captured showing the current page, and the download link shows a + warning message. Can be specified as number of milliseconds. + Defaults to `30s`. |=== @@ -163,11 +162,10 @@ available, but there will likely be errors in the visualizations in the report. job, as many times as this setting. Defaults to `3`. | `xpack.reporting.capture.loadDelay` - | When visualizations are not evented, this is the amount of time before - taking a screenshot. All visualizations that ship with {kib} are evented, so this - setting should not have much effect. If you are seeing empty images instead of - visualizations, try increasing this value. - Defaults to `3000` (3 seconds). + | Specify the {ref}/common-options.html#time-units[amount of time] before taking a screenshot when visualizations are not evented. + All visualizations that ship with {kib} are evented, so this setting should not have much effect. If you are seeing empty images + instead of visualizations, try increasing this value. + Defaults to `3s`. | [[xpack-reporting-browser]] `xpack.reporting.capture.browser.type` {ess-icon} | Specifies the browser to use to capture screenshots. This setting exists for @@ -213,9 +211,9 @@ a| `xpack.reporting.capture.browser` [cols="2*<"] |=== | [[xpack-reporting-csv]] `xpack.reporting.csv.maxSizeBytes` {ess-icon} - | The maximum size of a CSV file before being truncated. This setting exists to prevent - large exports from causing performance and storage issues. - Defaults to `10485760` (10mB). + | The maximum {ref}/common-options.html#byte-units[byte size] of a CSV file before being truncated. This setting exists to + prevent large exports from causing performance and storage issues. Can be specified as number of bytes. + Defaults to `10mb`. | `xpack.reporting.csv.scroll.size` | Number of documents retrieved from {es} for each scroll iteration during a CSV @@ -223,7 +221,7 @@ a| `xpack.reporting.capture.browser` Defaults to `500`. | `xpack.reporting.csv.scroll.duration` - | Amount of time allowed before {kib} cleans the scroll context during a CSV export. + | Amount of {ref}/common-options.html#time-units[time] allowed before {kib} cleans the scroll context during a CSV export. Defaults to `30s`. | `xpack.reporting.csv.checkForFormulas` diff --git a/x-pack/plugins/reporting/common/schema_utils.ts b/x-pack/plugins/reporting/common/schema_utils.ts new file mode 100644 index 0000000000000..f9b5c90e3c366 --- /dev/null +++ b/x-pack/plugins/reporting/common/schema_utils.ts @@ -0,0 +1,28 @@ +/* + * 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 { ByteSizeValue } from '@kbn/config-schema'; +import moment from 'moment'; + +/* + * For cleaner code: use these functions when a config schema value could be + * one type or another. This allows you to treat the value as one type. + */ + +export const durationToNumber = (value: number | moment.Duration): number => { + if (typeof value === 'number') { + return value; + } + return value.asMilliseconds(); +}; + +export const byteSizeValueToNumber = (value: number | ByteSizeValue) => { + if (typeof value === 'number') { + return value; + } + + return value.getValueInBytes(); +}; diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index 65db13f22788b..f326d365351f2 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -6,8 +6,8 @@ import { EuiBasicTable, - EuiFlexItem, EuiFlexGroup, + EuiFlexItem, EuiPageContent, EuiSpacer, EuiText, @@ -23,6 +23,7 @@ import { Subscription } from 'rxjs'; import { ApplicationStart, ToastsSetup } from 'src/core/public'; import { ILicense, LicensingPluginSetup } from '../../../licensing/public'; import { Poller } from '../../common/poller'; +import { durationToNumber } from '../../common/schema_utils'; import { JobStatuses } from '../../constants'; import { checkLicense } from '../lib/license_check'; import { JobQueueEntry, ReportingAPIClient } from '../lib/reporting_api_client'; @@ -183,17 +184,19 @@ class ReportListingUi extends Component { public componentDidMount() { this.mounted = true; + const { pollConfig, license$ } = this.props; + const pollFrequencyInMillis = durationToNumber(pollConfig.jobsRefresh.interval); this.poller = new Poller({ functionToPoll: () => { return this.fetchJobs(); }, - pollFrequencyInMillis: this.props.pollConfig.jobsRefresh.interval, + pollFrequencyInMillis, trailing: false, continuePollingOnError: true, - pollFrequencyErrorMultiplier: this.props.pollConfig.jobsRefresh.intervalErrorMultiplier, + pollFrequencyErrorMultiplier: pollConfig.jobsRefresh.intervalErrorMultiplier, }); this.poller.start(); - this.licenseSubscription = this.props.license$.subscribe(this.licenseHandler); + this.licenseSubscription = license$.subscribe(this.licenseHandler); } private licenseHandler = (license: ILicense) => { diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.tsx index d003d4c581699..a134377e194b8 100644 --- a/x-pack/plugins/reporting/public/plugin.tsx +++ b/x-pack/plugins/reporting/public/plugin.tsx @@ -26,6 +26,7 @@ import { import { ManagementSetup } from '../../../../src/plugins/management/public'; import { SharePluginSetup } from '../../../../src/plugins/share/public'; import { LicensingPluginSetup } from '../../licensing/public'; +import { durationToNumber } from '../common/schema_utils'; import { JobId, JobStatusBuckets, ReportingConfigType } from '../common/types'; import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../constants'; import { getGeneralErrorToast } from './components'; @@ -158,8 +159,7 @@ export class ReportingPublicPlugin implements Plugin { const { http, notifications } = core; const apiClient = new ReportingAPIClient(http); const streamHandler = new StreamHandler(notifications, apiClient); - const { interval } = this.config.poll.jobsRefresh; - + const interval = durationToNumber(this.config.poll.jobsRefresh.interval); Rx.timer(0, interval) .pipe( takeUntil(this.stop$), // stop the interval when stop method is called diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 88be86d1ecc30..6897f07c45e2b 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -21,6 +21,7 @@ import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber'; import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators'; import { getChromiumDisconnectedError } from '../'; import { BROWSER_TYPE } from '../../../../common/constants'; +import { durationToNumber } from '../../../../common/schema_utils'; import { CaptureConfig } from '../../../../server/types'; import { LevelLogger } from '../../../lib'; import { safeChildProcess } from '../../safe_child_process'; @@ -90,7 +91,7 @@ export class HeadlessChromiumDriverFactory { // Set the default timeout for all navigation methods to the openUrl timeout (30 seconds) // All waitFor methods have their own timeout config passed in to them - page.setDefaultTimeout(this.captureConfig.timeouts.openUrl); + page.setDefaultTimeout(durationToNumber(this.captureConfig.timeouts.openUrl)); logger.debug(`Browser page driver created`); } catch (err) { diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index a89b952702e1b..b9c6f8e7591e3 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -17,6 +17,8 @@ export const config: PluginConfigDescriptor = { unused('capture.concurrency'), unused('capture.settleTime'), unused('capture.timeout'), + unused('poll.jobCompletionNotifier.intervalErrorMultiplier'), + unused('poll.jobsRefresh.intervalErrorMultiplier'), unused('kibanaApp'), ], }; diff --git a/x-pack/plugins/reporting/server/config/schema.test.ts b/x-pack/plugins/reporting/server/config/schema.test.ts index 69e4d443cf040..9fc3d4329879e 100644 --- a/x-pack/plugins/reporting/server/config/schema.test.ts +++ b/x-pack/plugins/reporting/server/config/schema.test.ts @@ -8,101 +8,242 @@ import { ConfigSchema } from './schema'; describe('Reporting Config Schema', () => { it(`context {"dev":false,"dist":false} produces correct config`, () => { - expect(ConfigSchema.validate({}, { dev: false, dist: false })).toMatchObject({ - capture: { - browser: { - autoDownload: true, - chromium: { proxy: { enabled: false } }, - type: 'chromium', + expect(ConfigSchema.validate({}, { dev: false, dist: false })).toMatchInlineSnapshot(` + Object { + "capture": Object { + "browser": Object { + "autoDownload": true, + "chromium": Object { + "proxy": Object { + "enabled": false, + }, + }, + "type": "chromium", + }, + "loadDelay": "PT3S", + "maxAttempts": 1, + "networkPolicy": Object { + "enabled": true, + "rules": Array [ + Object { + "allow": true, + "host": undefined, + "protocol": "http:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "https:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "ws:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "wss:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "data:", + }, + Object { + "allow": false, + "host": undefined, + "protocol": undefined, + }, + ], + }, + "timeouts": Object { + "openUrl": "PT1M", + "renderComplete": "PT30S", + "waitForElements": "PT30S", + }, + "viewport": Object { + "height": 1200, + "width": 1950, + }, + "zoom": 2, + }, + "csv": Object { + "checkForFormulas": true, + "enablePanelActionDownload": true, + "escapeFormulaValues": false, + "maxSizeBytes": ByteSizeValue { + "valueInBytes": 10485760, + }, + "scroll": Object { + "duration": "30s", + "size": 500, + }, + "useByteOrderMarkEncoding": false, }, - loadDelay: 3000, - maxAttempts: 1, - networkPolicy: { - enabled: true, - rules: [ - { allow: true, host: undefined, protocol: 'http:' }, - { allow: true, host: undefined, protocol: 'https:' }, - { allow: true, host: undefined, protocol: 'ws:' }, - { allow: true, host: undefined, protocol: 'wss:' }, - { allow: true, host: undefined, protocol: 'data:' }, - { allow: false, host: undefined, protocol: undefined }, + "enabled": true, + "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "index": ".reporting", + "kibanaServer": Object {}, + "poll": Object { + "jobCompletionNotifier": Object { + "interval": 10000, + "intervalErrorMultiplier": 5, + }, + "jobsRefresh": Object { + "interval": 5000, + "intervalErrorMultiplier": 5, + }, + }, + "queue": Object { + "indexInterval": "week", + "pollEnabled": true, + "pollInterval": "PT3S", + "pollIntervalErrorMultiplier": 10, + "timeout": "PT2M", + }, + "roles": Object { + "allow": Array [ + "reporting_user", ], }, - viewport: { height: 1200, width: 1950 }, - zoom: 2, - }, - csv: { - checkForFormulas: true, - enablePanelActionDownload: true, - maxSizeBytes: 10485760, - scroll: { duration: '30s', size: 500 }, - }, - encryptionKey: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - index: '.reporting', - kibanaServer: {}, - poll: { - jobCompletionNotifier: { interval: 10000, intervalErrorMultiplier: 5 }, - jobsRefresh: { interval: 5000, intervalErrorMultiplier: 5 }, - }, - queue: { - indexInterval: 'week', - pollEnabled: true, - pollInterval: 3000, - pollIntervalErrorMultiplier: 10, - timeout: 120000, - }, - roles: { allow: ['reporting_user'] }, - }); + } + `); }); it(`context {"dev":false,"dist":true} produces correct config`, () => { - expect(ConfigSchema.validate({}, { dev: false, dist: true })).toMatchObject({ - capture: { - browser: { - autoDownload: false, - chromium: { - inspect: false, - proxy: { enabled: false }, - }, - type: 'chromium', + expect(ConfigSchema.validate({}, { dev: false, dist: true })).toMatchInlineSnapshot(` + Object { + "capture": Object { + "browser": Object { + "autoDownload": false, + "chromium": Object { + "inspect": false, + "proxy": Object { + "enabled": false, + }, + }, + "type": "chromium", + }, + "loadDelay": "PT3S", + "maxAttempts": 3, + "networkPolicy": Object { + "enabled": true, + "rules": Array [ + Object { + "allow": true, + "host": undefined, + "protocol": "http:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "https:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "ws:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "wss:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "data:", + }, + Object { + "allow": false, + "host": undefined, + "protocol": undefined, + }, + ], + }, + "timeouts": Object { + "openUrl": "PT1M", + "renderComplete": "PT30S", + "waitForElements": "PT30S", + }, + "viewport": Object { + "height": 1200, + "width": 1950, + }, + "zoom": 2, + }, + "csv": Object { + "checkForFormulas": true, + "enablePanelActionDownload": true, + "escapeFormulaValues": false, + "maxSizeBytes": ByteSizeValue { + "valueInBytes": 10485760, + }, + "scroll": Object { + "duration": "30s", + "size": 500, + }, + "useByteOrderMarkEncoding": false, }, - loadDelay: 3000, - maxAttempts: 3, - networkPolicy: { - enabled: true, - rules: [ - { allow: true, host: undefined, protocol: 'http:' }, - { allow: true, host: undefined, protocol: 'https:' }, - { allow: true, host: undefined, protocol: 'ws:' }, - { allow: true, host: undefined, protocol: 'wss:' }, - { allow: true, host: undefined, protocol: 'data:' }, - { allow: false, host: undefined, protocol: undefined }, + "enabled": true, + "index": ".reporting", + "kibanaServer": Object {}, + "poll": Object { + "jobCompletionNotifier": Object { + "interval": 10000, + "intervalErrorMultiplier": 5, + }, + "jobsRefresh": Object { + "interval": 5000, + "intervalErrorMultiplier": 5, + }, + }, + "queue": Object { + "indexInterval": "week", + "pollEnabled": true, + "pollInterval": "PT3S", + "pollIntervalErrorMultiplier": 10, + "timeout": "PT2M", + }, + "roles": Object { + "allow": Array [ + "reporting_user", ], }, - viewport: { height: 1200, width: 1950 }, - zoom: 2, - }, - csv: { - checkForFormulas: true, - enablePanelActionDownload: true, - maxSizeBytes: 10485760, - scroll: { duration: '30s', size: 500 }, - }, - index: '.reporting', - kibanaServer: {}, - poll: { - jobCompletionNotifier: { interval: 10000, intervalErrorMultiplier: 5 }, - jobsRefresh: { interval: 5000, intervalErrorMultiplier: 5 }, - }, - queue: { - indexInterval: 'week', - pollEnabled: true, - pollInterval: 3000, - pollIntervalErrorMultiplier: 10, - timeout: 120000, - }, - roles: { allow: ['reporting_user'] }, - }); + } + `); + }); + + it('allows Duration values for certain keys', () => { + expect(ConfigSchema.validate({ queue: { timeout: '2m' } }).queue.timeout).toMatchInlineSnapshot( + `"PT2M"` + ); + + expect( + ConfigSchema.validate({ capture: { loadDelay: '3s' } }).capture.loadDelay + ).toMatchInlineSnapshot(`"PT3S"`); + + expect( + ConfigSchema.validate({ + capture: { timeouts: { openUrl: '1m', waitForElements: '30s', renderComplete: '10s' } }, + }).capture.timeouts + ).toMatchInlineSnapshot(` + Object { + "openUrl": "PT1M", + "renderComplete": "PT10S", + "waitForElements": "PT30S", + } + `); + }); + + it('allows ByteSizeValue values for certain keys', () => { + expect(ConfigSchema.validate({ csv: { maxSizeBytes: '12mb' } }).csv.maxSizeBytes) + .toMatchInlineSnapshot(` + ByteSizeValue { + "valueInBytes": 12582912, + } + `); }); it(`allows optional settings`, () => { diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts index a81ffd754946b..8276e8b49d348 100644 --- a/x-pack/plugins/reporting/server/config/schema.ts +++ b/x-pack/plugins/reporting/server/config/schema.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema, TypeOf } from '@kbn/config-schema'; +import { ByteSizeValue, schema, TypeOf } from '@kbn/config-schema'; import moment from 'moment'; const KibanaServerSchema = schema.object({ @@ -33,9 +33,13 @@ const KibanaServerSchema = schema.object({ const QueueSchema = schema.object({ indexInterval: schema.string({ defaultValue: 'week' }), pollEnabled: schema.boolean({ defaultValue: true }), - pollInterval: schema.number({ defaultValue: 3000 }), + pollInterval: schema.oneOf([schema.number(), schema.duration()], { + defaultValue: moment.duration({ seconds: 3 }), + }), pollIntervalErrorMultiplier: schema.number({ defaultValue: 10 }), - timeout: schema.number({ defaultValue: moment.duration(2, 'm').asMilliseconds() }), + timeout: schema.oneOf([schema.number(), schema.duration()], { + defaultValue: moment.duration({ minutes: 2 }), + }), }); const RulesSchema = schema.object({ @@ -46,9 +50,15 @@ const RulesSchema = schema.object({ const CaptureSchema = schema.object({ timeouts: schema.object({ - openUrl: schema.number({ defaultValue: 60000 }), - waitForElements: schema.number({ defaultValue: 30000 }), - renderComplete: schema.number({ defaultValue: 30000 }), + openUrl: schema.oneOf([schema.number(), schema.duration()], { + defaultValue: moment.duration({ minutes: 1 }), + }), + waitForElements: schema.oneOf([schema.number(), schema.duration()], { + defaultValue: moment.duration({ seconds: 30 }), + }), + renderComplete: schema.oneOf([schema.number(), schema.duration()], { + defaultValue: moment.duration({ seconds: 30 }), + }), }), networkPolicy: schema.object({ enabled: schema.boolean({ defaultValue: true }), @@ -68,9 +78,9 @@ const CaptureSchema = schema.object({ width: schema.number({ defaultValue: 1950 }), height: schema.number({ defaultValue: 1200 }), }), - loadDelay: schema.number({ - defaultValue: moment.duration(3, 's').asMilliseconds(), - }), // TODO: use schema.duration + loadDelay: schema.oneOf([schema.number(), schema.duration()], { + defaultValue: moment.duration({ seconds: 3 }), + }), browser: schema.object({ autoDownload: schema.conditional( schema.contextRef('dist'), @@ -116,13 +126,13 @@ const CsvSchema = schema.object({ checkForFormulas: schema.boolean({ defaultValue: true }), escapeFormulaValues: schema.boolean({ defaultValue: false }), enablePanelActionDownload: schema.boolean({ defaultValue: true }), - maxSizeBytes: schema.number({ - defaultValue: 1024 * 1024 * 10, // 10MB - }), // TODO: use schema.byteSize + maxSizeBytes: schema.oneOf([schema.number(), schema.byteSize()], { + defaultValue: ByteSizeValue.parse('10mb'), + }), useByteOrderMarkEncoding: schema.boolean({ defaultValue: false }), scroll: schema.object({ duration: schema.string({ - defaultValue: '30s', + defaultValue: '30s', // this value is passed directly to ES, so string only format is preferred validate(value) { if (!/^[0-9]+(d|h|m|s|ms|micros|nanos)$/.test(value)) { return 'must be a duration string'; @@ -146,18 +156,16 @@ const RolesSchema = schema.object({ const IndexSchema = schema.string({ defaultValue: '.reporting' }); +// Browser side polling: job completion notifier, management table auto-refresh +// NOTE: can not use schema.duration, a bug prevents it being passed to the browser correctly const PollSchema = schema.object({ jobCompletionNotifier: schema.object({ - interval: schema.number({ - defaultValue: moment.duration(10, 's').asMilliseconds(), - }), // TODO: use schema.duration - intervalErrorMultiplier: schema.number({ defaultValue: 5 }), + interval: schema.number({ defaultValue: 10000 }), + intervalErrorMultiplier: schema.number({ defaultValue: 5 }), // unused }), jobsRefresh: schema.object({ - interval: schema.number({ - defaultValue: moment.duration(5, 's').asMilliseconds(), - }), // TODO: use schema.duration - intervalErrorMultiplier: schema.number({ defaultValue: 5 }), + interval: schema.number({ defaultValue: 5000 }), + intervalErrorMultiplier: schema.number({ defaultValue: 5 }), // unused }), }); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts index 754bc7bc75cb5..a0d8ff0852544 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts @@ -4,10 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import sinon from 'sinon'; import { ReportingConfig } from '../../'; import { ReportingCore } from '../../core'; -import { createMockReportingCore } from '../../test_helpers'; +import { + createMockConfig, + createMockConfigSchema, + createMockReportingCore, +} from '../../test_helpers'; import { BasePayload } from '../../types'; import { TaskPayloadPDF } from '../printable_pdf/types'; import { getConditionalHeaders, getCustomLogo } from './'; @@ -15,17 +18,10 @@ import { getConditionalHeaders, getCustomLogo } from './'; let mockConfig: ReportingConfig; let mockReportingPlugin: ReportingCore; -const getMockConfig = (mockConfigGet: sinon.SinonStub) => ({ - get: mockConfigGet, - kbnConfig: { get: mockConfigGet }, -}); - beforeEach(async () => { - const mockConfigGet = sinon - .stub() - .withArgs('kibanaServer', 'hostname') - .returns('custom-hostname'); - mockConfig = getMockConfig(mockConfigGet); + const reportingConfig = { kibanaServer: { hostname: 'custom-hostname' } }; + const mockSchema = createMockConfigSchema(reportingConfig); + mockConfig = createMockConfig(mockSchema); mockReportingPlugin = await createMockReportingCore(mockConfig); }); @@ -84,10 +80,9 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav const mockGetSavedObjectsClient = jest.fn(); mockReportingPlugin.getSavedObjectsClient = mockGetSavedObjectsClient; - const mockConfigGet = sinon.stub(); - mockConfigGet.withArgs('kibanaServer', 'hostname').returns('localhost'); - mockConfigGet.withArgs('server', 'basePath').returns('/sbp'); - mockConfig = getMockConfig(mockConfigGet); + const reportingConfig = { kibanaServer: { hostname: 'localhost' }, server: { basePath: '/sbp' } }; + const mockSchema = createMockConfigSchema(reportingConfig); + mockConfig = createMockConfig(mockSchema); const permittedHeaders = { foo: 'bar', @@ -134,25 +129,12 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav }); describe('config formatting', () => { - test(`lowercases server.host`, async () => { - const mockConfigGet = sinon.stub().withArgs('server', 'host').returns('COOL-HOSTNAME'); - mockConfig = getMockConfig(mockConfigGet); - - const conditionalHeaders = await getConditionalHeaders({ - job: {} as BasePayload, - filteredHeaders: {}, - config: mockConfig, - }); - expect(conditionalHeaders.conditions.hostname).toEqual('cool-hostname'); - }); - test(`lowercases kibanaServer.hostname`, async () => { - const mockConfigGet = sinon - .stub() - .withArgs('kibanaServer', 'hostname') - .returns('GREAT-HOSTNAME'); - mockConfig = getMockConfig(mockConfigGet); - const conditionalHeaders = await getConditionalHeaders({ + const reportingConfig = { kibanaServer: { hostname: 'GREAT-HOSTNAME' } }; + const mockSchema = createMockConfigSchema(reportingConfig); + mockConfig = createMockConfig(mockSchema); + + const conditionalHeaders = getConditionalHeaders({ job: { title: 'cool-job-bro', type: 'csv', diff --git a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts index 8c02fdd69de8b..ec4e54632eef5 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts @@ -4,18 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReportingCore } from '../../core'; -import { createMockReportingCore } from '../../test_helpers'; +import { ReportingConfig, ReportingCore } from '../../'; +import { + createMockConfig, + createMockConfigSchema, + createMockReportingCore, +} from '../../test_helpers'; import { TaskPayloadPDF } from '../printable_pdf/types'; import { getConditionalHeaders, getCustomLogo } from './'; -const mockConfigGet = jest.fn().mockImplementation((key: string) => { - return 'localhost'; -}); -const mockConfig = { get: mockConfigGet, kbnConfig: { get: mockConfigGet } }; - +let mockConfig: ReportingConfig; let mockReportingPlugin: ReportingCore; + beforeEach(async () => { + mockConfig = createMockConfig(createMockConfigSchema()); mockReportingPlugin = await createMockReportingCore(mockConfig); }); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts index 355536000326e..fae66b26a83e0 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts @@ -5,6 +5,7 @@ */ import { ReportingConfig } from '../../'; +import { createMockConfig } from '../../test_helpers'; import { TaskPayloadPNG } from '../png/types'; import { TaskPayloadPDF } from '../printable_pdf/types'; import { getFullUrls } from './get_full_urls'; @@ -15,12 +16,6 @@ interface FullUrlsOpts { } let mockConfig: ReportingConfig; -const getMockConfig = (mockConfigGet: jest.Mock) => { - return { - get: mockConfigGet, - kbnConfig: { get: mockConfigGet }, - }; -}; beforeEach(() => { const reportingConfig: Record = { @@ -29,10 +24,7 @@ beforeEach(() => { 'kibanaServer.protocol': 'http', 'server.basePath': '/sbp', }; - const mockConfigGet = jest.fn().mockImplementation((...keys: string[]) => { - return reportingConfig[keys.join('.') as string]; - }); - mockConfig = getMockConfig(mockConfigGet); + mockConfig = createMockConfig(reportingConfig); }); const getMockJob = (base: object) => base as TaskPayloadPNG & TaskPayloadPDF; diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts index 15432d0cbd147..72b42143a24f7 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts @@ -6,6 +6,7 @@ import nodeCrypto from '@elastic/node-crypto'; import { ElasticsearchServiceSetup, IUiSettingsClient } from 'kibana/server'; +import moment from 'moment'; // @ts-ignore import Puid from 'puid'; import sinon from 'sinon'; @@ -73,6 +74,7 @@ describe('CSV Execute Job', function () { beforeEach(async function () { configGetStub = sinon.stub(); + configGetStub.withArgs('queue', 'timeout').returns(moment.duration('2m')); configGetStub.withArgs('index').returns('.reporting-foo-test'); configGetStub.withArgs('encryptionKey').returns(encryptionKey); configGetStub.withArgs('csv', 'maxSizeBytes').returns(1024 * 1000); // 1mB diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts index 06aa2434afc3f..e383f21143149 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts @@ -6,11 +6,12 @@ import { i18n } from '@kbn/i18n'; import { IUiSettingsClient } from 'src/core/server'; -import { getFieldFormats } from '../../../services'; import { ReportingConfig } from '../../../'; import { CancellationToken } from '../../../../../../plugins/reporting/common'; import { CSV_BOM_CHARS } from '../../../../common/constants'; +import { byteSizeValueToNumber } from '../../../../common/schema_utils'; import { LevelLogger } from '../../../lib'; +import { getFieldFormats } from '../../../services'; import { IndexPatternSavedObject, SavedSearchGeneratorResult } from '../types'; import { checkIfRowsHaveFormulas } from './check_cells_for_formulas'; import { createEscapeValue } from './escape_value'; @@ -64,7 +65,7 @@ export function createGenerateCsv(logger: LevelLogger) { ); const escapeValue = createEscapeValue(settings.quoteValues, settings.escapeFormulaValues); const bom = config.get('csv', 'useByteOrderMarkEncoding') ? CSV_BOM_CHARS : ''; - const builder = new MaxSizeStringBuilder(settings.maxSizeBytes, bom); + const builder = new MaxSizeStringBuilder(byteSizeValueToNumber(settings.maxSizeBytes), bom); const { fields, metaFields, conflictedTypesFields } = job; const header = `${fields.map(escapeValue).join(settings.separator)}\n`; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts index fdc51dc1c9c87..e7322bdc0d408 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts @@ -10,7 +10,11 @@ import * as Rx from 'rxjs'; import { ReportingCore } from '../../../'; import { CancellationToken } from '../../../../common'; import { cryptoFactory, LevelLogger } from '../../../lib'; -import { createMockReportingCore } from '../../../test_helpers'; +import { + createMockConfig, + createMockConfigSchema, + createMockReportingCore, +} from '../../../test_helpers'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; import { TaskPayloadPDF } from '../types'; import { runTaskFnFactory } from './'; @@ -39,20 +43,16 @@ const encryptHeaders = async (headers: Record) => { const getBasePayload = (baseObj: any) => baseObj as TaskPayloadPDF; beforeEach(async () => { - const kbnConfig = { - 'server.basePath': '/sbp', - }; const reportingConfig = { + 'server.basePath': '/sbp', index: '.reports-test', encryptionKey: mockEncryptionKey, 'kibanaServer.hostname': 'localhost', 'kibanaServer.port': 5601, 'kibanaServer.protocol': 'http', }; - const mockReportingConfig = { - get: (...keys: string[]) => (reportingConfig as any)[keys.join('.')], - kbnConfig: { get: (...keys: string[]) => (kbnConfig as any)[keys.join('.')] }, - }; + const mockSchema = createMockConfigSchema(reportingConfig); + const mockReportingConfig = createMockConfig(mockSchema); mockReporting = await createMockReportingCore(mockReportingConfig); @@ -79,7 +79,7 @@ test(`passes browserTimezone to generatePdf`, async () => { const generatePdfObservable = (await generatePdfObservableFactory(mockReporting)) as jest.Mock; generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); + const runTask = runTaskFnFactory(mockReporting, getMockLogger()); const browserTimezone = 'UTC'; await runTask( 'pdfJobId', @@ -98,7 +98,7 @@ test(`passes browserTimezone to generatePdf`, async () => { test(`returns content_type of application/pdf`, async () => { const logger = getMockLogger(); - const runTask = await runTaskFnFactory(mockReporting, logger); + const runTask = runTaskFnFactory(mockReporting, logger); const encryptedHeaders = await encryptHeaders({}); const generatePdfObservable = await generatePdfObservableFactory(mockReporting); @@ -117,7 +117,7 @@ test(`returns content of generatePdf getBuffer base64 encoded`, async () => { const generatePdfObservable = await generatePdfObservableFactory(mockReporting); (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); - const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); + const runTask = runTaskFnFactory(mockReporting, getMockLogger()); const encryptedHeaders = await encryptHeaders({}); const { content } = await runTask( 'pdfJobId', diff --git a/x-pack/plugins/reporting/server/lib/create_worker.test.ts b/x-pack/plugins/reporting/server/lib/create_worker.test.ts index 85188c07eeb20..1fcd750849331 100644 --- a/x-pack/plugins/reporting/server/lib/create_worker.test.ts +++ b/x-pack/plugins/reporting/server/lib/create_worker.test.ts @@ -6,7 +6,12 @@ import * as sinon from 'sinon'; import { ReportingConfig, ReportingCore } from '../../server'; -import { createMockReportingCore } from '../test_helpers'; +import { + createMockConfig, + createMockConfigSchema, + createMockLevelLogger, + createMockReportingCore, +} from '../test_helpers'; import { createWorkerFactory } from './create_worker'; // @ts-ignore import { Esqueue } from './esqueue'; @@ -14,16 +19,13 @@ import { Esqueue } from './esqueue'; import { ClientMock } from './esqueue/__tests__/fixtures/legacy_elasticsearch'; import { ExportTypesRegistry } from './export_types_registry'; -const configGetStub = sinon.stub(); -configGetStub.withArgs('queue').returns({ - pollInterval: 3300, - pollIntervalErrorMultiplier: 10, -}); -configGetStub.withArgs('server', 'name').returns('test-server-123'); -configGetStub.withArgs('server', 'uuid').returns('g9ymiujthvy6v8yrh7567g6fwzgzftzfr'); +const logger = createMockLevelLogger(); +const reportingConfig = { + queue: { pollInterval: 3300, pollIntervalErrorMultiplier: 10 }, + server: { name: 'test-server-123', uuid: 'g9ymiujthvy6v8yrh7567g6fwzgzftzfr' }, +}; const executeJobFactoryStub = sinon.stub(); -const getMockLogger = sinon.stub(); const getMockExportTypesRegistry = ( exportTypes: any[] = [{ runTaskFnFactory: executeJobFactoryStub }] @@ -39,18 +41,18 @@ describe('Create Worker', () => { let client: ClientMock; beforeEach(async () => { - mockConfig = { get: configGetStub, kbnConfig: { get: configGetStub } }; + const mockSchema = createMockConfigSchema(reportingConfig); + mockConfig = createMockConfig(mockSchema); mockReporting = await createMockReportingCore(mockConfig); mockReporting.getExportTypesRegistry = () => getMockExportTypesRegistry(); - // @ts-ignore over-riding config manually - mockReporting.config = mockConfig; + client = new ClientMock(); queue = new Esqueue('reporting-queue', { client }); executeJobFactoryStub.reset(); }); test('Creates a single Esqueue worker for Reporting', async () => { - const createWorker = createWorkerFactory(mockReporting, getMockLogger()); + const createWorker = createWorkerFactory(mockReporting, logger); const registerWorkerSpy = sinon.spy(queue, 'registerWorker'); await createWorker(queue); @@ -82,7 +84,7 @@ Object { { runTaskFnFactory: executeJobFactoryStub }, ]); mockReporting.getExportTypesRegistry = () => exportTypesRegistry; - const createWorker = createWorkerFactory(mockReporting, getMockLogger()); + const createWorker = createWorkerFactory(mockReporting, logger); const registerWorkerSpy = sinon.spy(queue, 'registerWorker'); await createWorker(queue); diff --git a/x-pack/plugins/reporting/server/lib/create_worker.ts b/x-pack/plugins/reporting/server/lib/create_worker.ts index dd5c560455274..c1c88dd8a54ba 100644 --- a/x-pack/plugins/reporting/server/lib/create_worker.ts +++ b/x-pack/plugins/reporting/server/lib/create_worker.ts @@ -6,6 +6,7 @@ import { CancellationToken } from '../../common'; import { PLUGIN_ID } from '../../common/constants'; +import { durationToNumber } from '../../common/schema_utils'; import { ReportingCore } from '../../server'; import { LevelLogger } from '../../server/lib'; import { ExportTypeDefinition, JobSource, RunTaskFn } from '../../server/types'; @@ -57,7 +58,7 @@ export function createWorkerFactory(reporting: ReportingCore, log const workerOptions = { kibanaName, kibanaId, - interval: queueConfig.pollInterval, + interval: durationToNumber(queueConfig.pollInterval), intervalErrorMultiplier: queueConfig.pollIntervalErrorMultiplier, }; const worker = queue.registerWorker(PLUGIN_ID, workerFn, workerOptions); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts index 49c690e8c024d..89cb4221c96b2 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts @@ -5,9 +5,10 @@ */ import { i18n } from '@kbn/i18n'; +import { durationToNumber } from '../../../common/schema_utils'; +import { LevelLogger, startTrace } from '../'; import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig } from '../../types'; -import { LevelLogger, startTrace } from '../'; import { LayoutInstance } from '../layouts'; import { CONTEXT_GETNUMBEROFITEMS, CONTEXT_READMETADATA } from './constants'; @@ -31,9 +32,10 @@ export const getNumberOfItems = async ( // the dashboard is using the `itemsCountAttribute` attribute to let us // know how many items to expect since gridster incrementally adds panels // we have to use this hint to wait for all of them + const timeout = durationToNumber(captureConfig.timeouts.waitForElements); await browser.waitForSelector( `${renderCompleteSelector},[${itemsCountAttribute}]`, - { timeout: captureConfig.timeouts.waitForElements }, + { timeout }, { context: CONTEXT_READMETADATA }, logger ); @@ -59,6 +61,7 @@ export const getNumberOfItems = async ( logger ); } catch (err) { + logger.error(err); throw new Error( i18n.translate('xpack.reporting.screencapture.readVisualizationsError', { defaultMessage: `An error occurred when trying to read the page for visualization panel info. You may need to increase '{configKey}'. {error}`, diff --git a/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts b/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts index f893951815e9e..2fc711d4d6f07 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts @@ -43,6 +43,7 @@ export const injectCustomCss = async ( logger ); } catch (err) { + logger.error(err); throw new Error( i18n.translate('xpack.reporting.screencapture.injectCss', { defaultMessage: `An error occurred when trying to update Kibana CSS for reporting. {error}`, diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts index 3749e4372bdab..5b671e9f5b47e 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts @@ -15,12 +15,17 @@ jest.mock('../../browsers/chromium/puppeteer', () => ({ }), })); +import moment from 'moment'; import * as Rx from 'rxjs'; -import { LevelLogger } from '../'; -import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { HeadlessChromiumDriver } from '../../browsers'; -import { createMockBrowserDriverFactory, createMockLayoutInstance } from '../../test_helpers'; -import { CaptureConfig, ConditionalHeaders } from '../../types'; +import { + createMockBrowserDriverFactory, + createMockConfig, + createMockConfigSchema, + createMockLayoutInstance, + createMockLevelLogger, +} from '../../test_helpers'; +import { ConditionalHeaders } from '../../types'; import { ElementsPositionAndAttribute } from './'; import * as contexts from './constants'; import { screenshotsObservableFactory } from './observable'; @@ -28,11 +33,22 @@ import { screenshotsObservableFactory } from './observable'; /* * Mocks */ -const mockLogger = jest.fn(loggingSystemMock.create); -const logger = new LevelLogger(mockLogger()); +const logger = createMockLevelLogger(); -const mockConfig = { timeouts: { openUrl: 13 } } as CaptureConfig; -const mockLayout = createMockLayoutInstance(mockConfig); +const reportingConfig = { + capture: { + loadDelay: moment.duration(2, 's'), + timeouts: { + openUrl: moment.duration(2, 'm'), + waitForElements: moment.duration(20, 's'), + renderComplete: moment.duration(10, 's'), + }, + }, +}; +const mockSchema = createMockConfigSchema(reportingConfig); +const mockConfig = createMockConfig(mockSchema); +const captureConfig = mockConfig.get('capture'); +const mockLayout = createMockLayoutInstance(captureConfig); /* * Tests @@ -45,7 +61,7 @@ describe('Screenshot Observable Pipeline', () => { }); it('pipelines a single url into screenshot and timeRange', async () => { - const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); + const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); const result = await getScreenshots$({ logger, urls: ['/welcome/home/start/index.htm'], @@ -106,7 +122,7 @@ describe('Screenshot Observable Pipeline', () => { }); // test - const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); + const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); const result = await getScreenshots$({ logger, urls: ['/welcome/home/start/index2.htm', '/welcome/home/start/index.php3?page=./home.php'], @@ -205,7 +221,7 @@ describe('Screenshot Observable Pipeline', () => { }); // test - const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); + const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); const getScreenshot = async () => { return await getScreenshots$({ logger, @@ -300,7 +316,7 @@ describe('Screenshot Observable Pipeline', () => { }); // test - const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); + const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); const getScreenshot = async () => { return await getScreenshots$({ logger, @@ -333,7 +349,7 @@ describe('Screenshot Observable Pipeline', () => { mockLayout.getViewport = () => null; // test - const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); + const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); const getScreenshot = async () => { return await getScreenshots$({ logger, diff --git a/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts index c21ef3b91fab3..e28f50851f4d9 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts @@ -5,9 +5,10 @@ */ import { i18n } from '@kbn/i18n'; +import { durationToNumber } from '../../../common/schema_utils'; +import { LevelLogger, startTrace } from '../'; import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig, ConditionalHeaders } from '../../types'; -import { LevelLogger, startTrace } from '../'; export const openUrl = async ( captureConfig: CaptureConfig, @@ -19,16 +20,14 @@ export const openUrl = async ( ): Promise => { const endTrace = startTrace('open_url', 'wait'); try { + const timeout = durationToNumber(captureConfig.timeouts.openUrl); await browser.open( url, - { - conditionalHeaders, - waitForSelector: pageLoadSelector, - timeout: captureConfig.timeouts.openUrl, - }, + { conditionalHeaders, waitForSelector: pageLoadSelector, timeout }, logger ); } catch (err) { + logger.error(err); throw new Error( i18n.translate('xpack.reporting.screencapture.couldntLoadKibana', { defaultMessage: `An error occurred when trying to open the Kibana URL. You may need to increase '{configKey}'. {error}`, diff --git a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts index f36a7b6f73664..edd4f71b2adac 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { durationToNumber } from '../../../common/schema_utils'; import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig } from '../../types'; import { LevelLogger, startTrace } from '../'; @@ -67,7 +68,7 @@ export const waitForRenderComplete = async ( return Promise.all(renderedTasks).then(hackyWaitForVisualizations); }, - args: [layout.selectors.renderComplete, captureConfig.loadDelay], + args: [layout.selectors.renderComplete, durationToNumber(captureConfig.loadDelay)], }, { context: CONTEXT_WAITFORRENDER }, logger diff --git a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts index 779d00442522d..5f86a2b3bf00b 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts @@ -5,8 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../browsers'; +import { durationToNumber } from '../../../common/schema_utils'; import { LevelLogger, startTrace } from '../'; +import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig } from '../../types'; import { LayoutInstance } from '../layouts'; import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants'; @@ -25,7 +26,7 @@ const getCompletedItemsCount = ({ renderCompleteSelector }: SelectorArgs) => { export const waitForVisualizations = async ( captureConfig: CaptureConfig, browser: HeadlessChromiumDriver, - itemsCount: number, + toEqual: number, layout: LayoutInstance, logger: LevelLogger ): Promise => { @@ -35,29 +36,26 @@ export const waitForVisualizations = async ( logger.debug( i18n.translate('xpack.reporting.screencapture.waitingForRenderedElements', { defaultMessage: `waiting for {itemsCount} rendered elements to be in the DOM`, - values: { itemsCount }, + values: { itemsCount: toEqual }, }) ); try { + const timeout = durationToNumber(captureConfig.timeouts.renderComplete); await browser.waitFor( - { - fn: getCompletedItemsCount, - args: [{ renderCompleteSelector }], - toEqual: itemsCount, - timeout: captureConfig.timeouts.renderComplete, - }, + { fn: getCompletedItemsCount, args: [{ renderCompleteSelector }], toEqual, timeout }, { context: CONTEXT_WAITFORELEMENTSTOBEINDOM }, logger ); - logger.debug(`found ${itemsCount} rendered elements in the DOM`); + logger.debug(`found ${toEqual} rendered elements in the DOM`); } catch (err) { + logger.error(err); throw new Error( i18n.translate('xpack.reporting.screencapture.couldntFinishRendering', { defaultMessage: `An error occurred when trying to wait for {count} visualizations to finish rendering. You may need to increase '{configKey}'. {error}`, values: { - count: itemsCount, + count: toEqual, configKey: 'xpack.reporting.capture.timeouts.renderComplete', error: err, }, diff --git a/x-pack/plugins/reporting/server/lib/store/index_timestamp.ts b/x-pack/plugins/reporting/server/lib/store/index_timestamp.ts index 71ce0b1e572f8..7b8b851f5bd72 100644 --- a/x-pack/plugins/reporting/server/lib/store/index_timestamp.ts +++ b/x-pack/plugins/reporting/server/lib/store/index_timestamp.ts @@ -8,7 +8,6 @@ import moment, { unitOfTime } from 'moment'; export const intervals = ['year', 'month', 'week', 'day', 'hour', 'minute']; -// TODO: This helper function can be removed by using `schema.duration` objects in the reporting config schema export function indexTimestamp(intervalStr: string, separator = '-') { const startOf = intervalStr as unitOfTime.StartOf; if (separator.match(/[a-z]/i)) throw new Error('Interval separator can not be a letter'); diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts index b87466ca289cf..8dc4edd200052 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts @@ -7,15 +7,15 @@ import sinon from 'sinon'; import { ElasticsearchServiceSetup } from 'src/core/server'; import { ReportingConfig, ReportingCore } from '../..'; -import { createMockReportingCore, createMockLevelLogger } from '../../test_helpers'; +import { + createMockConfig, + createMockConfigSchema, + createMockLevelLogger, + createMockReportingCore, +} from '../../test_helpers'; import { Report } from './report'; import { ReportingStore } from './store'; -const getMockConfig = (mockConfigGet: sinon.SinonStub) => ({ - get: mockConfigGet, - kbnConfig: { get: mockConfigGet }, -}); - describe('ReportingStore', () => { const mockLogger = createMockLevelLogger(); let mockConfig: ReportingConfig; @@ -25,10 +25,12 @@ describe('ReportingStore', () => { const mockElasticsearch = { legacy: { client: { callAsInternalUser: callClusterStub } } }; beforeEach(async () => { - const mockConfigGet = sinon.stub(); - mockConfigGet.withArgs('index').returns('.reporting-test'); - mockConfigGet.withArgs('queue', 'indexInterval').returns('week'); - mockConfig = getMockConfig(mockConfigGet); + const reportingConfig = { + index: '.reporting-test', + queue: { indexInterval: 'week' }, + }; + const mockSchema = createMockConfigSchema(reportingConfig); + mockConfig = createMockConfig(mockSchema); mockCore = await createMockReportingCore(mockConfig); callClusterStub.reset(); @@ -67,15 +69,17 @@ describe('ReportingStore', () => { priority: 10, started_at: undefined, status: 'pending', - timeout: undefined, + timeout: 120000, }); }); it('throws if options has invalid indexInterval', async () => { - const mockConfigGet = sinon.stub(); - mockConfigGet.withArgs('index').returns('.reporting-test'); - mockConfigGet.withArgs('queue', 'indexInterval').returns('centurially'); - mockConfig = getMockConfig(mockConfigGet); + const reportingConfig = { + index: '.reporting-test', + queue: { indexInterval: 'centurially' }, + }; + const mockSchema = createMockConfigSchema(reportingConfig); + mockConfig = createMockConfig(mockSchema); mockCore = await createMockReportingCore(mockConfig); const store = new ReportingStore(mockCore, mockLogger); @@ -159,7 +163,7 @@ describe('ReportingStore', () => { priority: 10, started_at: undefined, status: 'pending', - timeout: undefined, + timeout: 120000, }); }); @@ -190,7 +194,7 @@ describe('ReportingStore', () => { priority: 10, started_at: undefined, status: 'pending', - timeout: undefined, + timeout: 120000, }); }); }); diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index b1309cbdeb94d..0aae8b567bcdb 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -5,6 +5,7 @@ */ import { ElasticsearchServiceSetup } from 'src/core/server'; +import { durationToNumber } from '../../../common/schema_utils'; import { LevelLogger, statuses } from '../'; import { ReportingCore } from '../../'; import { BaseParams, BaseParamsEncryptedFields, ReportingUser } from '../../types'; @@ -45,7 +46,7 @@ export class ReportingStore { this.indexPrefix = config.get('index'); this.indexInterval = config.get('queue', 'indexInterval'); this.jobSettings = { - timeout: config.get('queue', 'timeout'), + timeout: durationToNumber(config.get('queue', 'timeout')), browser_type: config.get('capture', 'browser', 'type'), max_attempts: config.get('capture', 'maxAttempts'), priority: 10, // unused diff --git a/x-pack/plugins/reporting/server/plugin.test.ts b/x-pack/plugins/reporting/server/plugin.test.ts index d323a281c06ff..3f2f472ab0623 100644 --- a/x-pack/plugins/reporting/server/plugin.test.ts +++ b/x-pack/plugins/reporting/server/plugin.test.ts @@ -32,8 +32,8 @@ describe('Reporting Plugin', () => { beforeEach(async () => { configSchema = createMockConfigSchema(); initContext = coreMock.createPluginInitializerContext(configSchema); - coreSetup = await coreMock.createSetup(configSchema); - coreStart = await coreMock.createStart(); + coreSetup = coreMock.createSetup(configSchema); + coreStart = coreMock.createStart(); pluginSetup = ({ licensing: {}, features: featuresPluginMock.createSetup(), diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts index f92fbfc7013cf..71ca0661a42a9 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts @@ -33,7 +33,15 @@ describe('POST /diagnose/browser', () => { const mockedCreateInterface: any = createInterface; const config = { - get: jest.fn().mockImplementation(() => ({})), + get: jest.fn().mockImplementation((...keys) => { + const key = keys.join('.'); + switch (key) { + case 'queue.timeout': + return 120000; + case 'capture.browser.chromium.proxy': + return { enabled: false }; + } + }), kbnConfig: { get: jest.fn() }, }; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts index 24b85220defb4..33620bc9a0038 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts @@ -54,25 +54,30 @@ export const registerDiagnoseBrowser = (reporting: ReportingCore, logger: Logger validate: {}, }, userHandler(async (user, context, req, res) => { - const logs = await browserStartLogs(reporting, logger).toPromise(); - const knownIssues = Object.keys(logsToHelpMap) as Array; + try { + const logs = await browserStartLogs(reporting, logger).toPromise(); + const knownIssues = Object.keys(logsToHelpMap) as Array; - const boundSuccessfully = logs.includes(`DevTools listening on`); - const help = knownIssues.reduce((helpTexts: string[], knownIssue) => { - const helpText = logsToHelpMap[knownIssue]; - if (logs.includes(knownIssue)) { - helpTexts.push(helpText); - } - return helpTexts; - }, []); + const boundSuccessfully = logs.includes(`DevTools listening on`); + const help = knownIssues.reduce((helpTexts: string[], knownIssue) => { + const helpText = logsToHelpMap[knownIssue]; + if (logs.includes(knownIssue)) { + helpTexts.push(helpText); + } + return helpTexts; + }, []); - const response: DiagnosticResponse = { - success: boundSuccessfully && !help.length, - help, - logs, - }; + const response: DiagnosticResponse = { + success: boundSuccessfully && !help.length, + help, + logs, + }; - return res.ok({ body: response }); + return res.ok({ body: response }); + } catch (err) { + logger.error(err); + return res.custom({ statusCode: 500 }); + } }) ); }; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts index 624397246656d..a112d04f38c7b 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts @@ -35,7 +35,15 @@ describe('POST /diagnose/config', () => { } as unknown) as any; config = { - get: jest.fn(), + get: jest.fn().mockImplementation((...keys) => { + const key = keys.join('.'); + switch (key) { + case 'queue.timeout': + return 120000; + case 'csv.maxSizeBytes': + return 1024; + } + }), kbnConfig: { get: jest.fn() }, }; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/config.ts b/x-pack/plugins/reporting/server/routes/diagnostic/config.ts index 198ba63e2614d..95c3a05bbf680 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/config.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/config.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ByteSizeValue } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import numeral from '@elastic/numeral'; import { defaults, get } from 'lodash'; import { ReportingCore } from '../..'; import { API_DIAGNOSE_URL } from '../../../common/constants'; @@ -16,6 +16,14 @@ import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_rout const KIBANA_MAX_SIZE_BYTES_PATH = 'csv.maxSizeBytes'; const ES_MAX_SIZE_BYTES_PATH = 'http.max_content_length'; +const numberToByteSizeValue = (value: number | ByteSizeValue) => { + if (typeof value === 'number') { + return new ByteSizeValue(value); + } + + return value; +}; + export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger) => { const setupDeps = reporting.getPluginSetupDeps(); const userHandler = authorizedUserPreRoutingFactory(reporting); @@ -42,12 +50,10 @@ export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger) 'http.max_content_length', '100mb' ); - const elasticSearchMaxContentBytes = numeral().unformat( - elasticSearchMaxContent.toUpperCase() - ); - const kibanaMaxContentBytes = config.get('csv', 'maxSizeBytes'); + const elasticSearchMaxContentBytes = ByteSizeValue.parse(elasticSearchMaxContent); + const kibanaMaxContentBytes = numberToByteSizeValue(config.get('csv', 'maxSizeBytes')); - if (kibanaMaxContentBytes > elasticSearchMaxContentBytes) { + if (kibanaMaxContentBytes.isGreaterThan(elasticSearchMaxContentBytes)) { const maxContentSizeWarning = i18n.translate( 'xpack.reporting.diagnostic.configSizeMismatch', { @@ -55,8 +61,8 @@ export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger) `xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH} ({kibanaMaxContentBytes}) is higher than ElasticSearch's {ES_MAX_SIZE_BYTES_PATH} ({elasticSearchMaxContentBytes}). ` + `Please set {ES_MAX_SIZE_BYTES_PATH} in ElasticSearch to match, or lower your xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH} in Kibana.`, values: { - kibanaMaxContentBytes, - elasticSearchMaxContentBytes, + kibanaMaxContentBytes: kibanaMaxContentBytes.getValueInBytes(), + elasticSearchMaxContentBytes: elasticSearchMaxContentBytes.getValueInBytes(), KIBANA_MAX_SIZE_BYTES_PATH, ES_MAX_SIZE_BYTES_PATH, }, diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts index ec4ab0446ae5f..287da0d2ed5ec 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts @@ -33,7 +33,11 @@ describe('POST /diagnose/screenshot', () => { }; const config = { - get: jest.fn(), + get: jest.fn().mockImplementation((...keys) => { + if (keys.join('.') === 'queue.timeout') { + return 120000; + } + }), kbnConfig: { get: jest.fn() }, }; const mockLogger = createMockLevelLogger(); diff --git a/x-pack/plugins/reporting/server/routes/jobs.test.ts b/x-pack/plugins/reporting/server/routes/jobs.test.ts index 2957bc76f4682..187c69f4a72ef 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.test.ts +++ b/x-pack/plugins/reporting/server/routes/jobs.test.ts @@ -10,9 +10,8 @@ import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; import { ReportingCore } from '..'; import { ReportingInternalSetup } from '../core'; -import { LevelLogger } from '../lib'; import { ExportTypesRegistry } from '../lib/export_types_registry'; -import { createMockReportingCore } from '../test_helpers'; +import { createMockConfig, createMockConfigSchema, createMockReportingCore } from '../test_helpers'; import { ExportTypeDefinition } from '../types'; import { registerJobInfoRoutes } from './jobs'; @@ -25,11 +24,7 @@ describe('GET /api/reporting/jobs/download', () => { let exportTypesRegistry: ExportTypesRegistry; let core: ReportingCore; - const config = { get: jest.fn(), kbnConfig: { get: jest.fn() } }; - const mockLogger = ({ - error: jest.fn(), - debug: jest.fn(), - } as unknown) as jest.Mocked; + const config = createMockConfig(createMockConfigSchema()); const getHits = (...sources: any) => { return { @@ -86,8 +81,6 @@ describe('GET /api/reporting/jobs/download', () => { }); afterEach(async () => { - mockLogger.debug.mockReset(); - mockLogger.error.mockReset(); await server.stop(); }); diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts index 50780a577af02..932ebfdd22bbc 100644 --- a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts +++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts @@ -4,24 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest, RequestHandlerContext, KibanaResponseFactory } from 'kibana/server'; +import { KibanaRequest, KibanaResponseFactory, RequestHandlerContext } from 'kibana/server'; import { coreMock, httpServerMock } from 'src/core/server/mocks'; import { ReportingCore } from '../../'; -import { createMockReportingCore } from '../../test_helpers'; -import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; import { ReportingInternalSetup } from '../../core'; +import { + createMockConfig, + createMockConfigSchema, + createMockReportingCore, +} from '../../test_helpers'; +import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; let mockCore: ReportingCore; -const kbnConfig = { - 'server.basePath': '/sbp', -}; -const reportingConfig = { - 'roles.allow': ['reporting_user'], -}; -const mockReportingConfig = { - get: (...keys: string[]) => (reportingConfig as any)[keys.join('.')] || 'whoah!', - kbnConfig: { get: (...keys: string[]) => (kbnConfig as any)[keys.join('.')] }, -}; +const mockConfig: any = { 'server.basePath': '/sbp', 'roles.allow': ['reporting_user'] }; +const mockReportingConfigSchema = createMockConfigSchema(mockConfig); +const mockReportingConfig = createMockConfig(mockReportingConfigSchema); const getMockContext = () => (({ diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts index f2785bce10964..d6996d2caf1bc 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; import { Page } from 'puppeteer'; import * as Rx from 'rxjs'; import { chromium, HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from '../browsers'; @@ -15,6 +16,7 @@ import { CaptureConfig } from '../types'; interface CreateMockBrowserDriverFactoryOpts { evaluate: jest.Mock, any[]>; waitForSelector: jest.Mock, any[]>; + waitFor: jest.Mock, any[]>; screenshot: jest.Mock, any[]>; open: jest.Mock, any[]>; getCreatePage: (driver: HeadlessChromiumDriver) => jest.Mock; @@ -86,6 +88,7 @@ const getCreatePage = (driver: HeadlessChromiumDriver) => const defaultOpts: CreateMockBrowserDriverFactoryOpts = { evaluate: mockBrowserEvaluate, waitForSelector: mockWaitForSelector, + waitFor: jest.fn(), screenshot: mockScreenshot, open: jest.fn(), getCreatePage, @@ -96,7 +99,11 @@ export const createMockBrowserDriverFactory = async ( opts: Partial = {} ): Promise => { const captureConfig: CaptureConfig = { - timeouts: { openUrl: 30000, waitForElements: 30000, renderComplete: 30000 }, + timeouts: { + openUrl: moment.duration(60, 's'), + waitForElements: moment.duration(30, 's'), + renderComplete: moment.duration(30, 's'), + }, browser: { type: 'chromium', chromium: { @@ -108,18 +115,14 @@ export const createMockBrowserDriverFactory = async ( }, networkPolicy: { enabled: true, rules: [] }, viewport: { width: 800, height: 600 }, - loadDelay: 2000, + loadDelay: moment.duration(2, 's'), zoom: 2, maxAttempts: 1, }; const binaryPath = '/usr/local/share/common/secure/super_awesome_binary'; - const mockBrowserDriverFactory = await chromium.createDriverFactory( - binaryPath, - captureConfig, - logger - ); - const mockPage = {} as Page; + const mockBrowserDriverFactory = chromium.createDriverFactory(binaryPath, captureConfig, logger); + const mockPage = ({ setViewport: () => {} } as unknown) as Page; const mockBrowserDriver = new HeadlessChromiumDriver(mockPage, { inspect: true, networkPolicy: captureConfig.networkPolicy, @@ -127,6 +130,7 @@ export const createMockBrowserDriverFactory = async ( // mock the driver methods as either default mocks or passed-in mockBrowserDriver.waitForSelector = opts.waitForSelector ? opts.waitForSelector : defaultOpts.waitForSelector; // prettier-ignore + mockBrowserDriver.waitFor = opts.waitFor ? opts.waitFor : defaultOpts.waitFor; mockBrowserDriver.evaluate = opts.evaluate ? opts.evaluate : defaultOpts.evaluate; mockBrowserDriver.screenshot = opts.screenshot ? opts.screenshot : defaultOpts.screenshot; mockBrowserDriver.open = opts.open ? opts.open : defaultOpts.open; diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index 559726e0b8a99..6ec35db5caec6 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -9,14 +9,16 @@ jest.mock('../usage'); jest.mock('../browsers'); jest.mock('../lib/create_queue'); +import _ from 'lodash'; import * as Rx from 'rxjs'; -import { featuresPluginMock } from '../../../features/server/mocks'; import { ReportingConfig, ReportingCore } from '../'; +import { featuresPluginMock } from '../../../features/server/mocks'; import { chromium, HeadlessChromiumDriverFactory, initializeBrowserDriverFactory, } from '../browsers'; +import { ReportingConfigType } from '../config'; import { ReportingInternalSetup, ReportingInternalStart } from '../core'; import { ReportingStore } from '../lib'; import { ReportingStartDeps } from '../types'; @@ -57,12 +59,58 @@ const createMockPluginStart = ( }; }; -export const createMockConfigSchema = (overrides?: any) => ({ - index: '.reporting', - kibanaServer: { hostname: 'localhost', port: '80' }, - capture: { browser: { chromium: { disableSandbox: true } } }, - ...overrides, -}); +interface ReportingConfigTestType { + index: string; + encryptionKey: string; + queue: Partial; + kibanaServer: Partial; + csv: Partial; + capture: any; + server?: any; +} + +export const createMockConfigSchema = ( + overrides: Partial = {} +): ReportingConfigTestType => { + // deeply merge the defaults and the provided partial schema + return { + index: '.reporting', + encryptionKey: 'cool-encryption-key-where-did-you-find-it', + ...overrides, + kibanaServer: { + hostname: 'localhost', + port: 80, + ...overrides.kibanaServer, + }, + capture: { + browser: { + chromium: { + disableSandbox: true, + }, + }, + ...overrides.capture, + }, + queue: { + timeout: 120000, + ...overrides.queue, + }, + csv: { + ...overrides.csv, + }, + }; +}; + +export const createMockConfig = ( + reportingConfig: Partial +): ReportingConfig => { + const mockConfigGet = jest.fn().mockImplementation((...keys: string[]) => { + return _.get(reportingConfig, keys.join('.')); + }); + return { + get: mockConfigGet, + kbnConfig: { get: mockConfigGet }, + }; +}; export const createMockStartDeps = (startMock?: any): ReportingStartDeps => ({ data: startMock.data, diff --git a/x-pack/plugins/reporting/server/test_helpers/index.ts b/x-pack/plugins/reporting/server/test_helpers/index.ts index 2d5ef9fdd768d..96357dc915eef 100644 --- a/x-pack/plugins/reporting/server/test_helpers/index.ts +++ b/x-pack/plugins/reporting/server/test_helpers/index.ts @@ -4,8 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export { createMockServer } from './create_mock_server'; -export { createMockReportingCore, createMockConfigSchema } from './create_mock_reportingplugin'; export { createMockBrowserDriverFactory } from './create_mock_browserdriverfactory'; export { createMockLayoutInstance } from './create_mock_layoutinstance'; export { createMockLevelLogger } from './create_mock_levellogger'; +export { + createMockConfig, + createMockConfigSchema, + createMockReportingCore, +} from './create_mock_reportingplugin'; +export { createMockServer } from './create_mock_server'; diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts index ed2abef2542de..fc2dce441c621 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -8,8 +8,8 @@ import * as Rx from 'rxjs'; import sinon from 'sinon'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { ReportingConfig, ReportingCore } from '../'; -import { createMockReportingCore } from '../test_helpers'; import { getExportTypesRegistry } from '../lib/export_types_registry'; +import { createMockConfig, createMockConfigSchema, createMockReportingCore } from '../test_helpers'; import { ReportingSetupDeps } from '../types'; import { FeaturesAvailability } from './'; import { @@ -54,17 +54,13 @@ function getPluginsMock( } as unknown) as ReportingSetupDeps & { usageCollection: UsageCollectionSetup }; } -const getMockReportingConfig = () => ({ - get: () => {}, - kbnConfig: { get: () => '' }, -}); const getResponseMock = (base = {}) => base; describe('license checks', () => { let mockConfig: ReportingConfig; let mockCore: ReportingCore; beforeAll(async () => { - mockConfig = getMockReportingConfig(); + mockConfig = createMockConfig(createMockConfigSchema()); mockCore = await createMockReportingCore(mockConfig); }); @@ -189,7 +185,7 @@ describe('data modeling', () => { let mockConfig: ReportingConfig; let mockCore: ReportingCore; beforeAll(async () => { - mockConfig = getMockReportingConfig(); + mockConfig = createMockConfig(createMockConfigSchema()); mockCore = await createMockReportingCore(mockConfig); }); test('with normal looking usage data', async () => { @@ -455,7 +451,7 @@ describe('data modeling', () => { describe('Ready for collection observable', () => { test('converts observable to promise', async () => { - const mockConfig = getMockReportingConfig(); + const mockConfig = createMockConfig(createMockConfigSchema()); const mockReporting = await createMockReportingCore(mockConfig); const usageCollection = getMockUsageCollection();