mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[ResponseOps][Connectors] Add support of additional fields for ServiceNow ITSM and SecOps (#184023)
## Summary
This PR adds support for additional fields for the ServiceNow ITSM and
SecOps connector. The additional fields will not be available to the
recovered action.
<img width="607" alt="Screenshot 2024-05-27 at 6 29 26 PM"
src="7d397d7b
-2b0b-4399-8d3a-0725ad04a10d">
## Testing
Verify that:
1. Existing rules with ITSM and SecOps configured continue working as
expected.
2. Can create rules with an ITSM action and set some additional fields
supported by ITSM. You can find the available in the Elastic
transformation map inside ServiceNow.
3. The "additional fields" verification in the UI is working as
expected.
4. The "additional fields" are not shown when you set a recovered
action.
Fixes: https://github.com/elastic/kibana/issues/183609
### Checklist
Delete any items that are not applicable to this PR.
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
### For maintainers
- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
## Release notes
Pass any field to ServiceNow using the ServiceNow ITSM and SecOps
connectors with a JSON field called "additional fields".
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
ad646cae46
commit
75f3af5711
41 changed files with 1174 additions and 52 deletions
|
@ -26462,6 +26462,85 @@ Object {
|
|||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"additional_fields": Object {
|
||||
"flags": Object {
|
||||
"default": null,
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"matches": Array [
|
||||
Object {
|
||||
"schema": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"metas": Array [
|
||||
Object {
|
||||
"x-oas-get-additional-properties": [Function],
|
||||
},
|
||||
],
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"key": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"value": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"metas": Array [
|
||||
Object {
|
||||
"x-oas-any-type": true,
|
||||
},
|
||||
],
|
||||
"type": "any",
|
||||
},
|
||||
},
|
||||
"name": "entries",
|
||||
},
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "record",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"schema": Object {
|
||||
"allow": Array [
|
||||
null,
|
||||
],
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
"only": true,
|
||||
},
|
||||
"type": "any",
|
||||
},
|
||||
},
|
||||
],
|
||||
"type": "alternatives",
|
||||
},
|
||||
"category": Object {
|
||||
"flags": Object {
|
||||
"default": null,
|
||||
|
@ -28617,6 +28696,85 @@ Object {
|
|||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"additional_fields": Object {
|
||||
"flags": Object {
|
||||
"default": null,
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"matches": Array [
|
||||
Object {
|
||||
"schema": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"metas": Array [
|
||||
Object {
|
||||
"x-oas-get-additional-properties": [Function],
|
||||
},
|
||||
],
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"key": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"value": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"metas": Array [
|
||||
Object {
|
||||
"x-oas-any-type": true,
|
||||
},
|
||||
],
|
||||
"type": "any",
|
||||
},
|
||||
},
|
||||
"name": "entries",
|
||||
},
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "record",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"schema": Object {
|
||||
"allow": Array [
|
||||
null,
|
||||
],
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
"only": true,
|
||||
},
|
||||
"type": "any",
|
||||
},
|
||||
},
|
||||
],
|
||||
"type": "alternatives",
|
||||
},
|
||||
"category": Object {
|
||||
"flags": Object {
|
||||
"default": null,
|
||||
|
|
|
@ -222,6 +222,7 @@ function getServiceNowActionParams({ defaultActionMessage }: Translations): Serv
|
|||
externalId: null,
|
||||
correlation_id: null,
|
||||
correlation_display: null,
|
||||
additional_fields: null,
|
||||
},
|
||||
comments: [],
|
||||
},
|
||||
|
|
|
@ -214,6 +214,7 @@ function getServiceNowActionParams({ defaultActionMessage }: Translations): Serv
|
|||
externalId: null,
|
||||
correlation_id: null,
|
||||
correlation_display: null,
|
||||
additional_fields: null,
|
||||
},
|
||||
comments: [],
|
||||
},
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const MAX_ADDITIONAL_FIELDS_LENGTH = 20;
|
|
@ -89,9 +89,7 @@ describe('jira action params validation', () => {
|
|||
errors: {
|
||||
'subActionParams.incident.summary': [],
|
||||
'subActionParams.incident.labels': [],
|
||||
'subActionParams.incident.otherFields': [
|
||||
'Additional fields field must be a valid JSON object.',
|
||||
],
|
||||
'subActionParams.incident.otherFields': ['Invalid JSON.'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,6 +13,7 @@ import type {
|
|||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { MAX_OTHER_FIELDS_LENGTH } from '../../../common/jira/constants';
|
||||
import { JiraConfig, JiraSecrets, JiraActionParams } from './types';
|
||||
import { validateJSON } from '../lib/validate_json';
|
||||
|
||||
export const JIRA_DESC = i18n.translate('xpack.stackConnectors.components.jira.selectMessageText', {
|
||||
defaultMessage: 'Create an incident in Jira.',
|
||||
|
@ -58,19 +59,15 @@ export function getConnectorType(): ConnectorTypeModel<JiraConfig, JiraSecrets,
|
|||
errors['subActionParams.incident.labels'].push(translations.LABELS_WHITE_SPACES);
|
||||
}
|
||||
|
||||
try {
|
||||
const otherFields = actionParams.subActionParams?.incident?.otherFields;
|
||||
if (otherFields) {
|
||||
const parsedOtherFields = JSON.parse(otherFields);
|
||||
if (Object.keys(parsedOtherFields).length > MAX_OTHER_FIELDS_LENGTH) {
|
||||
errors['subActionParams.incident.otherFields'] = [
|
||||
translations.OTHER_FIELDS_LENGTH_ERROR(MAX_OTHER_FIELDS_LENGTH),
|
||||
];
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
errors['subActionParams.incident.otherFields'] = [translations.INVALID_JSON_FORMAT];
|
||||
const jsonErrors = validateJSON({
|
||||
value: actionParams.subActionParams?.incident?.otherFields,
|
||||
maxProperties: MAX_OTHER_FIELDS_LENGTH,
|
||||
});
|
||||
|
||||
if (jsonErrors) {
|
||||
errors['subActionParams.incident.otherFields'] = [jsonErrors];
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
},
|
||||
actionParamsFields: lazy(() => import('./jira_params')),
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { AdditionalFields } from './additional_fields';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('Credentials', () => {
|
||||
const onChange = jest.fn();
|
||||
const value = JSON.stringify({ foo: 'test' });
|
||||
const props = { value, errors: [], onChange };
|
||||
|
||||
it('renders the additional fields correctly', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AdditionalFields {...props} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(await screen.findByTestId('additionalFields')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('sets the value correctly', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AdditionalFields {...props} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(await screen.findByText(value)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
/**
|
||||
* Test for the intermediate release process
|
||||
*/
|
||||
it('does not show the component if the value is undefined', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AdditionalFields {...props} value={undefined} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('additional_fieldsJsonEditor')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('changes the value correctly', async () => {
|
||||
const newValue = JSON.stringify({ bar: 'test' });
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AdditionalFields {...props} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const editor = await screen.findByTestId('additional_fieldsJsonEditor');
|
||||
|
||||
userEvent.clear(editor);
|
||||
userEvent.paste(editor, newValue);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalledWith(newValue);
|
||||
});
|
||||
|
||||
expect(await screen.findByText(newValue)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('updating wth an empty string sets its value to null', async () => {
|
||||
const newValue = JSON.stringify({ bar: 'test' });
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AdditionalFields {...props} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const editor = await screen.findByTestId('additional_fieldsJsonEditor');
|
||||
|
||||
userEvent.paste(editor, newValue);
|
||||
userEvent.clear(editor);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { EuiIconTip } from '@elastic/eui';
|
||||
import { JsonEditorWithMessageVariables } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import React from 'react';
|
||||
import { ActionVariable } from '@kbn/alerting-types';
|
||||
import { isEmpty } from 'lodash';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface AdditionalFieldsProps {
|
||||
value?: string | null;
|
||||
errors?: string[];
|
||||
messageVariables?: ActionVariable[];
|
||||
onChange: (value: string | null) => void;
|
||||
}
|
||||
|
||||
export const AdditionalFieldsComponent: React.FC<AdditionalFieldsProps> = ({
|
||||
value,
|
||||
errors,
|
||||
messageVariables,
|
||||
onChange,
|
||||
}) => {
|
||||
/**
|
||||
* Hide the component if the value is not defined.
|
||||
* This is needed for the intermediate release process.
|
||||
* Users will not be able to use the field if they have never set it.
|
||||
* On the next Serverless release the check will be removed.
|
||||
*/
|
||||
if (value === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<JsonEditorWithMessageVariables
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'additional_fields'}
|
||||
inputTargetValue={value}
|
||||
errors={errors ?? []}
|
||||
dataTestSubj="additionalFields"
|
||||
label={
|
||||
<>
|
||||
{i18n.ADDITIONAL_FIELDS}
|
||||
<EuiIconTip
|
||||
size="s"
|
||||
color="subdued"
|
||||
type="questionInCircle"
|
||||
className="eui-alignTop"
|
||||
data-test-subj="otherFieldsHelpTooltip"
|
||||
aria-label={i18n.ADDITIONAL_FIELDS_HELP}
|
||||
content={i18n.ADDITIONAL_FIELDS_HELP_TEXT}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
onDocumentsChange={(json: string) => onChange(isEmpty(json) ? null : json)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AdditionalFields = React.memo(AdditionalFieldsComponent);
|
|
@ -441,3 +441,32 @@ export const ADDITIONAL_INFO_JSON_ERROR = i18n.translate(
|
|||
defaultMessage: 'The additional info field does not have a valid JSON format.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADDITIONAL_FIELDS = i18n.translate(
|
||||
'xpack.stackConnectors.components.servicenow.additionalFieldsTooltip',
|
||||
{
|
||||
defaultMessage: 'Additional fields',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADDITIONAL_FIELDS_HELP = i18n.translate(
|
||||
'xpack.stackConnectors.components.servicenow.additionalFieldsHelpTooltip',
|
||||
{
|
||||
defaultMessage: 'Additional fields help',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADDITIONAL_FIELDS_HELP_TEXT = i18n.translate(
|
||||
'xpack.stackConnectors.components.servicenow.additionalFieldsHelpTooltipText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Additional fields in JSON format as defined in the Elastic ServiceNow application',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADDITIONAL_FIELDS_JSON_ERROR = i18n.translate(
|
||||
'xpack.stackConnectors.components.servicenow.additionalFieldsError',
|
||||
{
|
||||
defaultMessage: 'No valid JSON.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { validateJSON } from './validate_json';
|
||||
|
||||
describe('validateJSON', () => {
|
||||
it('does not return an error for valid JSON and no maxProperties', () => {
|
||||
expect(validateJSON({ value: JSON.stringify({ foo: 'test' }) })).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not return an error for valid JSON and attributes less than maxProperties', () => {
|
||||
expect(
|
||||
validateJSON({ value: JSON.stringify({ foo: 'test' }), maxProperties: 1 })
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not return an error with empty value and maxProperties=0', () => {
|
||||
expect(validateJSON({ maxProperties: 0 })).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not return an error with no values', () => {
|
||||
expect(validateJSON({})).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not return an error with empty object and maxProperties=0', () => {
|
||||
expect(validateJSON({ value: JSON.stringify({}), maxProperties: 0 })).toBeUndefined();
|
||||
});
|
||||
|
||||
it('validates syntax errors correctly', () => {
|
||||
expect(validateJSON({ value: 'foo' })).toBe('Invalid JSON.');
|
||||
});
|
||||
|
||||
it('validates max properties correctly', () => {
|
||||
const value = { foo: 'test', bar: 'test 2' };
|
||||
|
||||
expect(validateJSON({ value: JSON.stringify(value), maxProperties: 1 })).toBe(
|
||||
'A maximum of 1 additional fields can be defined at a time.'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not return an error for an object', () => {
|
||||
expect(validateJSON({ value: { foo: 'test' } })).toBeUndefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { isPlainObject } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface ValidateJSONArgs {
|
||||
value?: string | null | Record<string, unknown>;
|
||||
maxProperties?: number;
|
||||
}
|
||||
|
||||
const isObject = (value?: ValidateJSONArgs['value']): value is Record<string, unknown> => {
|
||||
return isPlainObject(value);
|
||||
};
|
||||
|
||||
export const MAX_ATTRIBUTES_ERROR = (length: number) =>
|
||||
i18n.translate('xpack.stackConnectors.schema.additionalFieldsLengthError', {
|
||||
values: { length },
|
||||
defaultMessage: 'A maximum of {length} additional fields can be defined at a time.',
|
||||
});
|
||||
|
||||
export const INVALID_JSON_FORMAT = i18n.translate(
|
||||
'xpack.stackConnectors.components.otherFieldsFormatErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Invalid JSON.',
|
||||
}
|
||||
);
|
||||
|
||||
export const validateJSON = ({ value, maxProperties }: ValidateJSONArgs) => {
|
||||
try {
|
||||
if (isObject(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
const parsedOtherFields = JSON.parse(value);
|
||||
|
||||
if (maxProperties && Object.keys(parsedOtherFields).length > maxProperties) {
|
||||
return MAX_ATTRIBUTES_ERROR(maxProperties);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return INVALID_JSON_FORMAT;
|
||||
}
|
||||
};
|
|
@ -122,7 +122,9 @@ describe('Tags', () => {
|
|||
});
|
||||
|
||||
act(() => {
|
||||
userEvent.click(screen.getByText('The tags of the rule.'));
|
||||
userEvent.click(screen.getByText('The tags of the rule.'), undefined, {
|
||||
skipPointerEventsCheck: true,
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
|
|
|
@ -10,6 +10,7 @@ import { registerConnectorTypes } from '..';
|
|||
import type { ActionTypeModel as ConnectorTypeModel } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import { experimentalFeaturesMock, registrationServicesMock } from '../../mocks';
|
||||
import { ExperimentalFeaturesService } from '../../common/experimental_features_service';
|
||||
import { MAX_ADDITIONAL_FIELDS_LENGTH } from '../../../common/servicenow/constants';
|
||||
|
||||
const SERVICENOW_ITSM_CONNECTOR_TYPE_ID = '.servicenow';
|
||||
let connectorTypeRegistry: TypeRegistry<ConnectorTypeModel>;
|
||||
|
@ -31,6 +32,7 @@ describe('servicenow action params validation', () => {
|
|||
test(`${SERVICENOW_ITSM_CONNECTOR_TYPE_ID}: action params validation succeeds when action params is valid`, async () => {
|
||||
const connectorTypeModel = connectorTypeRegistry.get(SERVICENOW_ITSM_CONNECTOR_TYPE_ID);
|
||||
const actionParams = {
|
||||
subAction: 'pushToService',
|
||||
subActionParams: { incident: { short_description: 'some title {{test}}' }, comments: [] },
|
||||
};
|
||||
|
||||
|
@ -38,6 +40,7 @@ describe('servicenow action params validation', () => {
|
|||
errors: {
|
||||
['subActionParams.incident.correlation_id']: [],
|
||||
['subActionParams.incident.short_description']: [],
|
||||
['subActionParams.incident.additional_fields']: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -53,6 +56,7 @@ describe('servicenow action params validation', () => {
|
|||
errors: {
|
||||
['subActionParams.incident.correlation_id']: [],
|
||||
['subActionParams.incident.short_description']: [],
|
||||
['subActionParams.incident.additional_fields']: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -67,6 +71,7 @@ describe('servicenow action params validation', () => {
|
|||
errors: {
|
||||
['subActionParams.incident.correlation_id']: [],
|
||||
['subActionParams.incident.short_description']: ['Short description is required.'],
|
||||
['subActionParams.incident.additional_fields']: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -82,6 +87,52 @@ describe('servicenow action params validation', () => {
|
|||
errors: {
|
||||
['subActionParams.incident.correlation_id']: ['Correlation id is required.'],
|
||||
['subActionParams.incident.short_description']: [],
|
||||
['subActionParams.incident.additional_fields']: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('params validation fails when additional_fields is not valid JSON', async () => {
|
||||
const connectorTypeModel = connectorTypeRegistry.get(SERVICENOW_ITSM_CONNECTOR_TYPE_ID);
|
||||
const actionParams = {
|
||||
subAction: 'pushToService',
|
||||
subActionParams: {
|
||||
incident: { short_description: 'some title', additional_fields: 'invalid json' },
|
||||
},
|
||||
};
|
||||
|
||||
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
'subActionParams.incident.correlation_id': [],
|
||||
'subActionParams.incident.short_description': [],
|
||||
'subActionParams.incident.additional_fields': ['Invalid JSON.'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test(`params validation succeeds when its valid json and additional_fields has ${
|
||||
MAX_ADDITIONAL_FIELDS_LENGTH + 1
|
||||
} fields`, async () => {
|
||||
const longJSON: { [key in string]: string } = {};
|
||||
for (let i = 0; i < MAX_ADDITIONAL_FIELDS_LENGTH + 1; i++) {
|
||||
longJSON[`key${i}`] = 'value';
|
||||
}
|
||||
|
||||
const connectorTypeModel = connectorTypeRegistry.get(SERVICENOW_ITSM_CONNECTOR_TYPE_ID);
|
||||
const actionParams = {
|
||||
subAction: 'pushToService',
|
||||
subActionParams: {
|
||||
incident: { short_description: 'some title', additional_fields: JSON.stringify(longJSON) },
|
||||
},
|
||||
};
|
||||
|
||||
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
'subActionParams.incident.correlation_id': [],
|
||||
'subActionParams.incident.short_description': [],
|
||||
['subActionParams.incident.additional_fields']: [
|
||||
`A maximum of ${MAX_ADDITIONAL_FIELDS_LENGTH} additional fields can be defined at a time.`,
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
|||
ActionTypeModel as ConnectorTypeModel,
|
||||
GenericValidationResult,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { MAX_ADDITIONAL_FIELDS_LENGTH } from '../../../common/servicenow/constants';
|
||||
import { ServiceNowConfig, ServiceNowSecrets } from '../lib/servicenow/types';
|
||||
import { ServiceNowITSMActionParams } from './types';
|
||||
import {
|
||||
|
@ -18,6 +19,7 @@ import {
|
|||
getConnectorDescriptiveTitle,
|
||||
getSelectedConnectorIcon,
|
||||
} from '../lib/servicenow/helpers';
|
||||
import { validateJSON } from '../lib/validate_json';
|
||||
|
||||
export const SERVICENOW_ITSM_DESC = i18n.translate(
|
||||
'xpack.stackConnectors.components.serviceNowITSM.selectMessageText',
|
||||
|
@ -51,10 +53,13 @@ export function getServiceNowITSMConnectorType(): ConnectorTypeModel<
|
|||
const errors = {
|
||||
'subActionParams.incident.short_description': new Array<string>(),
|
||||
'subActionParams.incident.correlation_id': new Array<string>(),
|
||||
'subActionParams.incident.additional_fields': new Array<string>(),
|
||||
};
|
||||
|
||||
const validationResult = {
|
||||
errors,
|
||||
};
|
||||
|
||||
if (
|
||||
actionParams.subActionParams &&
|
||||
actionParams.subActionParams.incident &&
|
||||
|
@ -72,6 +77,16 @@ export function getServiceNowITSMConnectorType(): ConnectorTypeModel<
|
|||
translations.CORRELATION_ID_REQUIRED
|
||||
);
|
||||
}
|
||||
|
||||
const jsonErrors = validateJSON({
|
||||
value: actionParams.subActionParams?.incident?.additional_fields,
|
||||
maxProperties: MAX_ADDITIONAL_FIELDS_LENGTH,
|
||||
});
|
||||
|
||||
if (jsonErrors) {
|
||||
errors['subActionParams.incident.additional_fields'] = [jsonErrors];
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
},
|
||||
actionParamsFields: lazy(() => import('./servicenow_itsm_params')),
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { act, waitFor } from '@testing-library/react';
|
||||
import { act, render, waitFor, screen } from '@testing-library/react';
|
||||
import { merge } from 'lodash';
|
||||
|
||||
import { ActionConnector, ActionConnectorMode } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
|
@ -15,6 +15,8 @@ import { useGetChoices } from '../lib/servicenow/use_get_choices';
|
|||
import ServiceNowITSMParamsFields from './servicenow_itsm_params';
|
||||
import { Choice } from '../lib/servicenow/types';
|
||||
import { ACTION_GROUP_RECOVERED } from '../lib/servicenow/helpers';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
|
||||
jest.mock('../lib/servicenow/use_get_choices');
|
||||
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana');
|
||||
|
@ -35,6 +37,7 @@ const actionParams = {
|
|||
externalId: null,
|
||||
correlation_id: 'alertID',
|
||||
correlation_display: 'Alerting',
|
||||
additional_fields: null,
|
||||
},
|
||||
comments: [],
|
||||
},
|
||||
|
@ -366,6 +369,19 @@ describe('ServiceNowITSMParamsFields renders', () => {
|
|||
|
||||
expect(wrapper.find('.euiFormErrorText').text()).toBe('correlation_id_error');
|
||||
});
|
||||
|
||||
it('updates additional fields', async () => {
|
||||
const newValue = JSON.stringify({ bar: 'test' });
|
||||
render(<ServiceNowITSMParamsFields {...defaultProps} />, {
|
||||
wrapper: ({ children }) => <I18nProvider>{children}</I18nProvider>,
|
||||
});
|
||||
|
||||
userEvent.paste(await screen.findByTestId('additional_fieldsJsonEditor'), newValue);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(editAction.mock.calls[0][1].incident.additional_fields).toEqual(newValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test form', () => {
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
} from '../lib/servicenow/helpers';
|
||||
|
||||
import * as i18n from '../lib/servicenow/translations';
|
||||
import { AdditionalFields } from '../lib/servicenow/additional_fields';
|
||||
|
||||
const useGetChoicesFields = ['urgency', 'severity', 'impact', 'category', 'subcategory'];
|
||||
const defaultFields: Fields = {
|
||||
|
@ -263,6 +264,11 @@ const ServiceNowParamsFields: React.FunctionComponent<
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [actionConnector, isTestResolveAction, isTestTriggerAction]);
|
||||
|
||||
const additionalFieldsOnChange = useCallback(
|
||||
(value) => editSubActionProperty('additional_fields', value),
|
||||
[editSubActionProperty]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{executionMode === ActionConnectorMode.Test ? (
|
||||
|
@ -451,6 +457,14 @@ const ServiceNowParamsFields: React.FunctionComponent<
|
|||
inputTargetValue={comments && comments.length > 0 ? comments[0].comment : undefined}
|
||||
label={i18n.COMMENTS_LABEL}
|
||||
/>
|
||||
{!isDeprecatedActionConnector && (
|
||||
<AdditionalFields
|
||||
value={actionParams.subActionParams?.incident.additional_fields}
|
||||
messageVariables={messageVariables}
|
||||
errors={errors['subActionParams.incident.additional_fields'] as string[]}
|
||||
onChange={additionalFieldsOnChange}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{showOnlyCorrelationId && (
|
||||
|
|
|
@ -14,5 +14,10 @@ export enum EventAction {
|
|||
|
||||
export interface ServiceNowITSMActionParams {
|
||||
subAction: string;
|
||||
subActionParams: ExecutorSubActionPushParamsITSM;
|
||||
/* We override "additional_fields" to string because when users fill in the form, the structure won't match until done and
|
||||
we need to store the current state. To match with the data structure define in the backend, we make sure users can't
|
||||
send the form while not matching the original object structure. */
|
||||
subActionParams: ExecutorSubActionPushParamsITSM & {
|
||||
incident: { additional_fields: string | null };
|
||||
};
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { registerConnectorTypes } from '..';
|
|||
import type { ActionTypeModel as ConnectorTypeModel } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import { experimentalFeaturesMock, registrationServicesMock } from '../../mocks';
|
||||
import { ExperimentalFeaturesService } from '../../common/experimental_features_service';
|
||||
import { MAX_ADDITIONAL_FIELDS_LENGTH } from '../../../common/servicenow/constants';
|
||||
|
||||
const SERVICENOW_SIR_CONNECTOR_TYPE_ID = '.servicenow-sir';
|
||||
let connectorTypeRegistry: TypeRegistry<ConnectorTypeModel>;
|
||||
|
@ -35,7 +36,10 @@ describe('servicenow action params validation', () => {
|
|||
};
|
||||
|
||||
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: { ['subActionParams.incident.short_description']: [] },
|
||||
errors: {
|
||||
['subActionParams.incident.short_description']: [],
|
||||
['subActionParams.incident.additional_fields']: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -48,6 +52,48 @@ describe('servicenow action params validation', () => {
|
|||
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
['subActionParams.incident.short_description']: ['Short description is required.'],
|
||||
['subActionParams.incident.additional_fields']: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('params validation fails when additional_fields is not valid JSON', async () => {
|
||||
const connectorTypeModel = connectorTypeRegistry.get(SERVICENOW_SIR_CONNECTOR_TYPE_ID);
|
||||
const actionParams = {
|
||||
subActionParams: {
|
||||
incident: { short_description: 'some title', additional_fields: 'invalid json' },
|
||||
},
|
||||
};
|
||||
|
||||
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
'subActionParams.incident.short_description': [],
|
||||
'subActionParams.incident.additional_fields': ['Invalid JSON.'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test(`params validation succeeds when its valid json and additional_fields has ${
|
||||
MAX_ADDITIONAL_FIELDS_LENGTH + 1
|
||||
} fields`, async () => {
|
||||
const longJSON: { [key in string]: string } = {};
|
||||
for (let i = 0; i < MAX_ADDITIONAL_FIELDS_LENGTH + 1; i++) {
|
||||
longJSON[`key${i}`] = 'value';
|
||||
}
|
||||
|
||||
const connectorTypeModel = connectorTypeRegistry.get(SERVICENOW_SIR_CONNECTOR_TYPE_ID);
|
||||
const actionParams = {
|
||||
subActionParams: {
|
||||
incident: { short_description: 'some title', additional_fields: JSON.stringify(longJSON) },
|
||||
},
|
||||
};
|
||||
|
||||
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
'subActionParams.incident.short_description': [],
|
||||
['subActionParams.incident.additional_fields']: [
|
||||
`A maximum of ${MAX_ADDITIONAL_FIELDS_LENGTH} additional fields can be defined at a time.`,
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,9 +11,11 @@ import type {
|
|||
ActionTypeModel as ConnectorTypeModel,
|
||||
GenericValidationResult,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { MAX_ADDITIONAL_FIELDS_LENGTH } from '../../../common/servicenow/constants';
|
||||
import { ServiceNowConfig, ServiceNowSecrets } from '../lib/servicenow/types';
|
||||
import { ServiceNowSIRActionParams } from './types';
|
||||
import { getConnectorDescriptiveTitle, getSelectedConnectorIcon } from '../lib/servicenow/helpers';
|
||||
import { validateJSON } from '../lib/validate_json';
|
||||
|
||||
export const SERVICENOW_SIR_DESC = i18n.translate(
|
||||
'xpack.stackConnectors.components.serviceNowSIR.selectMessageText',
|
||||
|
@ -46,7 +48,9 @@ export function getServiceNowSIRConnectorType(): ConnectorTypeModel<
|
|||
const translations = await import('../lib/servicenow/translations');
|
||||
const errors = {
|
||||
'subActionParams.incident.short_description': new Array<string>(),
|
||||
'subActionParams.incident.additional_fields': new Array<string>(),
|
||||
};
|
||||
|
||||
const validationResult = {
|
||||
errors,
|
||||
};
|
||||
|
@ -57,6 +61,16 @@ export function getServiceNowSIRConnectorType(): ConnectorTypeModel<
|
|||
) {
|
||||
errors['subActionParams.incident.short_description'].push(translations.TITLE_REQUIRED);
|
||||
}
|
||||
|
||||
const jsonErrors = validateJSON({
|
||||
value: actionParams.subActionParams?.incident?.additional_fields,
|
||||
maxProperties: MAX_ADDITIONAL_FIELDS_LENGTH,
|
||||
});
|
||||
|
||||
if (jsonErrors) {
|
||||
errors['subActionParams.incident.additional_fields'] = [jsonErrors];
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
},
|
||||
actionParamsFields: lazy(() => import('./servicenow_sir_params')),
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act } from '@testing-library/react';
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
|
||||
import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
|
@ -14,6 +14,8 @@ import { useGetChoices } from '../lib/servicenow/use_get_choices';
|
|||
import ServiceNowSIRParamsFields from './servicenow_sir_params';
|
||||
import { Choice } from '../lib/servicenow/types';
|
||||
import { merge } from 'lodash';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
|
||||
jest.mock('../lib/servicenow/use_get_choices');
|
||||
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana');
|
||||
|
@ -36,6 +38,7 @@ const actionParams = {
|
|||
externalId: null,
|
||||
correlation_id: 'alertID',
|
||||
correlation_display: 'Alerting',
|
||||
additional_fields: null,
|
||||
},
|
||||
comments: [],
|
||||
},
|
||||
|
@ -341,5 +344,18 @@ describe('ServiceNowSIRParamsFields renders', () => {
|
|||
expect(comments.simulate('change', changeEvent));
|
||||
expect(editAction.mock.calls[0][1].comments.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('updates additional fields', async () => {
|
||||
const newValue = JSON.stringify({ bar: 'test' });
|
||||
render(<ServiceNowSIRParamsFields {...defaultProps} />, {
|
||||
wrapper: ({ children }) => <I18nProvider>{children}</I18nProvider>,
|
||||
});
|
||||
|
||||
userEvent.paste(await screen.findByTestId('additional_fieldsJsonEditor'), newValue);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(editAction.mock.calls[0][1].incident.additional_fields).toEqual(newValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,6 +29,7 @@ import { ServiceNowSIRActionParams } from './types';
|
|||
import { Fields, Choice } from '../lib/servicenow/types';
|
||||
import { choicesToEuiOptions, DEFAULT_CORRELATION_ID } from '../lib/servicenow/helpers';
|
||||
import { DeprecatedCallout } from '../lib/servicenow/deprecated_callout';
|
||||
import { AdditionalFields } from '../lib/servicenow/additional_fields';
|
||||
|
||||
const useGetChoicesFields = ['category', 'subcategory', 'priority'];
|
||||
const defaultFields: Fields = {
|
||||
|
@ -148,6 +149,11 @@ const ServiceNowSIRParamsFields: React.FunctionComponent<
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [actionParams]);
|
||||
|
||||
const additionalFieldsOnChange = useCallback(
|
||||
(value) => editSubActionProperty('additional_fields', value),
|
||||
[editSubActionProperty]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDeprecatedActionConnector && <DeprecatedCallout />}
|
||||
|
@ -288,6 +294,14 @@ const ServiceNowSIRParamsFields: React.FunctionComponent<
|
|||
label={i18n.COMMENTS_LABEL}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
{!isDeprecatedActionConnector && (
|
||||
<AdditionalFields
|
||||
value={actionParams.subActionParams?.incident.additional_fields}
|
||||
messageVariables={messageVariables}
|
||||
errors={errors['subActionParams.incident.additional_fields'] as string[]}
|
||||
onChange={additionalFieldsOnChange}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,5 +9,10 @@ import type { ExecutorSubActionPushParamsSIR } from '../../../server/connector_t
|
|||
|
||||
export interface ServiceNowSIRActionParams {
|
||||
subAction: string;
|
||||
subActionParams: ExecutorSubActionPushParamsSIR;
|
||||
/* We override "additional_fields" to string because when users fill in the form, the structure won't match until done and
|
||||
we need to store the current state. To match with the data structure define in the backend, we make sure users can't
|
||||
send the form while not matching the original object structure. */
|
||||
subActionParams: ExecutorSubActionPushParamsSIR & {
|
||||
incident: { additional_fields: string | null };
|
||||
};
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ describe('Jira schema', () => {
|
|||
otherFields,
|
||||
},
|
||||
})
|
||||
).toThrow('A maximum of 20 otherFields can be defined at a time.');
|
||||
).toThrow('A maximum of 20 fields in otherFields can be defined at a time.');
|
||||
});
|
||||
|
||||
it.each(incidentSchemaObjectProperties)(
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { validateOtherFieldsKeys, validateOtherFieldsLength } from './validators';
|
||||
import { MAX_OTHER_FIELDS_LENGTH } from '../../../common/jira/constants';
|
||||
import { validateRecordMaxKeys } from '../lib/validators';
|
||||
import { validateOtherFieldsKeys } from './validators';
|
||||
|
||||
export const ExternalIncidentServiceConfiguration = {
|
||||
apiUrl: schema.string(),
|
||||
|
@ -49,7 +51,12 @@ const incidentSchemaObject = {
|
|||
}),
|
||||
schema.any(),
|
||||
{
|
||||
validate: (value) => validateOtherFieldsLength(value),
|
||||
validate: (value) =>
|
||||
validateRecordMaxKeys({
|
||||
record: value,
|
||||
maxNumberOfFields: MAX_OTHER_FIELDS_LENGTH,
|
||||
fieldName: 'otherFields',
|
||||
}),
|
||||
}
|
||||
)
|
||||
),
|
||||
|
|
|
@ -13,8 +13,8 @@ import {
|
|||
} from './types';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { MAX_OTHER_FIELDS_LENGTH } from '../../../common/jira/constants';
|
||||
import { incidentSchemaObjectProperties } from './schema';
|
||||
import { validateKeysAllowed } from '../lib/validators';
|
||||
|
||||
export const validateCommonConfig = (
|
||||
configObject: JiraPublicConfigurationType,
|
||||
|
@ -38,18 +38,10 @@ export const validate: ExternalServiceValidation = {
|
|||
secrets: validateCommonSecrets,
|
||||
};
|
||||
|
||||
export const validateOtherFieldsLength = (
|
||||
otherFields: Record<string, unknown>
|
||||
): string | undefined => {
|
||||
if (Object.keys(otherFields).length > MAX_OTHER_FIELDS_LENGTH) {
|
||||
return i18n.OTHER_FIELDS_LENGTH_ERROR(MAX_OTHER_FIELDS_LENGTH);
|
||||
}
|
||||
};
|
||||
|
||||
export const validateOtherFieldsKeys = (key: string): string | undefined => {
|
||||
const propertiesSet = new Set(incidentSchemaObjectProperties);
|
||||
|
||||
if (propertiesSet.has(key)) {
|
||||
return i18n.OTHER_FIELDS_PROPERTY_ERROR(key);
|
||||
}
|
||||
return validateKeysAllowed({
|
||||
key,
|
||||
disallowList: incidentSchemaObjectProperties,
|
||||
fieldName: 'otherFields',
|
||||
});
|
||||
};
|
||||
|
|
|
@ -99,6 +99,7 @@ describe('api', () => {
|
|||
correlation_display: 'Alerting',
|
||||
correlation_id: 'ruleId',
|
||||
opened_by: 'elastic',
|
||||
additional_fields: {},
|
||||
},
|
||||
});
|
||||
expect(externalService.updateIncident).not.toHaveBeenCalled();
|
||||
|
@ -114,6 +115,7 @@ describe('api', () => {
|
|||
logger: mockedLogger,
|
||||
commentFieldKey: 'comments',
|
||||
});
|
||||
|
||||
expect(externalService.updateIncident).toHaveBeenCalledTimes(2);
|
||||
expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, {
|
||||
incident: {
|
||||
|
@ -127,6 +129,7 @@ describe('api', () => {
|
|||
short_description: 'Incident title',
|
||||
correlation_display: 'Alerting',
|
||||
correlation_id: 'ruleId',
|
||||
additional_fields: {},
|
||||
},
|
||||
incidentId: 'incident-1',
|
||||
});
|
||||
|
@ -143,6 +146,7 @@ describe('api', () => {
|
|||
short_description: 'Incident title',
|
||||
correlation_display: 'Alerting',
|
||||
correlation_id: 'ruleId',
|
||||
additional_fields: {},
|
||||
},
|
||||
incidentId: 'incident-1',
|
||||
});
|
||||
|
@ -171,6 +175,7 @@ describe('api', () => {
|
|||
short_description: 'Incident title',
|
||||
correlation_display: 'Alerting',
|
||||
correlation_id: 'ruleId',
|
||||
additional_fields: {},
|
||||
},
|
||||
incidentId: 'incident-1',
|
||||
});
|
||||
|
@ -187,6 +192,7 @@ describe('api', () => {
|
|||
short_description: 'Incident title',
|
||||
correlation_display: 'Alerting',
|
||||
correlation_id: 'ruleId',
|
||||
additional_fields: {},
|
||||
},
|
||||
incidentId: 'incident-1',
|
||||
});
|
||||
|
@ -264,6 +270,7 @@ describe('api', () => {
|
|||
short_description: 'Incident title',
|
||||
correlation_display: 'Alerting',
|
||||
correlation_id: 'ruleId',
|
||||
additional_fields: {},
|
||||
},
|
||||
});
|
||||
expect(externalService.createIncident).not.toHaveBeenCalled();
|
||||
|
@ -291,6 +298,7 @@ describe('api', () => {
|
|||
short_description: 'Incident title',
|
||||
correlation_display: 'Alerting',
|
||||
correlation_id: 'ruleId',
|
||||
additional_fields: {},
|
||||
},
|
||||
incidentId: 'incident-3',
|
||||
});
|
||||
|
@ -307,6 +315,7 @@ describe('api', () => {
|
|||
short_description: 'Incident title',
|
||||
correlation_display: 'Alerting',
|
||||
correlation_id: 'ruleId',
|
||||
additional_fields: {},
|
||||
},
|
||||
incidentId: 'incident-2',
|
||||
});
|
||||
|
@ -334,6 +343,7 @@ describe('api', () => {
|
|||
short_description: 'Incident title',
|
||||
correlation_display: 'Alerting',
|
||||
correlation_id: 'ruleId',
|
||||
additional_fields: {},
|
||||
},
|
||||
incidentId: 'incident-3',
|
||||
});
|
||||
|
@ -350,6 +360,7 @@ describe('api', () => {
|
|||
short_description: 'Incident title',
|
||||
correlation_display: 'Alerting',
|
||||
correlation_id: 'ruleId',
|
||||
additional_fields: {},
|
||||
},
|
||||
incidentId: 'incident-2',
|
||||
});
|
||||
|
|
|
@ -205,6 +205,7 @@ export const executorParams: ExecutorSubActionPushParams = {
|
|||
subcategory: 'os',
|
||||
correlation_id: 'ruleId',
|
||||
correlation_display: 'Alerting',
|
||||
additional_fields: {},
|
||||
},
|
||||
comments: [
|
||||
{
|
||||
|
@ -232,6 +233,7 @@ export const sirParams: PushToServiceApiParamsSIR = {
|
|||
correlation_id: 'ruleId',
|
||||
correlation_display: 'Alerting',
|
||||
priority: '1',
|
||||
additional_fields: {},
|
||||
},
|
||||
comments: [
|
||||
{
|
||||
|
|
|
@ -6,7 +6,10 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { MAX_ADDITIONAL_FIELDS_LENGTH } from '../../../../common/servicenow/constants';
|
||||
import { validateRecordMaxKeys } from '../validators';
|
||||
import { DEFAULT_ALERTS_GROUPING_KEY } from './config';
|
||||
import { validateOtherFieldsKeys } from './validators';
|
||||
|
||||
export const ExternalIncidentServiceConfigurationBase = {
|
||||
apiUrl: schema.string(),
|
||||
|
@ -58,8 +61,26 @@ const CommonAttributes = {
|
|||
subcategory: schema.nullable(schema.string()),
|
||||
correlation_id: schema.nullable(schema.string({ defaultValue: DEFAULT_ALERTS_GROUPING_KEY })),
|
||||
correlation_display: schema.nullable(schema.string()),
|
||||
additional_fields: schema.nullable(
|
||||
schema.recordOf(
|
||||
schema.string({
|
||||
validate: (value) => validateOtherFieldsKeys(value),
|
||||
}),
|
||||
schema.any(),
|
||||
{
|
||||
validate: (value) =>
|
||||
validateRecordMaxKeys({
|
||||
record: value,
|
||||
maxNumberOfFields: MAX_ADDITIONAL_FIELDS_LENGTH,
|
||||
fieldName: 'additional_fields',
|
||||
}),
|
||||
}
|
||||
)
|
||||
),
|
||||
};
|
||||
|
||||
export const commonIncidentSchemaObjectProperties = Object.keys(CommonAttributes);
|
||||
|
||||
// Schema for ServiceNow Incident Management (ITSM)
|
||||
export const ExecutorSubActionPushParamsSchemaITSM = schema.object({
|
||||
incident: schema.object({
|
||||
|
|
|
@ -101,7 +101,10 @@ const mockCorrelationIdIncidentResponse = () =>
|
|||
},
|
||||
}));
|
||||
|
||||
const createIncident = async (service: ExternalService) => {
|
||||
const createIncident = async (
|
||||
service: ExternalService,
|
||||
incident?: Partial<ServiceNowITSMIncident>
|
||||
) => {
|
||||
// Get application version
|
||||
mockApplicationVersion();
|
||||
// Import set api response
|
||||
|
@ -110,11 +113,18 @@ const createIncident = async (service: ExternalService) => {
|
|||
mockIncidentResponse(false);
|
||||
|
||||
return await service.createIncident({
|
||||
incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
|
||||
incident: {
|
||||
short_description: 'title',
|
||||
description: 'desc',
|
||||
...incident,
|
||||
} as ServiceNowITSMIncident,
|
||||
});
|
||||
};
|
||||
|
||||
const updateIncident = async (service: ExternalService) => {
|
||||
const updateIncident = async (
|
||||
service: ExternalService,
|
||||
incident?: Partial<ServiceNowITSMIncident>
|
||||
) => {
|
||||
// Get application version
|
||||
mockApplicationVersion();
|
||||
// Import set api response
|
||||
|
@ -124,7 +134,11 @@ const updateIncident = async (service: ExternalService) => {
|
|||
|
||||
return await service.updateIncident({
|
||||
incidentId: '1',
|
||||
incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
|
||||
incident: {
|
||||
short_description: 'title',
|
||||
description: 'desc',
|
||||
...incident,
|
||||
} as ServiceNowITSMIncident,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -682,6 +696,19 @@ describe('ServiceNow service', () => {
|
|||
'[Action][ServiceNow]: Unable to create incident. Error: An error has occurred while importing the incident Reason: unknown'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should create an incident with additional fields correctly without prefixing them with u_', async () => {
|
||||
await createIncident(service, { additional_fields: { foo: 'test' } });
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(2, {
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/import/x_elas2_inc_int_elastic_incident',
|
||||
method: 'post',
|
||||
data: { u_short_description: 'title', u_description: 'desc', foo: 'test' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// old connectors
|
||||
|
@ -755,6 +782,18 @@ describe('ServiceNow service', () => {
|
|||
|
||||
expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
|
||||
});
|
||||
|
||||
test('it should throw if tries to update an incident with additional_fields', async () => {
|
||||
await expect(
|
||||
service.createIncident({
|
||||
incident: {
|
||||
additional_fields: {},
|
||||
} as ServiceNowITSMIncident,
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"[Action][ServiceNow]: Unable to create incident. Error: ServiceNow additional fields are not supported for deprecated connectors. Reason: unknown: errorResponse was null"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -860,6 +899,24 @@ describe('ServiceNow service', () => {
|
|||
'[Action][ServiceNow]: Unable to update incident with id 1. Error: An error has occurred while importing the incident Reason: unknown'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should update an incident with additional fields correctly without prefixing them with u_', async () => {
|
||||
await updateIncident(service, { additional_fields: { foo: 'test' } });
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(2, {
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/import/x_elas2_inc_int_elastic_incident',
|
||||
method: 'post',
|
||||
data: {
|
||||
u_short_description: 'title',
|
||||
u_description: 'desc',
|
||||
elastic_incident_id: '1',
|
||||
foo: 'test',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// old connectors
|
||||
|
@ -935,6 +992,19 @@ describe('ServiceNow service', () => {
|
|||
|
||||
expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
|
||||
});
|
||||
|
||||
test('it should throw if tries to update an incident with additional_fields', async () => {
|
||||
await expect(
|
||||
service.updateIncident({
|
||||
incidentId: '1',
|
||||
incident: {
|
||||
additional_fields: {},
|
||||
} as ServiceNowITSMIncident,
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"[Action][ServiceNow]: Unable to update incident with id 1. Error: ServiceNow additional fields are not supported for deprecated connectors. Reason: unknown: errorResponse was null"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -22,7 +22,12 @@ import {
|
|||
|
||||
import * as i18n from './translations';
|
||||
import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType } from './types';
|
||||
import { createServiceError, getPushedDate, prepareIncident } from './utils';
|
||||
import {
|
||||
createServiceError,
|
||||
getPushedDate,
|
||||
prepareIncident,
|
||||
throwIfAdditionalFieldsNotSupported,
|
||||
} from './utils';
|
||||
|
||||
export const SYS_DICTIONARY_ENDPOINT = `api/now/table/sys_dictionary`;
|
||||
|
||||
|
@ -186,6 +191,7 @@ export const createExternalService: ServiceFactory = ({
|
|||
|
||||
const createIncident = async ({ incident }: ExternalServiceParamsCreate) => {
|
||||
try {
|
||||
throwIfAdditionalFieldsNotSupported(useTableApi, incident);
|
||||
await checkIfApplicationIsInstalled();
|
||||
|
||||
const res = await request({
|
||||
|
@ -219,6 +225,7 @@ export const createExternalService: ServiceFactory = ({
|
|||
|
||||
const updateIncident = async ({ incidentId, incident }: ExternalServiceParamsUpdate) => {
|
||||
try {
|
||||
throwIfAdditionalFieldsNotSupported(useTableApi, incident);
|
||||
await checkIfApplicationIsInstalled();
|
||||
|
||||
const res = await request({
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
getPushedDate,
|
||||
throwIfSubActionIsNotSupported,
|
||||
getAxiosInstance,
|
||||
throwIfAdditionalFieldsNotSupported,
|
||||
} from './utils';
|
||||
import type { ResponseError } from './types';
|
||||
import { connectorTokenClientMock } from '@kbn/actions-plugin/server/lib/connector_token_client.mock';
|
||||
|
@ -62,6 +63,46 @@ describe('utils', () => {
|
|||
const newIncident = prepareIncident(true, incident);
|
||||
expect(newIncident).toEqual(incident);
|
||||
});
|
||||
|
||||
test('does not prefix additional fields with u_', async () => {
|
||||
const incident = {
|
||||
short_description: 'title',
|
||||
description: 'desc',
|
||||
additional_fields: { foo: 'test' },
|
||||
};
|
||||
|
||||
const newIncident = prepareIncident(false, incident);
|
||||
expect(newIncident).toEqual({
|
||||
u_short_description: 'title',
|
||||
u_description: 'desc',
|
||||
foo: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
test('strips out additional fields if it is a deprecated connector', async () => {
|
||||
const incident = {
|
||||
short_description: 'title',
|
||||
description: 'desc',
|
||||
additional_fields: { foo: 'test' },
|
||||
};
|
||||
|
||||
const newIncident = prepareIncident(true, incident);
|
||||
expect(newIncident).toEqual({ short_description: 'title', description: 'desc' });
|
||||
});
|
||||
|
||||
test('does not overrides base fields', async () => {
|
||||
const incident = {
|
||||
short_description: 'title',
|
||||
description: 'desc',
|
||||
additional_fields: { u_short_description: 'foo' },
|
||||
};
|
||||
|
||||
const newIncident = prepareIncident(false, incident);
|
||||
expect(newIncident).toEqual({
|
||||
u_short_description: 'title',
|
||||
u_description: 'desc',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createServiceError', () => {
|
||||
|
@ -417,4 +458,28 @@ describe('utils', () => {
|
|||
expect(connectorTokenClient.deleteConnectorTokens).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('throwIfAdditionalFieldsNotSupported', () => {
|
||||
it('throws if the connector is deprecated and it sets additional_fields', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
expect(() => throwIfAdditionalFieldsNotSupported(true, { additional_fields: {} })).toThrow(
|
||||
'ServiceNow additional fields are not supported for deprecated connectors.'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not throw if the connector is deprecated and it does not set additional_fields', async () => {
|
||||
expect(() => throwIfAdditionalFieldsNotSupported(true, {})).not.toThrow();
|
||||
});
|
||||
|
||||
it('does not throw if the connector is not and it set additional_fields', async () => {
|
||||
expect(() =>
|
||||
throwIfAdditionalFieldsNotSupported(false, { additional_fields: {} })
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('does not throw if the connector is not and it does not set additional_fields', async () => {
|
||||
expect(() => throwIfAdditionalFieldsNotSupported(false, {})).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,13 +24,23 @@ import {
|
|||
import { FIELD_PREFIX } from './config';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const prepareIncident = (useOldApi: boolean, incident: PartialIncident): PartialIncident =>
|
||||
useOldApi
|
||||
? incident
|
||||
: Object.entries(incident).reduce(
|
||||
(acc, [key, value]) => ({ ...acc, [`${FIELD_PREFIX}${key}`]: value }),
|
||||
{} as Incident
|
||||
);
|
||||
export const prepareIncident = (
|
||||
useOldApi: boolean,
|
||||
incident: PartialIncident
|
||||
): Record<string, unknown> => {
|
||||
const { additional_fields: additionalFields, ...restIncidentFields } = incident;
|
||||
|
||||
if (useOldApi) {
|
||||
return restIncidentFields;
|
||||
}
|
||||
|
||||
const baseFields = Object.entries(restIncidentFields).reduce<Partial<Incident>>(
|
||||
(acc, [key, value]) => ({ ...acc, [`${FIELD_PREFIX}${key}`]: value }),
|
||||
{}
|
||||
);
|
||||
|
||||
return { ...additionalFields, ...baseFields };
|
||||
};
|
||||
|
||||
const createErrorMessage = (errorResponse?: ServiceNowError): string => {
|
||||
if (errorResponse == null) {
|
||||
|
@ -91,6 +101,18 @@ export const throwIfSubActionIsNotSupported = ({
|
|||
}
|
||||
};
|
||||
|
||||
export const throwIfAdditionalFieldsNotSupported = (
|
||||
useOldApi: boolean,
|
||||
incident: PartialIncident
|
||||
) => {
|
||||
if (useOldApi && incident.additional_fields) {
|
||||
throw new AxiosError(
|
||||
'ServiceNow additional fields are not supported for deprecated connectors.',
|
||||
'400'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export interface GetAxiosInstanceOpts {
|
||||
connectorId: string;
|
||||
logger: Logger;
|
||||
|
|
|
@ -5,7 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { validateCommonConfig, validateCommonSecrets, validateCommonConnector } from './validators';
|
||||
import {
|
||||
validateCommonConfig,
|
||||
validateCommonSecrets,
|
||||
validateCommonConnector,
|
||||
validateOtherFieldsKeys,
|
||||
} from './validators';
|
||||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
|
||||
const configurationUtilities = actionsConfigMock.create();
|
||||
|
@ -430,4 +435,12 @@ describe('validateCommonConnector', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateOtherFieldsKeys', () => {
|
||||
it('returns an error if the keys are not allowed', () => {
|
||||
expect(validateOtherFieldsKeys('short_description')).toEqual(
|
||||
'The following properties cannot be defined inside additional_fields: short_description.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,6 +13,8 @@ import {
|
|||
} from './types';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { validateKeysAllowed } from '../validators';
|
||||
import { commonIncidentSchemaObjectProperties } from './schema';
|
||||
|
||||
export const validateCommonConfig = (
|
||||
config: ServiceNowPublicConfigurationType,
|
||||
|
@ -120,3 +122,11 @@ export const validate: ExternalServiceValidation = {
|
|||
secrets: validateCommonSecrets,
|
||||
connector: validateCommonConnector,
|
||||
};
|
||||
|
||||
export const validateOtherFieldsKeys = (key: string): string | undefined => {
|
||||
return validateKeysAllowed({
|
||||
key,
|
||||
disallowList: commonIncidentSchemaObjectProperties,
|
||||
fieldName: 'additional_fields',
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { validateKeysAllowed, validateRecordMaxKeys } from './validators';
|
||||
|
||||
describe('validators', () => {
|
||||
describe('validateRecordMaxKeys', () => {
|
||||
it('returns undefined if the keys of the record are less than the maximum', () => {
|
||||
expect(
|
||||
validateRecordMaxKeys({
|
||||
record: { foo: 'bar' },
|
||||
maxNumberOfFields: 2,
|
||||
fieldName: 'myFieldName',
|
||||
})
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns an error if the keys of the record are greater than the maximum', () => {
|
||||
expect(
|
||||
validateRecordMaxKeys({
|
||||
record: { foo: 'bar', bar: 'test', test: 'foo' },
|
||||
maxNumberOfFields: 2,
|
||||
fieldName: 'myFieldName',
|
||||
})
|
||||
).toEqual('A maximum of 2 fields in myFieldName can be defined at a time.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateKeysAllowed', () => {
|
||||
it('returns undefined if the keys are allowed', () => {
|
||||
expect(
|
||||
validateKeysAllowed({
|
||||
key: 'foo',
|
||||
disallowList: ['bar'],
|
||||
fieldName: 'myFieldName',
|
||||
})
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns an error if the keys are not allowed', () => {
|
||||
expect(
|
||||
validateKeysAllowed({
|
||||
key: 'foo',
|
||||
disallowList: ['foo'],
|
||||
fieldName: 'myFieldName',
|
||||
})
|
||||
).toEqual('The following properties cannot be defined inside myFieldName: foo.');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const FIELDS_MAX_LENGTH_ERROR = (length: number, fieldName: string) =>
|
||||
i18n.translate('xpack.stackConnectors.schema.otherFieldsLengthError', {
|
||||
values: { length, fieldName },
|
||||
defaultMessage:
|
||||
'A maximum of {length, plural, =1 {{length} field} other {{length} fields}} in {fieldName} can be defined at a time.',
|
||||
});
|
||||
|
||||
export const FIELDS_KEY_NOT_ALLOWED_ERROR = (properties: string, fieldName: string) =>
|
||||
i18n.translate('xpack.stackConnectors.schema.otherFieldsPropertyError', {
|
||||
values: { properties, fieldName },
|
||||
defaultMessage: 'The following properties cannot be defined inside {fieldName}: {properties}.',
|
||||
});
|
||||
|
||||
export const validateRecordMaxKeys = ({
|
||||
record,
|
||||
maxNumberOfFields,
|
||||
fieldName,
|
||||
}: {
|
||||
record: Record<string, unknown>;
|
||||
maxNumberOfFields: number;
|
||||
fieldName: string;
|
||||
}): string | undefined => {
|
||||
if (Object.keys(record).length > maxNumberOfFields) {
|
||||
return FIELDS_MAX_LENGTH_ERROR(maxNumberOfFields, fieldName);
|
||||
}
|
||||
};
|
||||
|
||||
export const validateKeysAllowed = ({
|
||||
key,
|
||||
disallowList,
|
||||
fieldName,
|
||||
}: {
|
||||
key: string;
|
||||
disallowList: string[];
|
||||
fieldName: string;
|
||||
}): string | undefined => {
|
||||
const propertiesSet = new Set(disallowList);
|
||||
|
||||
if (propertiesSet.has(key)) {
|
||||
return FIELDS_KEY_NOT_ALLOWED_ERROR(key, fieldName);
|
||||
}
|
||||
};
|
|
@ -40,6 +40,7 @@
|
|||
"@kbn/cases-components",
|
||||
"@kbn/code-editor-mock",
|
||||
"@kbn/utility-types",
|
||||
"@kbn/alerting-types",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -262,6 +262,7 @@ const statusRule = {
|
|||
externalId: null,
|
||||
correlation_id: null,
|
||||
correlation_display: null,
|
||||
additional_fields: null,
|
||||
},
|
||||
comments: [],
|
||||
},
|
||||
|
@ -464,6 +465,7 @@ const tlsRule = {
|
|||
externalId: null,
|
||||
correlation_id: null,
|
||||
correlation_display: null,
|
||||
additional_fields: null,
|
||||
},
|
||||
comments: [],
|
||||
},
|
||||
|
|
|
@ -426,7 +426,7 @@ export default function jiraTest({ getService }: FtrProviderContext) {
|
|||
status: 'error',
|
||||
retry: false,
|
||||
message:
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.otherFields]: types that failed validation:\n - [subActionParams.incident.otherFields.0]: A maximum of 20 otherFields can be defined at a time.\n - [subActionParams.incident.otherFields.1]: expected value to equal [null]\n- [4.subAction]: expected value to equal [issueTypes]\n- [5.subAction]: expected value to equal [fieldsByIssueType]\n- [6.subAction]: expected value to equal [issues]\n- [7.subAction]: expected value to equal [issue]',
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.otherFields]: types that failed validation:\n - [subActionParams.incident.otherFields.0]: A maximum of 20 fields in otherFields can be defined at a time.\n - [subActionParams.incident.otherFields.1]: expected value to equal [null]\n- [4.subAction]: expected value to equal [issueTypes]\n- [5.subAction]: expected value to equal [fieldsByIssueType]\n- [6.subAction]: expected value to equal [issues]\n- [7.subAction]: expected value to equal [issue]',
|
||||
errorSource: TaskErrorSource.FRAMEWORK,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,6 +14,7 @@ import http from 'http';
|
|||
import { getHttpProxyServer } from '@kbn/alerting-api-integration-helpers';
|
||||
import { getServiceNowServer } from '@kbn/actions-simulators-plugin/server/plugin';
|
||||
import { TaskErrorSource } from '@kbn/task-manager-plugin/common';
|
||||
import { MAX_ADDITIONAL_FIELDS_LENGTH } from '@kbn/stack-connectors-plugin/common/servicenow/constants';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
@ -578,6 +579,81 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
it('throws when trying to create an incident with too many "additional_fields"', async () => {
|
||||
const additionalFields = new Array(MAX_ADDITIONAL_FIELDS_LENGTH + 1)
|
||||
.fill('foobar')
|
||||
.reduce((acc, curr, idx) => {
|
||||
acc[idx] = curr;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const res = await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
...mockServiceNowBasic.params,
|
||||
subActionParams: {
|
||||
...mockServiceNowBasic.params.subActionParams,
|
||||
incident: {
|
||||
...mockServiceNowBasic.params.subActionParams.incident,
|
||||
additional_fields: additionalFields,
|
||||
},
|
||||
comments: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(res.body.status).to.eql('error');
|
||||
});
|
||||
|
||||
it('throws when trying to create an incident with "additional_fields" keys that are not allowed', async () => {
|
||||
const res = await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
...mockServiceNowBasic.params,
|
||||
subActionParams: {
|
||||
...mockServiceNowBasic.params.subActionParams,
|
||||
incident: {
|
||||
...mockServiceNowBasic.params.subActionParams.incident,
|
||||
additional_fields: {
|
||||
short_description: 'foo',
|
||||
},
|
||||
},
|
||||
comments: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(res.body.status).to.eql('error');
|
||||
});
|
||||
|
||||
it('does not throw when "additional_fields" is a valid JSON object send as string', async () => {
|
||||
const res = await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
...mockServiceNowBasic.params,
|
||||
subActionParams: {
|
||||
...mockServiceNowBasic.params.subActionParams,
|
||||
incident: {
|
||||
...mockServiceNowBasic.params.subActionParams.incident,
|
||||
otherFields: '{ "foo": "bar" }',
|
||||
},
|
||||
comments: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(res.body.status).to.eql('error');
|
||||
});
|
||||
|
||||
describe('getChoices', () => {
|
||||
it('should fail when field is not provided', async () => {
|
||||
await supertest
|
||||
|
|
|
@ -14,6 +14,7 @@ import http from 'http';
|
|||
import { getHttpProxyServer } from '@kbn/alerting-api-integration-helpers';
|
||||
import { getServiceNowServer } from '@kbn/actions-simulators-plugin/server/plugin';
|
||||
import { TaskErrorSource } from '@kbn/task-manager-plugin/common';
|
||||
import { MAX_ADDITIONAL_FIELDS_LENGTH } from '@kbn/stack-connectors-plugin/common/servicenow/constants';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
@ -591,6 +592,81 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
it('throws when trying to create an incident with too many "additional_fields"', async () => {
|
||||
const additionalFields = new Array(MAX_ADDITIONAL_FIELDS_LENGTH + 1)
|
||||
.fill('foobar')
|
||||
.reduce((acc, curr, idx) => {
|
||||
acc[idx] = curr;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const res = await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
...mockServiceNowBasic.params,
|
||||
subActionParams: {
|
||||
...mockServiceNowBasic.params.subActionParams,
|
||||
incident: {
|
||||
...mockServiceNowBasic.params.subActionParams.incident,
|
||||
additional_fields: additionalFields,
|
||||
},
|
||||
comments: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(res.body.status).to.eql('error');
|
||||
});
|
||||
|
||||
it('throws when trying to create an incident with "additional_fields" keys that are not allowed', async () => {
|
||||
const res = await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
...mockServiceNowBasic.params,
|
||||
subActionParams: {
|
||||
...mockServiceNowBasic.params.subActionParams,
|
||||
incident: {
|
||||
...mockServiceNowBasic.params.subActionParams.incident,
|
||||
additional_fields: {
|
||||
short_description: 'foo',
|
||||
},
|
||||
},
|
||||
comments: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(res.body.status).to.eql('error');
|
||||
});
|
||||
|
||||
it('does not throw when "additional_fields" is a valid JSON object send as string', async () => {
|
||||
const res = await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
...mockServiceNowBasic.params,
|
||||
subActionParams: {
|
||||
...mockServiceNowBasic.params.subActionParams,
|
||||
incident: {
|
||||
...mockServiceNowBasic.params.subActionParams.incident,
|
||||
otherFields: '{ "foo": "bar" }',
|
||||
},
|
||||
comments: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(res.body.status).to.eql('error');
|
||||
});
|
||||
|
||||
describe('getChoices', () => {
|
||||
it('should fail when field is not provided', async () => {
|
||||
await supertest
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue