Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion packages/nodes-base/nodes/Linear/GenericFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,22 @@ export async function linearApiRequest(
);

if (response?.errors) {
const errorMessage = response.errors[0].message ?? 'Unknown API Error';
const description = response.errors[0].extensions?.userPresentableMessage;

throw new NodeApiError(this.getNode(), response.errors, {
message: response.errors[0].message ?? 'Unknown API Error',
message: `Linear API error: ${errorMessage}`,
description,
});
}

return response;
} catch (error) {
// If this is already a NodeApiError with custom formatting, re-throw it as-is
if (error instanceof NodeApiError) {
throw error;
}

throw new NodeApiError(
this.getNode(),
{},
Expand Down
12 changes: 9 additions & 3 deletions packages/nodes-base/nodes/Linear/test/GenericFunctions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,15 @@ describe('Linear -> GenericFunctions', () => {

mockHttpRequestWithAuthentication.mockResolvedValue(errorResponse);

await expect(
linearApiRequest.call(mockExecuteFunctions, { query: '{ viewer { id } }' }),
).rejects.toThrow(NodeApiError);
try {
await linearApiRequest.call(mockExecuteFunctions, { query: '{ viewer { id } }' });
fail('Expected error to be thrown');
} catch (error) {
expect(error).toBeInstanceOf(NodeApiError);
expect(error.message).toContain('Linear API error');
expect(error.message).toContain('Access denied');
expect(error.description).toContain('Admin');
}

expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
'linearApi',
Expand Down
98 changes: 98 additions & 0 deletions packages/nodes-base/nodes/Linear/test/LinearTrigger.node.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { IHookFunctions } from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';

import { linearApiRequest } from '../GenericFunctions';

describe('Linear -> LinearTrigger.node', () => {
const mockHttpRequestWithAuthentication = jest.fn();

describe('webhook creation error handling', () => {
let mockHookFunctions: IHookFunctions;

beforeEach(() => {
mockHookFunctions = {
getNodeParameter: jest.fn().mockReturnValue('apiToken'),
helpers: {
httpRequestWithAuthentication: mockHttpRequestWithAuthentication,
},
getNode: jest.fn().mockReturnValue({ name: 'Linear Trigger', type: 'n8n-nodes-base.linearTrigger' }),
} as unknown as IHookFunctions;
jest.clearAllMocks();
});

it('should throw error that clearly mentions Linear when webhook creation fails due to admin permission', async () => {
// Simulate Linear API error response for insufficient permissions
const linearErrorResponse = {
errors: [
{
message: 'Invalid role: admin required',
extensions: {
userPresentableMessage: 'You need to have the "Admin" scope to create webhooks.',
},
},
],
};

mockHttpRequestWithAuthentication.mockResolvedValue(linearErrorResponse);

const webhookCreateBody = {
query: `
mutation webhookCreate($url: String!, $teamId: String!, $resources: [String!]!) {
webhookCreate(
input: {
url: $url
teamId: $teamId
resourceTypes: $resources
}
) {
success
webhook {
id
enabled
}
}
}`,
variables: {
url: 'http://example.com/webhook',
teamId: 'team-123',
resources: ['Issue', 'Comment'],
},
};

try {
await linearApiRequest.call(mockHookFunctions, webhookCreateBody);
fail('Expected error to be thrown');
} catch (error) {
expect(error).toBeInstanceOf(NodeApiError);
// The error message should clearly mention Linear to avoid confusion with n8n permissions
expect(error.message).toMatch(/linear/i);
// Should include information about admin requirement
expect(error.message.toLowerCase()).toContain('admin');
}
});

it('should preserve userPresentableMessage in error description', async () => {
const linearErrorResponse = {
errors: [
{
message: 'Invalid role: admin required',
extensions: {
userPresentableMessage: 'You need to have the "Admin" scope to create webhooks.',
},
},
],
};

mockHttpRequestWithAuthentication.mockResolvedValue(linearErrorResponse);

try {
await linearApiRequest.call(mockHookFunctions, {});
fail('Expected error to be thrown');
} catch (error) {
expect(error).toBeInstanceOf(NodeApiError);
expect(error.description).toContain('Admin');
expect(error.description).toContain('webhooks');
}
});
});
});