mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Response Ops][Connectors] Refactor Jira Connector to use latest API only (#197787)
## Summary Jira Cloud and Datacenter work using the same API urls. In this PR we remove the calls to the capabilities API which was being used to know the API url we needed to hit To test it: - Create Jira Cloud and Datacenter connectors - Test all use cases related to them Related to https://github.com/elastic/kibana/issues/189017 ## Research Work **getCapabilities, createIncident and getIncident** are always the same, therefore ignored for the rest of this document - getCapabilities: `/rest/capabilities` - createIncident: `/rest/api/2/issue` - getIncident: `/rest/api/2/issue` ## API links - Cloud: https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/#version - DC: https://docs.atlassian.com/software/jira/docs/api/REST/9.17.0/ ### Expected API urls based on the API links - Get issue types - Cloud: `GET /rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes` - DC:`GET /rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes` - Get fields by issue type - Cloud: `GET /rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes/{issueTypeId}` - DC: `GET /rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes/{issueTypeId}` ### API we hit - Get issue types - Cloud `GET /rest/api/2/issue/createmeta?projectKeys=ROC&expand=projects.issuetypes.fields` (variable name we are using is `getIssueTypesOldAPIURL`) - DC `GET /rest/api/2/issue/createmeta/RES/issuetypes` (variable name is `getIssueTypesUrl`) - Get fields by issue type - Cloud `GET /rest/api/2/issue/createmeta?projectKeys=ROC&issuetypeIds={issueTypeId}&expand=projects.issuetypes.fields` (variable name is `getIssueTypeFieldsOldAPIURL`) - DC `GET /rest/api/2/issue/createmeta/RES/issuetypes/{issueTypeId}` (variable name is `getIssueTypeFieldsUrl`) #### Analysed use cases to retrieve API urls we hit - created a case with JIRA Cloud as Connector - did a connector test with JIRA Cloud as connector - created a case with JIRA DC as connector - did a connector test with JIRA DC as connector ### Conclusions - We are not using the right endpoints for Cloud, we should update them to use the same endpoints. --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Christos Nasikas <christos.nasikas@elastic.co> Co-authored-by: adcoelho <antonio.coelho@elastic.co> Co-authored-by: Antonio <antoniodcoelho@gmail.com> Co-authored-by: Lisa Cawley <lcawley@elastic.co>
This commit is contained in:
parent
996104f2ea
commit
953d877df0
7 changed files with 236 additions and 623 deletions
|
@ -14,7 +14,7 @@ The Jira connector uses the https://developer.atlassian.com/cloud/jira/platform/
|
|||
[[jira-compatibility]]
|
||||
=== Compatibility
|
||||
|
||||
Jira on-premise deployments (Server and Data Center) are not supported.
|
||||
Jira Cloud and Jira Data Center are supported. Jira on-premise deployments are not supported.
|
||||
|
||||
[float]
|
||||
[[define-jira-ui]]
|
||||
|
@ -37,7 +37,7 @@ Name:: The name of the connector.
|
|||
URL:: Jira instance URL.
|
||||
Project key:: Jira project key.
|
||||
Email:: The account email for HTTP Basic authentication.
|
||||
API token:: Jira API authentication token for HTTP Basic authentication.
|
||||
API token:: Jira API authentication token for HTTP Basic authentication. For Jira Data Center, this value should be the password associated with the email owner.
|
||||
|
||||
[float]
|
||||
[[jira-action-configuration]]
|
||||
|
|
|
@ -577,4 +577,12 @@ describe('throwIfResponseIsNotValid', () => {
|
|||
})
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
test('it does NOT throw if HTTP status code is 204 even if the content type is not supported', () => {
|
||||
expect(() =>
|
||||
throwIfResponseIsNotValid({
|
||||
res: { ...res, status: 204, headers: { ['content-type']: 'text/html' } },
|
||||
})
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -137,6 +137,16 @@ export const throwIfResponseIsNotValid = ({
|
|||
const requiredContentType = 'application/json';
|
||||
const contentType = res.headers['content-type'] ?? 'undefined';
|
||||
const data = res.data;
|
||||
const statusCode = res.status;
|
||||
|
||||
/**
|
||||
* Some external services may return a 204
|
||||
* status code but with unsupported content type like text/html.
|
||||
* To avoid throwing on valid requests we return.
|
||||
*/
|
||||
if (statusCode === 204) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the content-type of the response is application/json.
|
||||
|
|
|
@ -36,67 +36,14 @@ const configurationUtilities = actionsConfigMock.create();
|
|||
|
||||
const issueTypesResponse = createAxiosResponse({
|
||||
data: {
|
||||
projects: [
|
||||
issueTypes: [
|
||||
{
|
||||
issuetypes: [
|
||||
{
|
||||
id: '10006',
|
||||
name: 'Task',
|
||||
},
|
||||
{
|
||||
id: '10007',
|
||||
name: 'Bug',
|
||||
},
|
||||
],
|
||||
id: '10006',
|
||||
name: 'Task',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const fieldsResponse = createAxiosResponse({
|
||||
data: {
|
||||
projects: [
|
||||
{
|
||||
issuetypes: [
|
||||
{
|
||||
id: '10006',
|
||||
name: 'Task',
|
||||
fields: {
|
||||
summary: { required: true, schema: { type: 'string' }, fieldId: 'summary' },
|
||||
priority: {
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
fieldId: 'priority',
|
||||
allowedValues: [
|
||||
{
|
||||
name: 'Highest',
|
||||
id: '1',
|
||||
},
|
||||
{
|
||||
name: 'High',
|
||||
id: '2',
|
||||
},
|
||||
{
|
||||
name: 'Medium',
|
||||
id: '3',
|
||||
},
|
||||
{
|
||||
name: 'Low',
|
||||
id: '4',
|
||||
},
|
||||
{
|
||||
name: 'Lowest',
|
||||
id: '5',
|
||||
},
|
||||
],
|
||||
defaultValue: {
|
||||
name: 'Medium',
|
||||
id: '3',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
id: '10007',
|
||||
name: 'Bug',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -110,30 +57,6 @@ const issueResponse = {
|
|||
|
||||
const issuesResponse = [issueResponse];
|
||||
|
||||
const mockNewAPI = () =>
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
capabilities: {
|
||||
'list-project-issuetypes':
|
||||
'https://coolsite.net/rest/capabilities/list-project-issuetypes',
|
||||
'list-issuetype-fields': 'https://coolsite.net/rest/capabilities/list-issuetype-fields',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const mockOldAPI = () =>
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
capabilities: {
|
||||
navigation: 'https://coolsite.net/rest/capabilities/navigation',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
describe('Jira service', () => {
|
||||
let service: ExternalService;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
@ -347,23 +270,6 @@ describe('Jira service', () => {
|
|||
});
|
||||
|
||||
test('it creates the incident correctly without issue type', async () => {
|
||||
/* The response from Jira when creating an issue contains only the key and the id.
|
||||
The function makes the following calls when creating an issue:
|
||||
1. Get issueTypes to set a default ONLY when incident.issueType is missing
|
||||
2. Create the issue.
|
||||
3. Get the created issue with all the necessary fields.
|
||||
*/
|
||||
// getIssueType mocks
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
capabilities: {
|
||||
navigation: 'https://coolsite.net/rest/capabilities/navigation',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// getIssueType mocks
|
||||
requestMock.mockImplementationOnce(() => issueTypesResponse);
|
||||
|
||||
|
@ -419,16 +325,6 @@ describe('Jira service', () => {
|
|||
});
|
||||
|
||||
test('removes newline characters and trialing spaces from summary', async () => {
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
capabilities: {
|
||||
navigation: 'https://coolsite.net/rest/capabilities/navigation',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// getIssueType mocks
|
||||
requestMock.mockImplementationOnce(() => issueTypesResponse);
|
||||
|
||||
|
@ -800,28 +696,47 @@ describe('Jira service', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getCapabilities', () => {
|
||||
test('it should return the capabilities', async () => {
|
||||
mockOldAPI();
|
||||
const res = await service.getCapabilities();
|
||||
expect(res).toEqual({
|
||||
capabilities: {
|
||||
navigation: 'https://coolsite.net/rest/capabilities/navigation',
|
||||
describe('getIssueTypes', () => {
|
||||
test('it should return the issue types', async () => {
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
issueTypes: issueTypesResponse.data.issueTypes,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const res = await service.getIssueTypes();
|
||||
|
||||
expect(res).toEqual([
|
||||
{
|
||||
id: '10006',
|
||||
name: 'Task',
|
||||
},
|
||||
});
|
||||
{
|
||||
id: '10007',
|
||||
name: 'Bug',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it should call request with correct arguments', async () => {
|
||||
mockOldAPI();
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
issueTypes: issueTypesResponse.data.issueTypes,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await service.getCapabilities();
|
||||
await service.getIssueTypes();
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
expect(requestMock).toHaveBeenLastCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
method: 'get',
|
||||
configurationUtilities,
|
||||
url: 'https://coolsite.net/rest/capabilities',
|
||||
url: 'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
@ -829,25 +744,12 @@ describe('Jira service', () => {
|
|||
test('it should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
const error: ResponseError = new Error('An error has occurred');
|
||||
error.response = { data: { errors: { capabilities: 'Could not get capabilities' } } };
|
||||
error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } };
|
||||
throw error;
|
||||
});
|
||||
|
||||
await expect(service.getCapabilities()).rejects.toThrow(
|
||||
'[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: Could not get capabilities'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should return unknown if the error is a string', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
const error = new Error('An error has occurred');
|
||||
// @ts-ignore
|
||||
error.response = { data: 'Unauthorized' };
|
||||
throw error;
|
||||
});
|
||||
|
||||
await expect(service.getCapabilities()).rejects.toThrow(
|
||||
'[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: unknown: errorResponse.errors was null'
|
||||
await expect(service.getIssueTypes()).rejects.toThrow(
|
||||
'[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -856,346 +758,178 @@ describe('Jira service', () => {
|
|||
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
|
||||
);
|
||||
|
||||
await expect(service.getCapabilities()).rejects.toThrow(
|
||||
'[Action][Jira]: Unable to get capabilities. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null'
|
||||
await expect(service.getIssueTypes()).rejects.toThrow(
|
||||
'[Action][Jira]: Unable to get issue types. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw if the required attributes are not there', async () => {
|
||||
requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } }));
|
||||
|
||||
await expect(service.getCapabilities()).rejects.toThrow(
|
||||
'[Action][Jira]: Unable to get capabilities. Error: Response is missing at least one of the expected fields: capabilities. Reason: unknown: errorResponse was null'
|
||||
test('it should work with data center response - issueTypes returned in data.values', async () => {
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
values: issueTypesResponse.data.issueTypes,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIssueTypes', () => {
|
||||
describe('Old API', () => {
|
||||
test('it should return the issue types', async () => {
|
||||
mockOldAPI();
|
||||
await service.getIssueTypes();
|
||||
|
||||
requestMock.mockImplementationOnce(() => issueTypesResponse);
|
||||
|
||||
const res = await service.getIssueTypes();
|
||||
|
||||
expect(res).toEqual([
|
||||
{
|
||||
id: '10006',
|
||||
name: 'Task',
|
||||
},
|
||||
{
|
||||
id: '10007',
|
||||
name: 'Bug',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it should call request with correct arguments', async () => {
|
||||
mockOldAPI();
|
||||
|
||||
requestMock.mockImplementationOnce(() => issueTypesResponse);
|
||||
|
||||
await service.getIssueTypes();
|
||||
|
||||
expect(requestMock).toHaveBeenLastCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
method: 'get',
|
||||
configurationUtilities,
|
||||
url: 'https://coolsite.net/rest/api/2/issue/createmeta?projectKeys=CK&expand=projects.issuetypes.fields',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
mockOldAPI();
|
||||
|
||||
requestMock.mockImplementation(() => {
|
||||
const error: ResponseError = new Error('An error has occurred');
|
||||
error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } };
|
||||
throw error;
|
||||
});
|
||||
|
||||
await expect(service.getIssueTypes()).rejects.toThrow(
|
||||
'[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw if the request is not a JSON', async () => {
|
||||
mockOldAPI();
|
||||
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
|
||||
);
|
||||
|
||||
await expect(service.getIssueTypes()).rejects.toThrow(
|
||||
'[Action][Jira]: Unable to get issue types. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null'
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('New API', () => {
|
||||
test('it should return the issue types', async () => {
|
||||
mockNewAPI();
|
||||
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
values: issueTypesResponse.data.projects[0].issuetypes,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const res = await service.getIssueTypes();
|
||||
|
||||
expect(res).toEqual([
|
||||
{
|
||||
id: '10006',
|
||||
name: 'Task',
|
||||
},
|
||||
{
|
||||
id: '10007',
|
||||
name: 'Bug',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it should call request with correct arguments', async () => {
|
||||
mockNewAPI();
|
||||
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
values: issueTypesResponse.data.projects[0].issuetypes,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await service.getIssueTypes();
|
||||
|
||||
expect(requestMock).toHaveBeenLastCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
method: 'get',
|
||||
configurationUtilities,
|
||||
url: 'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
mockNewAPI();
|
||||
|
||||
requestMock.mockImplementation(() => {
|
||||
const error: ResponseError = new Error('An error has occurred');
|
||||
error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } };
|
||||
throw error;
|
||||
});
|
||||
|
||||
await expect(service.getIssueTypes()).rejects.toThrow(
|
||||
'[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw if the request is not a JSON', async () => {
|
||||
mockNewAPI();
|
||||
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
|
||||
);
|
||||
|
||||
await expect(service.getIssueTypes()).rejects.toThrow(
|
||||
'[Action][Jira]: Unable to get issue types. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null'
|
||||
);
|
||||
expect(requestMock).toHaveBeenLastCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
method: 'get',
|
||||
configurationUtilities,
|
||||
url: 'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFieldsByIssueType', () => {
|
||||
describe('Old API', () => {
|
||||
test('it should return the fields', async () => {
|
||||
mockOldAPI();
|
||||
|
||||
requestMock.mockImplementationOnce(() => fieldsResponse);
|
||||
|
||||
const res = await service.getFieldsByIssueType('10006');
|
||||
|
||||
expect(res).toEqual({
|
||||
priority: {
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
allowedValues: [
|
||||
{ id: '1', name: 'Highest' },
|
||||
{ id: '2', name: 'High' },
|
||||
{ id: '3', name: 'Medium' },
|
||||
{ id: '4', name: 'Low' },
|
||||
{ id: '5', name: 'Lowest' },
|
||||
test('it should return the fields', async () => {
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
fields: [
|
||||
{ required: true, schema: { type: 'string' }, fieldId: 'summary' },
|
||||
{
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
fieldId: 'priority',
|
||||
allowedValues: [
|
||||
{
|
||||
name: 'Medium',
|
||||
id: '3',
|
||||
},
|
||||
],
|
||||
defaultValue: {
|
||||
name: 'Medium',
|
||||
id: '3',
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultValue: { id: '3', name: 'Medium' },
|
||||
},
|
||||
summary: {
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
allowedValues: [],
|
||||
defaultValue: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
test('it should call request with correct arguments', async () => {
|
||||
mockOldAPI();
|
||||
const res = await service.getFieldsByIssueType('10006');
|
||||
|
||||
requestMock.mockImplementationOnce(() => fieldsResponse);
|
||||
|
||||
await service.getFieldsByIssueType('10006');
|
||||
|
||||
expect(requestMock).toHaveBeenLastCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
method: 'get',
|
||||
configurationUtilities,
|
||||
url: 'https://coolsite.net/rest/api/2/issue/createmeta?projectKeys=CK&issuetypeIds=10006&expand=projects.issuetypes.fields',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
mockOldAPI();
|
||||
|
||||
requestMock.mockImplementation(() => {
|
||||
const error: ResponseError = new Error('An error has occurred');
|
||||
error.response = { data: { errors: { fields: 'Could not get fields' } } };
|
||||
throw error;
|
||||
});
|
||||
|
||||
await expect(service.getFieldsByIssueType('10006')).rejects.toThrow(
|
||||
'[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get fields'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw if the request is not a JSON', async () => {
|
||||
mockOldAPI();
|
||||
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
|
||||
);
|
||||
|
||||
await expect(service.getFieldsByIssueType('10006')).rejects.toThrow(
|
||||
'[Action][Jira]: Unable to get fields. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null'
|
||||
);
|
||||
expect(res).toEqual({
|
||||
priority: {
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
allowedValues: [{ id: '3', name: 'Medium' }],
|
||||
defaultValue: { id: '3', name: 'Medium' },
|
||||
},
|
||||
summary: {
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
allowedValues: [],
|
||||
defaultValue: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('New API', () => {
|
||||
test('it should return the fields', async () => {
|
||||
mockNewAPI();
|
||||
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
values: [
|
||||
{ required: true, schema: { type: 'string' }, fieldId: 'summary' },
|
||||
{
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
fieldId: 'priority',
|
||||
allowedValues: [
|
||||
{
|
||||
name: 'Medium',
|
||||
id: '3',
|
||||
},
|
||||
],
|
||||
defaultValue: {
|
||||
test('it should call request with correct arguments', async () => {
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
fields: [
|
||||
{ required: true, schema: { type: 'string' }, fieldId: 'summary' },
|
||||
{
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
fieldId: 'priority',
|
||||
allowedValues: [
|
||||
{
|
||||
name: 'Medium',
|
||||
id: '3',
|
||||
},
|
||||
],
|
||||
defaultValue: {
|
||||
name: 'Medium',
|
||||
id: '3',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const res = await service.getFieldsByIssueType('10006');
|
||||
|
||||
expect(res).toEqual({
|
||||
priority: {
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
allowedValues: [{ id: '3', name: 'Medium' }],
|
||||
defaultValue: { id: '3', name: 'Medium' },
|
||||
},
|
||||
],
|
||||
},
|
||||
summary: {
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
allowedValues: [],
|
||||
defaultValue: {},
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
await service.getFieldsByIssueType('10006');
|
||||
|
||||
expect(requestMock).toHaveBeenLastCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
method: 'get',
|
||||
configurationUtilities,
|
||||
url: 'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes/10006',
|
||||
});
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
const error: ResponseError = new Error('An error has occurred');
|
||||
error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } };
|
||||
throw error;
|
||||
});
|
||||
|
||||
test('it should call request with correct arguments', async () => {
|
||||
mockNewAPI();
|
||||
await expect(service.getFieldsByIssueType('10006')).rejects.toThrowError(
|
||||
'[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get issue types'
|
||||
);
|
||||
});
|
||||
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
values: [
|
||||
{ required: true, schema: { type: 'string' }, fieldId: 'summary' },
|
||||
{
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
fieldId: 'priority',
|
||||
allowedValues: [
|
||||
{
|
||||
name: 'Medium',
|
||||
id: '3',
|
||||
},
|
||||
],
|
||||
defaultValue: {
|
||||
test('it should throw if the request is not a JSON', async () => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
|
||||
);
|
||||
|
||||
await expect(service.getFieldsByIssueType('10006')).rejects.toThrow(
|
||||
'[Action][Jira]: Unable to get fields. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should work with data center response - issueTypes returned in data.values', async () => {
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
values: [
|
||||
{ required: true, schema: { type: 'string' }, fieldId: 'summary' },
|
||||
{
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
fieldId: 'priority',
|
||||
allowedValues: [
|
||||
{
|
||||
name: 'Medium',
|
||||
id: '3',
|
||||
},
|
||||
],
|
||||
defaultValue: {
|
||||
name: 'Medium',
|
||||
id: '3',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await service.getFieldsByIssueType('10006');
|
||||
const res = await service.getFieldsByIssueType('10006');
|
||||
|
||||
expect(requestMock).toHaveBeenLastCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
method: 'get',
|
||||
configurationUtilities,
|
||||
url: 'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes/10006',
|
||||
});
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
mockNewAPI();
|
||||
|
||||
requestMock.mockImplementation(() => {
|
||||
const error: ResponseError = new Error('An error has occurred');
|
||||
error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } };
|
||||
throw error;
|
||||
});
|
||||
|
||||
await expect(service.getFieldsByIssueType('10006')).rejects.toThrowError(
|
||||
'[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get issue types'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw if the request is not a JSON', async () => {
|
||||
mockNewAPI();
|
||||
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
|
||||
);
|
||||
|
||||
await expect(service.getFieldsByIssueType('10006')).rejects.toThrow(
|
||||
'[Action][Jira]: Unable to get fields. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null'
|
||||
);
|
||||
expect(res).toEqual({
|
||||
priority: {
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
allowedValues: [{ id: '3', name: 'Medium' }],
|
||||
defaultValue: { id: '3', name: 'Medium' },
|
||||
},
|
||||
summary: {
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
allowedValues: [],
|
||||
defaultValue: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1403,50 +1137,14 @@ describe('Jira service', () => {
|
|||
.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
capabilities: {
|
||||
'list-project-issuetypes':
|
||||
'https://coolsite.net/rest/capabilities/list-project-issuetypes',
|
||||
'list-issuetype-fields':
|
||||
'https://coolsite.net/rest/capabilities/list-issuetype-fields',
|
||||
},
|
||||
issueTypes: issueTypesResponse.data.issueTypes,
|
||||
},
|
||||
})
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
values: issueTypesResponse.data.projects[0].issuetypes,
|
||||
},
|
||||
})
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
capabilities: {
|
||||
'list-project-issuetypes':
|
||||
'https://coolsite.net/rest/capabilities/list-project-issuetypes',
|
||||
'list-issuetype-fields':
|
||||
'https://coolsite.net/rest/capabilities/list-issuetype-fields',
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
capabilities: {
|
||||
'list-project-issuetypes':
|
||||
'https://coolsite.net/rest/capabilities/list-project-issuetypes',
|
||||
'list-issuetype-fields':
|
||||
'https://coolsite.net/rest/capabilities/list-issuetype-fields',
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
values: [
|
||||
fields: [
|
||||
{ required: true, schema: { type: 'string' }, fieldId: 'summary' },
|
||||
{ required: true, schema: { type: 'string' }, fieldId: 'description' },
|
||||
{
|
||||
|
@ -1471,7 +1169,7 @@ describe('Jira service', () => {
|
|||
.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
values: [
|
||||
fields: [
|
||||
{ required: true, schema: { type: 'string' }, fieldId: 'summary' },
|
||||
{ required: true, schema: { type: 'string' }, fieldId: 'description' },
|
||||
],
|
||||
|
@ -1488,10 +1186,7 @@ describe('Jira service', () => {
|
|||
callMocks();
|
||||
await service.getFields();
|
||||
const callUrls = [
|
||||
'https://coolsite.net/rest/capabilities',
|
||||
'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes',
|
||||
'https://coolsite.net/rest/capabilities',
|
||||
'https://coolsite.net/rest/capabilities',
|
||||
'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes/10006',
|
||||
'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes/10007',
|
||||
];
|
||||
|
@ -1525,7 +1220,7 @@ describe('Jira service', () => {
|
|||
throw error;
|
||||
});
|
||||
await expect(service.getFields()).rejects.toThrow(
|
||||
'[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: Required field'
|
||||
'[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Required field'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -39,12 +39,9 @@ import * as i18n from './translations';
|
|||
|
||||
const VERSION = '2';
|
||||
const BASE_URL = `rest/api/${VERSION}`;
|
||||
const CAPABILITIES_URL = `rest/capabilities`;
|
||||
|
||||
const VIEW_INCIDENT_URL = `browse`;
|
||||
|
||||
const createMetaCapabilities = ['list-project-issuetypes', 'list-issuetype-fields'];
|
||||
|
||||
export const createExternalService = (
|
||||
{ config, secrets }: ExternalServiceCredentials,
|
||||
logger: Logger,
|
||||
|
@ -60,10 +57,7 @@ export const createExternalService = (
|
|||
|
||||
const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url;
|
||||
const incidentUrl = `${urlWithoutTrailingSlash}/${BASE_URL}/issue`;
|
||||
const capabilitiesUrl = `${urlWithoutTrailingSlash}/${CAPABILITIES_URL}`;
|
||||
const commentUrl = `${incidentUrl}/{issueId}/comment`;
|
||||
const getIssueTypesOldAPIURL = `${urlWithoutTrailingSlash}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&expand=projects.issuetypes.fields`;
|
||||
const getIssueTypeFieldsOldAPIURL = `${urlWithoutTrailingSlash}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&issuetypeIds={issueTypeId}&expand=projects.issuetypes.fields`;
|
||||
const getIssueTypesUrl = `${urlWithoutTrailingSlash}/${BASE_URL}/issue/createmeta/${projectKey}/issuetypes`;
|
||||
const getIssueTypeFieldsUrl = `${urlWithoutTrailingSlash}/${BASE_URL}/issue/createmeta/${projectKey}/issuetypes/{issueTypeId}`;
|
||||
const searchUrl = `${urlWithoutTrailingSlash}/${BASE_URL}/search`;
|
||||
|
@ -144,9 +138,6 @@ export const createExternalService = (
|
|||
}, '');
|
||||
};
|
||||
|
||||
const hasSupportForNewAPI = (capabilities: { capabilities?: {} }) =>
|
||||
createMetaCapabilities.every((c) => Object.keys(capabilities?.capabilities ?? {}).includes(c));
|
||||
|
||||
const normalizeIssueTypes = (issueTypes: Array<{ id: string; name: string }>) =>
|
||||
issueTypes.map((type) => ({ id: type.id, name: type.name }));
|
||||
|
||||
|
@ -356,12 +347,12 @@ export const createExternalService = (
|
|||
}
|
||||
};
|
||||
|
||||
const getCapabilities = async () => {
|
||||
const getIssueTypes = async () => {
|
||||
try {
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
method: 'get',
|
||||
url: capabilitiesUrl,
|
||||
url: getIssueTypesUrl,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
|
@ -369,59 +360,11 @@ export const createExternalService = (
|
|||
|
||||
throwIfResponseIsNotValid({
|
||||
res,
|
||||
requiredAttributesToBeInTheResponse: ['capabilities'],
|
||||
});
|
||||
|
||||
return { ...res.data };
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(
|
||||
i18n.NAME,
|
||||
`Unable to get capabilities. Error: ${error.message}. Reason: ${createErrorMessage(
|
||||
error.response?.data
|
||||
)}`
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getIssueTypes = async () => {
|
||||
const capabilitiesResponse = await getCapabilities();
|
||||
const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse);
|
||||
try {
|
||||
if (!supportsNewAPI) {
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
method: 'get',
|
||||
url: getIssueTypesOldAPIURL,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
res,
|
||||
});
|
||||
|
||||
const issueTypes = res.data.projects[0]?.issuetypes ?? [];
|
||||
return normalizeIssueTypes(issueTypes);
|
||||
} else {
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
method: 'get',
|
||||
url: getIssueTypesUrl,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
res,
|
||||
});
|
||||
|
||||
const issueTypes = res.data.values;
|
||||
return normalizeIssueTypes(issueTypes);
|
||||
}
|
||||
// Cloud returns issueTypes and Data Center returns values
|
||||
const { issueTypes, values } = res.data;
|
||||
return normalizeIssueTypes(issueTypes || values);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(
|
||||
|
@ -435,47 +378,29 @@ export const createExternalService = (
|
|||
};
|
||||
|
||||
const getFieldsByIssueType = async (issueTypeId: string) => {
|
||||
const capabilitiesResponse = await getCapabilities();
|
||||
const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse);
|
||||
try {
|
||||
if (!supportsNewAPI) {
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
method: 'get',
|
||||
url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsOldAPIURL, issueTypeId),
|
||||
logger,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
method: 'get',
|
||||
url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsUrl, issueTypeId),
|
||||
logger,
|
||||
configurationUtilities,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
res,
|
||||
});
|
||||
throwIfResponseIsNotValid({
|
||||
res,
|
||||
});
|
||||
|
||||
const fields = res.data.projects[0]?.issuetypes[0]?.fields || {};
|
||||
return normalizeFields(fields);
|
||||
} else {
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
method: 'get',
|
||||
url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsUrl, issueTypeId),
|
||||
logger,
|
||||
configurationUtilities,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
res,
|
||||
});
|
||||
|
||||
const fields = res.data.values.reduce(
|
||||
(acc: { [x: string]: {} }, value: { fieldId: string }) => ({
|
||||
...acc,
|
||||
[value.fieldId]: { ...value },
|
||||
}),
|
||||
{}
|
||||
);
|
||||
return normalizeFields(fields);
|
||||
}
|
||||
// Cloud returns fields and Data Center returns values
|
||||
const { fields: rawFields, values } = res.data;
|
||||
const fields = (rawFields || values).reduce(
|
||||
(acc: { [x: string]: {} }, value: { fieldId: string }) => ({
|
||||
...acc,
|
||||
[value.fieldId]: { ...value },
|
||||
}),
|
||||
{}
|
||||
);
|
||||
return normalizeFields(fields);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(
|
||||
|
@ -580,7 +505,6 @@ export const createExternalService = (
|
|||
createIncident,
|
||||
updateIncident,
|
||||
createComment,
|
||||
getCapabilities,
|
||||
getIssueTypes,
|
||||
getFieldsByIssueType,
|
||||
getIssues,
|
||||
|
|
|
@ -101,7 +101,6 @@ export interface ExternalService {
|
|||
createComment: (params: CreateCommentParams) => Promise<ExternalServiceCommentResponse>;
|
||||
createIncident: (params: CreateIncidentParams) => Promise<ExternalServiceIncidentResponse>;
|
||||
getFields: () => Promise<GetCommonFieldsResponse>;
|
||||
getCapabilities: () => Promise<ExternalServiceParams>;
|
||||
getFieldsByIssueType: (issueTypeId: string) => Promise<GetFieldsByIssueTypeResponse>;
|
||||
getIncident: (id: string) => Promise<ExternalServiceParams | undefined>;
|
||||
getIssue: (id: string) => Promise<GetIssueResponse>;
|
||||
|
|
|
@ -100,7 +100,7 @@ export function initPlugin(router: IRouter, path: string) {
|
|||
|
||||
router.get(
|
||||
{
|
||||
path: `${path}/rest/capabilities`,
|
||||
path: `${path}/rest/api/2/issue/createmeta/{projectId}/issuetypes`,
|
||||
options: {
|
||||
authRequired: false,
|
||||
},
|
||||
|
@ -112,37 +112,14 @@ export function initPlugin(router: IRouter, path: string) {
|
|||
res: KibanaResponseFactory
|
||||
): Promise<IKibanaResponse<any>> {
|
||||
return jsonResponse(res, 200, {
|
||||
capabilities: {},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
{
|
||||
path: `${path}/rest/api/2/issue/createmeta`,
|
||||
options: {
|
||||
authRequired: false,
|
||||
},
|
||||
validate: {},
|
||||
},
|
||||
async function (
|
||||
context: RequestHandlerContext,
|
||||
req: KibanaRequest<any, any, any, any>,
|
||||
res: KibanaResponseFactory
|
||||
): Promise<IKibanaResponse<any>> {
|
||||
return jsonResponse(res, 200, {
|
||||
projects: [
|
||||
issueTypes: [
|
||||
{
|
||||
issuetypes: [
|
||||
{
|
||||
id: '10006',
|
||||
name: 'Task',
|
||||
},
|
||||
{
|
||||
id: '10007',
|
||||
name: 'Sub-task',
|
||||
},
|
||||
],
|
||||
id: '10006',
|
||||
name: 'Task',
|
||||
},
|
||||
{
|
||||
id: '10007',
|
||||
name: 'Sub-task',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue