[8.x] [Response Ops][Connectors] Refactor Jira Connector to use latest API only (#197787) (#199289)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Response Ops][Connectors] Refactor Jira Connector to use latest API
only (#197787)](https://github.com/elastic/kibana/pull/197787)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Julian
Gernun","email":"17549662+jcger@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-11-07T10:47:29Z","message":"[Response
Ops][Connectors] Refactor Jira Connector to use latest API only
(#197787)\n\n## Summary\r\n\r\nJira Cloud and Datacenter work using the
same API urls. In this PR we\r\nremove the calls to the capabilities API
which was being used to know\r\nthe API url we needed to hit\r\n\r\nTo
test it:\r\n- Create Jira Cloud and Datacenter connectors\r\n- Test all
use cases related to them\r\n\r\nRelated to
https://github.com/elastic/kibana/issues/189017\r\n\r\n## Research
Work\r\n\r\n**getCapabilities, createIncident and getIncident** are
always the same,\r\ntherefore ignored for the rest of this
document\r\n\r\n- getCapabilities: `/rest/capabilities`\r\n-
createIncident: `/rest/api/2/issue`\r\n- getIncident:
`/rest/api/2/issue`\r\n\r\n## API links\r\n\r\n-
Cloud:\r\nhttps://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/#version\r\n-
DC:
https://docs.atlassian.com/software/jira/docs/api/REST/9.17.0/\r\n\r\n###
Expected API urls based on the API links\r\n\r\n- Get issue
types\r\n\r\n- Cloud: `GET
/rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes`\r\n -
DC:`GET /rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes`\r\n\r\n-
Get fields by issue type\r\n- Cloud:
`GET\r\n/rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes/{issueTypeId}`\r\n-
DC:\r\n`GET /rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes/{issueTypeId}`\r\n\r\n###
API we hit\r\n\r\n- Get issue types\r\n\r\n- Cloud
`GET\r\n/rest/api/2/issue/createmeta?projectKeys=ROC&expand=projects.issuetypes.fields`\r\n(variable
name we are using is `getIssueTypesOldAPIURL`)\r\n- DC `GET
/rest/api/2/issue/createmeta/RES/issuetypes` (variable name
is\r\n`getIssueTypesUrl`)\r\n\r\n- Get fields by issue type\r\n- Cloud
`GET\r\n/rest/api/2/issue/createmeta?projectKeys=ROC&issuetypeIds={issueTypeId}&expand=projects.issuetypes.fields`\r\n(variable
name is `getIssueTypeFieldsOldAPIURL`)\r\n- DC `GET
/rest/api/2/issue/createmeta/RES/issuetypes/{issueTypeId}`\r\n(variable
name is `getIssueTypeFieldsUrl`)\r\n\r\n#### Analysed use cases to
retrieve API urls we hit\r\n\r\n- created a case with JIRA Cloud as
Connector\r\n- did a connector test with JIRA Cloud as connector\r\n-
created a case with JIRA DC as connector\r\n- did a connector test with
JIRA DC as connector\r\n\r\n### Conclusions\r\n\r\n- We are not using
the right endpoints for Cloud, we should update them\r\nto use the same
endpoints.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>\r\nCo-authored-by: Christos
Nasikas <christos.nasikas@elastic.co>\r\nCo-authored-by: adcoelho
<antonio.coelho@elastic.co>\r\nCo-authored-by: Antonio
<antoniodcoelho@gmail.com>\r\nCo-authored-by: Lisa Cawley
<lcawley@elastic.co>","sha":"953d877df04ce5d9b1c736e7da4d775febebfb68","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:ResponseOps","v9.0.0","backport:prev-minor","v8.17.0"],"title":"[Response
Ops][Connectors] Refactor Jira Connector to use latest API
only","number":197787,"url":"https://github.com/elastic/kibana/pull/197787","mergeCommit":{"message":"[Response
Ops][Connectors] Refactor Jira Connector to use latest API only
(#197787)\n\n## Summary\r\n\r\nJira Cloud and Datacenter work using the
same API urls. In this PR we\r\nremove the calls to the capabilities API
which was being used to know\r\nthe API url we needed to hit\r\n\r\nTo
test it:\r\n- Create Jira Cloud and Datacenter connectors\r\n- Test all
use cases related to them\r\n\r\nRelated to
https://github.com/elastic/kibana/issues/189017\r\n\r\n## Research
Work\r\n\r\n**getCapabilities, createIncident and getIncident** are
always the same,\r\ntherefore ignored for the rest of this
document\r\n\r\n- getCapabilities: `/rest/capabilities`\r\n-
createIncident: `/rest/api/2/issue`\r\n- getIncident:
`/rest/api/2/issue`\r\n\r\n## API links\r\n\r\n-
Cloud:\r\nhttps://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/#version\r\n-
DC:
https://docs.atlassian.com/software/jira/docs/api/REST/9.17.0/\r\n\r\n###
Expected API urls based on the API links\r\n\r\n- Get issue
types\r\n\r\n- Cloud: `GET
/rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes`\r\n -
DC:`GET /rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes`\r\n\r\n-
Get fields by issue type\r\n- Cloud:
`GET\r\n/rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes/{issueTypeId}`\r\n-
DC:\r\n`GET /rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes/{issueTypeId}`\r\n\r\n###
API we hit\r\n\r\n- Get issue types\r\n\r\n- Cloud
`GET\r\n/rest/api/2/issue/createmeta?projectKeys=ROC&expand=projects.issuetypes.fields`\r\n(variable
name we are using is `getIssueTypesOldAPIURL`)\r\n- DC `GET
/rest/api/2/issue/createmeta/RES/issuetypes` (variable name
is\r\n`getIssueTypesUrl`)\r\n\r\n- Get fields by issue type\r\n- Cloud
`GET\r\n/rest/api/2/issue/createmeta?projectKeys=ROC&issuetypeIds={issueTypeId}&expand=projects.issuetypes.fields`\r\n(variable
name is `getIssueTypeFieldsOldAPIURL`)\r\n- DC `GET
/rest/api/2/issue/createmeta/RES/issuetypes/{issueTypeId}`\r\n(variable
name is `getIssueTypeFieldsUrl`)\r\n\r\n#### Analysed use cases to
retrieve API urls we hit\r\n\r\n- created a case with JIRA Cloud as
Connector\r\n- did a connector test with JIRA Cloud as connector\r\n-
created a case with JIRA DC as connector\r\n- did a connector test with
JIRA DC as connector\r\n\r\n### Conclusions\r\n\r\n- We are not using
the right endpoints for Cloud, we should update them\r\nto use the same
endpoints.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>\r\nCo-authored-by: Christos
Nasikas <christos.nasikas@elastic.co>\r\nCo-authored-by: adcoelho
<antonio.coelho@elastic.co>\r\nCo-authored-by: Antonio
<antoniodcoelho@gmail.com>\r\nCo-authored-by: Lisa Cawley
<lcawley@elastic.co>","sha":"953d877df04ce5d9b1c736e7da4d775febebfb68"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/197787","number":197787,"mergeCommit":{"message":"[Response
Ops][Connectors] Refactor Jira Connector to use latest API only
(#197787)\n\n## Summary\r\n\r\nJira Cloud and Datacenter work using the
same API urls. In this PR we\r\nremove the calls to the capabilities API
which was being used to know\r\nthe API url we needed to hit\r\n\r\nTo
test it:\r\n- Create Jira Cloud and Datacenter connectors\r\n- Test all
use cases related to them\r\n\r\nRelated to
https://github.com/elastic/kibana/issues/189017\r\n\r\n## Research
Work\r\n\r\n**getCapabilities, createIncident and getIncident** are
always the same,\r\ntherefore ignored for the rest of this
document\r\n\r\n- getCapabilities: `/rest/capabilities`\r\n-
createIncident: `/rest/api/2/issue`\r\n- getIncident:
`/rest/api/2/issue`\r\n\r\n## API links\r\n\r\n-
Cloud:\r\nhttps://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/#version\r\n-
DC:
https://docs.atlassian.com/software/jira/docs/api/REST/9.17.0/\r\n\r\n###
Expected API urls based on the API links\r\n\r\n- Get issue
types\r\n\r\n- Cloud: `GET
/rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes`\r\n -
DC:`GET /rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes`\r\n\r\n-
Get fields by issue type\r\n- Cloud:
`GET\r\n/rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes/{issueTypeId}`\r\n-
DC:\r\n`GET /rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes/{issueTypeId}`\r\n\r\n###
API we hit\r\n\r\n- Get issue types\r\n\r\n- Cloud
`GET\r\n/rest/api/2/issue/createmeta?projectKeys=ROC&expand=projects.issuetypes.fields`\r\n(variable
name we are using is `getIssueTypesOldAPIURL`)\r\n- DC `GET
/rest/api/2/issue/createmeta/RES/issuetypes` (variable name
is\r\n`getIssueTypesUrl`)\r\n\r\n- Get fields by issue type\r\n- Cloud
`GET\r\n/rest/api/2/issue/createmeta?projectKeys=ROC&issuetypeIds={issueTypeId}&expand=projects.issuetypes.fields`\r\n(variable
name is `getIssueTypeFieldsOldAPIURL`)\r\n- DC `GET
/rest/api/2/issue/createmeta/RES/issuetypes/{issueTypeId}`\r\n(variable
name is `getIssueTypeFieldsUrl`)\r\n\r\n#### Analysed use cases to
retrieve API urls we hit\r\n\r\n- created a case with JIRA Cloud as
Connector\r\n- did a connector test with JIRA Cloud as connector\r\n-
created a case with JIRA DC as connector\r\n- did a connector test with
JIRA DC as connector\r\n\r\n### Conclusions\r\n\r\n- We are not using
the right endpoints for Cloud, we should update them\r\nto use the same
endpoints.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>\r\nCo-authored-by: Christos
Nasikas <christos.nasikas@elastic.co>\r\nCo-authored-by: adcoelho
<antonio.coelho@elastic.co>\r\nCo-authored-by: Antonio
<antoniodcoelho@gmail.com>\r\nCo-authored-by: Lisa Cawley
<lcawley@elastic.co>","sha":"953d877df04ce5d9b1c736e7da4d775febebfb68"}},{"branch":"8.x","label":"v8.17.0","branchLabelMappingKey":"^v8.17.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Julian Gernun <17549662+jcger@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2024-11-07 23:33:54 +11:00 committed by GitHub
parent 31e0899604
commit daa1b3c829
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 236 additions and 623 deletions

View file

@ -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]]

View file

@ -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();
});
});

View file

@ -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.

View file

@ -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'
);
});
});

View file

@ -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,

View file

@ -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>;

View file

@ -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',
},
],
});