first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
import { type DeepMockProxy, mockDeep } from 'jest-mock-extended';
|
||||
import type { IDataObject, IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { handlePagination, jiraSoftwareCloudApiRequestAllItems } from '../GenericFunctions';
|
||||
|
||||
describe('Jira -> GenericFunctions', () => {
|
||||
describe('jiraSoftwareCloudApiRequestAllItems', () => {
|
||||
let mockExecuteFunctions: DeepMockProxy<IExecuteFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValue('server');
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({ domain: 'jira.domain.com' });
|
||||
mockExecuteFunctions.helpers.requestWithAuthentication.mockImplementation(
|
||||
async function (_, options) {
|
||||
if (!options.qs?.startAt) {
|
||||
return {
|
||||
issues: [{ id: 1000 }, { id: 1001 }],
|
||||
startAt: 0,
|
||||
maxResults: 2,
|
||||
total: 3,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
issues: [{ id: 1002 }],
|
||||
startAt: 2,
|
||||
maxResults: 2,
|
||||
total: 3,
|
||||
};
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get all items and not pass the body when the method is GET', async () => {
|
||||
const result = await jiraSoftwareCloudApiRequestAllItems.call(
|
||||
mockExecuteFunctions,
|
||||
'issues',
|
||||
'/api/2/search',
|
||||
'GET',
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ id: 1000 }, { id: 1001 }, { id: 1002 }]);
|
||||
expect(mockExecuteFunctions.helpers.requestWithAuthentication).toBeCalledTimes(2);
|
||||
expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
|
||||
'jiraSoftwareServerApi',
|
||||
expect.not.objectContaining({
|
||||
body: expect.anything(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handlePagination', () => {
|
||||
it('should initialize offset pagination parameters with GET when responseData is not provided', () => {
|
||||
const body: IDataObject = {};
|
||||
const query: IDataObject = {};
|
||||
|
||||
const result = handlePagination('GET', body, query, 'offset');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(query.startAt).toBe(0);
|
||||
expect(query.maxResults).toBe(100);
|
||||
expect(body).toEqual({});
|
||||
});
|
||||
|
||||
it('should initialize offset pagination parameters with POST when responseData is not provided', () => {
|
||||
const body: IDataObject = {};
|
||||
const query: IDataObject = {};
|
||||
|
||||
const result = handlePagination('POST', body, query, 'offset');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(body.startAt).toBe(0);
|
||||
expect(body.maxResults).toBe(100);
|
||||
expect(query).toEqual({});
|
||||
});
|
||||
|
||||
it('should initialize token pagination parameters with GET when responseData is not provided', () => {
|
||||
const body: IDataObject = {};
|
||||
const query: IDataObject = {};
|
||||
|
||||
const result = handlePagination('GET', body, query, 'token');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(query.maxResults).toBe(100);
|
||||
expect(body).toEqual({});
|
||||
});
|
||||
|
||||
it('should initialize token pagination parameters with POST when responseData is not provided', () => {
|
||||
const body: IDataObject = {};
|
||||
const query: IDataObject = {};
|
||||
|
||||
const result = handlePagination('POST', body, query, 'token');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(query).toEqual({});
|
||||
expect(body.maxResults).toBe(100);
|
||||
});
|
||||
|
||||
it('should handle offset pagination with GET and more pages available', () => {
|
||||
const body: IDataObject = {};
|
||||
const query: IDataObject = {};
|
||||
const responseData = {
|
||||
startAt: 0,
|
||||
maxResults: 100,
|
||||
total: 250,
|
||||
};
|
||||
|
||||
const result = handlePagination('GET', body, query, 'offset', responseData);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(query.startAt).toBe(100);
|
||||
expect(body).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle offset pagination with POST and more pages available', () => {
|
||||
const body: IDataObject = {};
|
||||
const query: IDataObject = {};
|
||||
const responseData = {
|
||||
startAt: 0,
|
||||
maxResults: 100,
|
||||
total: 250,
|
||||
};
|
||||
|
||||
const result = handlePagination('POST', body, query, 'offset', responseData);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(body.startAt).toBe(100);
|
||||
expect(query).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle offset pagination with GET and no more pages available', () => {
|
||||
const body: IDataObject = {};
|
||||
const query: IDataObject = {};
|
||||
const responseData = {
|
||||
startAt: 200,
|
||||
maxResults: 100,
|
||||
total: 250,
|
||||
};
|
||||
|
||||
const result = handlePagination('GET', body, query, 'offset', responseData);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(query.startAt).toBe(300);
|
||||
expect(body).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle offset pagination with POST and no more pages available', () => {
|
||||
const body: IDataObject = {};
|
||||
const query: IDataObject = {};
|
||||
const responseData = {
|
||||
startAt: 200,
|
||||
maxResults: 100,
|
||||
total: 250,
|
||||
};
|
||||
|
||||
const result = handlePagination('POST', body, query, 'offset', responseData);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(body.startAt).toBe(300);
|
||||
expect(query).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle token pagination with GET and more pages available', () => {
|
||||
const body: IDataObject = {};
|
||||
const query: IDataObject = {};
|
||||
const responseData = {
|
||||
nextPageToken: 'someToken123',
|
||||
};
|
||||
|
||||
const result = handlePagination('GET', body, query, 'token', responseData);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(query.nextPageToken).toBe('someToken123');
|
||||
expect(body).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle token pagination with POST and more pages available', () => {
|
||||
const body: IDataObject = {};
|
||||
const query: IDataObject = {};
|
||||
const responseData = {
|
||||
nextPageToken: 'someToken123',
|
||||
};
|
||||
|
||||
const result = handlePagination('POST', body, query, 'token', responseData);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(body.nextPageToken).toBe('someToken123');
|
||||
expect(query).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle token pagination with GET and no more pages available', () => {
|
||||
const body: IDataObject = {};
|
||||
const query: IDataObject = {};
|
||||
const responseData = {
|
||||
nextPageToken: '',
|
||||
};
|
||||
|
||||
const result = handlePagination('GET', body, query, 'token', responseData);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(query.nextPageToken).toBe('');
|
||||
expect(body).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle token pagination with POST and no more pages available', () => {
|
||||
const body: IDataObject = {};
|
||||
const query: IDataObject = {};
|
||||
const responseData = {
|
||||
nextPageToken: '',
|
||||
};
|
||||
|
||||
const result = handlePagination('POST', body, query, 'token', responseData);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(body.nextPageToken).toBe('');
|
||||
expect(query).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,248 @@
|
||||
import type { DeepMockProxy } from 'jest-mock-extended';
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import * as GenericFunctions from '../GenericFunctions';
|
||||
import { Jira } from '../Jira.node';
|
||||
|
||||
jest.mock('../GenericFunctions', () => ({
|
||||
jiraSoftwareCloudApiRequest: jest.fn().mockResolvedValue({ issues: [] }),
|
||||
jiraSoftwareCloudApiRequestAllItems: jest.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
const jiraSoftwareCloudApiRequestMock = GenericFunctions.jiraSoftwareCloudApiRequest as jest.Mock;
|
||||
const jiraSoftwareCloudApiRequestAllItems =
|
||||
GenericFunctions.jiraSoftwareCloudApiRequestAllItems as jest.Mock;
|
||||
|
||||
describe('Jira Node', () => {
|
||||
let jiraNode: Jira;
|
||||
let executeFunctionsMock: DeepMockProxy<IExecuteFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jiraNode = new Jira();
|
||||
executeFunctionsMock = mockDeep<IExecuteFunctions>();
|
||||
executeFunctionsMock.getInputData.mockReturnValue([{ json: {} }]);
|
||||
executeFunctionsMock.helpers.returnJsonArray.mockReturnValue([]);
|
||||
executeFunctionsMock.helpers.constructExecutionMetaData.mockReturnValue([]);
|
||||
});
|
||||
|
||||
describe('issue getAll', () => {
|
||||
it('should set default fields to "*navigable" when not provided', async () => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
switch (parameterName) {
|
||||
case 'resource':
|
||||
return 'issue';
|
||||
case 'operation':
|
||||
return 'getAll';
|
||||
case 'jiraVersion':
|
||||
return 'cloud';
|
||||
case 'returnAll':
|
||||
return false;
|
||||
case 'limit':
|
||||
return 10;
|
||||
case 'options':
|
||||
return { fields: undefined };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
await jiraNode.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(jiraSoftwareCloudApiRequestMock).toHaveBeenCalledWith(
|
||||
'/api/2/search/jql',
|
||||
'POST',
|
||||
expect.objectContaining({
|
||||
fields: ['*navigable'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should set default JQL filter to "created >= 1970-01-01" when not provided', async () => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
switch (parameterName) {
|
||||
case 'resource':
|
||||
return 'issue';
|
||||
case 'operation':
|
||||
return 'getAll';
|
||||
case 'jiraVersion':
|
||||
return 'cloud';
|
||||
case 'returnAll':
|
||||
return false;
|
||||
case 'limit':
|
||||
return 10;
|
||||
case 'options':
|
||||
return { jql: undefined };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
await jiraNode.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(jiraSoftwareCloudApiRequestMock).toHaveBeenCalledWith(
|
||||
'/api/2/search/jql',
|
||||
'POST',
|
||||
expect.objectContaining({
|
||||
jql: 'created >= "1970-01-01"',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use custom fields when provided', async () => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
switch (parameterName) {
|
||||
case 'resource':
|
||||
return 'issue';
|
||||
case 'operation':
|
||||
return 'getAll';
|
||||
case 'jiraVersion':
|
||||
return 'cloud';
|
||||
case 'returnAll':
|
||||
return false;
|
||||
case 'limit':
|
||||
return 10;
|
||||
case 'options':
|
||||
return { fields: 'summary,description' };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
await jiraNode.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(jiraSoftwareCloudApiRequestMock).toHaveBeenCalledWith(
|
||||
'/api/2/search/jql',
|
||||
'POST',
|
||||
expect.objectContaining({
|
||||
fields: ['summary', 'description'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use custom JQL filter when provided', async () => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
switch (parameterName) {
|
||||
case 'resource':
|
||||
return 'issue';
|
||||
case 'operation':
|
||||
return 'getAll';
|
||||
case 'jiraVersion':
|
||||
return 'cloud';
|
||||
case 'returnAll':
|
||||
return false;
|
||||
case 'limit':
|
||||
return 10;
|
||||
case 'options':
|
||||
return { jql: 'project = TEST' };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
await jiraNode.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(jiraSoftwareCloudApiRequestMock).toHaveBeenCalledWith(
|
||||
'/api/2/search/jql',
|
||||
'POST',
|
||||
expect.objectContaining({
|
||||
jql: 'project = TEST',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should call new endpoint for the cloud version with return all = true', async () => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
switch (parameterName) {
|
||||
case 'resource':
|
||||
return 'issue';
|
||||
case 'operation':
|
||||
return 'getAll';
|
||||
case 'jiraVersion':
|
||||
return 'cloud';
|
||||
case 'returnAll':
|
||||
return true;
|
||||
case 'options':
|
||||
return {};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
await jiraNode.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(jiraSoftwareCloudApiRequestAllItems).toHaveBeenCalledWith(
|
||||
'issues',
|
||||
'/api/2/search/jql',
|
||||
'POST',
|
||||
expect.anything(),
|
||||
{},
|
||||
'token',
|
||||
);
|
||||
});
|
||||
|
||||
it.each([['server'], ['serverPat']])(
|
||||
'should call old endpoint for the self-hosted version with return all = false',
|
||||
async (jiraVersion: string) => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
switch (parameterName) {
|
||||
case 'resource':
|
||||
return 'issue';
|
||||
case 'operation':
|
||||
return 'getAll';
|
||||
case 'jiraVersion':
|
||||
return jiraVersion;
|
||||
case 'returnAll':
|
||||
return false;
|
||||
case 'limit':
|
||||
return 10;
|
||||
case 'options':
|
||||
return {};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
await jiraNode.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(jiraSoftwareCloudApiRequestMock).toHaveBeenCalledWith(
|
||||
'/api/2/search',
|
||||
'POST',
|
||||
expect.anything(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([['server'], ['serverPat']])(
|
||||
'should call old endpoint for the self-hosted version with return all = true',
|
||||
async (jiraVersion: string) => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
switch (parameterName) {
|
||||
case 'resource':
|
||||
return 'issue';
|
||||
case 'operation':
|
||||
return 'getAll';
|
||||
case 'jiraVersion':
|
||||
return jiraVersion;
|
||||
case 'returnAll':
|
||||
return true;
|
||||
case 'options':
|
||||
return {};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
await jiraNode.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(jiraSoftwareCloudApiRequestAllItems).toHaveBeenCalledWith(
|
||||
'issues',
|
||||
'/api/2/search',
|
||||
'POST',
|
||||
expect.anything(),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,263 @@
|
||||
import { mock, mockDeep } from 'jest-mock-extended';
|
||||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
IDataObject,
|
||||
IHookFunctions,
|
||||
INode,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { testWebhookTriggerNode } from '@test/nodes/TriggerHelpers';
|
||||
|
||||
import { JiraTrigger } from '../JiraTrigger.node';
|
||||
|
||||
describe('JiraTrigger', () => {
|
||||
describe('Webhook lifecycle', () => {
|
||||
let staticData: IDataObject;
|
||||
|
||||
beforeEach(() => {
|
||||
staticData = {};
|
||||
});
|
||||
|
||||
function mockHookFunctions(
|
||||
mockRequest: IHookFunctions['helpers']['requestWithAuthentication'],
|
||||
) {
|
||||
const baseUrl = 'https://jira.local';
|
||||
const credential = {
|
||||
email: 'test@n8n.io',
|
||||
password: 'secret',
|
||||
domain: baseUrl,
|
||||
};
|
||||
|
||||
return mockDeep<IHookFunctions>({
|
||||
getWorkflowStaticData: () => staticData,
|
||||
getNode: jest.fn(() => mock<INode>({ typeVersion: 1 })),
|
||||
getNodeWebhookUrl: jest.fn(() => 'https://n8n.local/webhook/id'),
|
||||
getNodeParameter: jest.fn((param: string) => {
|
||||
if (param === 'events') return ['jira:issue_created'];
|
||||
return {};
|
||||
}),
|
||||
getCredentials: async <T extends object = ICredentialDataDecryptedObject>() =>
|
||||
credential as T,
|
||||
helpers: {
|
||||
requestWithAuthentication: mockRequest,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
test('should register a webhook subscription on Jira 10', async () => {
|
||||
const trigger = new JiraTrigger();
|
||||
|
||||
const mockExistsRequest = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ versionNumbers: [10, 0, 1] })
|
||||
.mockResolvedValueOnce([]);
|
||||
|
||||
const exists = await trigger.webhookMethods.default?.checkExists.call(
|
||||
mockHookFunctions(mockExistsRequest),
|
||||
);
|
||||
|
||||
expect(mockExistsRequest).toHaveBeenCalledTimes(2);
|
||||
expect(mockExistsRequest).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({ uri: 'https://jira.local/rest/api/2/serverInfo' }),
|
||||
);
|
||||
expect(mockExistsRequest).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({ uri: 'https://jira.local/rest/jira-webhook/1.0/webhooks' }),
|
||||
);
|
||||
expect(staticData.endpoint).toBe('/jira-webhook/1.0/webhooks');
|
||||
expect(exists).toBe(false);
|
||||
|
||||
const mockCreateRequest = jest.fn().mockResolvedValueOnce({ id: 1 });
|
||||
|
||||
const created = await trigger.webhookMethods.default?.create.call(
|
||||
mockHookFunctions(mockCreateRequest),
|
||||
);
|
||||
|
||||
expect(mockCreateRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockCreateRequest).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
uri: 'https://jira.local/rest/jira-webhook/1.0/webhooks',
|
||||
body: expect.objectContaining({
|
||||
events: ['jira:issue_created'],
|
||||
excludeBody: false,
|
||||
filters: {},
|
||||
name: 'n8n-webhook:https://n8n.local/webhook/id',
|
||||
url: 'https://n8n.local/webhook/id',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(created).toBe(true);
|
||||
|
||||
const mockDeleteRequest = jest.fn().mockResolvedValueOnce({});
|
||||
const deleted = await trigger.webhookMethods.default?.delete.call(
|
||||
mockHookFunctions(mockDeleteRequest),
|
||||
);
|
||||
|
||||
expect(deleted).toBe(true);
|
||||
expect(mockDeleteRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockDeleteRequest).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
method: 'DELETE',
|
||||
uri: 'https://jira.local/rest/jira-webhook/1.0/webhooks/1',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should register a webhook subscription on Jira 9', async () => {
|
||||
const trigger = new JiraTrigger();
|
||||
|
||||
const mockExistsRequest = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ versionNumbers: [9, 0, 1] })
|
||||
.mockResolvedValueOnce([]);
|
||||
|
||||
const exists = await trigger.webhookMethods.default?.checkExists.call(
|
||||
mockHookFunctions(mockExistsRequest),
|
||||
);
|
||||
|
||||
expect(mockExistsRequest).toHaveBeenCalledTimes(2);
|
||||
expect(mockExistsRequest).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({ uri: 'https://jira.local/rest/api/2/serverInfo' }),
|
||||
);
|
||||
expect(mockExistsRequest).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({ uri: 'https://jira.local/rest/webhooks/1.0/webhook' }),
|
||||
);
|
||||
expect(staticData.endpoint).toBe('/webhooks/1.0/webhook');
|
||||
expect(exists).toBe(false);
|
||||
|
||||
const mockCreateRequest = jest.fn().mockResolvedValueOnce({ id: 1 });
|
||||
|
||||
const created = await trigger.webhookMethods.default?.create.call(
|
||||
mockHookFunctions(mockCreateRequest),
|
||||
);
|
||||
|
||||
expect(mockCreateRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockCreateRequest).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
uri: 'https://jira.local/rest/webhooks/1.0/webhook',
|
||||
body: expect.objectContaining({
|
||||
events: ['jira:issue_created'],
|
||||
excludeBody: false,
|
||||
filters: {},
|
||||
name: 'n8n-webhook:https://n8n.local/webhook/id',
|
||||
url: 'https://n8n.local/webhook/id',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(created).toBe(true);
|
||||
|
||||
const mockDeleteRequest = jest.fn().mockResolvedValueOnce({});
|
||||
const deleted = await trigger.webhookMethods.default?.delete.call(
|
||||
mockHookFunctions(mockDeleteRequest),
|
||||
);
|
||||
|
||||
expect(deleted).toBe(true);
|
||||
expect(mockDeleteRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockDeleteRequest).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
method: 'DELETE',
|
||||
uri: 'https://jira.local/rest/webhooks/1.0/webhook/1',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should register a webhook subscription on Jira Cloud', async () => {
|
||||
const trigger = new JiraTrigger();
|
||||
|
||||
const mockExistsRequest = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ deploymentType: 'Cloud', versionNumbers: [1000, 0, 1] })
|
||||
.mockResolvedValueOnce([]);
|
||||
|
||||
const exists = await trigger.webhookMethods.default?.checkExists.call(
|
||||
mockHookFunctions(mockExistsRequest),
|
||||
);
|
||||
|
||||
expect(mockExistsRequest).toHaveBeenCalledTimes(2);
|
||||
expect(mockExistsRequest).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({ uri: 'https://jira.local/rest/api/2/serverInfo' }),
|
||||
);
|
||||
expect(mockExistsRequest).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({ uri: 'https://jira.local/rest/webhooks/1.0/webhook' }),
|
||||
);
|
||||
expect(staticData.endpoint).toBe('/webhooks/1.0/webhook');
|
||||
expect(exists).toBe(false);
|
||||
|
||||
const mockCreateRequest = jest.fn().mockResolvedValueOnce({ id: 1 });
|
||||
|
||||
const created = await trigger.webhookMethods.default?.create.call(
|
||||
mockHookFunctions(mockCreateRequest),
|
||||
);
|
||||
|
||||
expect(mockCreateRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockCreateRequest).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
uri: 'https://jira.local/rest/webhooks/1.0/webhook',
|
||||
body: expect.objectContaining({
|
||||
events: ['jira:issue_created'],
|
||||
excludeBody: false,
|
||||
filters: {},
|
||||
name: 'n8n-webhook:https://n8n.local/webhook/id',
|
||||
url: 'https://n8n.local/webhook/id',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(created).toBe(true);
|
||||
|
||||
const mockDeleteRequest = jest.fn().mockResolvedValueOnce({});
|
||||
const deleted = await trigger.webhookMethods.default?.delete.call(
|
||||
mockHookFunctions(mockDeleteRequest),
|
||||
);
|
||||
|
||||
expect(deleted).toBe(true);
|
||||
expect(mockDeleteRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockDeleteRequest).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
method: 'DELETE',
|
||||
uri: 'https://jira.local/rest/webhooks/1.0/webhook/1',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Webhook', () => {
|
||||
test('should receive a webhook event', async () => {
|
||||
const event = {
|
||||
timestamp: 1743524005044,
|
||||
webhookEvent: 'jira:issue_created',
|
||||
issue_event_type_name: 'issue_created',
|
||||
user: {
|
||||
self: 'http://localhost:8080/rest/api/2/user?key=JIRAUSER10000',
|
||||
name: 'elias',
|
||||
key: 'JIRAUSER10000',
|
||||
emailAddress: 'elias@meire.dev',
|
||||
displayName: 'Test',
|
||||
},
|
||||
issue: {
|
||||
id: '10018',
|
||||
self: 'http://localhost:8080/rest/api/2/issue/10018',
|
||||
key: 'TEST-19',
|
||||
},
|
||||
};
|
||||
const { responseData } = await testWebhookTriggerNode(JiraTrigger, {
|
||||
bodyData: event,
|
||||
});
|
||||
|
||||
expect(responseData).toEqual({ workflowData: [[{ json: event }]] });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { MockProxy } from 'jest-mock-extended';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { IHttpRequestMethods, ILoadOptionsFunctions } from 'n8n-workflow';
|
||||
|
||||
import { Jira } from '../Jira.node';
|
||||
|
||||
const ISSUE_KEY = 'KEY-1';
|
||||
|
||||
jest.mock('../GenericFunctions', () => {
|
||||
const originalModule = jest.requireActual('../GenericFunctions');
|
||||
return {
|
||||
...originalModule,
|
||||
jiraSoftwareCloudApiRequest: jest.fn(async function (
|
||||
endpoint: string,
|
||||
method: IHttpRequestMethods,
|
||||
) {
|
||||
if (method === 'GET' && endpoint === `/api/2/issue/${ISSUE_KEY}`) {
|
||||
return {
|
||||
id: 10000,
|
||||
fields: {
|
||||
project: {
|
||||
id: 10001,
|
||||
},
|
||||
issuetype: {
|
||||
id: 10002,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (method === 'GET' && endpoint === '/api/2/issue/10000/editmeta') {
|
||||
return {
|
||||
fields: {
|
||||
customfield_123: {
|
||||
name: 'Field 123',
|
||||
},
|
||||
customfield_456: {
|
||||
name: 'Field 456',
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (
|
||||
method === 'GET' &&
|
||||
endpoint ===
|
||||
'/api/2/issue/createmeta?projectIds=10001&issueTypeIds=10002&expand=projects.issuetypes.fields'
|
||||
) {
|
||||
return {
|
||||
projects: [
|
||||
{
|
||||
id: 10001,
|
||||
issuetypes: [
|
||||
{
|
||||
id: 10002,
|
||||
fields: {
|
||||
customfield_abc: {
|
||||
name: 'Field ABC',
|
||||
schema: { customId: 'customfield_abc' },
|
||||
fieldId: 'customfield_abc',
|
||||
},
|
||||
customfield_def: {
|
||||
name: 'Field DEF',
|
||||
schema: { customId: 'customfield_def' },
|
||||
fieldId: 'customfield_def',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Jira Node, methods', () => {
|
||||
let jira: Jira;
|
||||
let loadOptionsFunctions: MockProxy<ILoadOptionsFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
jira = new Jira();
|
||||
loadOptionsFunctions = mock<ILoadOptionsFunctions>();
|
||||
});
|
||||
|
||||
describe('listSearch.getCustomFields', () => {
|
||||
it('should call correct endpoint and return custom fields for server version', async () => {
|
||||
loadOptionsFunctions.getCurrentNodeParameter.mockReturnValueOnce('update');
|
||||
loadOptionsFunctions.getNodeParameter.mockReturnValue('server');
|
||||
loadOptionsFunctions.getCurrentNodeParameter.mockReturnValueOnce(ISSUE_KEY);
|
||||
|
||||
const { results } = await jira.methods.listSearch.getCustomFields.call(
|
||||
loadOptionsFunctions as ILoadOptionsFunctions,
|
||||
);
|
||||
|
||||
expect(results).toEqual([
|
||||
{
|
||||
name: 'Field 123',
|
||||
value: 'customfield_123',
|
||||
},
|
||||
{
|
||||
name: 'Field 456',
|
||||
value: 'customfield_456',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should call correct endpoint and return custom fields for cloud version', async () => {
|
||||
loadOptionsFunctions.getCurrentNodeParameter.mockReturnValueOnce('update');
|
||||
loadOptionsFunctions.getNodeParameter.mockReturnValue('cloud');
|
||||
loadOptionsFunctions.getCurrentNodeParameter.mockReturnValueOnce(ISSUE_KEY);
|
||||
|
||||
const { results } = await jira.methods.listSearch.getCustomFields.call(
|
||||
loadOptionsFunctions as ILoadOptionsFunctions,
|
||||
);
|
||||
|
||||
expect(results).toEqual([
|
||||
{
|
||||
name: 'Field ABC',
|
||||
value: 'customfield_abc',
|
||||
},
|
||||
{
|
||||
name: 'Field DEF',
|
||||
value: 'customfield_def',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user