diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.js b/.buildkite/scripts/pipelines/pull_request/pipeline.js index 7b5c944d31c1c..02d6fc270ddb0 100644 --- a/.buildkite/scripts/pipelines/pull_request/pipeline.js +++ b/.buildkite/scripts/pipelines/pull_request/pipeline.js @@ -66,12 +66,12 @@ const uploadPipeline = (pipelineContent) => { pipeline.push(getPipeline('.buildkite/pipelines/pull_request/security_solution.yml')); } - // if ( - // (await doAnyChangesMatch([/^x-pack\/plugins\/apm/])) || - // process.env.GITHUB_PR_LABELS.includes('ci:all-cypress-suites') - // ) { - // pipeline.push(getPipeline('.buildkite/pipelines/pull_request/apm_cypress.yml')); - // } + if ( + (await doAnyChangesMatch([/^x-pack\/plugins\/apm/])) || + process.env.GITHUB_PR_LABELS.includes('ci:all-cypress-suites') + ) { + pipeline.push(getPipeline('.buildkite/pipelines/pull_request/apm_cypress.yml')); + } if (await doAnyChangesMatch([/^x-pack\/plugins\/uptime/])) { pipeline.push(getPipeline('.buildkite/pipelines/pull_request/uptime.yml')); diff --git a/.buildkite/scripts/steps/functional/apm_cypress.sh b/.buildkite/scripts/steps/functional/apm_cypress.sh index 800f22c78d14c..77b26fafee920 100755 --- a/.buildkite/scripts/steps/functional/apm_cypress.sh +++ b/.buildkite/scripts/steps/functional/apm_cypress.sh @@ -2,7 +2,10 @@ set -euo pipefail -source .buildkite/scripts/steps/functional/common.sh +source .buildkite/scripts/common/util.sh + +.buildkite/scripts/bootstrap.sh +.buildkite/scripts/download_build_artifacts.sh export JOB=kibana-apm-cypress @@ -11,4 +14,5 @@ echo "--- APM Cypress Tests" cd "$XPACK_DIR" checks-reporter-with-killswitch "APM Cypress Tests" \ - node plugins/apm/scripts/test/e2e.js + node plugins/apm/scripts/test/e2e.js \ + --kibana-install-dir "$KIBANA_BUILD_LOCATION" diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 01e7beae61ce8..ed6763db69ffe 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -229,6 +229,7 @@ readonly links: { readonly snapshotRestore: Record; readonly ingest: Record; readonly fleet: Readonly<{ + datastreamsILM: string; guide: string; fleetServer: string; fleetServerAddFleetServer: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index fdf469f443f28..96c2c0df9d782 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly elasticStackGetStarted: string;
readonly apm: {
readonly kibanaSettings: string;
readonly supportedServiceMaps: string;
readonly customLinks: string;
readonly droppedTransactionSpans: string;
readonly upgrading: string;
readonly metaData: string;
};
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
readonly suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly libbeat: {
readonly getStarted: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
readonly troubleshootGaps: string;
};
readonly securitySolution: {
readonly trustedApps: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Readonly<{
guide: string;
infrastructureThreshold: string;
logsThreshold: string;
metricsThreshold: string;
monitorStatus: string;
monitorUptime: string;
tlsCertificate: string;
uptimeDurationAnomaly: string;
}>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly spaces: Readonly<{
kibanaLegacyUrlAliases: string;
kibanaDisableLegacyUrlAliasesApi: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
installElasticAgent: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
learnMoreBlog: string;
apiKeysLearnMore: string;
}>;
readonly ecs: {
readonly guide: string;
};
readonly clients: {
readonly guide: string;
readonly goOverview: string;
readonly javaIndex: string;
readonly jsIntro: string;
readonly netGuide: string;
readonly perlGuide: string;
readonly phpGuide: string;
readonly pythonGuide: string;
readonly rubyOverview: string;
readonly rustGuide: string;
};
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly elasticStackGetStarted: string;
readonly apm: {
readonly kibanaSettings: string;
readonly supportedServiceMaps: string;
readonly customLinks: string;
readonly droppedTransactionSpans: string;
readonly upgrading: string;
readonly metaData: string;
};
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
readonly suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly libbeat: {
readonly getStarted: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
readonly troubleshootGaps: string;
};
readonly securitySolution: {
readonly trustedApps: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Readonly<{
guide: string;
infrastructureThreshold: string;
logsThreshold: string;
metricsThreshold: string;
monitorStatus: string;
monitorUptime: string;
tlsCertificate: string;
uptimeDurationAnomaly: string;
}>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
elasticsearchEnableApiKeys: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly spaces: Readonly<{
kibanaLegacyUrlAliases: string;
kibanaDisableLegacyUrlAliasesApi: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
datastreamsILM: string;
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
installElasticAgent: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
learnMoreBlog: string;
apiKeysLearnMore: string;
}>;
readonly ecs: {
readonly guide: string;
};
readonly clients: {
readonly guide: string;
readonly goOverview: string;
readonly javaIndex: string;
readonly jsIntro: string;
readonly netGuide: string;
readonly perlGuide: string;
readonly phpGuide: string;
readonly pythonGuide: string;
readonly rubyOverview: string;
readonly rustGuide: string;
};
} | | diff --git a/docs/maps/vector-layer.asciidoc b/docs/maps/vector-layer.asciidoc index 7191197c27dbe..f70e4d59796cc 100644 --- a/docs/maps/vector-layer.asciidoc +++ b/docs/maps/vector-layer.asciidoc @@ -27,9 +27,9 @@ Results exceeding `index.max_result_window` are not displayed. * *Show clusters when results exceed 10,000* When results exceed `index.max_result_window`, the layer uses {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[GeoTile grid aggregation] to group your documents into clusters and displays metrics for each cluster. When results are less then `index.max_result_window`, the layer displays features from individual documents. -* *Use vector tiles.* Vector tiles partition your map into 6 to 8 tiles. +* *Use vector tiles.* Vector tiles partition your map into tiles. Each tile request is limited to the `index.max_result_window` index setting. -Tiles exceeding `index.max_result_window` have a visual indicator when there are too many features to display. +When a tile exceeds `index.max_result_window`, results exceeding `index.max_result_window` are not contained in the tile and a dashed rectangle outlining the bounding box containing all geo values within the tile is displayed. *EMS Boundaries*:: Administrative boundaries from https://www.elastic.co/elastic-maps-service[Elastic Maps Service]. diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index c291b65c3c35b..7737745c7cfa8 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -12,20 +12,6 @@ You do not need to configure any additional settings to use the [[general-security-settings]] ==== General security settings -[cols="2*<"] -|=== -| `xpack.security.enabled` - | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] - By default, {kib} automatically detects whether to enable the - {security-features} based on the license and whether {es} {security-features} - are enabled. + - + - Do not set this to `false`; it disables the login form, user and role management - screens, and authorization using <>. To disable - {security-features} entirely, see - {ref}/security-settings.html[{es} security settings]. -|=== - [float] [[authentication-security-settings]] ==== Authentication security settings diff --git a/package.json b/package.json index e6b17783197bc..d9dd4912481b9 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.21", "@elastic/ems-client": "7.16.0", - "@elastic/eui": "39.1.1", + "@elastic/eui": "40.0.0", "@elastic/filesaver": "1.1.2", "@elastic/maki": "6.3.0", "@elastic/node-crypto": "1.2.1", @@ -166,7 +166,6 @@ "@mapbox/geojson-rewind": "^0.5.0", "@mapbox/mapbox-gl-draw": "1.3.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", - "@mapbox/vector-tile": "1.3.1", "@reduxjs/toolkit": "^1.6.1", "@slack/webhook": "^5.0.4", "@turf/along": "6.0.1", @@ -460,6 +459,7 @@ "@kbn/test": "link:bazel-bin/packages/kbn-test", "@kbn/test-subj-selector": "link:bazel-bin/packages/kbn-test-subj-selector", "@loaders.gl/polyfills": "^2.3.5", + "@mapbox/vector-tile": "1.3.1", "@microsoft/api-documenter": "7.7.2", "@microsoft/api-extractor": "7.7.0", "@octokit/rest": "^16.35.0", diff --git a/src/cli_setup/utils.ts b/src/cli_setup/utils.ts index 65a46b8f5b278..21406bf7e57e0 100644 --- a/src/cli_setup/utils.ts +++ b/src/cli_setup/utils.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { getConfigPath } from '@kbn/utils'; +import { getConfigPath, getDataPath } from '@kbn/utils'; import inquirer from 'inquirer'; import { duration } from 'moment'; import { merge } from 'lodash'; @@ -30,7 +30,7 @@ const logger: Logger = { get: () => logger, }; -export const kibanaConfigWriter = new KibanaConfigWriter(getConfigPath(), logger); +export const kibanaConfigWriter = new KibanaConfigWriter(getConfigPath(), getDataPath(), logger); export const elasticsearch = new ElasticsearchService(logger).setup({ connectionCheckInterval: duration(Infinity), elasticsearch: { diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 6987b779d5d45..571b564f90329 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -644,6 +644,11 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` > } + buttonElement="button" className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-recentlyViewed" + element="div" id="generated-id" initialIsOpen={true} isLoading={false} @@ -685,28 +692,13 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` + + +
} + buttonElement="button" className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-kibana" + element="div" id="generated-id" initialIsOpen={true} isLoading={false} @@ -941,28 +977,13 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` + + +
} + buttonElement="button" className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-observability" + element="div" id="generated-id" initialIsOpen={true} isLoading={false} @@ -1233,28 +1298,13 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` + + +
} + buttonElement="button" className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-securitySolution" + element="div" id="generated-id" initialIsOpen={true} isLoading={false} @@ -1486,28 +1580,13 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` + + +
} + buttonElement="button" className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-management" + element="div" id="generated-id" initialIsOpen={true} isLoading={false} @@ -1700,28 +1823,13 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` + + +
{ diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 20757463737fc..2bbb4703ecd19 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -480,6 +480,7 @@ export class DocLinksService { troubleshooting: `${FLEET_DOCS}fleet-troubleshooting.html`, elasticAgent: `${FLEET_DOCS}elastic-agent-installation.html`, datastreams: `${FLEET_DOCS}data-streams.html`, + datastreamsILM: `${FLEET_DOCS}data-streams.html#data-streams-ilm`, datastreamsNamingScheme: `${FLEET_DOCS}data-streams.html#data-streams-naming-scheme`, installElasticAgent: `${FLEET_DOCS}install-fleet-managed-elastic-agent.html`, upgradeElasticAgent: `${FLEET_DOCS}upgrade-elastic-agent.html`, @@ -734,6 +735,7 @@ export interface DocLinksStart { readonly snapshotRestore: Record; readonly ingest: Record; readonly fleet: Readonly<{ + datastreamsILM: string; guide: string; fleetServer: string; fleetServerAddFleetServer: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 353e5aa4607e4..bd274d7994bfa 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -698,6 +698,7 @@ export interface DocLinksStart { readonly snapshotRestore: Record; readonly ingest: Record; readonly fleet: Readonly<{ + datastreamsILM: string; guide: string; fleetServer: string; fleetServerAddFleetServer: string; diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 235a5fbe1a1a3..3a38789fbcac6 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -343,7 +343,6 @@ kibana_vars=( xpack.security.authc.saml.realm xpack.security.authc.selector.enabled xpack.security.cookieName - xpack.security.enabled xpack.security.encryptionKey xpack.security.loginAssistanceMessage xpack.security.loginHelp diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts index efa54e74fdf2f..305eeb9a6a358 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -75,6 +75,6 @@ export const LICENSE_OVERRIDES = { 'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts '@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint '@elastic/ems-client@7.16.0': ['Elastic License 2.0'], - '@elastic/eui@39.1.1': ['SSPL-1.0 OR Elastic License 2.0'], + '@elastic/eui@40.0.0': ['SSPL-1.0 OR Elastic License 2.0'], 'language-subtag-registry@0.3.21': ['CC-BY-4.0'], // retired ODC‑By license https://github.com/mattcg/language-subtag-registry }; diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx index c3b4075690261..3237eb106e4ec 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx @@ -206,7 +206,9 @@ describe('Dashboard container lifecycle', () => { }); }); -describe('Dashboard initial state', () => { +// FLAKY: https://github.com/elastic/kibana/issues/116050 +// FLAKY: https://github.com/elastic/kibana/issues/105018 +describe.skip('Dashboard initial state', () => { it('Extracts state from Dashboard Saved Object', async () => { const { renderHookResult, embeddableFactoryResult } = renderDashboardAppStateHook({}); const getResult = () => renderHookResult.result.current; @@ -276,7 +278,8 @@ describe('Dashboard initial state', () => { }); }); -describe('Dashboard state sync', () => { +// FLAKY: https://github.com/elastic/kibana/issues/116043 +describe.skip('Dashboard state sync', () => { let defaultDashboardAppStateHookResult: RenderDashboardStateHookReturn; const getResult = () => defaultDashboardAppStateHookResult.renderHookResult.result.current; diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts index e141bfbef3c89..1c457fdb64ce2 100644 --- a/src/plugins/data/common/constants.ts +++ b/src/plugins/data/common/constants.ts @@ -35,4 +35,5 @@ export const UI_SETTINGS = { AUTOCOMPLETE_USE_TIMERANGE: 'autocomplete:useTimeRange', AUTOCOMPLETE_VALUE_SUGGESTION_METHOD: 'autocomplete:valueSuggestionMethod', DATE_FORMAT: 'dateFormat', + DATEFORMAT_TZ: 'dateFormat:tz', } as const; diff --git a/src/plugins/data_views/server/saved_objects/data_views.ts b/src/plugins/data_views/server/saved_objects/data_views.ts index 5bb85a9bb6e98..ca7592732c3ee 100644 --- a/src/plugins/data_views/server/saved_objects/data_views.ts +++ b/src/plugins/data_views/server/saved_objects/data_views.ts @@ -13,7 +13,8 @@ import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../common'; export const dataViewSavedObjectType: SavedObjectsType = { name: DATA_VIEW_SAVED_OBJECT_TYPE, hidden: false, - namespaceType: 'single', + namespaceType: 'multiple-isolated', + convertToMultiNamespaceTypeVersion: '8.0.0', management: { displayName: 'Data view', icon: 'indexPatternApp', diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/_table_header.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/_table_header.scss index 3450084e19269..3ea6fb5502764 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/components/_table_header.scss +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/_table_header.scss @@ -1,7 +1,8 @@ .kbnDocTableHeader { white-space: nowrap; } -.kbnDocTableHeader button { +.kbnDocTableHeader button, +.kbnDocTableHeader svg { margin-left: $euiSizeXS * .5; } .kbnDocTableHeader__move, diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap index 18b0ae8699e3e..3f72349f3e2a0 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap @@ -15,7 +15,16 @@ exports[`TableHeader with time column renders correctly 1`] = ` class="kbnDocTableHeader__actions" data-test-subj="docTableHeader-time" > - Time + time + + + diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx index d313e95c1ebb1..f04454d33e9f2 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx @@ -28,7 +28,7 @@ export interface ColumnProps { export function getTimeColumn(timeFieldName: string): ColumnProps { return { name: timeFieldName, - displayName: 'Time', + displayName: timeFieldName, isSortable: true, isRemoveable: false, colLeftIdx: -1, diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx index f891e809ee702..1877c014ddcbd 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx @@ -45,6 +45,8 @@ export function TableHeader({ void; onMoveColumn?: (name: string, idx: number) => void; @@ -54,6 +56,8 @@ export function TableHeaderColumn({ displayName, isRemoveable, isSortable, + isTimeColumn, + customLabel, name, onChangeSortOrder, onMoveColumn, @@ -65,6 +69,14 @@ export function TableHeaderColumn({ const curColSort = sortOrder.find((pair) => pair[0] === name); const curColSortDir = (curColSort && curColSort[1]) || ''; + const timeAriaLabel = i18n.translate( + 'discover.docTable.tableHeader.timeFieldIconTooltipAriaLabel', + { defaultMessage: 'Primary time field.' } + ); + const timeTooltip = i18n.translate('discover.docTable.tableHeader.timeFieldIconTooltip', { + defaultMessage: 'This field represents the time that events occurred.', + }); + // If this is the _score column, and _score is not one of the columns inside the sort, show a // warning that the _score will not be retrieved from Elasticsearch const showScoreSortWarning = name === '_score' && !curColSort; @@ -183,7 +195,15 @@ export function TableHeaderColumn({ {showScoreSortWarning && } - {displayName} + {customLabel ?? displayName} + {isTimeColumn && ( + + )} {buttons .filter((button) => button.active) .map((button, idx) => ( diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx index 46e30dd23525b..e5ea657032403 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx @@ -117,7 +117,15 @@ describe('Discover grid columns ', function () { [Function], [Function], ], - "display": "Time (timestamp)", + "display": + timestamp + + + , "id": "timestamp", "initialWidth": 190, "isSortable": true, diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx index 2f4c0b5167df8..5eb55a8e99cde 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiDataGridColumn, EuiScreenReaderOnly } from '@elastic/eui'; +import { EuiDataGridColumn, EuiIconTip, EuiScreenReaderOnly } from '@elastic/eui'; import { ExpandButton } from './discover_grid_expand_button'; import { DiscoverGridSettings } from './types'; import type { IndexPattern } from '../../../../../data/common'; @@ -57,9 +57,6 @@ export function buildEuiGridColumn( defaultColumns: boolean, isSortEnabled: boolean ) { - const timeString = i18n.translate('discover.timeLabel', { - defaultMessage: 'Time', - }); const indexPatternField = indexPattern.getFieldByName(columnName); const column: EuiDataGridColumn = { id: columnName, @@ -88,7 +85,23 @@ export function buildEuiGridColumn( }; if (column.id === indexPattern.timeFieldName) { - column.display = `${timeString} (${indexPattern.timeFieldName})`; + const primaryTimeAriaLabel = i18n.translate( + 'discover.docTable.tableHeader.timeFieldIconTooltipAriaLabel', + { defaultMessage: 'Primary time field.' } + ); + const primaryTimeTooltip = i18n.translate( + 'discover.docTable.tableHeader.timeFieldIconTooltip', + { + defaultMessage: 'This field represents the time that events occurred.', + } + ); + + column.display = ( + + {indexPatternField?.customLabel ?? indexPattern.timeFieldName}{' '} + + + ); column.initialWidth = defaultTimeColumnWidth; } if (columnWidth > 0) { diff --git a/src/plugins/interactive_setup/server/kibana_config_writer.test.ts b/src/plugins/interactive_setup/server/kibana_config_writer.test.ts index 4b68451930a3d..0580a35d909ea 100644 --- a/src/plugins/interactive_setup/server/kibana_config_writer.test.ts +++ b/src/plugins/interactive_setup/server/kibana_config_writer.test.ts @@ -30,6 +30,7 @@ describe('KibanaConfigWriter', () => { kibanaConfigWriter = new KibanaConfigWriter( '/some/path/kibana.yml', + '/data', loggingSystemMock.createLogger() ); }); @@ -37,15 +38,15 @@ describe('KibanaConfigWriter', () => { afterEach(() => jest.resetAllMocks()); describe('#isConfigWritable()', () => { - it('returns `false` if config directory is not writable even if kibana yml is writable', async () => { + it('returns `false` if data directory is not writable even if kibana yml is writable', async () => { mockFsAccess.mockImplementation((path, modifier) => - path === '/some/path' && modifier === constants.W_OK ? Promise.reject() : Promise.resolve() + path === '/data' && modifier === constants.W_OK ? Promise.reject() : Promise.resolve() ); await expect(kibanaConfigWriter.isConfigWritable()).resolves.toBe(false); }); - it('returns `false` if kibana yml is NOT writable if even config directory is writable', async () => { + it('returns `false` if kibana yml is NOT writable if even data directory is writable', async () => { mockFsAccess.mockImplementation((path, modifier) => path === '/some/path/kibana.yml' && modifier === constants.W_OK ? Promise.reject() @@ -55,219 +56,208 @@ describe('KibanaConfigWriter', () => { await expect(kibanaConfigWriter.isConfigWritable()).resolves.toBe(false); }); - it('returns `true` if both kibana yml and config directory are writable', async () => { + it('returns `true` if both kibana yml and data directory are writable', async () => { mockFsAccess.mockResolvedValue(undefined); await expect(kibanaConfigWriter.isConfigWritable()).resolves.toBe(true); }); - it('returns `true` even if kibana yml does not exist when config directory is writable', async () => { + it('returns `true` even if kibana yml does not exist even if data directory is writable', async () => { mockFsAccess.mockImplementation((path) => path === '/some/path/kibana.yml' ? Promise.reject() : Promise.resolve() ); - await expect(kibanaConfigWriter.isConfigWritable()).resolves.toBe(true); + await expect(kibanaConfigWriter.isConfigWritable()).resolves.toBe(false); }); }); describe('#writeConfig()', () => { - describe('without existing config', () => { - beforeEach(() => { - mockReadFile.mockResolvedValue(''); - }); - - it('throws if cannot write CA file', async () => { - mockWriteFile.mockRejectedValue(new Error('Oh no!')); - - await expect( - kibanaConfigWriter.writeConfig({ - caCert: 'ca-content', - host: '', - serviceAccountToken: { name: '', value: '' }, - }) - ).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`); - - expect(mockWriteFile).toHaveBeenCalledTimes(1); - expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content'); - }); - - it('throws if cannot write config to yaml file', async () => { - mockWriteFile.mockResolvedValueOnce(undefined).mockRejectedValueOnce(new Error('Oh no!')); - - await expect( - kibanaConfigWriter.writeConfig({ - caCert: 'ca-content', - host: 'some-host', - serviceAccountToken: { name: 'some-token', value: 'some-value' }, - }) - ).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`); - - expect(mockWriteFile).toHaveBeenCalledTimes(2); - expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content'); - expect(mockWriteFile).toHaveBeenCalledWith( - '/some/path/kibana.yml', - ` - -# This section was automatically generated during setup. -elasticsearch.hosts: [some-host] -elasticsearch.serviceAccountToken: some-value -elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt] - -` - ); - }); - - it('throws if cannot read existing config', async () => { - mockReadFile.mockRejectedValue(new Error('Oh no!')); - - await expect( - kibanaConfigWriter.writeConfig({ - caCert: 'ca-content', - host: 'some-host', - serviceAccountToken: { name: 'some-token', value: 'some-value' }, - }) - ).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`); - - expect(mockWriteFile).not.toHaveBeenCalled(); - }); - - it('throws if cannot parse existing config', async () => { - mockReadFile.mockResolvedValue('foo: bar\nfoo: baz'); - - await expect( - kibanaConfigWriter.writeConfig({ - caCert: 'ca-content', - host: 'some-host', - serviceAccountToken: { name: 'some-token', value: 'some-value' }, - }) - ).rejects.toMatchInlineSnapshot(` - [YAMLException: duplicated mapping key at line 2, column 1: - foo: baz - ^] - `); - - expect(mockWriteFile).not.toHaveBeenCalled(); - }); - - it('can successfully write CA certificate and elasticsearch config with service token', async () => { - await expect( - kibanaConfigWriter.writeConfig({ - caCert: 'ca-content', - host: 'some-host', - serviceAccountToken: { name: 'some-token', value: 'some-value' }, - }) - ).resolves.toBeUndefined(); + beforeEach(() => { + mockReadFile.mockResolvedValue( + '# Default Kibana configuration for docker target\nserver.host: "0.0.0.0"\nserver.shutdownTimeout: "5s"' + ); + }); - expect(mockWriteFile).toHaveBeenCalledTimes(2); - expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content'); - expect(mockWriteFile).toHaveBeenCalledWith( - '/some/path/kibana.yml', - ` + it('throws if cannot write CA file', async () => { + mockWriteFile.mockRejectedValue(new Error('Oh no!')); -# This section was automatically generated during setup. -elasticsearch.hosts: [some-host] -elasticsearch.serviceAccountToken: some-value -elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt] + await expect( + kibanaConfigWriter.writeConfig({ + caCert: 'ca-content', + host: '', + serviceAccountToken: { name: '', value: '' }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`); -` - ); - }); + expect(mockWriteFile).toHaveBeenCalledTimes(1); + expect(mockWriteFile).toHaveBeenCalledWith('/data/ca_1234.crt', 'ca-content'); + }); - it('can successfully write CA certificate and elasticsearch config with credentials', async () => { - await expect( - kibanaConfigWriter.writeConfig({ - caCert: 'ca-content', - host: 'some-host', - username: 'username', - password: 'password', - }) - ).resolves.toBeUndefined(); + it('throws if cannot write config to yaml file', async () => { + mockWriteFile.mockResolvedValueOnce(undefined).mockRejectedValueOnce(new Error('Oh no!')); - expect(mockWriteFile).toHaveBeenCalledTimes(2); - expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content'); - expect(mockWriteFile).toHaveBeenCalledWith( - '/some/path/kibana.yml', - ` - -# This section was automatically generated during setup. -elasticsearch.hosts: [some-host] -elasticsearch.password: password -elasticsearch.username: username -elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt] - -` - ); - }); + await expect( + kibanaConfigWriter.writeConfig({ + caCert: 'ca-content', + host: 'some-host', + serviceAccountToken: { name: 'some-token', value: 'some-value' }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`); - it('can successfully write elasticsearch config without CA certificate', async () => { - await expect( - kibanaConfigWriter.writeConfig({ - host: 'some-host', - username: 'username', - password: 'password', - }) - ).resolves.toBeUndefined(); + expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/data/ca_1234.crt", + "ca-content", + ], + Array [ + "/some/path/kibana.yml", + "# Default Kibana configuration for docker target + server.host: \\"0.0.0.0\\" + server.shutdownTimeout: \\"5s\\" + + # This section was automatically generated during setup. + elasticsearch.hosts: [some-host] + elasticsearch.serviceAccountToken: some-value + elasticsearch.ssl.certificateAuthorities: [/data/ca_1234.crt] + + ", + ], + ] + `); + }); - expect(mockWriteFile).toHaveBeenCalledTimes(1); - expect(mockWriteFile).toHaveBeenCalledWith( - '/some/path/kibana.yml', - ` + it('throws if cannot read existing config', async () => { + mockReadFile.mockRejectedValue(new Error('Oh no!')); -# This section was automatically generated during setup. -elasticsearch.hosts: [some-host] -elasticsearch.password: password -elasticsearch.username: username + await expect( + kibanaConfigWriter.writeConfig({ + caCert: 'ca-content', + host: 'some-host', + serviceAccountToken: { name: 'some-token', value: 'some-value' }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`); -` - ); - }); + expect(mockWriteFile).not.toHaveBeenCalled(); }); - describe('with existing config (no conflicts)', () => { - beforeEach(() => { - mockReadFile.mockResolvedValue( - '# Default Kibana configuration for docker target\nserver.host: "0.0.0.0"\nserver.shutdownTimeout: "5s"' - ); - }); - - it('can successfully write CA certificate and elasticsearch config', async () => { - await expect( - kibanaConfigWriter.writeConfig({ - caCert: 'ca-content', - host: 'some-host', - serviceAccountToken: { name: 'some-token', value: 'some-value' }, - }) - ).resolves.toBeUndefined(); - - expect(mockReadFile).toHaveBeenCalledTimes(1); - expect(mockReadFile).toHaveBeenCalledWith('/some/path/kibana.yml', 'utf-8'); + it('throws if cannot parse existing config', async () => { + mockReadFile.mockResolvedValue('foo: bar\nfoo: baz'); + + await expect( + kibanaConfigWriter.writeConfig({ + caCert: 'ca-content', + host: 'some-host', + serviceAccountToken: { name: 'some-token', value: 'some-value' }, + }) + ).rejects.toMatchInlineSnapshot(` + [YAMLException: duplicated mapping key at line 2, column 1: + foo: baz + ^] + `); + + expect(mockWriteFile).not.toHaveBeenCalled(); + }); - expect(mockWriteFile).toHaveBeenCalledTimes(2); - expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(` + it('can successfully write CA certificate and elasticsearch config with credentials', async () => { + await expect( + kibanaConfigWriter.writeConfig({ + caCert: 'ca-content', + host: 'some-host', + username: 'username', + password: 'password', + }) + ).resolves.toBeUndefined(); + + expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(` + Array [ Array [ - Array [ - "/some/path/ca_1234.crt", - "ca-content", - ], - Array [ - "/some/path/kibana.yml", - "# Default Kibana configuration for docker target - server.host: \\"0.0.0.0\\" - server.shutdownTimeout: \\"5s\\" + "/data/ca_1234.crt", + "ca-content", + ], + Array [ + "/some/path/kibana.yml", + "# Default Kibana configuration for docker target + server.host: \\"0.0.0.0\\" + server.shutdownTimeout: \\"5s\\" + + # This section was automatically generated during setup. + elasticsearch.hosts: [some-host] + elasticsearch.password: password + elasticsearch.username: username + elasticsearch.ssl.certificateAuthorities: [/data/ca_1234.crt] + + ", + ], + ] + `); + }); - # This section was automatically generated during setup. - elasticsearch.hosts: [some-host] - elasticsearch.serviceAccountToken: some-value - elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt] + it('can successfully write elasticsearch config without CA certificate', async () => { + await expect( + kibanaConfigWriter.writeConfig({ + host: 'some-host', + username: 'username', + password: 'password', + }) + ).resolves.toBeUndefined(); + + expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/some/path/kibana.yml", + "# Default Kibana configuration for docker target + server.host: \\"0.0.0.0\\" + server.shutdownTimeout: \\"5s\\" + + # This section was automatically generated during setup. + elasticsearch.hosts: [some-host] + elasticsearch.password: password + elasticsearch.username: username + + ", + ], + ] + `); + }); - ", - ], - ] - `); - }); + it('can successfully write CA certificate and elasticsearch config with service token', async () => { + await expect( + kibanaConfigWriter.writeConfig({ + caCert: 'ca-content', + host: 'some-host', + serviceAccountToken: { name: 'some-token', value: 'some-value' }, + }) + ).resolves.toBeUndefined(); + + expect(mockReadFile).toHaveBeenCalledTimes(1); + expect(mockReadFile).toHaveBeenCalledWith('/some/path/kibana.yml', 'utf-8'); + + expect(mockWriteFile).toHaveBeenCalledTimes(2); + expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/data/ca_1234.crt", + "ca-content", + ], + Array [ + "/some/path/kibana.yml", + "# Default Kibana configuration for docker target + server.host: \\"0.0.0.0\\" + server.shutdownTimeout: \\"5s\\" + + # This section was automatically generated during setup. + elasticsearch.hosts: [some-host] + elasticsearch.serviceAccountToken: some-value + elasticsearch.ssl.certificateAuthorities: [/data/ca_1234.crt] + + ", + ], + ] + `); }); - describe('with existing config (with conflicts)', () => { + describe('with conflicts', () => { beforeEach(() => { jest.spyOn(Date.prototype, 'toISOString').mockReturnValue('some date'); mockReadFile.mockResolvedValue( @@ -291,7 +281,7 @@ elasticsearch.username: username expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/some/path/ca_1234.crt", + "/data/ca_1234.crt", "ca-content", ], Array [ @@ -312,7 +302,7 @@ elasticsearch.username: username elasticsearch.hosts: [some-host] monitoring.ui.container.elasticsearch.enabled: true elasticsearch.serviceAccountToken: some-value - elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt] + elasticsearch.ssl.certificateAuthorities: [/data/ca_1234.crt] ", ], diff --git a/src/plugins/interactive_setup/server/kibana_config_writer.ts b/src/plugins/interactive_setup/server/kibana_config_writer.ts index ff67e887fab49..ea7f776aad82f 100644 --- a/src/plugins/interactive_setup/server/kibana_config_writer.ts +++ b/src/plugins/interactive_setup/server/kibana_config_writer.ts @@ -31,24 +31,23 @@ export type WriteConfigParameters = { ); export class KibanaConfigWriter { - constructor(private readonly configPath: string, private readonly logger: Logger) {} + constructor( + private readonly configPath: string, + private readonly dataDirectoryPath: string, + private readonly logger: Logger + ) {} /** - * Checks if we can write to the Kibana configuration file and configuration directory. + * Checks if we can write to the Kibana configuration file and data directory. */ public async isConfigWritable() { try { // We perform two separate checks here: - // 1. If we can write to config directory to add a new CA certificate file and potentially Kibana configuration - // file if it doesn't exist for some reason. + // 1. If we can write to data directory to add a new CA certificate file. // 2. If we can write to the Kibana configuration file if it exists. - const canWriteToConfigDirectory = fs.access(path.dirname(this.configPath), constants.W_OK); await Promise.all([ - canWriteToConfigDirectory, - fs.access(this.configPath, constants.F_OK).then( - () => fs.access(this.configPath, constants.W_OK), - () => canWriteToConfigDirectory - ), + fs.access(this.dataDirectoryPath, constants.W_OK), + fs.access(this.configPath, constants.W_OK), ]); return true; } catch { @@ -61,7 +60,7 @@ export class KibanaConfigWriter { * @param params */ public async writeConfig(params: WriteConfigParameters) { - const caPath = path.join(path.dirname(this.configPath), `ca_${Date.now()}.crt`); + const caPath = path.join(this.dataDirectoryPath, `ca_${Date.now()}.crt`); const config: Record = { 'elasticsearch.hosts': [params.host] }; if ('serviceAccountToken' in params) { config['elasticsearch.serviceAccountToken'] = params.serviceAccountToken.value; diff --git a/src/plugins/interactive_setup/server/plugin.ts b/src/plugins/interactive_setup/server/plugin.ts index 8c1d00a254764..067b8fd044f30 100644 --- a/src/plugins/interactive_setup/server/plugin.ts +++ b/src/plugins/interactive_setup/server/plugin.ts @@ -10,6 +10,7 @@ import chalk from 'chalk'; import type { Subscription } from 'rxjs'; import type { TypeOf } from '@kbn/config-schema'; +import { getDataPath } from '@kbn/utils'; import type { CorePreboot, Logger, PluginInitializerContext, PrebootPlugin } from 'src/core/server'; import { ElasticsearchConnectionStatus } from '../common'; @@ -146,7 +147,11 @@ Go to ${chalk.cyanBright.underline(url)} to get started. basePath: core.http.basePath, logger: this.#logger.get('routes'), preboot: { ...core.preboot, completeSetup }, - kibanaConfigWriter: new KibanaConfigWriter(configPath, this.#logger.get('kibana-config')), + kibanaConfigWriter: new KibanaConfigWriter( + configPath, + getDataPath(), + this.#logger.get('kibana-config') + ), elasticsearch, verificationCode, getConfig: this.#getConfig.bind(this), diff --git a/src/plugins/telemetry/server/fetcher.ts b/src/plugins/telemetry/server/fetcher.ts index 02ac428b07667..97180f351986e 100644 --- a/src/plugins/telemetry/server/fetcher.ts +++ b/src/plugins/telemetry/server/fetcher.ts @@ -230,6 +230,7 @@ export class FetcherTask { method: 'post', body: stats, headers: { + 'Content-Type': 'application/json', 'X-Elastic-Stack-Version': this.currentKibanaVersion, 'X-Elastic-Cluster-ID': clusterUuid, 'X-Elastic-Content-Encoding': PAYLOAD_CONTENT_ENCODING, diff --git a/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap b/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap index bc6d28bd5c1c4..b25444d16c46a 100644 --- a/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap +++ b/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap @@ -12,8 +12,10 @@ exports[`DefaultEditorAgg component should init with the default set of props 1` } buttonContentClassName="visEditorSidebar__aggGroupAccordionButtonContent eui-textTruncate" + buttonElement="button" className="visEditorSidebar__section visEditorSidebar__collapsible visEditorSidebar__collapsible--marginBottom" data-test-subj="visEditorAggAccordion1" + element="div" extraAction={
)} - + + +
+ } + > + + ); diff --git a/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx b/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx index 34cc1dc347ef8..ad069a4d7e2cc 100644 --- a/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx +++ b/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx @@ -13,6 +13,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { IUiSettingsClient } from 'kibana/public'; +import { EuiLoadingChart } from '@elastic/eui'; import { fetchIndexPattern } from '../common/index_patterns_utils'; import { VisualizationContainer, PersistedState } from '../../../visualizations/public'; @@ -43,10 +44,6 @@ export const getTimeseriesVisRenderer: (deps: { name: 'timeseries_vis', reuseDomNode: true, render: async (domNode, config, handlers) => { - // Build optimization. Move app styles from main bundle - // @ts-expect-error TS error, cannot find type declaration for scss - await import('./application/index.scss'); - handlers.onDestroy(() => { unmountComponentAtNode(domNode); }); @@ -55,33 +52,49 @@ export const getTimeseriesVisRenderer: (deps: { const { indexPatterns } = getDataStart(); const showNoResult = !checkIfDataExists(visData, model); - const [palettesService, { indexPattern }] = await Promise.all([ + + let servicesLoaded; + + Promise.all([ palettes.getPalettes(), fetchIndexPattern(model.index_pattern, indexPatterns), - ]); + ]).then(([palettesService, { indexPattern }]) => { + servicesLoaded = true; - render( - - - + - - , - domNode - ); + showNoResult={showNoResult} + error={get(visData, [model.id, 'error'])} + > + + + , + domNode + ); + }); + + if (!servicesLoaded) { + render( +
+ +
, + domNode + ); + } }, }); diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts index b7a22abd825e0..7c17f003dfbab 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts @@ -24,10 +24,15 @@ export function getIntervalAndTimefield( { min, max, maxBuckets }: IntervalParams, series?: Series ) { - const timeField = + let timeField = (series?.override_index_pattern ? series.series_time_field : panel.time_field) || index.indexPattern?.timeFieldName; + // should use @timestamp as default timeField for es indeces if user doesn't provide timeField + if (!panel.use_kibana_indexes && !timeField) { + timeField = '@timestamp'; + } + if (panel.use_kibana_indexes) { validateField(timeField!, index); } diff --git a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap index f50836e6ca8af..1dd916f827fe6 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap +++ b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap @@ -66,8 +66,10 @@ exports[`ValueAxesPanel component should init with the default set of props 1`] } buttonContentClassName="visEditorSidebar__aggGroupAccordionButtonContent eui-textTruncate" + buttonElement="button" className="visEditorSidebar__section visEditorSidebar__collapsible" data-test-subj="toggleYAxisOptions-ValueAxis-1" + element="div" extraAction={ } buttonContentClassName="visEditorSidebar__aggGroupAccordionButtonContent eui-textTruncate" + buttonElement="button" className="visEditorSidebar__section visEditorSidebar__collapsible" data-test-subj="toggleYAxisOptions-ValueAxis-2" + element="div" extraAction={ + +
, + this.domNode + ); + const expressions = getExpressions(); this.handler = await expressions.loader(this.domNode, undefined, { onRenderError: (element: HTMLElement, error: ExpressionRenderError) => { diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_async.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable_async.ts index c7480844adbea..2fa22cfe8d80b 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_async.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_async.ts @@ -12,10 +12,12 @@ export const createVisualizeEmbeddableAsync = async ( ...args: ConstructorParameters ) => { // Build optimization. Move app styles from main bundle - // @ts-expect-error TS error, cannot find type declaration for scss - await import('./embeddables.scss'); - const { VisualizeEmbeddable } = await import('./visualize_embeddable'); + const [{ VisualizeEmbeddable }] = await Promise.all([ + import('./visualize_embeddable'), + // @ts-expect-error TS error, cannot find type declaration for scss + import('./embeddables.scss'), + ]); return new VisualizeEmbeddable(...args); }; diff --git a/test/api_integration/apis/custom_integration/integrations.ts b/test/api_integration/apis/custom_integration/integrations.ts index e4797b334a866..0784a86e4b546 100644 --- a/test/api_integration/apis/custom_integration/integrations.ts +++ b/test/api_integration/apis/custom_integration/integrations.ts @@ -22,7 +22,7 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body).to.be.an('array'); - expect(resp.body.length).to.be(12); + expect(resp.body.length).to.be(33); // Test for sample data card expect(resp.body.findIndex((c: { id: string }) => c.id === 'sample_data_all')).to.be.above( diff --git a/test/api_integration/apis/saved_objects_management/find.ts b/test/api_integration/apis/saved_objects_management/find.ts index 0b744b7991b38..ea7f297dfeb08 100644 --- a/test/api_integration/apis/saved_objects_management/find.ts +++ b/test/api_integration/apis/saved_objects_management/find.ts @@ -249,7 +249,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/management/kibana/dataViews/dataView/8963ca30-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'management.kibana.indexPatterns', }, - namespaceType: 'single', + namespaceType: 'multiple-isolated', }); })); }); diff --git a/test/api_integration/apis/saved_objects_management/relationships.ts b/test/api_integration/apis/saved_objects_management/relationships.ts index 518ec29947016..838bc05346dda 100644 --- a/test/api_integration/apis/saved_objects_management/relationships.ts +++ b/test/api_integration/apis/saved_objects_management/relationships.ts @@ -91,7 +91,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/management/kibana/dataViews/dataView/8963ca30-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'management.kibana.indexPatterns', }, - namespaceType: 'single', + namespaceType: 'multiple-isolated', hiddenType: false, }, }, @@ -132,7 +132,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/management/kibana/dataViews/dataView/8963ca30-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'management.kibana.indexPatterns', }, - namespaceType: 'single', + namespaceType: 'multiple-isolated', hiddenType: false, }, relationship: 'child', diff --git a/test/common/services/security/test_user.ts b/test/common/services/security/test_user.ts index 695294f08b02d..1161e7b493f41 100644 --- a/test/common/services/security/test_user.ts +++ b/test/common/services/security/test_user.ts @@ -71,13 +71,12 @@ export class TestUser extends FtrService { export async function createTestUserService(ctx: FtrProviderContext, role: Role, user: User) { const log = ctx.getService('log'); const config = ctx.getService('config'); - const kibanaServer = ctx.getService('kibanaServer'); - const enabledPlugins = config.get('security.disableTestUser') - ? [] - : await kibanaServer.plugins.getEnabledIds(); - - const enabled = enabledPlugins.includes('security') && !config.get('security.disableTestUser'); + const enabled = + !config + .get('esTestCluster.serverArgs') + .some((arg: string) => arg === 'xpack.security.enabled=false') && + !config.get('security.disableTestUser'); if (enabled) { log.debug('===============creating roles and users==============='); diff --git a/test/functional/apps/context/_discover_navigation.ts b/test/functional/apps/context/_discover_navigation.ts index 1b8300f3345b1..60745bd64b8be 100644 --- a/test/functional/apps/context/_discover_navigation.ts +++ b/test/functional/apps/context/_discover_navigation.ts @@ -74,7 +74,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should open the context view with the same columns', async () => { const columnNames = await docTable.getHeaderFields(); - expect(columnNames).to.eql(['Time', ...TEST_COLUMN_NAMES]); + expect(columnNames).to.eql(['@timestamp', ...TEST_COLUMN_NAMES]); }); it('should open the context view with the filters disabled', async () => { diff --git a/test/functional/apps/discover/_data_grid.ts b/test/functional/apps/discover/_data_grid.ts index 4a343fb30384e..198691f3b8477 100644 --- a/test/functional/apps/discover/_data_grid.ts +++ b/test/functional/apps/discover/_data_grid.ts @@ -39,19 +39,19 @@ export default function ({ const getTitles = async () => (await testSubjects.getVisibleText('dataGridHeader')).replace(/\s|\r?\n|\r/g, ' '); - expect(await getTitles()).to.be('Time (@timestamp) Document'); + expect(await getTitles()).to.be('@timestamp Document'); await PageObjects.discover.clickFieldListItemAdd('bytes'); - expect(await getTitles()).to.be('Time (@timestamp) bytes'); + expect(await getTitles()).to.be('@timestamp bytes'); await PageObjects.discover.clickFieldListItemAdd('agent'); - expect(await getTitles()).to.be('Time (@timestamp) bytes agent'); + expect(await getTitles()).to.be('@timestamp bytes agent'); await PageObjects.discover.clickFieldListItemRemove('bytes'); - expect(await getTitles()).to.be('Time (@timestamp) agent'); + expect(await getTitles()).to.be('@timestamp agent'); await PageObjects.discover.clickFieldListItemRemove('agent'); - expect(await getTitles()).to.be('Time (@timestamp) Document'); + expect(await getTitles()).to.be('@timestamp Document'); }); }); } diff --git a/test/functional/apps/discover/_data_grid_context.ts b/test/functional/apps/discover/_data_grid_context.ts index 3d9e01e1dee19..d12ada2070cff 100644 --- a/test/functional/apps/discover/_data_grid_context.ts +++ b/test/functional/apps/discover/_data_grid_context.ts @@ -76,7 +76,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should open the context view with the same columns', async () => { const columnNames = await dataGrid.getHeaderFields(); - expect(columnNames).to.eql(['Time (@timestamp)', ...TEST_COLUMN_NAMES]); + expect(columnNames).to.eql(['@timestamp', ...TEST_COLUMN_NAMES]); }); it('should open the context view with the filters disabled', async () => { diff --git a/test/functional/apps/discover/_data_grid_field_data.ts b/test/functional/apps/discover/_data_grid_field_data.ts index 94e8e942f86ba..91c2d5914732d 100644 --- a/test/functional/apps/discover/_data_grid_field_data.ts +++ b/test/functional/apps/discover/_data_grid_field_data.ts @@ -59,8 +59,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - it('doc view should show Time and _source columns', async function () { - const expectedHeader = 'Time (@timestamp) Document'; + it('doc view should show @timestamp and _source columns', async function () { + const expectedHeader = '@timestamp Document'; const DocHeader = await dataGrid.getHeaderFields(); expect(DocHeader.join(' ')).to.be(expectedHeader); }); diff --git a/test/functional/apps/discover/_field_data.ts b/test/functional/apps/discover/_field_data.ts index 27407e9a0bc4d..28f147eeab55f 100644 --- a/test/functional/apps/discover/_field_data.ts +++ b/test/functional/apps/discover/_field_data.ts @@ -107,8 +107,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async function () { await kibanaServer.uiSettings.replace({}); }); - it('doc view should show Time and _source columns', async function () { - const expectedHeader = 'Time\n_source'; + it('doc view should show @timestamp and _source columns', async function () { + const expectedHeader = '@timestamp\n_source'; const docHeader = await find.byCssSelector('thead > tr:nth-child(1)'); const docHeaderText = await docHeader.getVisibleText(); expect(docHeaderText).to.be(expectedHeader); diff --git a/test/functional/apps/discover/_field_data_with_fields_api.ts b/test/functional/apps/discover/_field_data_with_fields_api.ts index 666377ae7f794..f0dedb155fc9b 100644 --- a/test/functional/apps/discover/_field_data_with_fields_api.ts +++ b/test/functional/apps/discover/_field_data_with_fields_api.ts @@ -64,9 +64,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - it('doc view should show Time and Document columns', async function () { + it('doc view should show @timestamp and Document columns', async function () { const Docheader = await PageObjects.discover.getDocHeader(); - expect(Docheader).to.contain('Time'); + expect(Docheader).to.contain('@timestamp'); expect(Docheader).to.contain('Document'); }); diff --git a/test/functional/apps/visualize/_timelion.ts b/test/functional/apps/visualize/_timelion.ts index bb85b6821df31..c531ada8a2573 100644 --- a/test/functional/apps/visualize/_timelion.ts +++ b/test/functional/apps/visualize/_timelion.ts @@ -257,7 +257,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(value).to.eql('.es()'); }); - describe('dynamic suggestions for argument values', () => { + // FLAKY: https://github.com/elastic/kibana/issues/116033 + describe.skip('dynamic suggestions for argument values', () => { describe('.es()', () => { it('should show index pattern suggestions for index argument', async () => { await monacoEditor.setCodeEditorValue(''); diff --git a/test/functional/config.js b/test/functional/config.js index e0195c4dadc8d..5b0b79e84e8df 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -45,7 +45,6 @@ export default async function ({ readConfigFile }) { '--savedObjects.maxImportPayloadBytes=10485760', // to be re-enabled once kibana/issues/102552 is completed - '--xpack.security.enabled=false', '--xpack.reporting.enabled=false', ], }, diff --git a/vars/tasks.groovy b/vars/tasks.groovy index da18d73e5b36c..1842e278282b1 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -146,13 +146,13 @@ def functionalXpack(Map params = [:]) { } } - // whenChanged([ - // 'x-pack/plugins/apm/', - // ]) { - // if (githubPr.isPr()) { - // task(kibanaPipeline.functionalTestProcess('xpack-APMCypress', './test/scripts/jenkins_apm_cypress.sh')) - // } - // } + whenChanged([ + 'x-pack/plugins/apm/', + ]) { + if (githubPr.isPr()) { + task(kibanaPipeline.functionalTestProcess('xpack-APMCypress', './test/scripts/jenkins_apm_cypress.sh')) + } + } whenChanged([ 'x-pack/plugins/uptime/', diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index 2300143925b1e..1254d86e99066 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -8,7 +8,7 @@ import axios from 'axios'; import { createExternalService } from './service'; -import * as utils from '../lib/axios_utils'; +import { request, createAxiosResponse } from '../lib/axios_utils'; import { ExternalService } from './types'; import { Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; @@ -29,10 +29,10 @@ jest.mock('../lib/axios_utils', () => { }); axios.create = jest.fn(() => axios); -const requestMock = utils.request as jest.Mock; +const requestMock = request as jest.Mock; const configurationUtilities = actionsConfigMock.create(); -const issueTypesResponse = { +const issueTypesResponse = createAxiosResponse({ data: { projects: [ { @@ -49,9 +49,9 @@ const issueTypesResponse = { }, ], }, -}; +}); -const fieldsResponse = { +const fieldsResponse = createAxiosResponse({ data: { projects: [ { @@ -98,7 +98,7 @@ const fieldsResponse = { }, ], }, -}; +}); const issueResponse = { id: '10267', @@ -108,6 +108,31 @@ const issueResponse = { const issuesResponse = [issueResponse]; +const mockNewAPI = () => + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, + }, + }) + ); + +const mockOldAPI = () => + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + }) + ); + describe('Jira service', () => { let service: ExternalService; @@ -183,18 +208,34 @@ describe('Jira service', () => { }); describe('getIncident', () => { + const axiosRes = { + data: { + id: '1', + key: 'CK-1', + fields: { + summary: 'title', + description: 'description', + created: '2021-10-20T19:41:02.754+0300', + updated: '2021-10-20T19:41:02.754+0300', + }, + }, + }; + test('it returns the incident correctly', async () => { - requestMock.mockImplementation(() => ({ - data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, - })); + requestMock.mockImplementation(() => createAxiosResponse(axiosRes)); const res = await service.getIncident('1'); - expect(res).toEqual({ id: '1', key: 'CK-1', summary: 'title', description: 'description' }); + expect(res).toEqual({ + id: '1', + key: 'CK-1', + summary: 'title', + description: 'description', + created: '2021-10-20T19:41:02.754+0300', + updated: '2021-10-20T19:41:02.754+0300', + }); }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { id: '1', key: 'CK-1' }, - })); + requestMock.mockImplementation(() => createAxiosResponse(axiosRes)); await service.getIncident('1'); expect(requestMock).toHaveBeenCalledWith({ @@ -215,9 +256,38 @@ describe('Jira service', () => { '[Action][Jira]: Unable to get incident with id 1. Error: An error has occurred Reason: Required field' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ ...axiosRes, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getIncident('1')).rejects.toThrow( + '[Action][Jira]: Unable to get incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json Reason: unknown: errorResponse was null' + ); + }); + + test('it should throw if the required attributes are not there', async () => { + requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } })); + + await expect(service.getIncident('1')).rejects.toThrow( + '[Action][Jira]: Unable to get incident with id 1. Error: Response is missing at least one of the expected fields: id,key Reason: unknown: errorResponse was null' + ); + }); }); describe('createIncident', () => { + const incident = { + incident: { + summary: 'title', + description: 'desc', + labels: [], + issueType: '10006', + priority: 'High', + parent: 'RJ-107', + }, + }; + test('it creates the incident correctly', async () => { /* The response from Jira when creating an issue contains only the key and the id. The function makes the following calls when creating an issue: @@ -225,24 +295,19 @@ describe('Jira service', () => { 2. Create the issue. 3. Get the created issue with all the necessary fields. */ - requestMock.mockImplementationOnce(() => ({ - data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, - })); + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, + }) + ); - requestMock.mockImplementationOnce(() => ({ - data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } }, - })); + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } }, + }) + ); - const res = await service.createIncident({ - incident: { - summary: 'title', - description: 'desc', - labels: [], - issueType: '10006', - priority: 'High', - parent: null, - }, - }); + const res = await service.createIncident(incident); expect(res).toEqual({ title: 'CK-1', @@ -260,24 +325,30 @@ describe('Jira service', () => { 3. Get the created issue with all the necessary fields. */ // getIssueType mocks - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, }, - }, - })); + }) + ); // getIssueType mocks requestMock.mockImplementationOnce(() => issueTypesResponse); - requestMock.mockImplementationOnce(() => ({ - data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, - })); + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, + }) + ); - requestMock.mockImplementationOnce(() => ({ - data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } }, - })); + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } }, + }) + ); const res = await service.createIncident({ incident: { @@ -317,25 +388,31 @@ describe('Jira service', () => { }); test('removes newline characters and trialing spaces from summary', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, }, - }, - })); + }) + ); // getIssueType mocks requestMock.mockImplementationOnce(() => issueTypesResponse); // getIssueType mocks - requestMock.mockImplementationOnce(() => ({ - data: { id: '1', key: 'CK-1', fields: { summary: 'test', description: 'description' } }, - })); + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { id: '1', key: 'CK-1', fields: { summary: 'test', description: 'description' } }, + }) + ); - requestMock.mockImplementationOnce(() => ({ - data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } }, - })); + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } }, + }) + ); await service.createIncident({ incident: { @@ -368,24 +445,17 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - key: 'CK-1', - fields: { created: '2020-04-27T10:59:46.202Z' }, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + key: 'CK-1', + fields: { created: '2020-04-27T10:59:46.202Z' }, + }, + }) + ); - await service.createIncident({ - incident: { - summary: 'title', - description: 'desc', - labels: [], - issueType: '10006', - priority: 'High', - parent: 'RJ-107', - }, - }); + await service.createIncident(incident); expect(requestMock).toHaveBeenCalledWith({ axios, @@ -414,44 +484,55 @@ describe('Jira service', () => { throw error; }); - await expect( - service.createIncident({ - incident: { - summary: 'title', - description: 'desc', - labels: [], - issueType: '10006', - priority: 'High', - parent: null, - }, - }) - ).rejects.toThrow( + await expect(service.createIncident(incident)).rejects.toThrow( '[Action][Jira]: Unable to create incident. Error: An error has occurred. Reason: Required field' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.createIncident(incident)).rejects.toThrow( + '[Action][Jira]: Unable to create incident. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); + + test('it should throw if the required attributes are not there', async () => { + requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } })); + + await expect(service.createIncident(incident)).rejects.toThrow( + '[Action][Jira]: Unable to create incident. Error: Response is missing at least one of the expected fields: id. Reason: unknown: errorResponse was null' + ); + }); }); describe('updateIncident', () => { + const incident = { + incidentId: '1', + incident: { + summary: 'title', + description: 'desc', + labels: [], + issueType: '10006', + priority: 'High', + parent: 'RJ-107', + }, + }; + test('it updates the incident correctly', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - key: 'CK-1', - fields: { updated: '2020-04-27T10:59:46.202Z' }, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + key: 'CK-1', + fields: { updated: '2020-04-27T10:59:46.202Z' }, + }, + }) + ); - const res = await service.updateIncident({ - incidentId: '1', - incident: { - summary: 'title', - description: 'desc', - labels: [], - issueType: '10006', - priority: 'High', - parent: null, - }, - }); + const res = await service.updateIncident(incident); expect(res).toEqual({ title: 'CK-1', @@ -462,25 +543,17 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - key: 'CK-1', - fields: { updated: '2020-04-27T10:59:46.202Z' }, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + key: 'CK-1', + fields: { updated: '2020-04-27T10:59:46.202Z' }, + }, + }) + ); - await service.updateIncident({ - incidentId: '1', - incident: { - summary: 'title', - description: 'desc', - labels: [], - issueType: '10006', - priority: 'High', - parent: 'RJ-107', - }, - }); + await service.updateIncident(incident); expect(requestMock).toHaveBeenCalledWith({ axios, @@ -509,41 +582,42 @@ describe('Jira service', () => { throw error; }); - await expect( - service.updateIncident({ - incidentId: '1', - incident: { - summary: 'title', - description: 'desc', - labels: [], - issueType: '10006', - priority: 'High', - parent: null, - }, - }) - ).rejects.toThrow( + await expect(service.updateIncident(incident)).rejects.toThrow( '[Action][Jira]: Unable to update incident with id 1. Error: An error has occurred. Reason: Required field' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.updateIncident(incident)).rejects.toThrow( + '[Action][Jira]: Unable to update incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); }); describe('createComment', () => { + const commentReq = { + incidentId: '1', + comment: { + comment: 'comment', + commentId: 'comment-1', + }, + }; test('it creates the comment correctly', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - key: 'CK-1', - created: '2020-04-27T10:59:46.202Z', - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + key: 'CK-1', + created: '2020-04-27T10:59:46.202Z', + }, + }) + ); - const res = await service.createComment({ - incidentId: '1', - comment: { - comment: 'comment', - commentId: 'comment-1', - }, - }); + const res = await service.createComment(commentReq); expect(res).toEqual({ commentId: 'comment-1', @@ -553,21 +627,17 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - key: 'CK-1', - created: '2020-04-27T10:59:46.202Z', - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + key: 'CK-1', + created: '2020-04-27T10:59:46.202Z', + }, + }) + ); - await service.createComment({ - incidentId: '1', - comment: { - comment: 'comment', - commentId: 'comment-1', - }, - }); + await service.createComment(commentReq); expect(requestMock).toHaveBeenCalledWith({ axios, @@ -586,29 +656,33 @@ describe('Jira service', () => { throw error; }); - await expect( - service.createComment({ - incidentId: '1', - comment: { - comment: 'comment', - commentId: 'comment-1', - }, - }) - ).rejects.toThrow( + await expect(service.createComment(commentReq)).rejects.toThrow( '[Action][Jira]: Unable to create comment at incident with id 1. Error: An error has occurred. Reason: Required field' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.createComment(commentReq)).rejects.toThrow( + '[Action][Jira]: Unable to create comment at incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); + + test('it should throw if the required attributes are not there', async () => { + requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } })); + + await expect(service.createComment(commentReq)).rejects.toThrow( + '[Action][Jira]: Unable to create comment at incident with id 1. Error: Response is missing at least one of the expected fields: id,created. Reason: unknown: errorResponse was null' + ); + }); }); describe('getCapabilities', () => { test('it should return the capabilities', async () => { - requestMock.mockImplementation(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', - }, - }, - })); + mockOldAPI(); const res = await service.getCapabilities(); expect(res).toEqual({ capabilities: { @@ -618,13 +692,7 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', - }, - }, - })); + mockOldAPI(); await service.getCapabilities(); @@ -649,16 +717,34 @@ describe('Jira service', () => { ); }); - test('it should throw an auth error', async () => { + test('it should return unknown if the error is a string', async () => { requestMock.mockImplementation(() => { const error = new Error('An error has occurred'); - // @ts-ignore this can happen! + // @ts-ignore error.response = { data: 'Unauthorized' }; throw error; }); await expect(service.getCapabilities()).rejects.toThrow( - '[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: Unauthorized' + '[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: unknown: errorResponse.errors was null' + ); + }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getCapabilities()).rejects.toThrow( + '[Action][Jira]: Unable to get capabilities. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); + + test('it should throw if the required attributes are not there', async () => { + requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } })); + + await expect(service.getCapabilities()).rejects.toThrow( + '[Action][Jira]: Unable to get capabilities. Error: Response is missing at least one of the expected fields: capabilities. Reason: unknown: errorResponse was null' ); }); }); @@ -666,13 +752,7 @@ describe('Jira service', () => { describe('getIssueTypes', () => { describe('Old API', () => { test('it should return the issue types', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', - }, - }, - })); + mockOldAPI(); requestMock.mockImplementationOnce(() => issueTypesResponse); @@ -691,13 +771,7 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', - }, - }, - })); + mockOldAPI(); requestMock.mockImplementationOnce(() => issueTypesResponse); @@ -713,13 +787,7 @@ describe('Jira service', () => { }); test('it should throw an error', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', - }, - }, - })); + mockOldAPI(); requestMock.mockImplementation(() => { const error: ResponseError = new Error('An error has occurred'); @@ -731,25 +799,30 @@ describe('Jira service', () => { '[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types' ); }); + + test('it should throw if the request is not a JSON', async () => { + mockOldAPI(); + + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getIssueTypes()).rejects.toThrow( + '[Action][Jira]: Unable to get issue types. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); }); describe('New API', () => { test('it should return the issue types', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', - }, - }, - })); + mockNewAPI(); - requestMock.mockImplementationOnce(() => ({ - data: { - values: issueTypesResponse.data.projects[0].issuetypes, - }, - })); + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + values: issueTypesResponse.data.projects[0].issuetypes, + }, + }) + ); const res = await service.getIssueTypes(); @@ -766,22 +839,15 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', - }, - }, - })); + mockNewAPI(); - requestMock.mockImplementationOnce(() => ({ - data: { - values: issueTypesResponse.data.projects[0].issuetypes, - }, - })); + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + values: issueTypesResponse.data.projects[0].issuetypes, + }, + }) + ); await service.getIssueTypes(); @@ -795,16 +861,7 @@ describe('Jira service', () => { }); test('it should throw an error', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', - }, - }, - })); + mockNewAPI(); requestMock.mockImplementation(() => { const error: ResponseError = new Error('An error has occurred'); @@ -816,19 +873,25 @@ describe('Jira service', () => { '[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types' ); }); + + test('it should throw if the request is not a JSON', async () => { + mockNewAPI(); + + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getIssueTypes()).rejects.toThrow( + '[Action][Jira]: Unable to get issue types. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); }); }); describe('getFieldsByIssueType', () => { describe('Old API', () => { test('it should return the fields', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', - }, - }, - })); + mockOldAPI(); requestMock.mockImplementationOnce(() => fieldsResponse); @@ -857,13 +920,7 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', - }, - }, - })); + mockOldAPI(); requestMock.mockImplementationOnce(() => fieldsResponse); @@ -879,13 +936,7 @@ describe('Jira service', () => { }); test('it should throw an error', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', - }, - }, - })); + mockOldAPI(); requestMock.mockImplementation(() => { const error: ResponseError = new Error('An error has occurred'); @@ -897,43 +948,48 @@ describe('Jira service', () => { '[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get fields' ); }); + + test('it should throw if the request is not a JSON', async () => { + mockOldAPI(); + + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getFieldsByIssueType('10006')).rejects.toThrow( + '[Action][Jira]: Unable to get fields. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); }); describe('New API', () => { test('it should return the fields', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', - }, - }, - })); - - requestMock.mockImplementationOnce(() => ({ - data: { - values: [ - { required: true, schema: { type: 'string' }, fieldId: 'summary' }, - { - required: false, - schema: { type: 'string' }, - fieldId: 'priority', - allowedValues: [ - { + mockNewAPI(); + + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + values: [ + { required: true, schema: { type: 'string' }, fieldId: 'summary' }, + { + required: false, + schema: { type: 'string' }, + fieldId: 'priority', + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { name: 'Medium', id: '3', }, - ], - defaultValue: { - name: 'Medium', - id: '3', }, - }, - ], - }, - })); + ], + }, + }) + ); const res = await service.getFieldsByIssueType('10006'); @@ -954,39 +1010,32 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', - }, - }, - })); - - requestMock.mockImplementationOnce(() => ({ - data: { - values: [ - { required: true, schema: { type: 'string' }, fieldId: 'summary' }, - { - required: true, - schema: { type: 'string' }, - fieldId: 'priority', - allowedValues: [ - { + mockNewAPI(); + + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + values: [ + { required: true, schema: { type: 'string' }, fieldId: 'summary' }, + { + required: true, + schema: { type: 'string' }, + fieldId: 'priority', + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { name: 'Medium', id: '3', }, - ], - defaultValue: { - name: 'Medium', - id: '3', }, - }, - ], - }, - })); + ], + }, + }) + ); await service.getFieldsByIssueType('10006'); @@ -1000,16 +1049,7 @@ describe('Jira service', () => { }); test('it should throw an error', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', - }, - }, - })); + mockNewAPI(); requestMock.mockImplementation(() => { const error: ResponseError = new Error('An error has occurred'); @@ -1021,16 +1061,30 @@ describe('Jira service', () => { '[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get issue types' ); }); + + test('it should throw if the request is not a JSON', async () => { + mockNewAPI(); + + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getFieldsByIssueType('10006')).rejects.toThrow( + '[Action][Jira]: Unable to get fields. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); }); }); describe('getIssues', () => { test('it should return the issues', async () => { - requestMock.mockImplementation(() => ({ - data: { - issues: issuesResponse, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + issues: issuesResponse, + }, + }) + ); const res = await service.getIssues('Test title'); @@ -1044,11 +1098,13 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { - issues: issuesResponse, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + issues: issuesResponse, + }, + }) + ); await service.getIssues('Test title'); expect(requestMock).toHaveBeenLastCalledWith({ @@ -1071,13 +1127,25 @@ describe('Jira service', () => { '[Action][Jira]: Unable to get issues. Error: An error has occurred. Reason: Could not get issue types' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getIssues('Test title')).rejects.toThrow( + '[Action][Jira]: Unable to get issues. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); }); describe('getIssue', () => { test('it should return a single issue', async () => { - requestMock.mockImplementation(() => ({ - data: issueResponse, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: issueResponse, + }) + ); const res = await service.getIssue('RJ-107'); @@ -1089,11 +1157,13 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { - issues: issuesResponse, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + issues: issuesResponse, + }, + }) + ); await service.getIssue('RJ-107'); expect(requestMock).toHaveBeenLastCalledWith({ @@ -1116,81 +1186,105 @@ describe('Jira service', () => { '[Action][Jira]: Unable to get issue with id RJ-107. Error: An error has occurred. Reason: Could not get issue types' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getIssue('Test title')).rejects.toThrow( + '[Action][Jira]: Unable to get issue with id Test title. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); }); describe('getFields', () => { const callMocks = () => { requestMock - .mockImplementationOnce(() => ({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + .mockImplementationOnce(() => + createAxiosResponse({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, }, - }, - })) - .mockImplementationOnce(() => ({ - data: { - values: issueTypesResponse.data.projects[0].issuetypes, - }, - })) - .mockImplementationOnce(() => ({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }) + ) + .mockImplementationOnce(() => + createAxiosResponse({ + data: { + values: issueTypesResponse.data.projects[0].issuetypes, }, - }, - })) - .mockImplementationOnce(() => ({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }) + ) + .mockImplementationOnce(() => + createAxiosResponse({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, }, - }, - })) - .mockImplementationOnce(() => ({ - data: { - values: [ - { required: true, schema: { type: 'string' }, fieldId: 'summary' }, - { required: true, schema: { type: 'string' }, fieldId: 'description' }, - { - required: false, - schema: { type: 'string' }, - fieldId: 'priority', - allowedValues: [ - { + }) + ) + .mockImplementationOnce(() => + createAxiosResponse({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, + }, + }) + ) + .mockImplementationOnce(() => + createAxiosResponse({ + data: { + values: [ + { required: true, schema: { type: 'string' }, fieldId: 'summary' }, + { required: true, schema: { type: 'string' }, fieldId: 'description' }, + { + required: false, + schema: { type: 'string' }, + fieldId: 'priority', + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { name: 'Medium', id: '3', }, - ], - defaultValue: { - name: 'Medium', - id: '3', }, - }, - ], - }, - })) - .mockImplementationOnce(() => ({ - data: { - values: [ - { required: true, schema: { type: 'string' }, fieldId: 'summary' }, - { required: true, schema: { type: 'string' }, fieldId: 'description' }, - ], - }, - })); + ], + }, + }) + ) + .mockImplementationOnce(() => + createAxiosResponse({ + data: { + values: [ + { required: true, schema: { type: 'string' }, fieldId: 'summary' }, + { required: true, schema: { type: 'string' }, fieldId: 'description' }, + ], + }, + }) + ); }; + beforeEach(() => { jest.resetAllMocks(); }); + test('it should call request with correct arguments', async () => { callMocks(); await service.getFields(); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index be0240e705a65..a3262a526e2f4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -27,7 +27,7 @@ import { } from './types'; import * as i18n from './translations'; -import { request, getErrorMessage } from '../lib/axios_utils'; +import { request, getErrorMessage, throwIfResponseIsNotValid } from '../lib/axios_utils'; import { ActionsConfigurationUtilities } from '../../actions_config'; const VERSION = '2'; @@ -111,19 +111,15 @@ export const createExternalService = ( .filter((item) => !isEmpty(item)) .join(', '); - const createErrorMessage = (errorResponse: ResponseError | string | null | undefined): string => { + const createErrorMessage = (errorResponse: ResponseError | null | undefined): string => { if (errorResponse == null) { - return ''; - } - if (typeof errorResponse === 'string') { - // Jira error.response.data can be string!! - return errorResponse; + return 'unknown: errorResponse was null'; } const { errorMessages, errors } = errorResponse; if (errors == null) { - return ''; + return 'unknown: errorResponse.errors was null'; } if (Array.isArray(errorMessages) && errorMessages.length > 0) { @@ -185,9 +181,14 @@ export const createExternalService = ( configurationUtilities, }); - const { fields, ...rest } = res.data; + throwIfResponseIsNotValid({ + res, + requiredAttributesToBeInTheResponse: ['id', 'key'], + }); + + const { fields, id: incidentId, key } = res.data; - return { ...rest, ...fields }; + return { id: incidentId, key, created: fields.created, updated: fields.updated, ...fields }; } catch (error) { throw new Error( getErrorMessage( @@ -234,6 +235,11 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + requiredAttributesToBeInTheResponse: ['id'], + }); + const updatedIncident = await getIncident(res.data.id); return { @@ -266,7 +272,7 @@ export const createExternalService = ( const fields = createFields(projectKey, incidentWithoutNullValues); try { - await request({ + const res = await request({ axios: axiosInstance, method: 'put', url: `${incidentUrl}/${incidentId}`, @@ -275,6 +281,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + const updatedIncident = await getIncident(incidentId as string); return { @@ -309,6 +319,11 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + requiredAttributesToBeInTheResponse: ['id', 'created'], + }); + return { commentId: comment.commentId, externalCommentId: res.data.id, @@ -336,6 +351,11 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + requiredAttributesToBeInTheResponse: ['capabilities'], + }); + return { ...res.data }; } catch (error) { throw new Error( @@ -362,6 +382,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + const issueTypes = res.data.projects[0]?.issuetypes ?? []; return normalizeIssueTypes(issueTypes); } else { @@ -373,6 +397,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + const issueTypes = res.data.values; return normalizeIssueTypes(issueTypes); } @@ -401,6 +429,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + const fields = res.data.projects[0]?.issuetypes[0]?.fields || {}; return normalizeFields(fields); } else { @@ -412,6 +444,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + const fields = res.data.values.reduce( (acc: { [x: string]: {} }, value: { fieldId: string }) => ({ ...acc, @@ -471,6 +507,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + return normalizeSearchResults(res.data?.issues ?? []); } catch (error) { throw new Error( @@ -495,6 +535,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + return normalizeIssue(res.data ?? {}); } catch (error) { throw new Error( diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts index 287f74c6bc703..d0177e0e5a8a2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts @@ -10,7 +10,14 @@ import { Agent as HttpsAgent } from 'https'; import HttpProxyAgent from 'http-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent'; import { Logger } from '../../../../../../src/core/server'; -import { addTimeZoneToDate, request, patch, getErrorMessage } from './axios_utils'; +import { + addTimeZoneToDate, + request, + patch, + getErrorMessage, + throwIfResponseIsNotValid, + createAxiosResponse, +} from './axios_utils'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { actionsConfigMock } from '../../actions_config.mock'; import { getCustomAgents } from './get_custom_agents'; @@ -292,3 +299,82 @@ describe('getErrorMessage', () => { expect(msg).toBe('[Action][My connector name]: An error has occurred'); }); }); + +describe('throwIfResponseIsNotValid', () => { + const res = createAxiosResponse({ + headers: { ['content-type']: 'application/json' }, + data: { incident: { id: '1' } }, + }); + + test('it does NOT throw if the request is valid', () => { + expect(() => throwIfResponseIsNotValid({ res })).not.toThrow(); + }); + + test('it does throw if the content-type is not json', () => { + expect(() => + throwIfResponseIsNotValid({ + res: { ...res, headers: { ['content-type']: 'text/html' } }, + }) + ).toThrow( + 'Unsupported content type: text/html in GET https://example.com. Supported content types: application/json' + ); + }); + + test('it does throw if the content-type is undefined', () => { + expect(() => + throwIfResponseIsNotValid({ + res: { ...res, headers: {} }, + }) + ).toThrow( + 'Unsupported content type: undefined in GET https://example.com. Supported content types: application/json' + ); + }); + + test('it does throw if the data is not an object or array', () => { + expect(() => + throwIfResponseIsNotValid({ + res: { ...res, data: 'string' }, + }) + ).toThrow('Response is not a valid JSON'); + }); + + test('it does NOT throw if the data is an array', () => { + expect(() => + throwIfResponseIsNotValid({ + res: { ...res, data: ['test'] }, + }) + ).not.toThrow(); + }); + + test.each(['', [], {}])('it does NOT throw if the data is %p', (data) => { + expect(() => + throwIfResponseIsNotValid({ + res: { ...res, data }, + }) + ).not.toThrow(); + }); + + test('it does throw if the required attribute is not in the response', () => { + expect(() => + throwIfResponseIsNotValid({ res, requiredAttributesToBeInTheResponse: ['not-exist'] }) + ).toThrow('Response is missing at least one of the expected fields: not-exist'); + }); + + test('it does throw if the required attribute are defined and the data is an array', () => { + expect(() => + throwIfResponseIsNotValid({ + res: { ...res, data: ['test'] }, + requiredAttributesToBeInTheResponse: ['not-exist'], + }) + ).toThrow('Response is missing at least one of the expected fields: not-exist'); + }); + + test('it does NOT throw if the value of the required attribute is null', () => { + expect(() => + throwIfResponseIsNotValid({ + res: { ...res, data: { id: null } }, + requiredAttributesToBeInTheResponse: ['id'], + }) + ).not.toThrow(); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts index af353e1d1da5a..43c9d276e6574 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { isObjectLike, isEmpty } from 'lodash'; import { AxiosInstance, Method, AxiosResponse, AxiosBasicCredentials } from 'axios'; import { Logger } from '../../../../../../src/core/server'; import { getCustomAgents } from './get_custom_agents'; @@ -76,3 +77,70 @@ export const addTimeZoneToDate = (date: string, timezone = 'GMT'): string => { export const getErrorMessage = (connector: string, msg: string) => { return `[Action][${connector}]: ${msg}`; }; + +export const throwIfResponseIsNotValid = ({ + res, + requiredAttributesToBeInTheResponse = [], +}: { + res: AxiosResponse; + requiredAttributesToBeInTheResponse?: string[]; +}) => { + const requiredContentType = 'application/json'; + const contentType = res.headers['content-type'] ?? 'undefined'; + const data = res.data; + + /** + * Check that the content-type of the response is application/json. + * Then includes is added because the header can be application/json;charset=UTF-8. + */ + if (!contentType.includes(requiredContentType)) { + throw new Error( + `Unsupported content type: ${contentType} in ${res.config.method} ${res.config.url}. Supported content types: ${requiredContentType}` + ); + } + + /** + * Check if the response is a JS object (data != null && typeof data === 'object') + * in case the content type is application/json but for some reason the response is not. + * Empty responses (204 No content) are ignored because the typeof data will be string and + * isObjectLike will fail. + * Axios converts automatically JSON to JS objects. + */ + if (!isEmpty(data) && !isObjectLike(data)) { + throw new Error('Response is not a valid JSON'); + } + + if (requiredAttributesToBeInTheResponse.length > 0) { + const requiredAttributesError = new Error( + `Response is missing at least one of the expected fields: ${requiredAttributesToBeInTheResponse.join( + ',' + )}` + ); + + /** + * If the response is an array and requiredAttributesToBeInTheResponse + * are not empty then we thrown an error assuming that the consumer + * expects an object response and not an array. + */ + + if (Array.isArray(data)) { + throw requiredAttributesError; + } + + requiredAttributesToBeInTheResponse.forEach((attr) => { + // Check only for undefined as null is a valid value + if (data[attr] === undefined) { + throw requiredAttributesError; + } + }); + } +}; + +export const createAxiosResponse = (res: Partial): AxiosResponse => ({ + data: {}, + status: 200, + statusText: 'OK', + headers: { ['content-type']: 'application/json' }, + config: { method: 'GET', url: 'https://example.com' }, + ...res, +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts index ba55543386225..094b8150850df 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts @@ -8,7 +8,7 @@ import axios from 'axios'; import { createExternalService, getValueTextContent, formatUpdateRequest } from './service'; -import * as utils from '../lib/axios_utils'; +import { request, createAxiosResponse } from '../lib/axios_utils'; import { ExternalService } from './types'; import { Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; @@ -27,7 +27,7 @@ jest.mock('../lib/axios_utils', () => { }); axios.create = jest.fn(() => axios); -const requestMock = utils.request as jest.Mock; +const requestMock = request as jest.Mock; const now = Date.now; const TIMESTAMP = 1589391874472; const configurationUtilities = actionsConfigMock.create(); @@ -38,44 +38,50 @@ const configurationUtilities = actionsConfigMock.create(); // b) Update the incident // c) Get the updated incident const mockIncidentUpdate = (withUpdateError = false) => { - requestMock.mockImplementationOnce(() => ({ - data: { - id: '1', - name: 'title', - description: { - format: 'html', - content: 'description', + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + id: '1', + name: 'title', + description: { + format: 'html', + content: 'description', + }, + incident_type_ids: [1001, 16, 12], + severity_code: 6, }, - incident_type_ids: [1001, 16, 12], - severity_code: 6, - }, - })); + }) + ); if (withUpdateError) { requestMock.mockImplementationOnce(() => { throw new Error('An error has occurred'); }); } else { - requestMock.mockImplementationOnce(() => ({ + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + success: true, + id: '1', + inc_last_modified_date: 1589391874472, + }, + }) + ); + } + + requestMock.mockImplementationOnce(() => + createAxiosResponse({ data: { - success: true, id: '1', + name: 'title_updated', + description: { + format: 'html', + content: 'desc_updated', + }, inc_last_modified_date: 1589391874472, }, - })); - } - - requestMock.mockImplementationOnce(() => ({ - data: { - id: '1', - name: 'title_updated', - description: { - format: 'html', - content: 'desc_updated', - }, - inc_last_modified_date: 1589391874472, - }, - })); + }) + ); }; describe('IBM Resilient service', () => { @@ -207,24 +213,28 @@ describe('IBM Resilient service', () => { describe('getIncident', () => { test('it returns the incident correctly', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - name: '1', - description: { - format: 'html', - content: 'description', + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + name: '1', + description: { + format: 'html', + content: 'description', + }, }, - }, - })); + }) + ); const res = await service.getIncident('1'); expect(res).toEqual({ id: '1', name: '1', description: 'description' }); }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { id: '1' }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { id: '1' }, + }) + ); await service.getIncident('1'); expect(requestMock).toHaveBeenCalledWith({ @@ -246,28 +256,42 @@ describe('IBM Resilient service', () => { 'Unable to get incident with id 1. Error: An error has occurred' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getIncident('1')).rejects.toThrow( + '[Action][IBM Resilient]: Unable to get incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' + ); + }); }); describe('createIncident', () => { + const incident = { + incident: { + name: 'title', + description: 'desc', + incidentTypes: [1001], + severityCode: 6, + }, + }; + test('it creates the incident correctly', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - name: 'title', - description: 'description', - discovered_date: 1589391874472, - create_date: 1589391874472, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + name: 'title', + description: 'description', + discovered_date: 1589391874472, + create_date: 1589391874472, + }, + }) + ); - const res = await service.createIncident({ - incident: { - name: 'title', - description: 'desc', - incidentTypes: [1001], - severityCode: 6, - }, - }); + const res = await service.createIncident(incident); expect(res).toEqual({ title: '1', @@ -278,24 +302,19 @@ describe('IBM Resilient service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - name: 'title', - description: 'description', - discovered_date: 1589391874472, - create_date: 1589391874472, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + name: 'title', + description: 'description', + discovered_date: 1589391874472, + create_date: 1589391874472, + }, + }) + ); - await service.createIncident({ - incident: { - name: 'title', - description: 'desc', - incidentTypes: [1001], - severityCode: 6, - }, - }); + await service.createIncident(incident); expect(requestMock).toHaveBeenCalledWith({ axios, @@ -334,20 +353,39 @@ describe('IBM Resilient service', () => { '[Action][IBM Resilient]: Unable to create incident. Error: An error has occurred' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.createIncident(incident)).rejects.toThrow( + '[Action][IBM Resilient]: Unable to create incident. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' + ); + }); + + test('it should throw if the required attributes are not there', async () => { + requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } })); + + await expect(service.createIncident(incident)).rejects.toThrow( + '[Action][IBM Resilient]: Unable to create incident. Error: Response is missing at least one of the expected fields: id,create_date.' + ); + }); }); describe('updateIncident', () => { + const req = { + incidentId: '1', + incident: { + name: 'title', + description: 'desc', + incidentTypes: [1001], + severityCode: 6, + }, + }; test('it updates the incident correctly', async () => { mockIncidentUpdate(); - const res = await service.updateIncident({ - incidentId: '1', - incident: { - name: 'title', - description: 'desc', - incidentTypes: [1001], - severityCode: 6, - }, - }); + const res = await service.updateIncident(req); expect(res).toEqual({ title: '1', @@ -430,38 +468,59 @@ describe('IBM Resilient service', () => { test('it should throw an error', async () => { mockIncidentUpdate(true); - await expect( - service.updateIncident({ - incidentId: '1', - incident: { + await expect(service.updateIncident(req)).rejects.toThrow( + '[Action][IBM Resilient]: Unable to update incident with id 1. Error: An error has occurred' + ); + }); + + test('it should throw if the request is not a JSON', async () => { + // get incident request + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + id: '1', name: 'title', - description: 'desc', - incidentTypes: [1001], - severityCode: 5, + description: { + format: 'html', + content: 'description', + }, + incident_type_ids: [1001, 16, 12], + severity_code: 6, }, }) - ).rejects.toThrow( - '[Action][IBM Resilient]: Unable to update incident with id 1. Error: An error has occurred' + ); + + // update incident request + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.updateIncident(req)).rejects.toThrow( + '[Action][IBM Resilient]: Unable to update incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json' ); }); }); describe('createComment', () => { + const req = { + incidentId: '1', + comment: { + comment: 'comment', + commentId: 'comment-1', + }, + }; + test('it creates the comment correctly', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - create_date: 1589391874472, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + create_date: 1589391874472, + }, + }) + ); - const res = await service.createComment({ - incidentId: '1', - comment: { - comment: 'comment', - commentId: 'comment-1', - }, - }); + const res = await service.createComment(req); expect(res).toEqual({ commentId: 'comment-1', @@ -471,20 +530,16 @@ describe('IBM Resilient service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - create_date: 1589391874472, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + create_date: 1589391874472, + }, + }) + ); - await service.createComment({ - incidentId: '1', - comment: { - comment: 'comment', - commentId: 'comment-1', - }, - }); + await service.createComment(req); expect(requestMock).toHaveBeenCalledWith({ axios, @@ -506,27 +561,31 @@ describe('IBM Resilient service', () => { throw new Error('An error has occurred'); }); - await expect( - service.createComment({ - incidentId: '1', - comment: { - comment: 'comment', - commentId: 'comment-1', - }, - }) - ).rejects.toThrow( + await expect(service.createComment(req)).rejects.toThrow( '[Action][IBM Resilient]: Unable to create comment at incident with id 1. Error: An error has occurred' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.createComment(req)).rejects.toThrow( + '[Action][IBM Resilient]: Unable to create comment at incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' + ); + }); }); describe('getIncidentTypes', () => { test('it creates the incident correctly', async () => { - requestMock.mockImplementation(() => ({ - data: { - values: incidentTypes, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + values: incidentTypes, + }, + }) + ); const res = await service.getIncidentTypes(); @@ -545,15 +604,27 @@ describe('IBM Resilient service', () => { '[Action][IBM Resilient]: Unable to get incident types. Error: An error has occurred.' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getIncidentTypes()).rejects.toThrow( + '[Action][IBM Resilient]: Unable to get incident types. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' + ); + }); }); describe('getSeverity', () => { test('it creates the incident correctly', async () => { - requestMock.mockImplementation(() => ({ - data: { - values: severity, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + values: severity, + }, + }) + ); const res = await service.getSeverity(); @@ -578,17 +649,29 @@ describe('IBM Resilient service', () => { throw new Error('An error has occurred'); }); - await expect(service.getIncidentTypes()).rejects.toThrow( - '[Action][IBM Resilient]: Unable to get incident types. Error: An error has occurred.' + await expect(service.getSeverity()).rejects.toThrow( + '[Action][IBM Resilient]: Unable to get severity. Error: An error has occurred.' + ); + }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getSeverity()).rejects.toThrow( + '[Action][IBM Resilient]: Unable to get severity. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' ); }); }); describe('getFields', () => { test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: resilientFields, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: resilientFields, + }) + ); await service.getFields(); expect(requestMock).toHaveBeenCalledWith({ @@ -598,10 +681,13 @@ describe('IBM Resilient service', () => { url: 'https://resilient.elastic.co/rest/orgs/201/types/incident/fields', }); }); + test('it returns common fields correctly', async () => { - requestMock.mockImplementation(() => ({ - data: resilientFields, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: resilientFields, + }) + ); const res = await service.getFields(); expect(res).toEqual(resilientFields); }); @@ -614,5 +700,15 @@ describe('IBM Resilient service', () => { 'Unable to get fields. Error: An error has occurred' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getFields()).rejects.toThrow( + '[Action][IBM Resilient]: Unable to get fields. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' + ); + }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts index 2f385315e4392..a469c631fac37 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts @@ -24,7 +24,7 @@ import { } from './types'; import * as i18n from './translations'; -import { getErrorMessage, request } from '../lib/axios_utils'; +import { getErrorMessage, request, throwIfResponseIsNotValid } from '../lib/axios_utils'; import { ActionsConfigurationUtilities } from '../../actions_config'; const VIEW_INCIDENT_URL = `#incidents`; @@ -134,6 +134,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + return { ...res.data, description: res.data.description?.content ?? '' }; } catch (error) { throw new Error( @@ -182,6 +186,11 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + requiredAttributesToBeInTheResponse: ['id', 'create_date'], + }); + return { title: `${res.data.id}`, id: `${res.data.id}`, @@ -212,6 +221,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + if (!res.data.success) { throw new Error(res.data.message); } @@ -245,6 +258,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + return { commentId: comment.commentId, externalCommentId: res.data.id, @@ -270,6 +287,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + const incidentTypes = res.data?.values ?? []; return incidentTypes.map((type: { value: string; label: string }) => ({ id: type.value, @@ -292,6 +313,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + const incidentTypes = res.data?.values ?? []; return incidentTypes.map((type: { value: string; label: string }) => ({ id: type.value, @@ -312,6 +337,11 @@ export const createExternalService = ( logger, configurationUtilities, }); + + throwIfResponseIsNotValid({ + res, + }); + return res.data ?? []; } catch (error) { throw new Error(getErrorMessage(i18n.NAME, `Unable to get fields. Error: ${error.message}.`)); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.test.ts index 358af7cd2e9ef..e5a161611fcb1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.test.ts @@ -132,7 +132,7 @@ describe('api_sir', () => { }); describe('prepareParams', () => { - test('it prepares the params correctly when the connector is legacy', async () => { + test('it prepares the params correctly when the connector uses the old API', async () => { expect(prepareParams(true, sirParams)).toEqual({ ...sirParams, incident: { @@ -145,7 +145,7 @@ describe('api_sir', () => { }); }); - test('it prepares the params correctly when the connector is not legacy', async () => { + test('it prepares the params correctly when the connector does not uses the old API', async () => { expect(prepareParams(false, sirParams)).toEqual({ ...sirParams, incident: { @@ -158,7 +158,7 @@ describe('api_sir', () => { }); }); - test('it prepares the params correctly when the connector is legacy and the observables are undefined', async () => { + test('it prepares the params correctly when the connector uses the old API and the observables are undefined', async () => { const { dest_ip: destIp, source_ip: sourceIp, @@ -192,7 +192,7 @@ describe('api_sir', () => { const res = await apiSIR.pushToService({ externalService, params, - config: { isLegacy: false }, + config: { usesTableApi: false }, secrets: {}, logger: mockedLogger, commentFieldKey: 'work_notes', @@ -221,7 +221,7 @@ describe('api_sir', () => { await apiSIR.pushToService({ externalService, params, - config: { isLegacy: false }, + config: { usesTableApi: false }, secrets: {}, logger: mockedLogger, commentFieldKey: 'work_notes', @@ -244,12 +244,12 @@ describe('api_sir', () => { ); }); - test('it does not call bulkAddObservableToIncident if it a legacy connector', async () => { + test('it does not call bulkAddObservableToIncident if the connector uses the old API', async () => { const params = { ...sirParams, incident: { ...sirParams.incident, externalId: null } }; await apiSIR.pushToService({ externalService, params, - config: { isLegacy: true }, + config: { usesTableApi: true }, secrets: {}, logger: mockedLogger, commentFieldKey: 'work_notes', @@ -274,7 +274,7 @@ describe('api_sir', () => { await apiSIR.pushToService({ externalService, params, - config: { isLegacy: false }, + config: { usesTableApi: false }, secrets: {}, logger: mockedLogger, commentFieldKey: 'work_notes', diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.ts index 326bb79a0e708..4e74d79c6f4a0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.ts @@ -59,13 +59,13 @@ const observablesToString = (obs: string | string[] | null | undefined): string }; export const prepareParams = ( - isLegacy: boolean, + usesTableApi: boolean, params: PushToServiceApiParamsSIR ): PushToServiceApiParamsSIR => { - if (isLegacy) { + if (usesTableApi) { /** * The schema has change to accept an array of observables - * or a string. In the case of a legacy connector we need to + * or a string. In the case of connector that uses the old API we need to * convert the observables to a string */ return { @@ -81,8 +81,8 @@ export const prepareParams = ( } /** - * For non legacy connectors the observables - * will be added in a different call. + * For connectors that do not use the old API + * the observables will be added in a different call. * They need to be set to null when sending the fields * to ServiceNow */ @@ -108,7 +108,7 @@ const pushToServiceHandler = async ({ }: PushToServiceApiHandlerArgs): Promise => { const res = await api.pushToService({ externalService, - params: prepareParams(!!config.isLegacy, params as PushToServiceApiParamsSIR), + params: prepareParams(!!config.usesTableApi, params as PushToServiceApiParamsSIR), config, secrets, commentFieldKey, @@ -130,7 +130,7 @@ const pushToServiceHandler = async ({ * through the pushToService call. */ - if (!config.isLegacy) { + if (!config.usesTableApi) { const sirExternalService = externalService as ExternalServiceSIR; const obsWithType: Array<[string[], ObservableTypes]> = [ diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index af8d1b9f38b17..e41eea24834c7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -14,7 +14,7 @@ export const ExternalIncidentServiceConfigurationBase = { export const ExternalIncidentServiceConfiguration = { ...ExternalIncidentServiceConfigurationBase, - isLegacy: schema.boolean({ defaultValue: true }), + usesTableApi: schema.boolean({ defaultValue: true }), }; export const ExternalIncidentServiceConfigurationBaseSchema = schema.object( @@ -49,7 +49,7 @@ const CommonAttributes = { externalId: schema.nullable(schema.string()), category: schema.nullable(schema.string()), subcategory: schema.nullable(schema.string()), - correlation_id: schema.nullable(schema.string()), + correlation_id: schema.nullable(schema.string({ defaultValue: DEFAULT_ALERTS_GROUPING_KEY })), correlation_display: schema.nullable(schema.string()), }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts index cb030c7bb6933..c90a7222ba10b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts @@ -35,7 +35,8 @@ export const createExternalService: ServiceFactory = ( configurationUtilities: ActionsConfigurationUtilities, { table, importSetTable, useImportAPI, appScope }: SNProductsConfigValue ): ExternalService => { - const { apiUrl: url, isLegacy } = config as ServiceNowPublicConfigurationType; + const { apiUrl: url, usesTableApi: usesTableApiConfigValue } = + config as ServiceNowPublicConfigurationType; const { username, password } = secrets as ServiceNowSecretConfigurationType; if (!url || !username || !password) { @@ -57,11 +58,11 @@ export const createExternalService: ServiceFactory = ( auth: { username, password }, }); - const useOldApi = !useImportAPI || isLegacy; + const useTableApi = !useImportAPI || usesTableApiConfigValue; - const getCreateIncidentUrl = () => (useOldApi ? tableApiIncidentUrl : importSetTableUrl); + const getCreateIncidentUrl = () => (useTableApi ? tableApiIncidentUrl : importSetTableUrl); const getUpdateIncidentUrl = (incidentId: string) => - useOldApi ? `${tableApiIncidentUrl}/${incidentId}` : importSetTableUrl; + useTableApi ? `${tableApiIncidentUrl}/${incidentId}` : importSetTableUrl; const getIncidentViewURL = (id: string) => { // Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html @@ -105,7 +106,7 @@ export const createExternalService: ServiceFactory = ( /** * Gets the Elastic SN Application information including the current version. - * It should not be used on legacy connectors. + * It should not be used on connectors that use the old API. */ const getApplicationInformation = async (): Promise => { try { @@ -129,7 +130,7 @@ export const createExternalService: ServiceFactory = ( logger.debug(`Create incident: Application scope: ${scope}: Application version${version}`); const checkIfApplicationIsInstalled = async () => { - if (!useOldApi) { + if (!useTableApi) { const { version, scope } = await getApplicationInformation(); logApplicationInfo(scope, version); } @@ -180,17 +181,17 @@ export const createExternalService: ServiceFactory = ( url: getCreateIncidentUrl(), logger, method: 'post', - data: prepareIncident(useOldApi, incident), + data: prepareIncident(useTableApi, incident), configurationUtilities, }); checkInstance(res); - if (!useOldApi) { + if (!useTableApi) { throwIfImportSetApiResponseIsAnError(res.data); } - const incidentId = useOldApi ? res.data.result.sys_id : res.data.result[0].sys_id; + const incidentId = useTableApi ? res.data.result.sys_id : res.data.result[0].sys_id; const insertedIncident = await getIncident(incidentId); return { @@ -212,23 +213,23 @@ export const createExternalService: ServiceFactory = ( axios: axiosInstance, url: getUpdateIncidentUrl(incidentId), // Import Set API supports only POST. - method: useOldApi ? 'patch' : 'post', + method: useTableApi ? 'patch' : 'post', logger, data: { - ...prepareIncident(useOldApi, incident), + ...prepareIncident(useTableApi, incident), // elastic_incident_id is used to update the incident when using the Import Set API. - ...(useOldApi ? {} : { elastic_incident_id: incidentId }), + ...(useTableApi ? {} : { elastic_incident_id: incidentId }), }, configurationUtilities, }); checkInstance(res); - if (!useOldApi) { + if (!useTableApi) { throwIfImportSetApiResponseIsAnError(res.data); } - const id = useOldApi ? res.data.result.sys_id : res.data.result[0].sys_id; + const id = useTableApi ? res.data.result.sys_id : res.data.result[0].sys_id; const updatedIncident = await getIncident(id); return { diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts index 7b3f310a99e0e..21bc4894c5717 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts @@ -10,7 +10,7 @@ import axios from 'axios'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { Logger } from '../../../../../../src/core/server'; import { actionsConfigMock } from '../../actions_config.mock'; -import * as utils from '../lib/axios_utils'; +import { request, createAxiosResponse } from '../lib/axios_utils'; import { createExternalService } from './service'; import { mappings } from './mocks'; import { ExternalService } from './types'; @@ -27,7 +27,7 @@ jest.mock('../lib/axios_utils', () => { }); axios.create = jest.fn(() => axios); -const requestMock = utils.request as jest.Mock; +const requestMock = request as jest.Mock; const configurationUtilities = actionsConfigMock.create(); describe('Swimlane Service', () => { @@ -152,9 +152,7 @@ describe('Swimlane Service', () => { }; test('it creates a record correctly', async () => { - requestMock.mockImplementation(() => ({ - data, - })); + requestMock.mockImplementation(() => createAxiosResponse({ data })); const res = await service.createRecord({ incident, @@ -169,9 +167,7 @@ describe('Swimlane Service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data, - })); + requestMock.mockImplementation(() => createAxiosResponse({ data })); await service.createRecord({ incident, @@ -207,6 +203,24 @@ describe('Swimlane Service', () => { `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.createRecord({ incident })).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown` + ); + }); + + test('it should throw if the required attributes are not there', async () => { + requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } })); + + await expect(service.createRecord({ incident })).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: Response is missing at least one of the expected fields: id,name,createdDate. Reason: unknown` + ); + }); }); describe('updateRecord', () => { @@ -218,9 +232,7 @@ describe('Swimlane Service', () => { const incidentId = '123'; test('it updates a record correctly', async () => { - requestMock.mockImplementation(() => ({ - data, - })); + requestMock.mockImplementation(() => createAxiosResponse({ data })); const res = await service.updateRecord({ incident, @@ -236,9 +248,7 @@ describe('Swimlane Service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data, - })); + requestMock.mockImplementation(() => createAxiosResponse({ data })); await service.updateRecord({ incident, @@ -276,6 +286,24 @@ describe('Swimlane Service', () => { `[Action][Swimlane]: Unable to update record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.updateRecord({ incident, incidentId })).rejects.toThrow( + `[Action][Swimlane]: Unable to update record in application with id ${config.appId}. Status: 500. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown` + ); + }); + + test('it should throw if the required attributes are not there', async () => { + requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } })); + + await expect(service.updateRecord({ incident, incidentId })).rejects.toThrow( + `[Action][Swimlane]: Unable to update record in application with id ${config.appId}. Status: 500. Error: Response is missing at least one of the expected fields: id,name,modifiedDate. Reason: unknown` + ); + }); }); describe('createComment', () => { @@ -289,9 +317,7 @@ describe('Swimlane Service', () => { const createdDate = '2021-06-01T17:29:51.092Z'; test('it updates a record correctly', async () => { - requestMock.mockImplementation(() => ({ - data, - })); + requestMock.mockImplementation(() => createAxiosResponse({ data })); const res = await service.createComment({ comment, @@ -306,9 +332,7 @@ describe('Swimlane Service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data, - })); + requestMock.mockImplementation(() => createAxiosResponse({ data })); await service.createComment({ comment, diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index f68d22121dbcc..d917d7f5677bb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -9,7 +9,7 @@ import { Logger } from '@kbn/logging'; import axios from 'axios'; import { ActionsConfigurationUtilities } from '../../actions_config'; -import { getErrorMessage, request } from '../lib/axios_utils'; +import { getErrorMessage, request, throwIfResponseIsNotValid } from '../lib/axios_utils'; import { getBodyForEventAction } from './helpers'; import { CreateCommentParams, @@ -89,6 +89,12 @@ export const createExternalService = ( method: 'post', url: getPostRecordUrl(appId), }); + + throwIfResponseIsNotValid({ + res, + requiredAttributesToBeInTheResponse: ['id', 'name', 'createdDate'], + }); + return { id: res.data.id, title: res.data.name, @@ -124,6 +130,11 @@ export const createExternalService = ( url: getPostRecordIdUrl(appId, params.incidentId), }); + throwIfResponseIsNotValid({ + res, + requiredAttributesToBeInTheResponse: ['id', 'name', 'modifiedDate'], + }); + return { id: res.data.id, title: res.data.name, diff --git a/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts b/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts index 9f8e62c77e3a7..6c61d9849c72c 100644 --- a/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts +++ b/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts @@ -166,7 +166,7 @@ describe('successful migrations', () => { expect(migratedAction).toEqual(action); }); - test('set isLegacy config property for .servicenow', () => { + test('set usesTableApi config property for .servicenow', () => { const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']; const action = getMockDataForServiceNow(); const migratedAction = migration716(action, context); @@ -177,13 +177,13 @@ describe('successful migrations', () => { ...action.attributes, config: { apiUrl: 'https://example.com', - isLegacy: true, + usesTableApi: true, }, }, }); }); - test('set isLegacy config property for .servicenow-sir', () => { + test('set usesTableApi config property for .servicenow-sir', () => { const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']; const action = getMockDataForServiceNow({ actionTypeId: '.servicenow-sir' }); const migratedAction = migration716(action, context); @@ -194,13 +194,13 @@ describe('successful migrations', () => { ...action.attributes, config: { apiUrl: 'https://example.com', - isLegacy: true, + usesTableApi: true, }, }, }); }); - test('it does not set isLegacy config for other connectors', () => { + test('it does not set usesTableApi config for other connectors', () => { const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']; const action = getMockData(); const migratedAction = migration716(action, context); diff --git a/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts b/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts index 688839eb89858..2e5b1b5d916fe 100644 --- a/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts +++ b/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts @@ -68,7 +68,7 @@ export function getActionsMigrations( doc.attributes.actionTypeId === '.servicenow' || doc.attributes.actionTypeId === '.servicenow-sir' || doc.attributes.actionTypeId === '.email', - pipeMigrations(markOldServiceNowITSMConnectorAsLegacy, setServiceConfigIfNotSet) + pipeMigrations(addUsesTableApiToServiceNowConnectors, setServiceConfigIfNotSet) ); const migrationActions800 = createEsoMigration( @@ -197,7 +197,7 @@ const addIsMissingSecretsField = ( }; }; -const markOldServiceNowITSMConnectorAsLegacy = ( +const addUsesTableApiToServiceNowConnectors = ( doc: SavedObjectUnsanitizedDoc ): SavedObjectUnsanitizedDoc => { if ( @@ -213,7 +213,7 @@ const markOldServiceNowITSMConnectorAsLegacy = ( ...doc.attributes, config: { ...doc.attributes.config, - isLegacy: true, + usesTableApi: true, }, }, }; diff --git a/x-pack/plugins/apm/dev_docs/linting.md b/x-pack/plugins/apm/dev_docs/linting.md index edf3e813a88e9..3dbd7b5b27484 100644 --- a/x-pack/plugins/apm/dev_docs/linting.md +++ b/x-pack/plugins/apm/dev_docs/linting.md @@ -19,3 +19,12 @@ yarn prettier "./x-pack/plugins/apm/**/*.{tsx,ts,js}" --write ``` node scripts/eslint.js x-pack/plugins/apm ``` + +## Install pre-commit hook (optional) +In case you want to run a couple of checks like linting or check the file casing of the files to commit, we provide a way to install a pre-commit hook. To configure it you just need to run the following: + +`node scripts/register_git_hook` + +After the script completes the pre-commit hook will be created within the file .git/hooks/pre-commit. If you choose to not install it, don’t worry, we still run a quick CI check to provide feedback earliest as we can about the same checks. + +More information about linting can be found in the [Kibana Guide](https://www.elastic.co/guide/en/kibana/current/kibana-linting.html). \ No newline at end of file diff --git a/x-pack/plugins/apm/ftr_e2e/cypress_start.ts b/x-pack/plugins/apm/ftr_e2e/cypress_start.ts index caf87d2627459..0cfc58653801a 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress_start.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress_start.ts @@ -16,15 +16,10 @@ import { esArchiverLoad, esArchiverUnload } from './cypress/tasks/es_archiver'; export function cypressRunTests(spec?: string) { return async ({ getService }: FtrProviderContext) => { - try { - const result = await cypressStart(getService, cypress.run, spec); + const result = await cypressStart(getService, cypress.run, spec); - if (result && (result.status === 'failed' || result.totalFailed > 0)) { - process.exit(1); - } - } catch (error) { - console.error('errors: ', error); - process.exit(1); + if (result && (result.status === 'failed' || result.totalFailed > 0)) { + throw new Error(`APM Cypress tests failed`); } }; } diff --git a/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx b/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx index fb1a99db0bf5b..2015bf2228b6c 100644 --- a/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx @@ -23,17 +23,21 @@ import { EuiSpacer } from '@elastic/eui'; import React from 'react'; import { Coordinate } from '../../../../typings/timeseries'; import { useTheme } from '../../../hooks/use_theme'; +import { IUiSettingsClient } from '../../../../../../../src/core/public'; +import { getTimeZone } from '../../shared/charts/helper/timezone'; interface ChartPreviewProps { yTickFormat?: TickFormatter; data?: Coordinate[]; threshold: number; + uiSettings?: IUiSettingsClient; } export function ChartPreview({ data = [], yTickFormat, threshold, + uiSettings, }: ChartPreviewProps) { const theme = useTheme(); const thresholdOpacity = 0.3; @@ -67,6 +71,8 @@ export function ChartPreview({ }, ]; + const timeZone = getTimeZone(uiSettings); + return ( <> @@ -99,6 +105,7 @@ export function ChartPreview({ domain={{ max: yMax, min: NaN }} /> ); diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx index 8957dfc823e44..fa75fcca579e5 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx @@ -139,6 +139,7 @@ export function TransactionDurationAlertTrigger(props: Props) { data={latencyChartPreview} threshold={thresholdMs} yTickFormat={yTickFormat} + uiSettings={services.uiSettings} /> ); diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx index ddddc4bbecbad..a818218cbf3ad 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx @@ -131,6 +131,7 @@ export function TransactionErrorRateAlertTrigger(props: Props) { data={data?.errorRateChartPreview} yTickFormat={(d: number | null) => asPercent(d, 1)} threshold={thresholdAsPercent} + uiSettings={services.uiSettings} /> ); diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.stories.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.stories.tsx index 4efc00ef71b91..101923a7678fb 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.stories.tsx @@ -19,14 +19,17 @@ export default { component: ErrorDistribution, decorators: [ (Story: ComponentType) => { - const apmPluginContextMock = { - observabilityRuleTypeRegistry: { getFormatter: () => undefined }, - } as unknown as ApmPluginContextValue; - const kibanaContextServices = { uiSettings: { get: () => {} }, }; + const apmPluginContextMock = { + observabilityRuleTypeRegistry: { getFormatter: () => undefined }, + core: { + uiSettings: kibanaContextServices.uiSettings, + }, + } as unknown as ApmPluginContextValue; + return ( diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx index 429ad989b9738..6a3157b3c4b7f 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx @@ -32,6 +32,7 @@ import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plug import { LazyAlertsFlyout } from '../../../../../../observability/public'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { Coordinate } from '../../../../../typings/timeseries'; +import { getTimeZone } from '../../../shared/charts/helper/timezone'; const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = ALERT_RULE_TYPE_ID_NON_TYPED; @@ -58,6 +59,7 @@ interface Props { } export function ErrorDistribution({ distribution, title, fetchStatus }: Props) { + const { core } = useApmPluginContext(); const theme = useTheme(); const currentPeriod = getCoordinatedBuckets(distribution.currentPeriod); const previousPeriod = getCoordinatedBuckets(distribution.previousPeriod); @@ -103,6 +105,8 @@ export function ErrorDistribution({ distribution, title, fetchStatus }: Props) { undefined ); + const timeZone = getTimeZone(core.uiSettings); + return ( <> @@ -138,6 +142,7 @@ export function ErrorDistribution({ distribution, title, fetchStatus }: Props) { {timeseries.map((serie) => { return ( @@ -150,6 +151,7 @@ export function BreakdownChart({ timeseries.map((serie) => { return ( { let originalTimezone: moment.MomentZone | null; @@ -67,4 +68,22 @@ describe('Timezone helper', () => { ]); }); }); + + describe('getTimeZone', () => { + it('returns local when uiSettings is undefined', () => { + expect(getTimeZone()).toEqual('local'); + }); + + it('returns local when uiSettings returns Browser', () => { + expect( + getTimeZone({ get: () => 'Browser' } as unknown as IUiSettingsClient) + ).toEqual('local'); + }); + it('returns timezone defined on uiSettings', () => { + const timezone = 'America/toronto'; + expect( + getTimeZone({ get: () => timezone } as unknown as IUiSettingsClient) + ).toEqual(timezone); + }); + }); }); diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/timezone.ts b/x-pack/plugins/apm/public/components/shared/charts/helper/timezone.ts index 539c81c61c3ce..f807d83c8977f 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/timezone.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/timezone.ts @@ -7,6 +7,8 @@ import d3 from 'd3'; import { getTimezoneOffsetInMs } from './get_timezone_offset_in_ms'; +import { IUiSettingsClient } from '../../../../../../../../src/core/public'; +import { UI_SETTINGS } from '../../../../../../../../src/plugins/data/common'; interface Params { domain: [number, number]; @@ -31,3 +33,15 @@ export const getDomainTZ = (min: number, max: number): [number, number] => { ); return [xMinZone, xMaxZone]; }; + +export function getTimeZone(uiSettings?: IUiSettingsClient) { + const kibanaTimeZone = uiSettings?.get<'Browser' | string>( + UI_SETTINGS.DATEFORMAT_TZ + ); + + if (!kibanaTimeZone || kibanaTimeZone === 'Browser') { + return 'local'; + } + + return kibanaTimeZone; +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx index 08e8908d50e7a..bcdfff2678cda 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx @@ -19,8 +19,8 @@ import { RectAnnotation, ScaleType, Settings, - YDomainRange, XYBrushEvent, + YDomainRange, } from '@elastic/charts'; import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -46,6 +46,7 @@ import { getLatencyChartSelector } from '../../../selectors/latency_chart_select import { unit } from '../../../utils/style'; import { ChartContainer } from './chart_container'; import { getAlertAnnotations } from './helper/get_alert_annotations'; +import { getTimeZone } from './helper/timezone'; import { isTimeseriesEmpty, onBrushEnd } from './helper/helper'; interface Props { @@ -85,7 +86,7 @@ export function TimeseriesChart({ alerts, }: Props) { const history = useHistory(); - const { observabilityRuleTypeRegistry } = useApmPluginContext(); + const { observabilityRuleTypeRegistry, core } = useApmPluginContext(); const { getFormatter } = observabilityRuleTypeRegistry; const { annotations } = useAnnotationsContext(); const { setPointerEvent, chartRef } = useChartPointerEventContext(); @@ -97,6 +98,8 @@ export function TimeseriesChart({ const xValues = timeseries.flatMap(({ data }) => data.map(({ x }) => x)); + const timeZone = getTimeZone(core.uiSettings); + const min = Math.min(...xValues); const max = Math.max(...xValues); @@ -180,6 +183,7 @@ export function TimeseriesChart({ return ( + + + + + + +