mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Actions][ServiceNow] Allow to close serviceNow incident when alert resolves (#171760)
## Summary
Fixes: https://github.com/elastic/kibana/issues/170522
This PR allows to `close service now incident` when alert is `recovered`
SN connector form shows only `correlation_id` field as it is mandatory
field to close an incident.

**How to test:**
- Create a rule and select serviceNow ITSM action with Run when option
as Recovered
- Verify that it closes an incident in SN when Alert is recovered
### 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)
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
b06980c8d8
commit
d31a15807b
22 changed files with 1120 additions and 220 deletions
|
@ -8,7 +8,7 @@
|
|||
import { RecoveredActionGroup } from './builtin_action_groups';
|
||||
|
||||
const DisabledActionGroupsByActionType: Record<string, string[]> = {
|
||||
[RecoveredActionGroup.id]: ['.jira', '.servicenow', '.resilient'],
|
||||
[RecoveredActionGroup.id]: ['.jira', '.resilient'],
|
||||
};
|
||||
|
||||
export const DisabledActionTypeIdsForActionGroup: Map<string, string[]> = new Map(
|
||||
|
|
|
@ -16,6 +16,8 @@ import { AppInfo, Choice, RESTApiError } from './types';
|
|||
|
||||
export const DEFAULT_CORRELATION_ID = '{{rule.id}}:{{alert.id}}';
|
||||
|
||||
export const ACTION_GROUP_RECOVERED = 'recovered';
|
||||
|
||||
export const choicesToEuiOptions = (choices: Choice[]): EuiSelectOption[] =>
|
||||
choices.map((choice) => ({ value: choice.value, text: choice.label }));
|
||||
|
||||
|
|
|
@ -56,6 +56,13 @@ export const TITLE_REQUIRED = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const CORRELATION_ID_REQUIRED = i18n.translate(
|
||||
'xpack.stackConnectors.components.serviceNow.requiredCorrelationIdTextField',
|
||||
{
|
||||
defaultMessage: 'Correlation id is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const INCIDENT = i18n.translate('xpack.stackConnectors.components.serviceNow.title', {
|
||||
defaultMessage: 'Incident',
|
||||
});
|
||||
|
|
|
@ -35,7 +35,25 @@ describe('servicenow action params validation', () => {
|
|||
};
|
||||
|
||||
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: { ['subActionParams.incident.short_description']: [] },
|
||||
errors: {
|
||||
['subActionParams.incident.correlation_id']: [],
|
||||
['subActionParams.incident.short_description']: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test(`${SERVICENOW_ITSM_CONNECTOR_TYPE_ID}: action params validation succeeds for closeIncident subAction`, async () => {
|
||||
const connectorTypeModel = connectorTypeRegistry.get(SERVICENOW_ITSM_CONNECTOR_TYPE_ID);
|
||||
const actionParams = {
|
||||
subAction: 'closeIncident',
|
||||
subActionParams: { incident: { correlation_id: '{{test}}{{rule_id}}' } },
|
||||
};
|
||||
|
||||
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
['subActionParams.incident.correlation_id']: [],
|
||||
['subActionParams.incident.short_description']: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -47,8 +65,24 @@ describe('servicenow action params validation', () => {
|
|||
|
||||
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
['subActionParams.incident.correlation_id']: [],
|
||||
['subActionParams.incident.short_description']: ['Short description is required.'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test(`${SERVICENOW_ITSM_CONNECTOR_TYPE_ID}: params validation fails when correlation_id is not valid and subAction is closeIncident`, async () => {
|
||||
const connectorTypeModel = connectorTypeRegistry.get(SERVICENOW_ITSM_CONNECTOR_TYPE_ID);
|
||||
const actionParams = {
|
||||
subAction: 'closeIncident',
|
||||
subActionParams: { incident: { correlation_id: '' } },
|
||||
};
|
||||
|
||||
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
['subActionParams.incident.correlation_id']: ['Correlation id is required.'],
|
||||
['subActionParams.incident.short_description']: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,11 @@ import type {
|
|||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { ServiceNowConfig, ServiceNowSecrets } from '../lib/servicenow/types';
|
||||
import { ServiceNowITSMActionParams } from './types';
|
||||
import { getConnectorDescriptiveTitle, getSelectedConnectorIcon } from '../lib/servicenow/helpers';
|
||||
import {
|
||||
DEFAULT_CORRELATION_ID,
|
||||
getConnectorDescriptiveTitle,
|
||||
getSelectedConnectorIcon,
|
||||
} from '../lib/servicenow/helpers';
|
||||
|
||||
export const SERVICENOW_ITSM_DESC = i18n.translate(
|
||||
'xpack.stackConnectors.components.serviceNowITSM.selectMessageText',
|
||||
|
@ -46,6 +50,7 @@ export function getServiceNowITSMConnectorType(): ConnectorTypeModel<
|
|||
const translations = await import('../lib/servicenow/translations');
|
||||
const errors = {
|
||||
'subActionParams.incident.short_description': new Array<string>(),
|
||||
'subActionParams.incident.correlation_id': new Array<string>(),
|
||||
};
|
||||
const validationResult = {
|
||||
errors,
|
||||
|
@ -53,10 +58,20 @@ export function getServiceNowITSMConnectorType(): ConnectorTypeModel<
|
|||
if (
|
||||
actionParams.subActionParams &&
|
||||
actionParams.subActionParams.incident &&
|
||||
actionParams.subAction !== 'closeIncident' &&
|
||||
!actionParams.subActionParams.incident.short_description?.length
|
||||
) {
|
||||
errors['subActionParams.incident.short_description'].push(translations.TITLE_REQUIRED);
|
||||
}
|
||||
|
||||
if (
|
||||
actionParams.subAction === 'closeIncident' &&
|
||||
!actionParams?.subActionParams?.incident?.correlation_id?.length
|
||||
) {
|
||||
errors['subActionParams.incident.correlation_id'].push(
|
||||
translations.CORRELATION_ID_REQUIRED
|
||||
);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
actionParamsFields: lazy(() => import('./servicenow_itsm_params')),
|
||||
|
@ -64,5 +79,18 @@ export function getServiceNowITSMConnectorType(): ConnectorTypeModel<
|
|||
getText: getConnectorDescriptiveTitle,
|
||||
getComponent: getSelectedConnectorIcon,
|
||||
},
|
||||
defaultActionParams: {
|
||||
subAction: 'pushToService',
|
||||
subActionParams: {
|
||||
incident: { correlation_id: DEFAULT_CORRELATION_ID },
|
||||
comments: [],
|
||||
},
|
||||
},
|
||||
defaultRecoveredActionParams: {
|
||||
subAction: 'closeIncident',
|
||||
subActionParams: {
|
||||
incident: { correlation_id: DEFAULT_CORRELATION_ID },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import { useGetChoices } from '../lib/servicenow/use_get_choices';
|
|||
import ServiceNowITSMParamsFields from './servicenow_itsm_params';
|
||||
import { Choice } from '../lib/servicenow/types';
|
||||
import { merge } from 'lodash';
|
||||
import { ACTION_GROUP_RECOVERED } from '../lib/servicenow/helpers';
|
||||
|
||||
jest.mock('../lib/servicenow/use_get_choices');
|
||||
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana');
|
||||
|
@ -151,33 +152,6 @@ describe('ServiceNowITSMParamsFields renders', () => {
|
|||
expect(title.prop('isInvalid')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('When subActionParams is undefined, set to default', () => {
|
||||
const { subActionParams, ...newParams } = actionParams;
|
||||
|
||||
const newProps = {
|
||||
...defaultProps,
|
||||
actionParams: newParams,
|
||||
};
|
||||
mountWithIntl(<ServiceNowITSMParamsFields {...newProps} />);
|
||||
expect(editAction.mock.calls[0][1]).toEqual({
|
||||
incident: {
|
||||
correlation_id: '{{rule.id}}:{{alert.id}}',
|
||||
},
|
||||
comments: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('When subAction is undefined, set to default', () => {
|
||||
const { subAction, ...newParams } = actionParams;
|
||||
|
||||
const newProps = {
|
||||
...defaultProps,
|
||||
actionParams: newParams,
|
||||
};
|
||||
mountWithIntl(<ServiceNowITSMParamsFields {...newProps} />);
|
||||
expect(editAction.mock.calls[0][1]).toEqual('pushToService');
|
||||
});
|
||||
|
||||
test('Resets fields when connector changes', () => {
|
||||
const wrapper = mountWithIntl(<ServiceNowITSMParamsFields {...defaultProps} />);
|
||||
expect(editAction.mock.calls.length).toEqual(0);
|
||||
|
@ -191,6 +165,20 @@ describe('ServiceNowITSMParamsFields renders', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('Resets fields when connector changes and action group is recovered', () => {
|
||||
const newProps = {
|
||||
...defaultProps,
|
||||
selectedActionGroupId: ACTION_GROUP_RECOVERED,
|
||||
};
|
||||
const wrapper = mountWithIntl(<ServiceNowITSMParamsFields {...newProps} />);
|
||||
expect(editAction.mock.calls.length).toEqual(0);
|
||||
wrapper.setProps({ actionConnector: { ...connector, id: '1234' } });
|
||||
expect(editAction.mock.calls.length).toEqual(1);
|
||||
expect(editAction.mock.calls[0][1]).toEqual({
|
||||
incident: { correlation_id: '{{rule.id}}:{{alert.id}}' },
|
||||
});
|
||||
});
|
||||
|
||||
test('it transforms the categories to options correctly', async () => {
|
||||
const wrapper = mountWithIntl(<ServiceNowITSMParamsFields {...defaultProps} />);
|
||||
act(() => {
|
||||
|
@ -299,5 +287,57 @@ describe('ServiceNowITSMParamsFields renders', () => {
|
|||
expect(comments.simulate('change', changeEvent));
|
||||
expect(editAction.mock.calls[0][1].comments.length).toEqual(1);
|
||||
});
|
||||
|
||||
test('shows only correlation_id field when actionGroup is recovered', () => {
|
||||
const newProps = {
|
||||
...defaultProps,
|
||||
selectedActionGroupId: 'recovered',
|
||||
};
|
||||
const wrapper = mountWithIntl(<ServiceNowITSMParamsFields {...newProps} />);
|
||||
expect(wrapper.find('input[data-test-subj="correlation_idInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('input[data-test-subj="short_descriptionInput"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('A short description change triggers editAction', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ServiceNowITSMParamsFields
|
||||
actionParams={{}}
|
||||
errors={{ ['subActionParams.incident.short_description']: [] }}
|
||||
editAction={editAction}
|
||||
index={0}
|
||||
/>
|
||||
);
|
||||
|
||||
const shortDescriptionField = wrapper.find('input[data-test-subj="short_descriptionInput"]');
|
||||
shortDescriptionField.simulate('change', {
|
||||
target: { value: 'new updated short description' },
|
||||
});
|
||||
|
||||
expect(editAction.mock.calls[0][1]).toEqual({
|
||||
incident: { short_description: 'new updated short description' },
|
||||
comments: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('A correlation_id field change triggers edit action correctly when actionGroup is recovered', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ServiceNowITSMParamsFields
|
||||
selectedActionGroupId={'recovered'}
|
||||
actionParams={{}}
|
||||
errors={{ ['subActionParams.incident.short_description']: [] }}
|
||||
editAction={editAction}
|
||||
index={0}
|
||||
/>
|
||||
);
|
||||
const correlationIdField = wrapper.find('input[data-test-subj="correlation_idInput"]');
|
||||
|
||||
correlationIdField.simulate('change', {
|
||||
target: { value: 'updated correlation id' },
|
||||
});
|
||||
|
||||
expect(editAction.mock.calls[0][1]).toEqual({
|
||||
incident: { correlation_id: 'updated correlation id' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,7 +25,11 @@ import {
|
|||
import { Choice, Fields } from '../lib/servicenow/types';
|
||||
import { ServiceNowITSMActionParams } from './types';
|
||||
import { useGetChoices } from '../lib/servicenow/use_get_choices';
|
||||
import { choicesToEuiOptions, DEFAULT_CORRELATION_ID } from '../lib/servicenow/helpers';
|
||||
import {
|
||||
ACTION_GROUP_RECOVERED,
|
||||
choicesToEuiOptions,
|
||||
DEFAULT_CORRELATION_ID,
|
||||
} from '../lib/servicenow/helpers';
|
||||
|
||||
import * as i18n from '../lib/servicenow/translations';
|
||||
|
||||
|
@ -39,11 +43,50 @@ const defaultFields: Fields = {
|
|||
priority: [],
|
||||
};
|
||||
|
||||
const CorrelationIdField: React.FunctionComponent<
|
||||
Pick<ActionParamsProps<ServiceNowITSMActionParams>, 'index' | 'messageVariables'> & {
|
||||
correlationId: string | null;
|
||||
editSubActionProperty: (key: string, value: any) => void;
|
||||
}
|
||||
> = ({ index, messageVariables, correlationId, editSubActionProperty }) => {
|
||||
const { docLinks } = useKibana().services;
|
||||
return (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.CORRELATION_ID}
|
||||
helpText={
|
||||
<EuiLink href={docLinks.links.alerting.serviceNowAction} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.stackConnectors.components.serviceNow.correlationIDHelpLabel"
|
||||
defaultMessage="Identifier for updating incidents"
|
||||
/>
|
||||
</EuiLink>
|
||||
}
|
||||
>
|
||||
<TextFieldWithMessageVariables
|
||||
index={index}
|
||||
editAction={editSubActionProperty}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'correlation_id'}
|
||||
inputTargetValue={correlationId ?? undefined}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
const ServiceNowParamsFields: React.FunctionComponent<
|
||||
ActionParamsProps<ServiceNowITSMActionParams>
|
||||
> = ({ actionConnector, actionParams, editAction, index, errors, messageVariables }) => {
|
||||
> = (props) => {
|
||||
const {
|
||||
actionConnector,
|
||||
actionParams,
|
||||
editAction,
|
||||
index,
|
||||
errors,
|
||||
messageVariables,
|
||||
selectedActionGroupId,
|
||||
} = props;
|
||||
const {
|
||||
docLinks,
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
@ -56,9 +99,9 @@ const ServiceNowParamsFields: React.FunctionComponent<
|
|||
actionParams.subActionParams ??
|
||||
({
|
||||
incident: {},
|
||||
comments: [],
|
||||
comments: selectedActionGroupId !== ACTION_GROUP_RECOVERED ? [] : undefined,
|
||||
} as unknown as ServiceNowITSMActionParams['subActionParams']),
|
||||
[actionParams.subActionParams]
|
||||
[actionParams.subActionParams, selectedActionGroupId]
|
||||
);
|
||||
|
||||
const [choices, setChoices] = useState<Fields>(defaultFields);
|
||||
|
@ -122,6 +165,16 @@ const ServiceNowParamsFields: React.FunctionComponent<
|
|||
useEffect(() => {
|
||||
if (actionConnector != null && actionConnectorRef.current !== actionConnector.id) {
|
||||
actionConnectorRef.current = actionConnector.id;
|
||||
if (selectedActionGroupId === ACTION_GROUP_RECOVERED) {
|
||||
editAction(
|
||||
'subActionParams',
|
||||
{ incident: { correlation_id: DEFAULT_CORRELATION_ID } },
|
||||
index
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
editAction(
|
||||
'subActionParams',
|
||||
{
|
||||
|
@ -134,196 +187,176 @@ const ServiceNowParamsFields: React.FunctionComponent<
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [actionConnector]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!actionParams.subAction) {
|
||||
editAction('subAction', 'pushToService', index);
|
||||
}
|
||||
if (!actionParams.subActionParams) {
|
||||
editAction(
|
||||
'subActionParams',
|
||||
{
|
||||
incident: { correlation_id: DEFAULT_CORRELATION_ID },
|
||||
comments: [],
|
||||
},
|
||||
index
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [actionParams]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="s">
|
||||
<h3>{i18n.INCIDENT}</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow fullWidth label={i18n.URGENCY_LABEL}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="urgencySelect"
|
||||
hasNoInitialSelection
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={urgencyOptions}
|
||||
value={incident.urgency ?? ''}
|
||||
onChange={(e) => editSubActionProperty('urgency', e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth label={i18n.SEVERITY_LABEL}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="severitySelect"
|
||||
hasNoInitialSelection
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={severityOptions}
|
||||
value={incident.severity ?? ''}
|
||||
onChange={(e) => editSubActionProperty('severity', e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth label={i18n.IMPACT_LABEL}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="impactSelect"
|
||||
hasNoInitialSelection
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={impactOptions}
|
||||
value={incident.impact ?? ''}
|
||||
onChange={(e) => editSubActionProperty('impact', e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth label={i18n.CATEGORY_LABEL}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="categorySelect"
|
||||
hasNoInitialSelection
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={categoryOptions}
|
||||
value={incident.category ?? undefined}
|
||||
onChange={(e) => {
|
||||
editAction(
|
||||
'subActionParams',
|
||||
{
|
||||
incident: { ...incident, category: e.target.value, subcategory: null },
|
||||
comments,
|
||||
},
|
||||
index
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{subcategoryOptions?.length > 0 ? (
|
||||
<EuiFormRow fullWidth label={i18n.SUBCATEGORY_LABEL}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="subcategorySelect"
|
||||
hasNoInitialSelection
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={subcategoryOptions}
|
||||
// Needs an empty string instead of undefined to select the blank option when changing categories
|
||||
value={incident.subcategory ?? ''}
|
||||
onChange={(e) => editSubActionProperty('subcategory', e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
{!isDeprecatedActionConnector && (
|
||||
{selectedActionGroupId !== ACTION_GROUP_RECOVERED ? (
|
||||
<>
|
||||
<EuiFormRow fullWidth label={i18n.URGENCY_LABEL}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="urgencySelect"
|
||||
hasNoInitialSelection
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={urgencyOptions}
|
||||
value={incident.urgency ?? ''}
|
||||
onChange={(e) => editSubActionProperty('urgency', e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.CORRELATION_ID}
|
||||
helpText={
|
||||
<EuiLink href={docLinks.links.alerting.serviceNowAction} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.stackConnectors.components.serviceNow.correlationIDHelpLabel"
|
||||
defaultMessage="Identifier for updating incidents"
|
||||
/>
|
||||
</EuiLink>
|
||||
}
|
||||
>
|
||||
<TextFieldWithMessageVariables
|
||||
index={index}
|
||||
editAction={editSubActionProperty}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'correlation_id'}
|
||||
inputTargetValue={incident?.correlation_id ?? undefined}
|
||||
<EuiFormRow fullWidth label={i18n.SEVERITY_LABEL}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="severitySelect"
|
||||
hasNoInitialSelection
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={severityOptions}
|
||||
value={incident.severity ?? ''}
|
||||
onChange={(e) => editSubActionProperty('severity', e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth label={i18n.CORRELATION_DISPLAY}>
|
||||
<TextFieldWithMessageVariables
|
||||
index={index}
|
||||
editAction={editSubActionProperty}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'correlation_display'}
|
||||
inputTargetValue={incident?.correlation_display ?? undefined}
|
||||
<EuiFormRow fullWidth label={i18n.IMPACT_LABEL}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="impactSelect"
|
||||
hasNoInitialSelection
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={impactOptions}
|
||||
value={incident.impact ?? ''}
|
||||
onChange={(e) => editSubActionProperty('impact', e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth label={i18n.CATEGORY_LABEL}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="categorySelect"
|
||||
hasNoInitialSelection
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={categoryOptions}
|
||||
value={incident.category ?? undefined}
|
||||
onChange={(e) => {
|
||||
editAction(
|
||||
'subActionParams',
|
||||
{
|
||||
incident: { ...incident, category: e.target.value, subcategory: null },
|
||||
comments,
|
||||
},
|
||||
index
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{subcategoryOptions?.length > 0 ? (
|
||||
<EuiFormRow fullWidth label={i18n.SUBCATEGORY_LABEL}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="subcategorySelect"
|
||||
hasNoInitialSelection
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={subcategoryOptions}
|
||||
// Needs an empty string instead of undefined to select the blank option when changing categories
|
||||
value={incident.subcategory ?? ''}
|
||||
onChange={(e) => editSubActionProperty('subcategory', e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
{!isDeprecatedActionConnector && (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<CorrelationIdField
|
||||
index={index}
|
||||
messageVariables={messageVariables}
|
||||
correlationId={incident.correlation_id}
|
||||
editSubActionProperty={editSubActionProperty}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth label={i18n.CORRELATION_DISPLAY}>
|
||||
<TextFieldWithMessageVariables
|
||||
index={index}
|
||||
editAction={editSubActionProperty}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'correlation_display'}
|
||||
inputTargetValue={incident?.correlation_display ?? undefined}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
)}
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
error={errors['subActionParams.incident.short_description']}
|
||||
isInvalid={
|
||||
errors['subActionParams.incident.short_description'] !== undefined &&
|
||||
errors['subActionParams.incident.short_description'].length > 0 &&
|
||||
incident.short_description !== undefined
|
||||
}
|
||||
label={i18n.SHORT_DESCRIPTION_LABEL}
|
||||
>
|
||||
<TextFieldWithMessageVariables
|
||||
index={index}
|
||||
editAction={editSubActionProperty}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'short_description'}
|
||||
inputTargetValue={incident?.short_description ?? undefined}
|
||||
errors={errors['subActionParams.incident.short_description'] as string[]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<TextAreaWithMessageVariables
|
||||
index={index}
|
||||
editAction={editSubActionProperty}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'description'}
|
||||
inputTargetValue={incident.description ?? undefined}
|
||||
label={i18n.DESCRIPTION_LABEL}
|
||||
/>
|
||||
<TextAreaWithMessageVariables
|
||||
index={index}
|
||||
editAction={editComment}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'comments'}
|
||||
inputTargetValue={comments && comments.length > 0 ? comments[0].comment : undefined}
|
||||
label={i18n.COMMENTS_LABEL}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<CorrelationIdField
|
||||
index={index}
|
||||
messageVariables={messageVariables}
|
||||
correlationId={incident.correlation_id}
|
||||
editSubActionProperty={editSubActionProperty}
|
||||
/>
|
||||
)}
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
error={errors['subActionParams.incident.short_description']}
|
||||
isInvalid={
|
||||
errors['subActionParams.incident.short_description'] !== undefined &&
|
||||
errors['subActionParams.incident.short_description'].length > 0 &&
|
||||
incident.short_description !== undefined
|
||||
}
|
||||
label={i18n.SHORT_DESCRIPTION_LABEL}
|
||||
>
|
||||
<TextFieldWithMessageVariables
|
||||
index={index}
|
||||
editAction={editSubActionProperty}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'short_description'}
|
||||
inputTargetValue={incident?.short_description ?? undefined}
|
||||
errors={errors['subActionParams.incident.short_description'] as string[]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<TextAreaWithMessageVariables
|
||||
index={index}
|
||||
editAction={editSubActionProperty}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'description'}
|
||||
inputTargetValue={incident.description ?? undefined}
|
||||
label={i18n.DESCRIPTION_LABEL}
|
||||
/>
|
||||
<TextAreaWithMessageVariables
|
||||
index={index}
|
||||
editAction={editComment}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'comments'}
|
||||
inputTargetValue={comments && comments.length > 0 ? comments[0].comment : undefined}
|
||||
label={i18n.COMMENTS_LABEL}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -356,6 +356,84 @@ describe('api', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('close incident', () => {
|
||||
test('it closes an incident with incidentId', async () => {
|
||||
const res = await api.closeIncident({
|
||||
externalService,
|
||||
params: {
|
||||
incident: {
|
||||
externalId: apiParams.incident.externalId,
|
||||
correlation_id: null,
|
||||
},
|
||||
},
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
id: 'incident-2',
|
||||
title: 'INC02',
|
||||
pushedDate: '2020-03-10T12:24:20.000Z',
|
||||
url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123',
|
||||
});
|
||||
});
|
||||
|
||||
test('it closes an incident with correlation_id', async () => {
|
||||
const res = await api.closeIncident({
|
||||
externalService,
|
||||
params: {
|
||||
incident: {
|
||||
externalId: null,
|
||||
correlation_id: apiParams.incident.correlation_id,
|
||||
},
|
||||
},
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
id: 'incident-2',
|
||||
title: 'INC02',
|
||||
pushedDate: '2020-03-10T12:24:20.000Z',
|
||||
url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123',
|
||||
});
|
||||
});
|
||||
|
||||
test('it calls closeIncident correctly', async () => {
|
||||
await api.closeIncident({
|
||||
externalService,
|
||||
params: {
|
||||
incident: {
|
||||
externalId: apiParams.incident.externalId,
|
||||
correlation_id: null,
|
||||
},
|
||||
},
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
||||
expect(externalService.closeIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
correlationId: null,
|
||||
});
|
||||
});
|
||||
|
||||
test('it calls closeIncident correctly with correlation_id', async () => {
|
||||
await api.closeIncident({
|
||||
externalService,
|
||||
params: {
|
||||
incident: {
|
||||
externalId: null,
|
||||
correlation_id: apiParams.incident.correlation_id,
|
||||
},
|
||||
},
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
||||
expect(externalService.closeIncident).toHaveBeenCalledWith({
|
||||
incidentId: null,
|
||||
correlationId: 'ruleId',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFields', () => {
|
||||
test('it returns the fields correctly', async () => {
|
||||
const res = await api.getFields({
|
||||
|
|
|
@ -16,6 +16,8 @@ import {
|
|||
Incident,
|
||||
PushToServiceApiHandlerArgs,
|
||||
PushToServiceResponse,
|
||||
CloseIncidentApiHandlerArgs,
|
||||
ExternalServiceIncidentResponse,
|
||||
} from './types';
|
||||
|
||||
const handshakeHandler = async ({ externalService, params }: HandshakeApiHandlerArgs) => {};
|
||||
|
@ -74,6 +76,20 @@ const pushToServiceHandler = async ({
|
|||
return res;
|
||||
};
|
||||
|
||||
const closeIncidentHandler = async ({
|
||||
externalService,
|
||||
params,
|
||||
}: CloseIncidentApiHandlerArgs): Promise<ExternalServiceIncidentResponse | null> => {
|
||||
const { externalId, correlation_id: correlationId } = params.incident;
|
||||
|
||||
const res = await externalService.closeIncident({
|
||||
correlationId,
|
||||
incidentId: externalId,
|
||||
});
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
const getFieldsHandler = async ({
|
||||
externalService,
|
||||
}: GetCommonFieldsHandlerArgs): Promise<GetCommonFieldsResponse> => {
|
||||
|
@ -95,4 +111,5 @@ export const api: ExternalServiceAPI = {
|
|||
getIncident: getIncidentHandler,
|
||||
handshake: handshakeHandler,
|
||||
pushToService: pushToServiceHandler,
|
||||
closeIncident: closeIncidentHandler,
|
||||
};
|
||||
|
|
|
@ -91,6 +91,16 @@ const createMock = (): jest.Mocked<ExternalService> => {
|
|||
description: 'description from servicenow',
|
||||
})
|
||||
),
|
||||
getIncidentByCorrelationId: jest.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
id: 'incident-1',
|
||||
title: 'INC01',
|
||||
pushedDate: '2020-03-10T12:24:20.000Z',
|
||||
url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123',
|
||||
short_description: 'title from servicenow',
|
||||
description: 'description from servicenow',
|
||||
})
|
||||
),
|
||||
createIncident: jest.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
id: 'incident-1',
|
||||
|
@ -107,6 +117,14 @@ const createMock = (): jest.Mocked<ExternalService> => {
|
|||
url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123',
|
||||
})
|
||||
),
|
||||
closeIncident: jest.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
id: 'incident-2',
|
||||
title: 'INC02',
|
||||
pushedDate: '2020-03-10T12:24:20.000Z',
|
||||
url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123',
|
||||
})
|
||||
),
|
||||
findIncidents: jest.fn(),
|
||||
getApplicationInformation: jest.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
|
|
|
@ -115,6 +115,13 @@ export const ExecutorSubActionGetIncidentParamsSchema = schema.object({
|
|||
externalId: schema.string(),
|
||||
});
|
||||
|
||||
export const ExecutorSubActionCloseIncidentParamsSchema = schema.object({
|
||||
incident: schema.object({
|
||||
externalId: schema.nullable(schema.string()),
|
||||
correlation_id: schema.nullable(schema.string({ defaultValue: DEFAULT_ALERTS_GROUPING_KEY })),
|
||||
}),
|
||||
});
|
||||
|
||||
// Reserved for future implementation
|
||||
export const ExecutorSubActionHandshakeParamsSchema = schema.object({});
|
||||
export const ExecutorSubActionCommonFieldsParamsSchema = schema.object({});
|
||||
|
@ -144,6 +151,10 @@ export const ExecutorParamsSchemaITSM = schema.oneOf([
|
|||
subAction: schema.literal('getChoices'),
|
||||
subActionParams: ExecutorSubActionGetChoicesParamsSchema,
|
||||
}),
|
||||
schema.object({
|
||||
subAction: schema.literal('closeIncident'),
|
||||
subActionParams: ExecutorSubActionCloseIncidentParamsSchema,
|
||||
}),
|
||||
]);
|
||||
|
||||
// Executor parameters for ServiceNow Security Incident Response (SIR)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import axios, { AxiosError, AxiosResponse } from 'axios';
|
||||
|
||||
import { createExternalService } from './service';
|
||||
import * as utils from '@kbn/actions-plugin/server/lib/axios_utils';
|
||||
|
@ -17,7 +17,10 @@ import { serviceNowCommonFields, serviceNowChoices } from './mocks';
|
|||
import { snExternalServiceConfig } from './config';
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
jest.mock('axios');
|
||||
jest.mock('axios', () => ({
|
||||
create: jest.fn(),
|
||||
AxiosError: jest.requireActual('axios').AxiosError,
|
||||
}));
|
||||
jest.mock('@kbn/actions-plugin/server/lib/axios_utils', () => {
|
||||
const originalUtils = jest.requireActual('@kbn/actions-plugin/server/lib/axios_utils');
|
||||
return {
|
||||
|
@ -73,7 +76,7 @@ const mockImportIncident = (update: boolean) =>
|
|||
}));
|
||||
|
||||
const mockIncidentResponse = (update: boolean) =>
|
||||
requestMock.mockImplementation(() => ({
|
||||
requestMock.mockImplementationOnce(() => ({
|
||||
data: {
|
||||
result: {
|
||||
sys_id: '1',
|
||||
|
@ -85,6 +88,19 @@ const mockIncidentResponse = (update: boolean) =>
|
|||
},
|
||||
}));
|
||||
|
||||
const mockCorrelationIdIncidentResponse = () =>
|
||||
requestMock.mockImplementationOnce(() => ({
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
sys_id: '1',
|
||||
number: 'INC01',
|
||||
sys_updated_on: '2020-03-10 12:24:20',
|
||||
},
|
||||
],
|
||||
},
|
||||
}));
|
||||
|
||||
const createIncident = async (service: ExternalService) => {
|
||||
// Get application version
|
||||
mockApplicationVersion();
|
||||
|
@ -112,6 +128,35 @@ const updateIncident = async (service: ExternalService) => {
|
|||
});
|
||||
};
|
||||
|
||||
const closeIncident = async ({
|
||||
service,
|
||||
incidentId,
|
||||
correlationId,
|
||||
}: {
|
||||
service: ExternalService;
|
||||
incidentId: string | null;
|
||||
correlationId: string | null;
|
||||
}) => {
|
||||
// Get incident response
|
||||
if (incidentId) {
|
||||
mockIncidentResponse(false);
|
||||
} else if (correlationId) {
|
||||
// get incident by correlationId response
|
||||
mockCorrelationIdIncidentResponse();
|
||||
}
|
||||
// Get application version
|
||||
mockApplicationVersion();
|
||||
// Import set api response
|
||||
mockImportIncident(true);
|
||||
// Get incident response
|
||||
mockIncidentResponse(true);
|
||||
|
||||
return await service.closeIncident({
|
||||
incidentId: incidentId ?? null,
|
||||
correlationId: correlationId ?? null,
|
||||
});
|
||||
};
|
||||
|
||||
const expectImportedIncident = (update: boolean) => {
|
||||
expect(requestMock).toHaveBeenNthCalledWith(1, {
|
||||
axios,
|
||||
|
@ -439,7 +484,7 @@ describe('ServiceNow service', () => {
|
|||
throw new Error('An error has occurred');
|
||||
});
|
||||
await expect(service.getIncident('1')).rejects.toThrow(
|
||||
'Unable to get incident with id 1. Error: An error has occurred'
|
||||
'[Action][ServiceNow]: Unable to get incident with id 1. Error: An error has occurred Reason: unknown: errorResponse was null'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -455,6 +500,88 @@ describe('ServiceNow service', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getIncidentByCorrelationId', () => {
|
||||
test('it returns the incident correctly', async () => {
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: { result: [{ sys_id: '1', number: 'INC01' }] },
|
||||
}));
|
||||
const res = await service.getIncidentByCorrelationId('custom_correlation_id');
|
||||
expect(res).toEqual({ sys_id: '1', number: 'INC01' });
|
||||
});
|
||||
|
||||
test('it should call request with correct arguments', async () => {
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: { result: [{ sys_id: '1', number: 'INC01' }] },
|
||||
}));
|
||||
|
||||
await service.getIncidentByCorrelationId('custom_correlation_id');
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/incident?sysparm_query=ORDERBYDESCsys_created_on^correlation_id=custom_correlation_id',
|
||||
method: 'get',
|
||||
});
|
||||
});
|
||||
|
||||
test('it should return null if response is empty', async () => {
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: { result: [] },
|
||||
}));
|
||||
|
||||
const res = await service.getIncidentByCorrelationId('custom_correlation_id');
|
||||
|
||||
expect(requestMock).toHaveBeenCalledTimes(1);
|
||||
expect(res).toBe(null);
|
||||
});
|
||||
|
||||
test('it should call request with correct arguments when table changes', async () => {
|
||||
service = createExternalService({
|
||||
credentials: {
|
||||
config: { apiUrl: 'https://example.com/', isOAuth: false },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' },
|
||||
axiosInstance: axios,
|
||||
});
|
||||
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: { result: [{ sys_id: '1', number: 'INC01' }] },
|
||||
}));
|
||||
|
||||
await service.getIncidentByCorrelationId('custom_correlation_id');
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/sn_si_incident?sysparm_query=ORDERBYDESCsys_created_on^correlation_id=custom_correlation_id',
|
||||
method: 'get',
|
||||
});
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
requestMock.mockImplementationOnce(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
await expect(service.getIncidentByCorrelationId('custom_correlation_id')).rejects.toThrow(
|
||||
'[Action][ServiceNow]: Unable to get incident by correlation ID custom_correlation_id. Error: An error has occurred Reason: unknown: errorResponse was null'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw an error when instance is not alive', async () => {
|
||||
requestMock.mockImplementationOnce(() => ({
|
||||
status: 200,
|
||||
data: {},
|
||||
request: { connection: { servername: 'Developer instance' } },
|
||||
}));
|
||||
await expect(service.getIncident('1')).rejects.toThrow(
|
||||
'There is an issue with your Service Now Instance. Please check Developer instance.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createIncident', () => {
|
||||
// new connectors
|
||||
describe('import set table', () => {
|
||||
|
@ -574,6 +701,8 @@ describe('ServiceNow service', () => {
|
|||
|
||||
test('it creates the incident correctly', async () => {
|
||||
mockIncidentResponse(false);
|
||||
mockIncidentResponse(false);
|
||||
|
||||
const res = await service.createIncident({
|
||||
incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
|
||||
});
|
||||
|
@ -608,6 +737,7 @@ describe('ServiceNow service', () => {
|
|||
axiosInstance: axios,
|
||||
});
|
||||
|
||||
mockIncidentResponse(false);
|
||||
mockIncidentResponse(false);
|
||||
|
||||
const res = await service.createIncident({
|
||||
|
@ -749,6 +879,8 @@ describe('ServiceNow service', () => {
|
|||
|
||||
test('it updates the incident correctly', async () => {
|
||||
mockIncidentResponse(true);
|
||||
mockIncidentResponse(true);
|
||||
|
||||
const res = await service.updateIncident({
|
||||
incidentId: '1',
|
||||
incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
|
||||
|
@ -785,6 +917,7 @@ describe('ServiceNow service', () => {
|
|||
});
|
||||
|
||||
mockIncidentResponse(false);
|
||||
mockIncidentResponse(true);
|
||||
|
||||
const res = await service.updateIncident({
|
||||
incidentId: '1',
|
||||
|
@ -805,6 +938,311 @@ describe('ServiceNow service', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('closeIncident', () => {
|
||||
// new connectors
|
||||
describe('import set table', () => {
|
||||
test('it closes the incident correctly with incident id', async () => {
|
||||
const res = await closeIncident({ service, incidentId: '1', correlationId: null });
|
||||
|
||||
expect(res).toEqual({
|
||||
title: 'INC01',
|
||||
id: '1',
|
||||
pushedDate: '2020-03-10T12:24:20.000Z',
|
||||
url: 'https://example.com/nav_to.do?uri=incident.do?sys_id=1',
|
||||
});
|
||||
});
|
||||
|
||||
test('it should call request with correct arguments with incidentId', async () => {
|
||||
const res = await closeIncident({ service, incidentId: '1', correlationId: null });
|
||||
expect(requestMock).toHaveBeenCalledTimes(4);
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(1, {
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/incident/1',
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(2, {
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/x_elas2_inc_int/elastic_api/health',
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(3, {
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/import/x_elas2_inc_int_elastic_incident',
|
||||
method: 'post',
|
||||
data: {
|
||||
elastic_incident_id: '1',
|
||||
u_close_code: 'Closed/Resolved by Caller',
|
||||
u_state: '7',
|
||||
u_close_notes: 'Closed by Caller',
|
||||
},
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(4, {
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/incident/1',
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
expect(res?.url).toEqual('https://example.com/nav_to.do?uri=incident.do?sys_id=1');
|
||||
});
|
||||
|
||||
test('it closes the incident correctly with correlation id', async () => {
|
||||
const res = await closeIncident({
|
||||
service,
|
||||
incidentId: null,
|
||||
correlationId: 'custom_correlation_id',
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
title: 'INC01',
|
||||
id: '1',
|
||||
pushedDate: '2020-03-10T12:24:20.000Z',
|
||||
url: 'https://example.com/nav_to.do?uri=incident.do?sys_id=1',
|
||||
});
|
||||
});
|
||||
|
||||
test('it should call request with correct arguments with correlationId', async () => {
|
||||
const res = await closeIncident({
|
||||
service,
|
||||
incidentId: null,
|
||||
correlationId: 'custom_correlation_id',
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenCalledTimes(4);
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(1, {
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/incident?sysparm_query=ORDERBYDESCsys_created_on^correlation_id=custom_correlation_id',
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(2, {
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/x_elas2_inc_int/elastic_api/health',
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(3, {
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/import/x_elas2_inc_int_elastic_incident',
|
||||
method: 'post',
|
||||
data: {
|
||||
elastic_incident_id: '1',
|
||||
u_close_code: 'Closed/Resolved by Caller',
|
||||
u_state: '7',
|
||||
u_close_notes: 'Closed by Caller',
|
||||
},
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(4, {
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/incident/1',
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
expect(res?.url).toEqual('https://example.com/nav_to.do?uri=incident.do?sys_id=1');
|
||||
});
|
||||
|
||||
test('it should throw an error when the incidentId and correlation Id are null', async () => {
|
||||
await expect(
|
||||
service.closeIncident({ incidentId: null, correlationId: null })
|
||||
).rejects.toThrow(
|
||||
'[Action][ServiceNow]: Unable to close incident. Error: No correlationId or incidentId found. Reason: unknown: errorResponse was null'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw an error when the no incidents found with given incidentId ', async () => {
|
||||
const axiosError = {
|
||||
message: 'Request failed with status code 404',
|
||||
response: { status: 404 },
|
||||
} as AxiosError;
|
||||
|
||||
requestMock.mockImplementation(() => {
|
||||
throw axiosError;
|
||||
});
|
||||
|
||||
const res = await service.closeIncident({
|
||||
incidentId: 'xyz',
|
||||
correlationId: null,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenCalledTimes(1);
|
||||
expect(logger.warn.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"[ServiceNow][CloseIncident] No incident found with incidentId: xyz.",
|
||||
]
|
||||
`);
|
||||
expect(res).toBeNull();
|
||||
});
|
||||
|
||||
test('it should log warning if found incident is closed', async () => {
|
||||
requestMock.mockImplementationOnce(() => ({
|
||||
data: {
|
||||
result: {
|
||||
sys_id: '1',
|
||||
number: 'INC01',
|
||||
state: '7',
|
||||
sys_created_on: '2020-03-10 12:24:20',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
await service.closeIncident({ incidentId: '1', correlationId: null });
|
||||
|
||||
expect(requestMock).toHaveBeenCalledTimes(1);
|
||||
expect(logger.warn.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"[ServiceNow][CloseIncident] Incident with correlation_id: null or incidentId: 1 is closed.",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('it should return null if found incident with correlation id is null', async () => {
|
||||
requestMock.mockImplementationOnce(() => ({
|
||||
data: {
|
||||
result: [],
|
||||
},
|
||||
}));
|
||||
|
||||
const res = await service.closeIncident({
|
||||
incidentId: null,
|
||||
correlationId: 'bar',
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenCalledTimes(1);
|
||||
expect(logger.warn.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"[ServiceNow][CloseIncident] No incident found with correlation_id: bar or incidentId: null.",
|
||||
]
|
||||
`);
|
||||
expect(res).toBeNull();
|
||||
});
|
||||
|
||||
test('it should throw an error when instance is not alive', async () => {
|
||||
mockIncidentResponse(false);
|
||||
requestMock.mockImplementation(() => ({
|
||||
status: 200,
|
||||
data: {},
|
||||
request: { connection: { servername: 'Developer instance' } },
|
||||
}));
|
||||
await expect(
|
||||
service.closeIncident({
|
||||
incidentId: '1',
|
||||
correlationId: null,
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'There is an issue with your Service Now Instance. Please check Developer instance.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// old connectors
|
||||
describe('table API', () => {
|
||||
beforeEach(() => {
|
||||
service = createExternalService({
|
||||
credentials: {
|
||||
config: { apiUrl: 'https://example.com/', isOAuth: false },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], useImportAPI: false },
|
||||
axiosInstance: axios,
|
||||
});
|
||||
});
|
||||
|
||||
test('it closes the incident correctly', async () => {
|
||||
mockIncidentResponse(false);
|
||||
mockImportIncident(true);
|
||||
mockIncidentResponse(true);
|
||||
|
||||
const res = await service.closeIncident({
|
||||
incidentId: '1',
|
||||
correlationId: null,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
title: 'INC01',
|
||||
id: '1',
|
||||
pushedDate: '2020-03-10T12:24:20.000Z',
|
||||
url: 'https://example.com/nav_to.do?uri=incident.do?sys_id=1',
|
||||
});
|
||||
});
|
||||
|
||||
test('it should call request with correct arguments when table changes', async () => {
|
||||
service = createExternalService({
|
||||
credentials: {
|
||||
config: { apiUrl: 'https://example.com/', isOAuth: false },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow-sir'], useImportAPI: false },
|
||||
axiosInstance: axios,
|
||||
});
|
||||
|
||||
mockIncidentResponse(false);
|
||||
mockIncidentResponse(true);
|
||||
mockIncidentResponse(true);
|
||||
|
||||
const res = await service.closeIncident({
|
||||
incidentId: '1',
|
||||
correlationId: null,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(1, {
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(2, {
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
|
||||
method: 'patch',
|
||||
data: {
|
||||
close_code: 'Closed/Resolved by Caller',
|
||||
state: '7',
|
||||
close_notes: 'Closed by Caller',
|
||||
},
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(3, {
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
expect(res?.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFields', () => {
|
||||
test('it should call request with correct arguments', async () => {
|
||||
requestMock.mockImplementation(() => ({
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
ServiceNowIncident,
|
||||
GetApplicationInfoResponse,
|
||||
ServiceFactory,
|
||||
ExternalServiceParamsClose,
|
||||
} from './types';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
@ -75,6 +76,10 @@ export const createExternalService: ServiceFactory = ({
|
|||
return `${urlWithoutTrailingSlash}/nav_to.do?uri=${table}.do?sys_id=${id}`;
|
||||
};
|
||||
|
||||
const getIncidentByCorrelationIdUrl = (correlationId: string) => {
|
||||
return `${tableApiIncidentUrl}?sysparm_query=ORDERBYDESCsys_created_on^correlation_id=${correlationId}`;
|
||||
};
|
||||
|
||||
const getChoicesURL = (fields: string[]) => {
|
||||
const elements = fields
|
||||
.slice(1)
|
||||
|
@ -169,6 +174,7 @@ export const createExternalService: ServiceFactory = ({
|
|||
params,
|
||||
configurationUtilities,
|
||||
});
|
||||
|
||||
checkInstance(res);
|
||||
return res.data.result.length > 0 ? { ...res.data.result } : undefined;
|
||||
} catch (error) {
|
||||
|
@ -249,6 +255,87 @@ export const createExternalService: ServiceFactory = ({
|
|||
}
|
||||
};
|
||||
|
||||
const getIncidentByCorrelationId = async (
|
||||
correlationId: string
|
||||
): Promise<ServiceNowIncident | null> => {
|
||||
try {
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
url: getIncidentByCorrelationIdUrl(correlationId),
|
||||
method: 'get',
|
||||
logger,
|
||||
configurationUtilities,
|
||||
});
|
||||
|
||||
checkInstance(res);
|
||||
|
||||
const foundIncident = res.data.result[0] ?? null;
|
||||
|
||||
return foundIncident;
|
||||
} catch (error) {
|
||||
throw createServiceError(error, `Unable to get incident by correlation ID ${correlationId}`);
|
||||
}
|
||||
};
|
||||
|
||||
const closeIncident = async (params: ExternalServiceParamsClose) => {
|
||||
try {
|
||||
const { correlationId, incidentId } = params;
|
||||
let incidentToBeClosed = null;
|
||||
|
||||
if (correlationId == null && incidentId == null) {
|
||||
throw new Error('No correlationId or incidentId found.');
|
||||
}
|
||||
|
||||
if (incidentId) {
|
||||
incidentToBeClosed = await getIncident(incidentId);
|
||||
} else if (correlationId) {
|
||||
incidentToBeClosed = await getIncidentByCorrelationId(correlationId);
|
||||
}
|
||||
|
||||
if (incidentToBeClosed === null) {
|
||||
logger.warn(
|
||||
`[ServiceNow][CloseIncident] No incident found with correlation_id: ${correlationId} or incidentId: ${incidentId}.`
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (incidentToBeClosed.state === '7') {
|
||||
logger.warn(
|
||||
`[ServiceNow][CloseIncident] Incident with correlation_id: ${correlationId} or incidentId: ${incidentId} is closed.`
|
||||
);
|
||||
|
||||
return {
|
||||
title: incidentToBeClosed.number,
|
||||
id: incidentToBeClosed.sys_id,
|
||||
pushedDate: getPushedDate(incidentToBeClosed.sys_updated_on),
|
||||
url: getIncidentViewURL(incidentToBeClosed.sys_id),
|
||||
};
|
||||
}
|
||||
|
||||
const closedIncident = await updateIncident({
|
||||
incidentId: incidentToBeClosed.sys_id,
|
||||
incident: {
|
||||
state: '7', // used for "closed" status in serviceNow
|
||||
close_code: 'Closed/Resolved by Caller',
|
||||
close_notes: 'Closed by Caller',
|
||||
},
|
||||
});
|
||||
|
||||
return closedIncident;
|
||||
} catch (error) {
|
||||
if (error?.response?.status === 404) {
|
||||
logger.warn(
|
||||
`[ServiceNow][CloseIncident] No incident found with incidentId: ${params.incidentId}.`
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
throw createServiceError(error, 'Unable to close incident');
|
||||
}
|
||||
};
|
||||
|
||||
const getFields = async () => {
|
||||
try {
|
||||
const res = await request({
|
||||
|
@ -292,5 +379,7 @@ export const createExternalService: ServiceFactory = ({
|
|||
checkInstance,
|
||||
getApplicationInformation,
|
||||
checkIfApplicationIsInstalled,
|
||||
closeIncident,
|
||||
getIncidentByCorrelationId,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
ExecutorParamsSchemaITOM,
|
||||
ExecutorSubActionAddEventParamsSchema,
|
||||
ExternalIncidentServiceConfigurationBaseSchema,
|
||||
ExecutorSubActionCloseIncidentParamsSchema,
|
||||
} from './schema';
|
||||
import { SNProductsConfigValue } from '../../../../common/servicenow_config';
|
||||
|
||||
|
@ -104,17 +105,26 @@ export interface ExternalServiceParamsUpdate {
|
|||
incident: PartialIncident & Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ExternalServiceParamsClose {
|
||||
incidentId: string | null;
|
||||
correlationId: string | null;
|
||||
}
|
||||
|
||||
export interface ExternalService {
|
||||
getChoices: (fields: string[]) => Promise<GetChoicesResponse>;
|
||||
getIncident: (id: string) => Promise<ServiceNowIncident>;
|
||||
getFields: () => Promise<GetCommonFieldsResponse>;
|
||||
createIncident: (params: ExternalServiceParamsCreate) => Promise<ExternalServiceIncidentResponse>;
|
||||
updateIncident: (params: ExternalServiceParamsUpdate) => Promise<ExternalServiceIncidentResponse>;
|
||||
closeIncident: (
|
||||
params: ExternalServiceParamsClose
|
||||
) => Promise<ExternalServiceIncidentResponse | null>;
|
||||
findIncidents: (params?: Record<string, string>) => Promise<ServiceNowIncident>;
|
||||
getUrl: () => string;
|
||||
checkInstance: (res: AxiosResponse) => void;
|
||||
getApplicationInformation: () => Promise<GetApplicationInfoResponse>;
|
||||
checkIfApplicationIsInstalled: () => Promise<void>;
|
||||
getIncidentByCorrelationId: (correlationId: string) => Promise<ServiceNowIncident | null>;
|
||||
}
|
||||
|
||||
export type PushToServiceApiParams = ExecutorSubActionPushParams;
|
||||
|
@ -134,6 +144,10 @@ export type ExecutorSubActionHandshakeParams = TypeOf<
|
|||
typeof ExecutorSubActionHandshakeParamsSchema
|
||||
>;
|
||||
|
||||
export type ExecutorSubActionCloseIncidentParams = TypeOf<
|
||||
typeof ExecutorSubActionCloseIncidentParamsSchema
|
||||
>;
|
||||
|
||||
export type ServiceNowITSMIncident = Omit<
|
||||
TypeOf<typeof ExecutorSubActionPushParamsSchemaITSM>['incident'],
|
||||
'externalId'
|
||||
|
@ -155,6 +169,10 @@ export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs
|
|||
params: ExecutorSubActionGetIncidentParams;
|
||||
}
|
||||
|
||||
export interface CloseIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs {
|
||||
params: ExecutorSubActionCloseIncidentParams;
|
||||
}
|
||||
|
||||
export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs {
|
||||
params: ExecutorSubActionHandshakeParams;
|
||||
}
|
||||
|
@ -199,6 +217,9 @@ export interface ExternalServiceAPI {
|
|||
handshake: (args: HandshakeApiHandlerArgs) => Promise<void>;
|
||||
pushToService: (args: PushToServiceApiHandlerArgs) => Promise<PushToServiceResponse>;
|
||||
getIncident: (args: GetIncidentApiHandlerArgs) => Promise<ServiceNowIncident>;
|
||||
closeIncident: (
|
||||
args: CloseIncidentApiHandlerArgs
|
||||
) => Promise<ExternalServiceIncidentResponse | null>;
|
||||
}
|
||||
|
||||
export interface ExternalServiceCommentResponse {
|
||||
|
|
|
@ -28,6 +28,7 @@ jest.mock('@kbn/actions-plugin/server/lib/get_oauth_jwt_access_token', () => ({
|
|||
jest.mock('axios', () => ({
|
||||
create: jest.fn(),
|
||||
AxiosHeaders: jest.requireActual('axios').AxiosHeaders,
|
||||
AxiosError: jest.requireActual('axios').AxiosError,
|
||||
}));
|
||||
const createAxiosInstanceMock = axios.create as jest.Mock;
|
||||
const axiosInstanceMock = {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import axios, { AxiosHeaders, AxiosInstance, AxiosResponse } from 'axios';
|
||||
import axios, { AxiosError, AxiosHeaders, AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { addTimeZoneToDate, getErrorMessage } from '@kbn/actions-plugin/server/lib/axios_utils';
|
||||
import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config';
|
||||
|
@ -42,14 +42,22 @@ const createErrorMessage = (errorResponse?: ServiceNowError): string => {
|
|||
: 'unknown: no error in error response';
|
||||
};
|
||||
|
||||
export const createServiceError = (error: ResponseError, message: string) =>
|
||||
new Error(
|
||||
export const createServiceError = (error: ResponseError, message: string): AxiosError => {
|
||||
const serviceError = new AxiosError(
|
||||
getErrorMessage(
|
||||
i18n.SERVICENOW,
|
||||
`${message}. Error: ${error.message} Reason: ${createErrorMessage(error.response?.data)}`
|
||||
)
|
||||
);
|
||||
|
||||
serviceError.code = error.code;
|
||||
serviceError.config = error.config;
|
||||
serviceError.request = error.request;
|
||||
serviceError.response = error.response;
|
||||
|
||||
return serviceError;
|
||||
};
|
||||
|
||||
export const getPushedDate = (timestamp?: string) => {
|
||||
if (timestamp != null) {
|
||||
return new Date(addTimeZoneToDate(timestamp)).toISOString();
|
||||
|
|
|
@ -23,6 +23,7 @@ jest.mock('./api', () => ({
|
|||
getIncident: jest.fn(),
|
||||
handshake: jest.fn(),
|
||||
pushToService: jest.fn(),
|
||||
closeIncident: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
|
@ -75,6 +76,33 @@ describe('ServiceNow', () => {
|
|||
'work_notes'
|
||||
);
|
||||
});
|
||||
|
||||
test('calls closeIncident sub action correctly', async () => {
|
||||
const actionId = 'some-action-id';
|
||||
const executorOptions = {
|
||||
actionId,
|
||||
config,
|
||||
secrets,
|
||||
params: {
|
||||
subAction: 'closeIncident',
|
||||
subActionParams: {
|
||||
incident: {
|
||||
correlationId: 'custom_correlation_id',
|
||||
externalId: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
services,
|
||||
logger: mockedLogger,
|
||||
} as unknown as ServiceNowConnectorTypeExecutorOptions<
|
||||
ServiceNowPublicConfigurationType,
|
||||
ExecutorParams
|
||||
>;
|
||||
await connectorType.executor(executorOptions);
|
||||
expect(
|
||||
(api.closeIncident as jest.Mock).mock.calls[0][0].params.incident.correlationId
|
||||
).toBe('custom_correlation_id');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -41,6 +41,7 @@ import {
|
|||
ServiceNowExecutorResultData,
|
||||
ServiceNowPublicConfigurationType,
|
||||
ServiceNowSecretConfigurationType,
|
||||
ExecutorSubActionCloseIncidentParams,
|
||||
} from '../lib/servicenow/types';
|
||||
import {
|
||||
ServiceNowITSMConnectorTypeId,
|
||||
|
@ -102,7 +103,13 @@ export function getServiceNowITSMConnectorType(): ServiceNowConnectorType<
|
|||
}
|
||||
|
||||
// action executor
|
||||
const supportedSubActions: string[] = ['getFields', 'pushToService', 'getChoices', 'getIncident'];
|
||||
const supportedSubActions: string[] = [
|
||||
'getFields',
|
||||
'pushToService',
|
||||
'getChoices',
|
||||
'getIncident',
|
||||
'closeIncident',
|
||||
];
|
||||
async function executor(
|
||||
{
|
||||
actionTypeId,
|
||||
|
@ -173,5 +180,14 @@ async function executor(
|
|||
});
|
||||
}
|
||||
|
||||
if (subAction === 'closeIncident') {
|
||||
const closeIncidentParams = subActionParams as ExecutorSubActionCloseIncidentParams;
|
||||
data = await api.closeIncident({
|
||||
externalService,
|
||||
params: closeIncidentParams,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
return { status: 'ok', data: data ?? {}, actionId };
|
||||
}
|
||||
|
|
|
@ -17,7 +17,10 @@ import { serviceNowCommonFields, serviceNowChoices } from '../lib/servicenow/moc
|
|||
import { snExternalServiceConfig } from '../lib/servicenow/config';
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
jest.mock('axios');
|
||||
jest.mock('axios', () => ({
|
||||
create: jest.fn(),
|
||||
AxiosError: jest.requireActual('axios').AxiosError,
|
||||
}));
|
||||
jest.mock('@kbn/actions-plugin/server/lib/axios_utils', () => {
|
||||
const originalUtils = jest.requireActual('@kbn/actions-plugin/server/lib/axios_utils');
|
||||
return {
|
||||
|
|
|
@ -549,6 +549,7 @@ export const ActionTypeForm = ({
|
|||
actionParams={actionItem.params as any}
|
||||
errors={actionParamsErrors.errors}
|
||||
index={index}
|
||||
selectedActionGroupId={selectedActionGroup?.id}
|
||||
editAction={(key: string, value: RuleActionParam, i: number) => {
|
||||
setWarning(
|
||||
validateParamsForWarnings(
|
||||
|
|
|
@ -224,6 +224,7 @@ export interface ActionParamsProps<TParams> {
|
|||
actionConnector?: ActionConnector;
|
||||
isLoading?: boolean;
|
||||
isDisabled?: boolean;
|
||||
selectedActionGroupId?: string;
|
||||
showEmailSubjectAndMessage?: boolean;
|
||||
executionMode?: ActionConnectorMode;
|
||||
onBlur?: (field?: string) => void;
|
||||
|
|
|
@ -466,7 +466,7 @@ export default function serviceNowITSMTest({ 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.subAction]: expected value to equal [pushToService]\n- [4.subAction]: expected value to equal [getChoices]',
|
||||
'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.subAction]: expected value to equal [pushToService]\n- [4.subAction]: expected value to equal [getChoices]\n- [5.subAction]: expected value to equal [closeIncident]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -484,7 +484,7 @@ export default function serviceNowITSMTest({ 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.short_description]: expected value of type [string] but got [undefined]\n- [4.subAction]: expected value to equal [getChoices]',
|
||||
'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.short_description]: expected value of type [string] but got [undefined]\n- [4.subAction]: expected value to equal [getChoices]\n- [5.subAction]: expected value to equal [closeIncident]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -507,7 +507,7 @@ export default function serviceNowITSMTest({ 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.short_description]: expected value of type [string] but got [undefined]\n- [4.subAction]: expected value to equal [getChoices]',
|
||||
'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.short_description]: expected value of type [string] but got [undefined]\n- [4.subAction]: expected value to equal [getChoices]\n- [5.subAction]: expected value to equal [closeIncident]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -534,7 +534,7 @@ export default function serviceNowITSMTest({ 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.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]\n- [4.subAction]: expected value to equal [getChoices]',
|
||||
'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.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]\n- [4.subAction]: expected value to equal [getChoices]\n- [5.subAction]: expected value to equal [closeIncident]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -561,7 +561,7 @@ export default function serviceNowITSMTest({ 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.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]\n- [4.subAction]: expected value to equal [getChoices]',
|
||||
'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.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]\n- [4.subAction]: expected value to equal [getChoices]\n- [5.subAction]: expected value to equal [closeIncident]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -583,7 +583,7 @@ export default function serviceNowITSMTest({ 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.subAction]: expected value to equal [pushToService]\n- [4.subActionParams.fields]: expected value of type [array] but got [undefined]',
|
||||
'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.subAction]: expected value to equal [pushToService]\n- [4.subActionParams.fields]: expected value of type [array] but got [undefined]\n- [5.subAction]: expected value to equal [closeIncident]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -716,6 +716,32 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('closeIncident', () => {
|
||||
it('should close the incident', async () => {
|
||||
const { body: result } = await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
subAction: 'closeIncident',
|
||||
subActionParams: {
|
||||
incident: {
|
||||
correlation_id: 'custom_correlation_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(proxyHaveBeenCalled).to.equal(true);
|
||||
expect(result).to.eql({
|
||||
status: 'ok',
|
||||
connector_id: simulatedActionId,
|
||||
data: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue