mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(Linear Node): Fix issue with error handling (#12191)
This commit is contained in:
parent
0c15e30778
commit
b8eae5f28a
|
@ -6,8 +6,7 @@ import type {
|
||||||
ILoadOptionsFunctions,
|
ILoadOptionsFunctions,
|
||||||
IHookFunctions,
|
IHookFunctions,
|
||||||
IWebhookFunctions,
|
IWebhookFunctions,
|
||||||
JsonObject,
|
IHttpRequestOptions,
|
||||||
IRequestOptions,
|
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeApiError } from 'n8n-workflow';
|
import { NodeApiError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
@ -24,24 +23,43 @@ export async function linearApiRequest(
|
||||||
const endpoint = 'https://api.linear.app/graphql';
|
const endpoint = 'https://api.linear.app/graphql';
|
||||||
const authenticationMethod = this.getNodeParameter('authentication', 0, 'apiToken') as string;
|
const authenticationMethod = this.getNodeParameter('authentication', 0, 'apiToken') as string;
|
||||||
|
|
||||||
let options: IRequestOptions = {
|
let options: IHttpRequestOptions = {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body,
|
body,
|
||||||
uri: endpoint,
|
url: endpoint,
|
||||||
json: true,
|
json: true,
|
||||||
};
|
};
|
||||||
options = Object.assign({}, options, option);
|
options = Object.assign({}, options, option);
|
||||||
try {
|
try {
|
||||||
return await this.helpers.requestWithAuthentication.call(
|
const response = await this.helpers.httpRequestWithAuthentication.call(
|
||||||
this,
|
this,
|
||||||
authenticationMethod === 'apiToken' ? 'linearApi' : 'linearOAuth2Api',
|
authenticationMethod === 'apiToken' ? 'linearApi' : 'linearOAuth2Api',
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (response.errors) {
|
||||||
|
throw new NodeApiError(this.getNode(), response.errors, {
|
||||||
|
message: response.errors[0].message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: error.errorResponse
|
||||||
|
? error.errorResponse[0].message
|
||||||
|
: error.context.data.errors[0].message,
|
||||||
|
description: error.errorResponse
|
||||||
|
? error.errorResponse[0].extensions.userPresentableMessage
|
||||||
|
: error.context.data.errors[0].extensions.userPresentableMessage,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +103,7 @@ export async function validateCredentials(
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const credentials = decryptedCredentials;
|
const credentials = decryptedCredentials;
|
||||||
|
|
||||||
const options: IRequestOptions = {
|
const options: IHttpRequestOptions = {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: credentials.apiKey,
|
Authorization: credentials.apiKey,
|
||||||
|
@ -97,7 +115,7 @@ export async function validateCredentials(
|
||||||
first: 1,
|
first: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
uri: 'https://api.linear.app/graphql',
|
url: 'https://api.linear.app/graphql',
|
||||||
json: true,
|
json: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -71,6 +71,12 @@ export class LinearTrigger implements INodeType {
|
||||||
],
|
],
|
||||||
default: 'apiToken',
|
default: 'apiToken',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Make sure your credential has the "Admin" scope to create webhooks.',
|
||||||
|
name: 'notice',
|
||||||
|
type: 'notice',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Team Name or ID',
|
displayName: 'Team Name or ID',
|
||||||
name: 'teamId',
|
name: 'teamId',
|
||||||
|
|
135
packages/nodes-base/nodes/Linear/test/GenericFunctions.test.ts
Normal file
135
packages/nodes-base/nodes/Linear/test/GenericFunctions.test.ts
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
import type {
|
||||||
|
IExecuteFunctions,
|
||||||
|
IHookFunctions,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
IWebhookFunctions,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import { NodeApiError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { capitalizeFirstLetter, linearApiRequest, sort } from '../GenericFunctions';
|
||||||
|
|
||||||
|
describe('Linear -> GenericFunctions', () => {
|
||||||
|
const mockHttpRequestWithAuthentication = jest.fn();
|
||||||
|
|
||||||
|
describe('linearApiRequest', () => {
|
||||||
|
let mockExecuteFunctions:
|
||||||
|
| IExecuteFunctions
|
||||||
|
| IWebhookFunctions
|
||||||
|
| IHookFunctions
|
||||||
|
| ILoadOptionsFunctions;
|
||||||
|
|
||||||
|
const setupMockFunctions = (authentication: string) => {
|
||||||
|
mockExecuteFunctions = {
|
||||||
|
getNodeParameter: jest.fn().mockReturnValue(authentication),
|
||||||
|
helpers: {
|
||||||
|
httpRequestWithAuthentication: mockHttpRequestWithAuthentication,
|
||||||
|
},
|
||||||
|
getNode: jest.fn().mockReturnValue({}),
|
||||||
|
} as unknown as
|
||||||
|
| IExecuteFunctions
|
||||||
|
| IWebhookFunctions
|
||||||
|
| IHookFunctions
|
||||||
|
| ILoadOptionsFunctions;
|
||||||
|
jest.clearAllMocks();
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setupMockFunctions('apiToken');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should make a successful API request', async () => {
|
||||||
|
const response = { data: { success: true } };
|
||||||
|
|
||||||
|
mockHttpRequestWithAuthentication.mockResolvedValue(response);
|
||||||
|
|
||||||
|
const result = await linearApiRequest.call(mockExecuteFunctions, {
|
||||||
|
query: '{ viewer { id } }',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(response);
|
||||||
|
expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
|
||||||
|
'linearApi',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
url: 'https://api.linear.app/graphql',
|
||||||
|
json: true,
|
||||||
|
body: { query: '{ viewer { id } }' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle API request errors', async () => {
|
||||||
|
const errorResponse = {
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: 'Access denied',
|
||||||
|
extensions: {
|
||||||
|
userPresentableMessage: 'You need to have the "Admin" scope to create webhooks.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockHttpRequestWithAuthentication.mockResolvedValue(errorResponse);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
linearApiRequest.call(mockExecuteFunctions, { query: '{ viewer { id } }' }),
|
||||||
|
).rejects.toThrow(NodeApiError);
|
||||||
|
|
||||||
|
expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
|
||||||
|
'linearApi',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
url: 'https://api.linear.app/graphql',
|
||||||
|
json: true,
|
||||||
|
body: { query: '{ viewer { id } }' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('capitalizeFirstLetter', () => {
|
||||||
|
it('should capitalize the first letter of a string', () => {
|
||||||
|
expect(capitalizeFirstLetter('hello')).toBe('Hello');
|
||||||
|
expect(capitalizeFirstLetter('world')).toBe('World');
|
||||||
|
expect(capitalizeFirstLetter('capitalize')).toBe('Capitalize');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty string if input is empty', () => {
|
||||||
|
expect(capitalizeFirstLetter('')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single character strings', () => {
|
||||||
|
expect(capitalizeFirstLetter('a')).toBe('A');
|
||||||
|
expect(capitalizeFirstLetter('b')).toBe('B');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not change the case of the rest of the string', () => {
|
||||||
|
expect(capitalizeFirstLetter('hELLO')).toBe('HELLO');
|
||||||
|
expect(capitalizeFirstLetter('wORLD')).toBe('WORLD');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sort', () => {
|
||||||
|
it('should sort objects by name in ascending order', () => {
|
||||||
|
const array = [{ name: 'banana' }, { name: 'apple' }, { name: 'cherry' }];
|
||||||
|
|
||||||
|
const sortedArray = array.sort(sort);
|
||||||
|
|
||||||
|
expect(sortedArray).toEqual([{ name: 'apple' }, { name: 'banana' }, { name: 'cherry' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle case insensitivity', () => {
|
||||||
|
const array = [{ name: 'Banana' }, { name: 'apple' }, { name: 'cherry' }];
|
||||||
|
|
||||||
|
const sortedArray = array.sort(sort);
|
||||||
|
|
||||||
|
expect(sortedArray).toEqual([{ name: 'apple' }, { name: 'Banana' }, { name: 'cherry' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 0 for objects with the same name', () => {
|
||||||
|
const result = sort({ name: 'apple' }, { name: 'apple' });
|
||||||
|
expect(result).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue