Skip to content

Commit

Permalink
fix(Linear Node): Fix issue with error handling (#12191)
Browse files Browse the repository at this point in the history
  • Loading branch information
Joffcom authored Dec 16, 2024
1 parent 0c15e30 commit b8eae5f
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 8 deletions.
34 changes: 26 additions & 8 deletions packages/nodes-base/nodes/Linear/GenericFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import type {
ILoadOptionsFunctions,
IHookFunctions,
IWebhookFunctions,
JsonObject,
IRequestOptions,
IHttpRequestOptions,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';

Expand All @@ -24,24 +23,43 @@ export async function linearApiRequest(
const endpoint = 'https://api.linear.app/graphql';
const authenticationMethod = this.getNodeParameter('authentication', 0, 'apiToken') as string;

let options: IRequestOptions = {
let options: IHttpRequestOptions = {
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
body,
uri: endpoint,
url: endpoint,
json: true,
};
options = Object.assign({}, options, option);
try {
return await this.helpers.requestWithAuthentication.call(
const response = await this.helpers.httpRequestWithAuthentication.call(
this,
authenticationMethod === 'apiToken' ? 'linearApi' : 'linearOAuth2Api',
options,
);

if (response.errors) {
throw new NodeApiError(this.getNode(), response.errors, {
message: response.errors[0].message,
});
}

return response;
} 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,
},
);
}
}

Expand Down Expand Up @@ -85,7 +103,7 @@ export async function validateCredentials(
): Promise<any> {
const credentials = decryptedCredentials;

const options: IRequestOptions = {
const options: IHttpRequestOptions = {
headers: {
'Content-Type': 'application/json',
Authorization: credentials.apiKey,
Expand All @@ -97,7 +115,7 @@ export async function validateCredentials(
first: 1,
},
},
uri: 'https://api.linear.app/graphql',
url: 'https://api.linear.app/graphql',
json: true,
};

Expand Down
6 changes: 6 additions & 0 deletions packages/nodes-base/nodes/Linear/LinearTrigger.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ export class LinearTrigger implements INodeType {
],
default: 'apiToken',
},
{
displayName: 'Make sure your credential has the "Admin" scope to create webhooks.',
name: 'notice',
type: 'notice',
default: '',
},
{
displayName: 'Team Name or ID',
name: 'teamId',
Expand Down
135 changes: 135 additions & 0 deletions packages/nodes-base/nodes/Linear/test/GenericFunctions.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});

0 comments on commit b8eae5f

Please sign in to comment.