From 48e146f2f1f94addee0f5aea9e28ae6529590bf4 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher <471693+Kerry350@users.noreply.github.com> Date: Thu, 14 Sep 2023 19:02:17 +0100 Subject: [PATCH 1/5] Enforce dataset name validation (client and server side) --- .../src/components/create/form.tsx | 34 ++++- .../src/components/create/utils.ts | 4 +- .../state_machines/create/notifications.ts | 19 ++- .../state_machines/create/pipelines/fields.ts | 134 ++++++++++++++++++ .../state_machines/create/state_machine.ts | 50 +++---- .../src/state_machines/create/types.ts | 14 +- .../custom_integrations/state_machine.ts | 38 +++-- .../services/integrations_client.ts | 6 +- packages/kbn-custom-integrations/src/types.ts | 11 +- .../fleet/server/routes/epm/handlers.ts | 8 ++ .../assets/dataset/utils.ts | 2 +- .../validation/check_dataset_name_format.ts | 37 +++++ .../server/services/epm/packages/install.ts | 2 + .../e2e/cypress/support/commands.ts | 6 +- .../app/custom_logs/configure_logs.tsx | 12 +- .../components/app/custom_logs/index.tsx | 8 +- .../apis/epm/install_custom.ts | 63 ++++++-- 17 files changed, 359 insertions(+), 89 deletions(-) create mode 100644 packages/kbn-custom-integrations/src/state_machines/create/pipelines/fields.ts create mode 100644 x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/validation/check_dataset_name_format.ts diff --git a/packages/kbn-custom-integrations/src/components/create/form.tsx b/packages/kbn-custom-integrations/src/components/create/form.tsx index 2273db68a479a..22fc0b26633e2 100644 --- a/packages/kbn-custom-integrations/src/components/create/form.tsx +++ b/packages/kbn-custom-integrations/src/components/create/form.tsx @@ -28,6 +28,12 @@ import { } from '../../state_machines/create/types'; import { Dataset, IntegrationError } from '../../types'; import { hasFailedSelector } from '../../state_machines/create/selectors'; +import { + datasetNameWillBePrefixed, + getDatasetNamePrefix, + getDatasetNameWithoutPrefix, + prefixDatasetName, +} from '../../state_machines/create/pipelines/fields'; // NOTE: Hardcoded for now. We will likely extend the functionality here to allow the selection of the type. // And also to allow adding multiple datasets. @@ -50,6 +56,7 @@ export const ConnectedCreateCustomIntegrationForm = ({ testSubjects?: CreateTestSubjects; }) => { const [state, send] = useActor(machineRef); + const updateIntegrationName = useCallback( (integrationName: string) => { send({ type: 'UPDATE_FIELDS', fields: { integrationName } }); @@ -181,17 +188,32 @@ export const CreateCustomIntegrationForm = ({ } - helpText={i18n.translate('customIntegrationsPackage.create.dataset.helper', { - defaultMessage: - "All lowercase, max 100 chars, special characters will be replaced with '_'.", - })} + helpText={[ + i18n.translate('customIntegrationsPackage.create.dataset.helper', { + defaultMessage: + "All lowercase, max 100 chars, special characters will be replaced with '_'.", + }), + datasetNameWillBePrefixed(datasetName, integrationName) + ? i18n.translate( + 'customIntegrationsPackage.create.dataset.name.tooltipPrefixMessage', + { + defaultMessage: + 'This name will be prefixed with {prefixValue}, e.g. {prefixedDatasetName}', + values: { + prefixValue: getDatasetNamePrefix(integrationName), + prefixedDatasetName: prefixDatasetName(datasetName, integrationName), + }, + } + ) + : '', + ].join(' ')} isInvalid={hasErrors(errors?.fields?.datasets?.[0]?.name) && touchedFields.datasets} error={errorsList(errors?.fields?.datasets?.[0]?.name)} > @@ -199,7 +221,7 @@ export const CreateCustomIntegrationForm = ({ placeholder={i18n.translate('customIntegrationsPackage.create.dataset.placeholder', { defaultMessage: "Give your integration's dataset a name", })} - value={datasetName} + value={getDatasetNameWithoutPrefix(datasetName, integrationName)} onChange={(event) => updateDatasetName(event.target.value)} isInvalid={hasErrors(errors?.fields?.datasets?.[0].name) && touchedFields.datasets} max={100} diff --git a/packages/kbn-custom-integrations/src/components/create/utils.ts b/packages/kbn-custom-integrations/src/components/create/utils.ts index 3af857be37236..688bea61128ed 100644 --- a/packages/kbn-custom-integrations/src/components/create/utils.ts +++ b/packages/kbn-custom-integrations/src/components/create/utils.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -export const replaceSpecialChars = (filename: string) => { +export const replaceSpecialChars = (value: string) => { // Replace special characters with _ - const replacedSpecialCharacters = filename.replaceAll(/[^a-zA-Z0-9_]/g, '_'); + const replacedSpecialCharacters = value.replaceAll(/[^a-zA-Z0-9_]/g, '_'); // Allow only one _ in a row const noRepetitions = replacedSpecialCharacters.replaceAll(/[\_]{2,}/g, '_'); return noRepetitions; diff --git a/packages/kbn-custom-integrations/src/state_machines/create/notifications.ts b/packages/kbn-custom-integrations/src/state_machines/create/notifications.ts index 38ef7fb993906..2ca7e09ae8f6e 100644 --- a/packages/kbn-custom-integrations/src/state_machines/create/notifications.ts +++ b/packages/kbn-custom-integrations/src/state_machines/create/notifications.ts @@ -27,11 +27,20 @@ export type CreateCustomIntegrationNotificationEvent = }; export const CreateIntegrationNotificationEventSelectors = { - integrationCreated: (context: CreateCustomIntegrationContext) => - ({ - type: 'INTEGRATION_CREATED', - fields: context.fields, - } as CreateCustomIntegrationNotificationEvent), + integrationCreated: ( + context: CreateCustomIntegrationContext, + event: CreateCustomIntegrationEvent + ) => { + return 'data' in event && 'integrationName' in event.data && 'datasets' in event.data + ? ({ + type: 'INTEGRATION_CREATED', + fields: { + integrationName: event.data.integrationName, + datasets: event.data.datasets, + }, + } as CreateCustomIntegrationNotificationEvent) + : null; + }, integrationCleanup: ( context: CreateCustomIntegrationContext, event: CreateCustomIntegrationEvent diff --git a/packages/kbn-custom-integrations/src/state_machines/create/pipelines/fields.ts b/packages/kbn-custom-integrations/src/state_machines/create/pipelines/fields.ts new file mode 100644 index 0000000000000..3a7fef9ce98a7 --- /dev/null +++ b/packages/kbn-custom-integrations/src/state_machines/create/pipelines/fields.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { replaceSpecialChars } from '../../../components/create/utils'; +import { CreateCustomIntegrationContext, UpdateFieldsEvent, WithTouchedFields } from '../types'; + +type ValuesTuple = [CreateCustomIntegrationContext, UpdateFieldsEvent]; + +// Pipeline for updating the fields and touchedFields properties within context +export const executeFieldsPipeline = ( + context: CreateCustomIntegrationContext, + event: UpdateFieldsEvent +) => { + return pipe( + [context, event] as ValuesTuple, + updateFields(context), + updateTouchedFields(context), + maybeMatchDatasetNameToIntegrationName(context), + replaceSpecialCharacters(context) + ); +}; + +const updateFields = + (originalContext: CreateCustomIntegrationContext) => + (values: ValuesTuple): ValuesTuple => { + const [context, event] = values; + + const mergedContext = { + ...context, + fields: { + ...context.fields, + ...event.fields, + }, + }; + return [mergedContext, event]; + }; + +const updateTouchedFields = + (originalContext: CreateCustomIntegrationContext) => + (values: ValuesTuple): ValuesTuple => { + const [context, event] = values; + + const mergedContext = { + ...context, + touchedFields: { + ...context.touchedFields, + ...Object.keys(event.fields).reduce( + (acc, field) => ({ ...acc, [field]: true }), + {} as WithTouchedFields['touchedFields'] + ), + }, + }; + return [mergedContext, event]; + }; + +const maybeMatchDatasetNameToIntegrationName = + (originalContext: CreateCustomIntegrationContext) => + (values: ValuesTuple): ValuesTuple => { + const [context, event] = values; + if (context.touchedFields.integrationName && !context.touchedFields.datasets) { + return [ + { + ...context, + fields: { + ...context.fields, + datasets: context.fields.datasets.map((dataset, index) => ({ + ...dataset, + name: index === 0 ? context.fields.integrationName : dataset.name, + })), + }, + }, + event, + ]; + } else { + return [context, event]; + } + }; + +const replaceSpecialCharacters = + (originalContext: CreateCustomIntegrationContext) => + (values: ValuesTuple): ValuesTuple => { + const [context, event] = values; + + const mergedContext = { + ...context, + fields: { + ...context.fields, + integrationName: replaceSpecialChars(context.fields.integrationName), + datasets: context.fields.datasets.map((dataset) => ({ + ...dataset, + name: replaceSpecialChars(dataset.name), + })), + }, + }; + + return [mergedContext, event]; + }; + +export const getDatasetNamePrefix = (integrationName: string) => `${integrationName}.`; +export const datasetNameIsPrefixed = (datasetName: string, integrationName: string) => + datasetName.startsWith(getDatasetNamePrefix(integrationName)); +export const datasetNameWillBePrefixed = (datasetName: string, integrationName: string) => + datasetName !== integrationName; +export const prefixDatasetName = (datasetName: string, integrationName: string) => + `${getDatasetNamePrefix(integrationName)}${datasetName}`; + +// The content after the integration name prefix. +export const getDatasetNameWithoutPrefix = (datasetName: string, integrationName: string) => + datasetNameIsPrefixed(datasetName, integrationName) + ? datasetName.split(getDatasetNamePrefix(integrationName))[1] + : datasetName; + +// The machine holds unprefixed names internally to dramatically reduce complexity and improve performance for input changes in the UI. +// Prefixed names are used at the outermost edges e.g. when providing initial state and submitting to the API. +export const normalizeDatasetNames = (fields: UpdateFieldsEvent['fields']) => { + const value = { + ...fields, + ...(fields.datasets !== undefined && fields.integrationName !== undefined + ? { + datasets: fields.datasets.map((dataset) => ({ + ...dataset, + name: getDatasetNameWithoutPrefix(dataset.name, fields.integrationName!), + })), + } + : {}), + }; + return value; +}; diff --git a/packages/kbn-custom-integrations/src/state_machines/create/state_machine.ts b/packages/kbn-custom-integrations/src/state_machines/create/state_machine.ts index ad93d3eb8cb05..2ae0e51bad3d8 100644 --- a/packages/kbn-custom-integrations/src/state_machines/create/state_machine.ts +++ b/packages/kbn-custom-integrations/src/state_machines/create/state_machine.ts @@ -28,10 +28,12 @@ import { DefaultCreateCustomIntegrationContext, WithErrors, WithPreviouslyCreatedIntegration, - WithTouchedFields, - WithFields, } from './types'; -import { replaceSpecialChars } from '../../components/create/utils'; +import { + datasetNameWillBePrefixed, + executeFieldsPipeline, + prefixDatasetName, +} from './pipelines/fields'; export const createPureCreateCustomIntegrationStateMachine = ( initialContext: DefaultCreateCustomIntegrationContext = DEFAULT_CONTEXT @@ -211,32 +213,12 @@ export const createPureCreateCustomIntegrationStateMachine = ( : {}; }), storeFields: actions.assign((context, event) => { - return event.type === 'UPDATE_FIELDS' - ? ({ - fields: { - ...context.fields, - ...event.fields, - integrationName: - event.fields.integrationName !== undefined - ? replaceSpecialChars(event.fields.integrationName) - : context.fields.integrationName, - datasets: - event.fields.datasets !== undefined - ? event.fields.datasets.map((dataset) => ({ - ...dataset, - name: replaceSpecialChars(dataset.name), - })) - : context.fields.datasets, - }, - touchedFields: { - ...context.touchedFields, - ...Object.keys(event.fields).reduce( - (acc, field) => ({ ...acc, [field]: true }), - {} as WithTouchedFields['touchedFields'] - ), - }, - } as WithFields & WithTouchedFields) - : {}; + if (event.type === 'UPDATE_FIELDS') { + const [contextResult] = executeFieldsPipeline(context, event); + return contextResult; + } else { + return {}; + } }), resetValues: actions.assign((context, event) => { return { @@ -315,7 +297,15 @@ export const createCreateCustomIntegrationStateMachine = ({ }), }), save: (context) => { - return integrationsClient.createCustomIntegration(context.fields); + return integrationsClient.createCustomIntegration({ + ...context.fields, + datasets: context.fields.datasets.map((dataset) => ({ + ...dataset, + name: datasetNameWillBePrefixed(dataset.name, context.fields.integrationName) + ? prefixDatasetName(dataset.name, context.fields.integrationName) + : dataset.name, + })), + }); }, cleanup: (context) => { return integrationsClient.deleteCustomIntegration({ diff --git a/packages/kbn-custom-integrations/src/state_machines/create/types.ts b/packages/kbn-custom-integrations/src/state_machines/create/types.ts index e7cfad728d78e..ec0e61f2309d9 100644 --- a/packages/kbn-custom-integrations/src/state_machines/create/types.ts +++ b/packages/kbn-custom-integrations/src/state_machines/create/types.ts @@ -19,7 +19,9 @@ export interface WithTouchedFields { touchedFields: Record; } -export type CreateInitialState = WithOptions & WithFields & WithPreviouslyCreatedIntegration; +export type CreateInitialState = Partial & + Partial & + WithPreviouslyCreatedIntegration; export interface WithOptions { options: { @@ -91,11 +93,13 @@ export type CreateCustomIntegrationTypestate = export type CreateCustomIntegrationContext = CreateCustomIntegrationTypestate['context']; +export interface UpdateFieldsEvent { + type: 'UPDATE_FIELDS'; + fields: Partial; +} + export type CreateCustomIntegrationEvent = - | { - type: 'UPDATE_FIELDS'; - fields: Partial; - } + | UpdateFieldsEvent | { type: 'INITIALIZE'; } diff --git a/packages/kbn-custom-integrations/src/state_machines/custom_integrations/state_machine.ts b/packages/kbn-custom-integrations/src/state_machines/custom_integrations/state_machine.ts index 4bd71313bf43a..3baa44d76a7e6 100644 --- a/packages/kbn-custom-integrations/src/state_machines/custom_integrations/state_machine.ts +++ b/packages/kbn-custom-integrations/src/state_machines/custom_integrations/state_machine.ts @@ -20,6 +20,8 @@ import { import { createCreateCustomIntegrationStateMachine } from '../create/state_machine'; import { IIntegrationsClient } from '../services/integrations_client'; import { CustomIntegrationsNotificationChannel } from './notifications'; +import { executeFieldsPipeline, normalizeDatasetNames } from '../create/pipelines/fields'; +import { CreateInitialState } from '../create/types'; export const createPureCustomIntegrationsStateMachine = ( initialContext: DefaultCustomIntegrationsContext = DEFAULT_CONTEXT @@ -95,22 +97,32 @@ export const createCustomIntegrationsStateMachine = ({ return createPureCustomIntegrationsStateMachine(initialContext).withConfig({ services: { createCustomIntegration: (context) => { + const getInitialContextForCreate = (initialCreateState: CreateInitialState) => { + const baseAndOptions = { + ...DEFAULT_CREATE_CONTEXT, + ...(initialCreateState ? initialCreateState : {}), + options: { + ...DEFAULT_CREATE_CONTEXT.options, + ...(initialCreateState?.options ? initialCreateState.options : {}), + }, + }; + const fields = initialCreateState.fields + ? executeFieldsPipeline(baseAndOptions, { + type: 'UPDATE_FIELDS', + fields: normalizeDatasetNames(initialCreateState.fields), + })[0] + : {}; + return { + ...baseAndOptions, + ...fields, + }; + }; + return createCreateCustomIntegrationStateMachine({ integrationsClient, initialContext: - initialState.mode === 'create' - ? { - ...DEFAULT_CREATE_CONTEXT, - ...(initialState?.context ? initialState?.context : {}), - options: { - ...DEFAULT_CREATE_CONTEXT.options, - ...(initialState?.context?.options ? initialState.context.options : {}), - }, - fields: { - ...DEFAULT_CREATE_CONTEXT.fields, - ...(initialState?.context?.fields ? initialState.context.fields : {}), - }, - } + initialState.mode === 'create' && initialState.context + ? getInitialContextForCreate(initialState.context) : DEFAULT_CREATE_CONTEXT, }); }, diff --git a/packages/kbn-custom-integrations/src/state_machines/services/integrations_client.ts b/packages/kbn-custom-integrations/src/state_machines/services/integrations_client.ts index c5606a7e2e154..047bd4a7644d0 100644 --- a/packages/kbn-custom-integrations/src/state_machines/services/integrations_client.ts +++ b/packages/kbn-custom-integrations/src/state_machines/services/integrations_client.ts @@ -19,6 +19,8 @@ import { IntegrationNotInstalledError, NamingCollisionError, UnknownError, + IntegrationName, + Dataset, } from '../../types'; const GENERIC_CREATE_ERROR_MESSAGE = i18n.translate( @@ -70,6 +72,7 @@ export class IntegrationsClient implements IIntegrationsClient { return { integrationName: params.integrationName, + datasets: params.datasets, installedAssets: data.items, }; } catch (error) { @@ -128,7 +131,8 @@ export const createCustomIntegrationResponseRT = rt.exact( ); export interface CreateCustomIntegrationValue { - integrationName: string; + integrationName: IntegrationName; + datasets: Dataset[]; installedAssets: AssetList; } diff --git a/packages/kbn-custom-integrations/src/types.ts b/packages/kbn-custom-integrations/src/types.ts index 062601239137a..84ecda1eb4df3 100644 --- a/packages/kbn-custom-integrations/src/types.ts +++ b/packages/kbn-custom-integrations/src/types.ts @@ -9,25 +9,26 @@ import * as rt from 'io-ts'; export const integrationNameRT = rt.string; +export type IntegrationName = rt.TypeOf; -const datasetTypes = rt.keyof({ +const datasetTypesRT = rt.keyof({ logs: null, metrics: null, }); -const dataset = rt.exact( +export const datasetRT = rt.exact( rt.type({ name: rt.string, - type: datasetTypes, + type: datasetTypesRT, }) ); -export type Dataset = rt.TypeOf; +export type Dataset = rt.TypeOf; export const customIntegrationOptionsRT = rt.exact( rt.type({ integrationName: integrationNameRT, - datasets: rt.array(dataset), + datasets: rt.array(datasetRT), }) ); diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index c507d9e6732a7..ffc0742706063 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -85,6 +85,7 @@ import type { } from '../../types'; import { getDataStreams } from '../../services/epm/data_streams'; import { NamingCollisionError } from '../../services/epm/packages/custom_integrations/validation/check_naming_collision'; +import { DatasetNamePrefixError } from '../../services/epm/packages/custom_integrations/validation/check_dataset_name_format'; const CACHE_CONTROL_10_MINUTES_HEADER: HttpResponseOptions['headers'] = { 'cache-control': 'max-age=600', @@ -452,6 +453,13 @@ export const createCustomIntegrationHandler: FleetRequestHandler< message: error.message, }, }); + } else if (error instanceof DatasetNamePrefixError) { + return response.customError({ + statusCode: 422, + body: { + message: error.message, + }, + }); } return await defaultFleetErrorHandler({ error, response }); } diff --git a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/utils.ts b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/utils.ts index b984b8cddbbd3..c3512443c8828 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/utils.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/utils.ts @@ -15,7 +15,7 @@ export const generateDatastreamEntries = ( const { name, type } = dataset; return { type, - dataset: `${packageName}.${name}`, + dataset: `${name}`, title: `Data stream for the ${packageName} custom integration, and ${name} dataset.`, package: packageName, path: name, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/validation/check_dataset_name_format.ts b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/validation/check_dataset_name_format.ts new file mode 100644 index 0000000000000..daa90f892034e --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/validation/check_dataset_name_format.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CustomPackageDatasetConfiguration } from '../../install'; + +// Dataset name must either match integration name exactly OR be prefixed with integration name and a dot. +export const checkDatasetsNameFormat = ( + datasets: CustomPackageDatasetConfiguration[], + integrationName: string +) => { + const invalidNames = datasets + .filter((dataset) => { + const { name } = dataset; + return name !== integrationName && !name.startsWith(`${integrationName}.`); + }) + .map((dataset) => dataset.name); + + if (invalidNames.length > 0) { + throw new DatasetNamePrefixError( + `Dataset names '${invalidNames.join( + ', ' + )}' must either match integration name '${integrationName}' exactly or be prefixed with integration name and a dot (e.g. '${integrationName}.').` + ); + } +}; + +export class DatasetNamePrefixError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + this.name = 'DatasetNamePrefixError'; + } +} diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index 4bb77d03996c7..da31b26d275e9 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -103,6 +103,7 @@ import { createAssets } from './custom_integrations'; import { cacheAssets } from './custom_integrations/assets/cache'; import { generateDatastreamEntries } from './custom_integrations/assets/dataset/utils'; import { checkForNamingCollision } from './custom_integrations/validation/check_naming_collision'; +import { checkDatasetsNameFormat } from './custom_integrations/validation/check_dataset_name_format'; export async function isPackageInstalled(options: { savedObjectsClient: SavedObjectsClientContract; @@ -784,6 +785,7 @@ export async function installCustomPackage( // Validate that we can create this package, validations will throw if they don't pass await checkForNamingCollision(savedObjectsClient, pkgName); + checkDatasetsNameFormat(datasets, pkgName); // Compose a packageInfo const packageInfo = { diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts b/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts index f4a30e896c8db..e989089f491eb 100644 --- a/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts +++ b/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts @@ -107,9 +107,9 @@ Cypress.Commands.add('installCustomIntegration', (integrationName: string) => { force: true, integrationName, datasets: [ - { name: 'access', type: 'logs' }, - { name: 'error', type: 'metrics' }, - { name: 'warning', type: 'logs' }, + { name: `${integrationName}.access`, type: 'logs' }, + { name: `${integrationName}.error`, type: 'metrics' }, + { name: `${integrationName}.warning`, type: 'logs' }, ], }, headers: { diff --git a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/configure_logs.tsx b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/configure_logs.tsx index 1e438e1dd0e66..6cf64b5a32fd0 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/configure_logs.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/configure_logs.tsx @@ -92,10 +92,14 @@ export function ConfigureLogs() { resetOnCreation: false, errorOnFailedCleanup: false, }, - fields: { - integrationName, - datasets: [{ name: datasetName, type: 'logs' as const }], - }, + ...(integrationName !== undefined && datasetName !== undefined + ? { + fields: { + integrationName, + datasets: [{ name: datasetName, type: 'logs' as const }], + }, + } + : {}), previouslyCreatedIntegration: lastCreatedIntegrationOptions, }, }} diff --git a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/index.tsx b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/index.tsx index 81d7faf448a48..afe575c08018c 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/index.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/index.tsx @@ -17,9 +17,9 @@ import { InstallElasticAgent } from './install_elastic_agent'; import { SelectLogs } from './select_logs'; interface WizardState { - integrationName: string; + integrationName?: string; lastCreatedIntegrationOptions?: CustomIntegrationOptions; - datasetName: string; + datasetName?: string; serviceName: string; logFilePaths: string[]; namespace: string; @@ -40,8 +40,8 @@ interface WizardState { } const initialState: WizardState = { - integrationName: '', - datasetName: '', + integrationName: undefined, + datasetName: undefined, serviceName: '', logFilePaths: [''], namespace: 'default', diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_custom.ts b/x-pack/test/fleet_api_integration/apis/epm/install_custom.ts index 88d71edb74d47..63dfd7690e887 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_custom.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_custom.ts @@ -39,9 +39,9 @@ export default function (providerContext: FtrProviderContext) { force: true, integrationName: INTEGRATION_NAME, datasets: [ - { name: 'access', type: 'logs' }, - { name: 'error', type: 'metrics' }, - { name: 'warning', type: 'logs' }, + { name: `${INTEGRATION_NAME}.access`, type: 'logs' }, + { name: `${INTEGRATION_NAME}.error`, type: 'metrics' }, + { name: `${INTEGRATION_NAME}.warning`, type: 'logs' }, ], }) .expect(200); @@ -108,9 +108,9 @@ export default function (providerContext: FtrProviderContext) { force: true, integrationName: INTEGRATION_NAME, datasets: [ - { name: 'access', type: 'logs' }, - { name: 'error', type: 'metrics' }, - { name: 'warning', type: 'logs' }, + { name: `${INTEGRATION_NAME}.access`, type: 'logs' }, + { name: `${INTEGRATION_NAME}.error`, type: 'metrics' }, + { name: `${INTEGRATION_NAME}.warning`, type: 'logs' }, ], }) .expect(200); @@ -123,9 +123,9 @@ export default function (providerContext: FtrProviderContext) { force: true, integrationName: INTEGRATION_NAME, datasets: [ - { name: 'access', type: 'logs' }, - { name: 'error', type: 'metrics' }, - { name: 'warning', type: 'logs' }, + { name: `${INTEGRATION_NAME}.access`, type: 'logs' }, + { name: `${INTEGRATION_NAME}.error`, type: 'metrics' }, + { name: `${INTEGRATION_NAME}.warning`, type: 'logs' }, ], }) .expect(409); @@ -145,7 +145,7 @@ export default function (providerContext: FtrProviderContext) { .send({ force: true, integrationName: pkgName, - datasets: [{ name: 'error', type: 'logs' }], + datasets: [{ name: `${INTEGRATION_NAME}.error`, type: 'logs' }], }) .expect(409); @@ -153,5 +153,48 @@ export default function (providerContext: FtrProviderContext) { `Failed to create the integration as an integration with the name ${pkgName} already exists in the package registry or as a bundled package.` ); }); + + it('Throws an error when dataset names are not prefixed correctly', async () => { + const response = await supertest + .post(`/api/fleet/epm/custom_integrations`) + .set('kbn-xsrf', 'xxxx') + .type('application/json') + .send({ + force: true, + integrationName: INTEGRATION_NAME, + datasets: [{ name: 'error', type: 'logs' }], + }) + .expect(422); + + expect(response.body.message).to.be( + `Dataset names 'error' must either match integration name '${INTEGRATION_NAME}' exactly or be prefixed with integration name and a dot (e.g. '${INTEGRATION_NAME}.').` + ); + + await uninstallPackage(); + + await supertest + .post(`/api/fleet/epm/custom_integrations`) + .set('kbn-xsrf', 'xxxx') + .type('application/json') + .send({ + force: true, + integrationName: INTEGRATION_NAME, + datasets: [{ name: INTEGRATION_NAME, type: 'logs' }], + }) + .expect(200); + + await uninstallPackage(); + + await supertest + .post(`/api/fleet/epm/custom_integrations`) + .set('kbn-xsrf', 'xxxx') + .type('application/json') + .send({ + force: true, + integrationName: INTEGRATION_NAME, + datasets: [{ name: `${INTEGRATION_NAME}.error`, type: 'logs' }], + }) + .expect(200); + }); }); } From 105b6bbcbbedcab691a01e1feeb59656f003deaa Mon Sep 17 00:00:00 2001 From: Kerry Gallagher <471693+Kerry350@users.noreply.github.com> Date: Wed, 20 Sep 2023 13:30:45 +0100 Subject: [PATCH 2/5] Remove formatting from previous approach --- .../kbn-custom-integrations/src/components/create/form.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/kbn-custom-integrations/src/components/create/form.tsx b/packages/kbn-custom-integrations/src/components/create/form.tsx index 22fc0b26633e2..a05fa97b78fdf 100644 --- a/packages/kbn-custom-integrations/src/components/create/form.tsx +++ b/packages/kbn-custom-integrations/src/components/create/form.tsx @@ -31,7 +31,6 @@ import { hasFailedSelector } from '../../state_machines/create/selectors'; import { datasetNameWillBePrefixed, getDatasetNamePrefix, - getDatasetNameWithoutPrefix, prefixDatasetName, } from '../../state_machines/create/pipelines/fields'; @@ -221,7 +220,7 @@ export const CreateCustomIntegrationForm = ({ placeholder={i18n.translate('customIntegrationsPackage.create.dataset.placeholder', { defaultMessage: "Give your integration's dataset a name", })} - value={getDatasetNameWithoutPrefix(datasetName, integrationName)} + value={datasetName} onChange={(event) => updateDatasetName(event.target.value)} isInvalid={hasErrors(errors?.fields?.datasets?.[0].name) && touchedFields.datasets} max={100} From f70e1c99d8f19d6a8b110953bdbce6cec4761a24 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher <471693+Kerry350@users.noreply.github.com> Date: Thu, 21 Sep 2023 14:55:24 +0100 Subject: [PATCH 3/5] Amend Cypress test --- .../create/state_machine.test.ts | 424 ++++++++++++++++++ 1 file changed, 424 insertions(+) create mode 100644 packages/kbn-custom-integrations/src/state_machines/create/state_machine.test.ts diff --git a/packages/kbn-custom-integrations/src/state_machines/create/state_machine.test.ts b/packages/kbn-custom-integrations/src/state_machines/create/state_machine.test.ts new file mode 100644 index 0000000000000..75bf08ae3fe09 --- /dev/null +++ b/packages/kbn-custom-integrations/src/state_machines/create/state_machine.test.ts @@ -0,0 +1,424 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IntegrationError } from '../../types'; +import { DEFAULT_CONTEXT } from './defaults'; +import { createPureCreateCustomIntegrationStateMachine } from './state_machine'; + +// Generated by CodiumAI + +describe('createPureCreateCustomIntegrationStateMachine', () => { + // State machine initializes with default context + it('should initialize state machine with default context', () => { + const machine = createPureCreateCustomIntegrationStateMachine(); + expect(machine.initialState.context).toEqual(DEFAULT_CONTEXT); + }); + + // State machine transitions from 'uninitialized' to 'untouched' if context is default + it('should transition from "uninitialized" to "untouched" if context is default', () => { + const machine = createPureCreateCustomIntegrationStateMachine(); + const nextState = machine.transition('uninitialized', { type: 'INITIALIZE' }); + expect(nextState.value).toBe('untouched'); + }); + + // State machine transitions from 'uninitialized' to 'validating' if context is not default + it('should transition from "uninitialized" to "validating" if context is not default', () => { + const initialContext = { + options: { + deletePrevious: true, + resetOnCreation: false, + errorOnFailedCleanup: true, + }, + fields: { + integrationName: 'test', + datasets: [ + { + type: 'logs', + name: 'test', + }, + ], + }, + touchedFields: { + integrationName: true, + datasets: true, + }, + errors: null, + }; + const machine = createPureCreateCustomIntegrationStateMachine(initialContext); + const nextState = machine.transition('uninitialized', { type: 'INITIALIZE' }); + expect(nextState.value).toBe('validating'); + }); + + // State machine transitions from 'uninitialized' to 'validating' if context is not default and shouldValidateInitialContext guard returns true + it('should transition from "uninitialized" to "validating" if context is not default and shouldValidateInitialContext guard returns true', () => { + const initialContext = { + options: { + deletePrevious: true, + resetOnCreation: false, + errorOnFailedCleanup: true, + }, + fields: { + integrationName: 'test', + datasets: [ + { + type: 'logs', + name: 'test', + }, + ], + }, + touchedFields: { + integrationName: true, + datasets: true, + }, + errors: null, + }; + const machine = createPureCreateCustomIntegrationStateMachine(initialContext); + const nextState = machine.transition('uninitialized', { type: 'INITIALIZE' }); + expect(nextState.value).toBe('validating'); + }); + + // State machine transitions from 'valid' to 'deletingPrevious' if SAVE event is triggered and deletePrevious option is true and previously created integration does not exist + it('should transition from "valid" to "deletingPrevious" if SAVE event is triggered and deletePrevious option is true and previously created integration does not exist', () => { + const initialContext = { + options: { + deletePrevious: true, + resetOnCreation: false, + errorOnFailedCleanup: true, + }, + fields: { + integrationName: 'test', + datasets: [ + { + type: 'logs', + name: 'test', + }, + ], + }, + touchedFields: { + integrationName: true, + datasets: true, + }, + errors: null, + }; + const machine = createPureCreateCustomIntegrationStateMachine(initialContext); + const nextState = machine.transition('valid', { type: 'SAVE' }); + expect(nextState.value).toBe('deletingPrevious'); + }); + + // State machine transitions from 'validating' to 'valid' if validation succeeds + it('should transition to "valid" when validation succeeds', () => { + const machine = createPureCreateCustomIntegrationStateMachine(); + const nextState = machine.transition('validating', { type: 'VALIDATION_SUCCESS' }); + expect(nextState.value).toBe('valid'); + }); + + // State machine transitions from 'valid' to 'success' if SAVE event is triggered and fields match previously created integration + it('should transition to "success" when SAVE event is triggered and fields match previously created integration', () => { + const machine = createPureCreateCustomIntegrationStateMachine(); + const context = { + ...DEFAULT_CONTEXT, + previouslyCreatedIntegration: { + integrationName: 'integration1', + datasets: [ + { + type: 'logs', + name: 'dataset1', + }, + ], + }, + }; + const nextState = machine.transition('valid', { type: 'SAVE' }, context); + expect(nextState.value).toBe('success'); + }); + + // State machine transitions from 'valid' to 'deletingPrevious' if SAVE event is triggered and deletePrevious option is true and previously created integration exists + it("should transition from 'valid' to 'deletingPrevious' when SAVE event is triggered and deletePrevious option is true and previously created integration exists", () => { + const initialContext = { + options: { + deletePrevious: true, + resetOnCreation: true, + errorOnFailedCleanup: false, + }, + fields: { + integrationName: 'Integration 1', + datasets: [ + { + type: 'logs', + name: 'Dataset 1', + }, + ], + }, + touchedFields: { + integrationName: true, + datasets: true, + }, + errors: null, + previouslyCreatedIntegration: { + integrationName: 'Integration 1', + datasets: [ + { + type: 'logs', + name: 'Dataset 1', + }, + ], + }, + }; + + const machine = createPureCreateCustomIntegrationStateMachine(initialContext); + const nextState = machine.transition('valid', { type: 'SAVE' }); + + expect(nextState.value).toBe('deletingPrevious'); + }); + + // State machine transitions from 'valid' to 'submitting' if SAVE event is triggered and fields do not match previously created integration and deletePrevious option is false + it("should transition from 'valid' to 'submitting' when SAVE event is triggered and fields do not match previously created integration and deletePrevious option is false", () => { + const initialContext = { + options: { + deletePrevious: false, + resetOnCreation: true, + errorOnFailedCleanup: false, + }, + fields: { + integrationName: 'Integration 1', + datasets: [ + { + type: 'logs', + name: 'Dataset 1', + }, + ], + }, + touchedFields: { + integrationName: true, + datasets: true, + }, + errors: null, + previouslyCreatedIntegration: { + integrationName: 'Integration 2', + datasets: [ + { + type: 'logs', + name: 'Dataset 2', + }, + ], + }, + }; + + const machine = createPureCreateCustomIntegrationStateMachine(initialContext); + const nextState = machine.transition('valid', { type: 'SAVE' }); + + expect(nextState.value).toBe('submitting'); + }); + + // State machine transitions from 'deletingPrevious' to 'submitting' if cleanup succeeds + it("should transition from 'deletingPrevious' to 'submitting' when cleanup succeeds", () => { + const initialContext = { + options: { + deletePrevious: true, + resetOnCreation: true, + errorOnFailedCleanup: false, + }, + fields: { + integrationName: 'Integration 1', + datasets: [ + { + type: 'logs', + name: 'Dataset 1', + }, + ], + }, + touchedFields: { + integrationName: true, + datasets: true, + }, + errors: null, + previouslyCreatedIntegration: { + integrationName: 'Integration 1', + datasets: [ + { + type: 'logs', + name: 'Dataset 1', + }, + ], + }, + }; + + const machine = createPureCreateCustomIntegrationStateMachine(initialContext); + const nextState = machine.transition('deletingPrevious', { + type: 'done.invoke.submitting:invocation[0]', + data: {}, + }); + + expect(nextState.value).toBe('submitting'); + }); + + // State machine transitions from 'submitting' to 'success' if save succeeds + it("should transition from 'submitting' to 'success' when save succeeds", () => { + const machine = createPureCreateCustomIntegrationStateMachine(); + const nextState = machine.transition('submitting', { + type: 'done.invoke.submitting:invocation[0]', + data: {}, + }); + expect(nextState.value).toBe('success'); + }); + + // State machine transitions from 'failure' to 'deletingPrevious' if RETRY event is triggered and deletePrevious option is true and previously created integration exists + it("should transition from 'failure' to 'deletingPrevious' when RETRY event is triggered and deletePrevious option is true and previously created integration exists", () => { + const machine = createPureCreateCustomIntegrationStateMachine({ + initialContext: { + options: { + deletePrevious: true, + }, + previouslyCreatedIntegration: {}, + }, + }); + const nextState = machine.transition('failure', { type: 'RETRY' }); + expect(nextState.value).toBe('deletingPrevious'); + }); + + // State machine transitions from 'failure' to 'submitting' if RETRY event is triggered and deletePrevious option is false + it("should transition from 'failure' to 'submitting' when RETRY event is triggered and deletePrevious option is false", () => { + const machine = createPureCreateCustomIntegrationStateMachine({ + initialContext: { + options: { + deletePrevious: false, + }, + }, + }); + const nextState = machine.transition('failure', { type: 'RETRY' }); + expect(nextState.value).toBe('submitting'); + }); + + // State machine transitions from 'deletingPrevious' to 'failure' if cleanup fails and shouldErrorOnFailedCleanup guard returns true + it("should transition from 'deletingPrevious' to 'failure' when cleanup fails and shouldErrorOnFailedCleanup is true", () => { + const initialContext = { + ...DEFAULT_CONTEXT, + options: { + ...DEFAULT_CONTEXT.options, + deletePrevious: true, + errorOnFailedCleanup: true, + }, + previouslyCreatedIntegration: { + fields: { + integrationName: 'test', + datasets: [ + { + type: 'logs', + name: 'test', + }, + ], + }, + }, + }; + const machine = createPureCreateCustomIntegrationStateMachine(initialContext); + const nextState = machine.transition('deletingPrevious', { + type: 'error.platform.CreateCustomIntegration.deletingPrevious:invocation[0]', + data: new IntegrationError('Cleanup failed'), + }); + expect(nextState.value).toBe('failure'); + }); + + // State machine transitions from 'failure' to 'success' if RETRY event is triggered and save succeeds and fields match previously created integration + it("should transition from 'failure' to 'success' when RETRY event is triggered and save succeeds and fields match previously created integration", () => { + const initialContext = { + ...DEFAULT_CONTEXT, + options: { + ...DEFAULT_CONTEXT.options, + deletePrevious: true, + errorOnFailedCleanup: false, + }, + previouslyCreatedIntegration: { + fields: { + integrationName: 'test', + datasets: [ + { + type: 'logs', + name: 'test', + }, + ], + }, + }, + }; + const machine = createPureCreateCustomIntegrationStateMachine(initialContext); + const nextState = machine.transition('failure', { + type: 'RETRY', + data: { + integrationName: 'test', + datasets: [ + { + type: 'logs', + name: 'test', + }, + ], + }, + }); + expect(nextState.value).toBe('success'); + }); + + // State machine transitions from 'valid' to 'validationFailed' if validation fails + it("should transition from 'valid' to 'validationFailed' when validation fails", () => { + const initialContext = { + ...DEFAULT_CONTEXT, + options: { + ...DEFAULT_CONTEXT.options, + deletePrevious: false, + errorOnFailedCleanup: false, + }, + fields: { + integrationName: 'test', + datasets: [ + { + type: 'logs', + name: '', + }, + ], + }, + }; + const machine = createPureCreateCustomIntegrationStateMachine(initialContext); + const nextState = machine.transition('valid', { + type: 'error.platform.validating:invocation[0]', + data: { + errors: { + fields: { + integrationName: [new IntegrationError('Invalid integration name')], + datasets: { + 0: [new IntegrationError('Invalid dataset name')], + }, + }, + general: null, + }, + }, + }); + expect(nextState.value).toBe('validationFailed'); + }); + + // State machine transitions from 'validationFailed' to 'validating' if UPDATE_FIELDS event is triggered + it("should transition from 'validationFailed' to 'validating' when UPDATE_FIELDS event is triggered", () => { + const machine = createPureCreateCustomIntegrationStateMachine(); + const initialState = machine.initialState; + const nextState = machine.transition(initialState, { type: 'UPDATE_FIELDS' }); + expect(nextState.value).toBe('validating'); + }); + + // State machine transitions from 'validationFailed' to 'valid' if UPDATE_FIELDS event is triggered and validation succeeds + it("should transition from 'validationFailed' to 'valid' when UPDATE_FIELDS event is triggered and validation succeeds", () => { + const machine = createPureCreateCustomIntegrationStateMachine(); + const initialState = machine.initialState; + const nextState = machine.transition(initialState, { type: 'UPDATE_FIELDS' }); + const validState = machine.transition(nextState, { type: 'VALIDATE_SUCCESS' }); + expect(validState.value).toBe('valid'); + }); + + // State machine transitions from 'submitting' to 'failure' if save fails + it("should transition from 'submitting' to 'failure' when save fails", () => { + const machine = createPureCreateCustomIntegrationStateMachine(); + const initialState = machine.initialState; + const nextState = machine.transition(initialState, { type: 'SAVE' }); + const failureState = machine.transition(nextState, { type: 'SAVE_FAILURE' }); + expect(failureState.value).toBe('failure'); + }); +}); From 565d70b409430e9b26cc295da1db27c5a39044ec Mon Sep 17 00:00:00 2001 From: Kerry Gallagher <471693+Kerry350@users.noreply.github.com> Date: Thu, 21 Sep 2023 14:56:57 +0100 Subject: [PATCH 4/5] Revert "Amend Cypress test" This reverts commit f70e1c99d8f19d6a8b110953bdbce6cec4761a24. --- .../create/state_machine.test.ts | 424 ------------------ 1 file changed, 424 deletions(-) delete mode 100644 packages/kbn-custom-integrations/src/state_machines/create/state_machine.test.ts diff --git a/packages/kbn-custom-integrations/src/state_machines/create/state_machine.test.ts b/packages/kbn-custom-integrations/src/state_machines/create/state_machine.test.ts deleted file mode 100644 index 75bf08ae3fe09..0000000000000 --- a/packages/kbn-custom-integrations/src/state_machines/create/state_machine.test.ts +++ /dev/null @@ -1,424 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { IntegrationError } from '../../types'; -import { DEFAULT_CONTEXT } from './defaults'; -import { createPureCreateCustomIntegrationStateMachine } from './state_machine'; - -// Generated by CodiumAI - -describe('createPureCreateCustomIntegrationStateMachine', () => { - // State machine initializes with default context - it('should initialize state machine with default context', () => { - const machine = createPureCreateCustomIntegrationStateMachine(); - expect(machine.initialState.context).toEqual(DEFAULT_CONTEXT); - }); - - // State machine transitions from 'uninitialized' to 'untouched' if context is default - it('should transition from "uninitialized" to "untouched" if context is default', () => { - const machine = createPureCreateCustomIntegrationStateMachine(); - const nextState = machine.transition('uninitialized', { type: 'INITIALIZE' }); - expect(nextState.value).toBe('untouched'); - }); - - // State machine transitions from 'uninitialized' to 'validating' if context is not default - it('should transition from "uninitialized" to "validating" if context is not default', () => { - const initialContext = { - options: { - deletePrevious: true, - resetOnCreation: false, - errorOnFailedCleanup: true, - }, - fields: { - integrationName: 'test', - datasets: [ - { - type: 'logs', - name: 'test', - }, - ], - }, - touchedFields: { - integrationName: true, - datasets: true, - }, - errors: null, - }; - const machine = createPureCreateCustomIntegrationStateMachine(initialContext); - const nextState = machine.transition('uninitialized', { type: 'INITIALIZE' }); - expect(nextState.value).toBe('validating'); - }); - - // State machine transitions from 'uninitialized' to 'validating' if context is not default and shouldValidateInitialContext guard returns true - it('should transition from "uninitialized" to "validating" if context is not default and shouldValidateInitialContext guard returns true', () => { - const initialContext = { - options: { - deletePrevious: true, - resetOnCreation: false, - errorOnFailedCleanup: true, - }, - fields: { - integrationName: 'test', - datasets: [ - { - type: 'logs', - name: 'test', - }, - ], - }, - touchedFields: { - integrationName: true, - datasets: true, - }, - errors: null, - }; - const machine = createPureCreateCustomIntegrationStateMachine(initialContext); - const nextState = machine.transition('uninitialized', { type: 'INITIALIZE' }); - expect(nextState.value).toBe('validating'); - }); - - // State machine transitions from 'valid' to 'deletingPrevious' if SAVE event is triggered and deletePrevious option is true and previously created integration does not exist - it('should transition from "valid" to "deletingPrevious" if SAVE event is triggered and deletePrevious option is true and previously created integration does not exist', () => { - const initialContext = { - options: { - deletePrevious: true, - resetOnCreation: false, - errorOnFailedCleanup: true, - }, - fields: { - integrationName: 'test', - datasets: [ - { - type: 'logs', - name: 'test', - }, - ], - }, - touchedFields: { - integrationName: true, - datasets: true, - }, - errors: null, - }; - const machine = createPureCreateCustomIntegrationStateMachine(initialContext); - const nextState = machine.transition('valid', { type: 'SAVE' }); - expect(nextState.value).toBe('deletingPrevious'); - }); - - // State machine transitions from 'validating' to 'valid' if validation succeeds - it('should transition to "valid" when validation succeeds', () => { - const machine = createPureCreateCustomIntegrationStateMachine(); - const nextState = machine.transition('validating', { type: 'VALIDATION_SUCCESS' }); - expect(nextState.value).toBe('valid'); - }); - - // State machine transitions from 'valid' to 'success' if SAVE event is triggered and fields match previously created integration - it('should transition to "success" when SAVE event is triggered and fields match previously created integration', () => { - const machine = createPureCreateCustomIntegrationStateMachine(); - const context = { - ...DEFAULT_CONTEXT, - previouslyCreatedIntegration: { - integrationName: 'integration1', - datasets: [ - { - type: 'logs', - name: 'dataset1', - }, - ], - }, - }; - const nextState = machine.transition('valid', { type: 'SAVE' }, context); - expect(nextState.value).toBe('success'); - }); - - // State machine transitions from 'valid' to 'deletingPrevious' if SAVE event is triggered and deletePrevious option is true and previously created integration exists - it("should transition from 'valid' to 'deletingPrevious' when SAVE event is triggered and deletePrevious option is true and previously created integration exists", () => { - const initialContext = { - options: { - deletePrevious: true, - resetOnCreation: true, - errorOnFailedCleanup: false, - }, - fields: { - integrationName: 'Integration 1', - datasets: [ - { - type: 'logs', - name: 'Dataset 1', - }, - ], - }, - touchedFields: { - integrationName: true, - datasets: true, - }, - errors: null, - previouslyCreatedIntegration: { - integrationName: 'Integration 1', - datasets: [ - { - type: 'logs', - name: 'Dataset 1', - }, - ], - }, - }; - - const machine = createPureCreateCustomIntegrationStateMachine(initialContext); - const nextState = machine.transition('valid', { type: 'SAVE' }); - - expect(nextState.value).toBe('deletingPrevious'); - }); - - // State machine transitions from 'valid' to 'submitting' if SAVE event is triggered and fields do not match previously created integration and deletePrevious option is false - it("should transition from 'valid' to 'submitting' when SAVE event is triggered and fields do not match previously created integration and deletePrevious option is false", () => { - const initialContext = { - options: { - deletePrevious: false, - resetOnCreation: true, - errorOnFailedCleanup: false, - }, - fields: { - integrationName: 'Integration 1', - datasets: [ - { - type: 'logs', - name: 'Dataset 1', - }, - ], - }, - touchedFields: { - integrationName: true, - datasets: true, - }, - errors: null, - previouslyCreatedIntegration: { - integrationName: 'Integration 2', - datasets: [ - { - type: 'logs', - name: 'Dataset 2', - }, - ], - }, - }; - - const machine = createPureCreateCustomIntegrationStateMachine(initialContext); - const nextState = machine.transition('valid', { type: 'SAVE' }); - - expect(nextState.value).toBe('submitting'); - }); - - // State machine transitions from 'deletingPrevious' to 'submitting' if cleanup succeeds - it("should transition from 'deletingPrevious' to 'submitting' when cleanup succeeds", () => { - const initialContext = { - options: { - deletePrevious: true, - resetOnCreation: true, - errorOnFailedCleanup: false, - }, - fields: { - integrationName: 'Integration 1', - datasets: [ - { - type: 'logs', - name: 'Dataset 1', - }, - ], - }, - touchedFields: { - integrationName: true, - datasets: true, - }, - errors: null, - previouslyCreatedIntegration: { - integrationName: 'Integration 1', - datasets: [ - { - type: 'logs', - name: 'Dataset 1', - }, - ], - }, - }; - - const machine = createPureCreateCustomIntegrationStateMachine(initialContext); - const nextState = machine.transition('deletingPrevious', { - type: 'done.invoke.submitting:invocation[0]', - data: {}, - }); - - expect(nextState.value).toBe('submitting'); - }); - - // State machine transitions from 'submitting' to 'success' if save succeeds - it("should transition from 'submitting' to 'success' when save succeeds", () => { - const machine = createPureCreateCustomIntegrationStateMachine(); - const nextState = machine.transition('submitting', { - type: 'done.invoke.submitting:invocation[0]', - data: {}, - }); - expect(nextState.value).toBe('success'); - }); - - // State machine transitions from 'failure' to 'deletingPrevious' if RETRY event is triggered and deletePrevious option is true and previously created integration exists - it("should transition from 'failure' to 'deletingPrevious' when RETRY event is triggered and deletePrevious option is true and previously created integration exists", () => { - const machine = createPureCreateCustomIntegrationStateMachine({ - initialContext: { - options: { - deletePrevious: true, - }, - previouslyCreatedIntegration: {}, - }, - }); - const nextState = machine.transition('failure', { type: 'RETRY' }); - expect(nextState.value).toBe('deletingPrevious'); - }); - - // State machine transitions from 'failure' to 'submitting' if RETRY event is triggered and deletePrevious option is false - it("should transition from 'failure' to 'submitting' when RETRY event is triggered and deletePrevious option is false", () => { - const machine = createPureCreateCustomIntegrationStateMachine({ - initialContext: { - options: { - deletePrevious: false, - }, - }, - }); - const nextState = machine.transition('failure', { type: 'RETRY' }); - expect(nextState.value).toBe('submitting'); - }); - - // State machine transitions from 'deletingPrevious' to 'failure' if cleanup fails and shouldErrorOnFailedCleanup guard returns true - it("should transition from 'deletingPrevious' to 'failure' when cleanup fails and shouldErrorOnFailedCleanup is true", () => { - const initialContext = { - ...DEFAULT_CONTEXT, - options: { - ...DEFAULT_CONTEXT.options, - deletePrevious: true, - errorOnFailedCleanup: true, - }, - previouslyCreatedIntegration: { - fields: { - integrationName: 'test', - datasets: [ - { - type: 'logs', - name: 'test', - }, - ], - }, - }, - }; - const machine = createPureCreateCustomIntegrationStateMachine(initialContext); - const nextState = machine.transition('deletingPrevious', { - type: 'error.platform.CreateCustomIntegration.deletingPrevious:invocation[0]', - data: new IntegrationError('Cleanup failed'), - }); - expect(nextState.value).toBe('failure'); - }); - - // State machine transitions from 'failure' to 'success' if RETRY event is triggered and save succeeds and fields match previously created integration - it("should transition from 'failure' to 'success' when RETRY event is triggered and save succeeds and fields match previously created integration", () => { - const initialContext = { - ...DEFAULT_CONTEXT, - options: { - ...DEFAULT_CONTEXT.options, - deletePrevious: true, - errorOnFailedCleanup: false, - }, - previouslyCreatedIntegration: { - fields: { - integrationName: 'test', - datasets: [ - { - type: 'logs', - name: 'test', - }, - ], - }, - }, - }; - const machine = createPureCreateCustomIntegrationStateMachine(initialContext); - const nextState = machine.transition('failure', { - type: 'RETRY', - data: { - integrationName: 'test', - datasets: [ - { - type: 'logs', - name: 'test', - }, - ], - }, - }); - expect(nextState.value).toBe('success'); - }); - - // State machine transitions from 'valid' to 'validationFailed' if validation fails - it("should transition from 'valid' to 'validationFailed' when validation fails", () => { - const initialContext = { - ...DEFAULT_CONTEXT, - options: { - ...DEFAULT_CONTEXT.options, - deletePrevious: false, - errorOnFailedCleanup: false, - }, - fields: { - integrationName: 'test', - datasets: [ - { - type: 'logs', - name: '', - }, - ], - }, - }; - const machine = createPureCreateCustomIntegrationStateMachine(initialContext); - const nextState = machine.transition('valid', { - type: 'error.platform.validating:invocation[0]', - data: { - errors: { - fields: { - integrationName: [new IntegrationError('Invalid integration name')], - datasets: { - 0: [new IntegrationError('Invalid dataset name')], - }, - }, - general: null, - }, - }, - }); - expect(nextState.value).toBe('validationFailed'); - }); - - // State machine transitions from 'validationFailed' to 'validating' if UPDATE_FIELDS event is triggered - it("should transition from 'validationFailed' to 'validating' when UPDATE_FIELDS event is triggered", () => { - const machine = createPureCreateCustomIntegrationStateMachine(); - const initialState = machine.initialState; - const nextState = machine.transition(initialState, { type: 'UPDATE_FIELDS' }); - expect(nextState.value).toBe('validating'); - }); - - // State machine transitions from 'validationFailed' to 'valid' if UPDATE_FIELDS event is triggered and validation succeeds - it("should transition from 'validationFailed' to 'valid' when UPDATE_FIELDS event is triggered and validation succeeds", () => { - const machine = createPureCreateCustomIntegrationStateMachine(); - const initialState = machine.initialState; - const nextState = machine.transition(initialState, { type: 'UPDATE_FIELDS' }); - const validState = machine.transition(nextState, { type: 'VALIDATE_SUCCESS' }); - expect(validState.value).toBe('valid'); - }); - - // State machine transitions from 'submitting' to 'failure' if save fails - it("should transition from 'submitting' to 'failure' when save fails", () => { - const machine = createPureCreateCustomIntegrationStateMachine(); - const initialState = machine.initialState; - const nextState = machine.transition(initialState, { type: 'SAVE' }); - const failureState = machine.transition(nextState, { type: 'SAVE_FAILURE' }); - expect(failureState.value).toBe('failure'); - }); -}); From 7a1b2d433a937c58ace905312715f349b39d311a Mon Sep 17 00:00:00 2001 From: Kerry Gallagher <471693+Kerry350@users.noreply.github.com> Date: Thu, 21 Sep 2023 14:57:29 +0100 Subject: [PATCH 5/5] Amend Cypress test --- .../cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts index 0843423f10b1b..19e84284b671b 100644 --- a/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts +++ b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts @@ -623,7 +623,7 @@ describe('[Logs onboarding] Custom logs - install elastic agent', () => { cy.getByTestSubj('obltOnboardingExploreLogs').should('exist').click(); cy.url().should('include', '/app/observability-log-explorer'); - cy.get('button').contains('[mylogs] mylogs').should('exist'); + cy.get('button').contains('[Mylogs] mylogs').should('exist'); }); }); });