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,18 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.bitbucketTrigger",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Development", "Productivity"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/bitbucket/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.bitbuckettrigger/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
import {
|
||||
type IHookFunctions,
|
||||
type IWebhookFunctions,
|
||||
type ICredentialsDecrypted,
|
||||
type ICredentialTestFunctions,
|
||||
type IDataObject,
|
||||
type ILoadOptionsFunctions,
|
||||
type INodeCredentialTestResult,
|
||||
type INodePropertyOptions,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
type IWebhookResponseData,
|
||||
type IRequestOptions,
|
||||
NodeConnectionTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { bitbucketApiRequest, bitbucketApiRequestAllItems } from './GenericFunctions';
|
||||
|
||||
export class BitbucketTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Bitbucket Trigger',
|
||||
name: 'bitbucketTrigger',
|
||||
icon: 'file:bitbucket.svg',
|
||||
group: ['trigger'],
|
||||
version: [1, 1.1],
|
||||
defaultVersion: 1.1,
|
||||
description: 'Handle Bitbucket events via webhooks',
|
||||
defaults: {
|
||||
name: 'Bitbucket Trigger',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'bitbucketApi',
|
||||
required: true,
|
||||
testedBy: 'bitbucketApiTest',
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['password'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'bitbucketAccessTokenApi',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['accessToken'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'POST',
|
||||
responseMode: 'onReceived',
|
||||
path: 'webhook',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Password (Deprecated)',
|
||||
value: 'password',
|
||||
},
|
||||
{
|
||||
name: 'Access Token',
|
||||
value: 'accessToken',
|
||||
},
|
||||
],
|
||||
default: 'password',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Password (Deprecated)',
|
||||
value: 'password',
|
||||
},
|
||||
{
|
||||
name: 'Access Token',
|
||||
value: 'accessToken',
|
||||
},
|
||||
],
|
||||
default: 'accessToken',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1.1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
required: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Repository',
|
||||
value: 'repository',
|
||||
},
|
||||
{
|
||||
name: 'Workspace',
|
||||
value: 'workspace',
|
||||
},
|
||||
],
|
||||
default: 'workspace',
|
||||
},
|
||||
{
|
||||
displayName: 'Workspace Name or ID',
|
||||
name: 'workspace',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['workspace', 'repository'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getWorkspaces',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
description:
|
||||
'The repository of which to listen to the events. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Event Names or IDs',
|
||||
name: 'events',
|
||||
type: 'multiOptions',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['workspace'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getWorkspaceEvents',
|
||||
},
|
||||
options: [],
|
||||
required: true,
|
||||
default: [],
|
||||
description:
|
||||
'The events to listen to. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Repository Name or ID',
|
||||
name: 'repository',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['repository'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getRepositories',
|
||||
loadOptionsDependsOn: ['workspace'],
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
description:
|
||||
'The repository of which to listen to the events. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Event Names or IDs',
|
||||
name: 'events',
|
||||
type: 'multiOptions',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['repository'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getRepositoriesEvents',
|
||||
},
|
||||
options: [],
|
||||
required: true,
|
||||
default: [],
|
||||
description:
|
||||
'The events to listen to. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
credentialTest: {
|
||||
async bitbucketApiTest(
|
||||
this: ICredentialTestFunctions,
|
||||
credential: ICredentialsDecrypted,
|
||||
): Promise<INodeCredentialTestResult> {
|
||||
const credentials = credential.data;
|
||||
|
||||
const options: IRequestOptions = {
|
||||
method: 'GET',
|
||||
auth: {
|
||||
user: credentials!.username as string,
|
||||
password: credentials!.appPassword as string,
|
||||
},
|
||||
uri: 'https://api.bitbucket.org/2.0/user',
|
||||
json: true,
|
||||
timeout: 5000,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await this.helpers.request(options);
|
||||
if (!response.username) {
|
||||
return {
|
||||
status: 'Error',
|
||||
message: `Token is not valid: ${response.error}`,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'Error',
|
||||
message: `Settings are not valid: ${error}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 'OK',
|
||||
message: 'Authentication successful!',
|
||||
};
|
||||
},
|
||||
},
|
||||
loadOptions: {
|
||||
async getWorkspaceEvents(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const events = await bitbucketApiRequestAllItems.call(
|
||||
this,
|
||||
'values',
|
||||
'GET',
|
||||
'/hook_events/workspace',
|
||||
);
|
||||
for (const event of events) {
|
||||
returnData.push({
|
||||
name: event.event,
|
||||
value: event.event,
|
||||
description: event.description,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
async getRepositoriesEvents(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const events = await bitbucketApiRequestAllItems.call(
|
||||
this,
|
||||
'values',
|
||||
'GET',
|
||||
'/hook_events/repository',
|
||||
);
|
||||
for (const event of events) {
|
||||
returnData.push({
|
||||
name: event.event,
|
||||
value: event.event,
|
||||
description: event.description,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
async getRepositories(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const workspace = this.getCurrentNodeParameter('workspace') as string;
|
||||
const repositories = await bitbucketApiRequestAllItems.call(
|
||||
this,
|
||||
'values',
|
||||
'GET',
|
||||
`/repositories/${workspace}`,
|
||||
);
|
||||
for (const repository of repositories) {
|
||||
returnData.push({
|
||||
name: repository.slug,
|
||||
value: repository.slug,
|
||||
description: repository.description,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
async getWorkspaces(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const workspaces = await bitbucketApiRequestAllItems.call(
|
||||
this,
|
||||
'values',
|
||||
'GET',
|
||||
'/workspaces',
|
||||
);
|
||||
for (const workspace of workspaces) {
|
||||
returnData.push({
|
||||
name: workspace.name,
|
||||
value: workspace.slug,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
webhookMethods = {
|
||||
default: {
|
||||
async checkExists(this: IHookFunctions): Promise<boolean> {
|
||||
let endpoint = '';
|
||||
const resource = this.getNodeParameter('resource', 0);
|
||||
const workspace = this.getNodeParameter('workspace', 0) as string;
|
||||
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
if (resource === 'workspace') {
|
||||
endpoint = `/workspaces/${workspace}/hooks`;
|
||||
}
|
||||
if (resource === 'repository') {
|
||||
const repository = this.getNodeParameter('repository', 0) as string;
|
||||
endpoint = `/repositories/${workspace}/${repository}/hooks`;
|
||||
}
|
||||
const { values: hooks } = await bitbucketApiRequest.call(this, 'GET', endpoint);
|
||||
for (const hook of hooks) {
|
||||
if (webhookUrl === hook.url && hook.active === true) {
|
||||
webhookData.webhookId = hook.uuid.replace('{', '').replace('}', '');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
async create(this: IHookFunctions): Promise<boolean> {
|
||||
let endpoint = '';
|
||||
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
const events = this.getNodeParameter('events') as string[];
|
||||
const resource = this.getNodeParameter('resource', 0);
|
||||
const workspace = this.getNodeParameter('workspace', 0) as string;
|
||||
|
||||
if (resource === 'workspace') {
|
||||
endpoint = `/workspaces/${workspace}/hooks`;
|
||||
}
|
||||
if (resource === 'repository') {
|
||||
const repository = this.getNodeParameter('repository', 0) as string;
|
||||
endpoint = `/repositories/${workspace}/${repository}/hooks`;
|
||||
}
|
||||
const body: IDataObject = {
|
||||
description: 'n8n webhook',
|
||||
url: webhookUrl,
|
||||
active: true,
|
||||
events,
|
||||
};
|
||||
const responseData = await bitbucketApiRequest.call(this, 'POST', endpoint, body);
|
||||
webhookData.webhookId = responseData.uuid.replace('{', '').replace('}', '');
|
||||
return true;
|
||||
},
|
||||
async delete(this: IHookFunctions): Promise<boolean> {
|
||||
let endpoint = '';
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
const workspace = this.getNodeParameter('workspace', 0) as string;
|
||||
const resource = this.getNodeParameter('resource', 0);
|
||||
if (resource === 'workspace') {
|
||||
endpoint = `/workspaces/${workspace}/hooks/${webhookData.webhookId}`;
|
||||
}
|
||||
if (resource === 'repository') {
|
||||
const repository = this.getNodeParameter('repository', 0) as string;
|
||||
endpoint = `/repositories/${workspace}/${repository}/hooks/${webhookData.webhookId}`;
|
||||
}
|
||||
try {
|
||||
await bitbucketApiRequest.call(this, 'DELETE', endpoint);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
delete webhookData.webhookId;
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||
const req = this.getRequestObject();
|
||||
const headerData = this.getHeaderData() as IDataObject;
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
if (headerData['x-hook-uuid'] !== webhookData.webhookId) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
workflowData: [this.helpers.returnJsonArray(req.body as IDataObject[])],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
JsonObject,
|
||||
IHttpRequestMethods,
|
||||
IRequestOptions,
|
||||
IHttpRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
export async function bitbucketApiRequest(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
resource: string,
|
||||
body: any = {},
|
||||
qs: IDataObject = {},
|
||||
uri?: string,
|
||||
): Promise<any> {
|
||||
try {
|
||||
const authentication = this.getNodeParameter('authentication', 0) as 'password' | 'accessToken';
|
||||
|
||||
if (authentication === 'accessToken') {
|
||||
const httpRequestOptions: IHttpRequestOptions = {
|
||||
method,
|
||||
qs,
|
||||
body,
|
||||
url: uri || `https://api.bitbucket.org/2.0${resource}`,
|
||||
json: true,
|
||||
};
|
||||
|
||||
if (Object.keys(httpRequestOptions.body as IDataObject).length === 0) {
|
||||
delete httpRequestOptions.body;
|
||||
}
|
||||
|
||||
return await this.helpers.httpRequestWithAuthentication.call(
|
||||
this,
|
||||
'bitbucketAccessTokenApi',
|
||||
httpRequestOptions,
|
||||
);
|
||||
}
|
||||
|
||||
const credentials = await this.getCredentials('bitbucketApi');
|
||||
const options: IRequestOptions = {
|
||||
method,
|
||||
auth: {
|
||||
user: credentials.username as string,
|
||||
password: credentials.appPassword as string,
|
||||
},
|
||||
qs,
|
||||
body,
|
||||
uri: uri || `https://api.bitbucket.org/2.0${resource}`,
|
||||
json: true,
|
||||
};
|
||||
|
||||
if (Object.keys(options.body as IDataObject).length === 0) {
|
||||
delete options.body;
|
||||
}
|
||||
|
||||
return await this.helpers.request(options);
|
||||
} catch (error) {
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an API request to paginated flow endpoint
|
||||
* and return all results
|
||||
*/
|
||||
export async function bitbucketApiRequestAllItems(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
||||
propertyName: string,
|
||||
method: IHttpRequestMethods,
|
||||
resource: string,
|
||||
|
||||
body: any = {},
|
||||
query: IDataObject = {},
|
||||
): Promise<any> {
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
let responseData;
|
||||
|
||||
let uri: string | undefined;
|
||||
|
||||
do {
|
||||
responseData = await bitbucketApiRequest.call(this, method, resource, body, query, uri);
|
||||
uri = responseData.next;
|
||||
returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]);
|
||||
} while (responseData.next !== undefined);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="none"><defs><linearGradient id="a" x1=".762" x2=".33" y1="-.17" y2=".656"><stop offset=".18" stop-color="#0052CC"/><stop offset="1" stop-color="#2684FF"/></linearGradient></defs><path fill="#2684FF" fill-rule="evenodd" stroke="null" d="M1.721 3.873a1.47 1.47 0 0 1 1.129-.509l42.772.007a1.466 1.466 0 0 1 1.466 1.7l-6.222 38.193a1.466 1.466 0 0 1-1.465 1.231H9.555a1.99 1.99 0 0 1-1.95-1.663L1.386 5.064c-.07-.427.053-.863.336-1.191zm17.8 26.787h9.526l2.308-13.475H16.941z" clip-rule="evenodd"/><path fill="url(#a)" stroke="null" d="M44.418 18.437h-12.64l-2.12 12.383h-8.755L10.567 43.09c.327.283.745.44 1.178.444H39.18a1.35 1.35 0 0 0 1.347-1.132l3.892-23.965z"/></svg>
|
||||
|
After Width: | Height: | Size: 739 B |
@@ -0,0 +1,670 @@
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import type {
|
||||
ICredentialsDecrypted,
|
||||
ICredentialTestFunctions,
|
||||
IHookFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
INode,
|
||||
IWebhookFunctions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { BitbucketTrigger } from '../BitbucketTrigger.node';
|
||||
import * as GenericFunctions from '../GenericFunctions';
|
||||
|
||||
describe('BitbucketTrigger', () => {
|
||||
let bitbucketTrigger: BitbucketTrigger;
|
||||
const bitbucketApiRequestSpy = jest.spyOn(GenericFunctions, 'bitbucketApiRequest');
|
||||
const bitbucketApiRequestAllItemsSpy = jest.spyOn(
|
||||
GenericFunctions,
|
||||
'bitbucketApiRequestAllItems',
|
||||
);
|
||||
|
||||
const mockNode: INode = {
|
||||
id: 'test-node-id',
|
||||
name: 'Bitbucket Trigger Test',
|
||||
type: 'n8n-nodes-base.bitbucketTrigger',
|
||||
typeVersion: 1.1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
bitbucketTrigger = new BitbucketTrigger();
|
||||
});
|
||||
|
||||
describe('credential test', () => {
|
||||
const mockCredentialTestFunctions = mockDeep<ICredentialTestFunctions>();
|
||||
|
||||
beforeEach(() => {
|
||||
mockCredentialTestFunctions.helpers.request.mockClear();
|
||||
});
|
||||
|
||||
it('should return success for valid credentials', async () => {
|
||||
const mockCredentials: ICredentialsDecrypted = {
|
||||
id: 'test-cred-id',
|
||||
name: 'Test Bitbucket Credentials',
|
||||
type: 'bitbucketApi',
|
||||
data: {
|
||||
username: 'testuser',
|
||||
appPassword: 'testpassword',
|
||||
},
|
||||
};
|
||||
|
||||
const mockResponse = {
|
||||
username: 'testuser',
|
||||
display_name: 'Test User',
|
||||
};
|
||||
|
||||
mockCredentialTestFunctions.helpers.request.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await bitbucketTrigger.methods.credentialTest.bitbucketApiTest.call(
|
||||
mockCredentialTestFunctions,
|
||||
mockCredentials,
|
||||
);
|
||||
|
||||
expect(mockCredentialTestFunctions.helpers.request).toHaveBeenCalledWith({
|
||||
method: 'GET',
|
||||
auth: {
|
||||
user: 'testuser',
|
||||
password: 'testpassword',
|
||||
},
|
||||
uri: 'https://api.bitbucket.org/2.0/user',
|
||||
json: true,
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
status: 'OK',
|
||||
message: 'Authentication successful!',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error for invalid credentials', async () => {
|
||||
const mockCredentials: ICredentialsDecrypted = {
|
||||
id: 'test-cred-id',
|
||||
name: 'Test Bitbucket Credentials',
|
||||
type: 'bitbucketApi',
|
||||
data: {
|
||||
username: 'testuser',
|
||||
appPassword: 'wrongpassword',
|
||||
},
|
||||
};
|
||||
|
||||
const mockResponse = {
|
||||
error: 'Invalid credentials',
|
||||
};
|
||||
|
||||
mockCredentialTestFunctions.helpers.request.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await bitbucketTrigger.methods.credentialTest.bitbucketApiTest.call(
|
||||
mockCredentialTestFunctions,
|
||||
mockCredentials,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
status: 'Error',
|
||||
message: 'Token is not valid: Invalid credentials',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error when request fails', async () => {
|
||||
const mockCredentials: ICredentialsDecrypted = {
|
||||
id: 'test-cred-id',
|
||||
name: 'Test Bitbucket Credentials',
|
||||
type: 'bitbucketApi',
|
||||
data: {
|
||||
username: 'testuser',
|
||||
appPassword: 'testpassword',
|
||||
},
|
||||
};
|
||||
|
||||
const mockError = new Error('Network error');
|
||||
mockCredentialTestFunctions.helpers.request.mockRejectedValue(mockError);
|
||||
|
||||
const result = await bitbucketTrigger.methods.credentialTest.bitbucketApiTest.call(
|
||||
mockCredentialTestFunctions,
|
||||
mockCredentials,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
status: 'Error',
|
||||
message: 'Settings are not valid: Error: Network error',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('load options methods', () => {
|
||||
const mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
|
||||
|
||||
beforeEach(() => {
|
||||
mockLoadOptionsFunctions.getNode.mockReturnValue(mockNode);
|
||||
});
|
||||
|
||||
describe('getWorkspaceEvents', () => {
|
||||
it('should return workspace events', async () => {
|
||||
const mockEvents = [
|
||||
{
|
||||
event: 'repo:push',
|
||||
description: 'Repository push',
|
||||
},
|
||||
{
|
||||
event: 'repo:fork',
|
||||
description: 'Repository fork',
|
||||
},
|
||||
];
|
||||
|
||||
bitbucketApiRequestAllItemsSpy.mockResolvedValue(mockEvents);
|
||||
|
||||
const result =
|
||||
await bitbucketTrigger.methods.loadOptions.getWorkspaceEvents.call(
|
||||
mockLoadOptionsFunctions,
|
||||
);
|
||||
|
||||
expect(bitbucketApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'values',
|
||||
'GET',
|
||||
'/hook_events/workspace',
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'repo:push',
|
||||
value: 'repo:push',
|
||||
description: 'Repository push',
|
||||
},
|
||||
{
|
||||
name: 'repo:fork',
|
||||
value: 'repo:fork',
|
||||
description: 'Repository fork',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty events list', async () => {
|
||||
bitbucketApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
|
||||
const result =
|
||||
await bitbucketTrigger.methods.loadOptions.getWorkspaceEvents.call(
|
||||
mockLoadOptionsFunctions,
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRepositoriesEvents', () => {
|
||||
it('should return repository events', async () => {
|
||||
const mockEvents = [
|
||||
{
|
||||
event: 'repo:push',
|
||||
description: 'Repository push',
|
||||
},
|
||||
{
|
||||
event: 'pullrequest:created',
|
||||
description: 'Pull request created',
|
||||
},
|
||||
];
|
||||
|
||||
bitbucketApiRequestAllItemsSpy.mockResolvedValue(mockEvents);
|
||||
|
||||
const result =
|
||||
await bitbucketTrigger.methods.loadOptions.getRepositoriesEvents.call(
|
||||
mockLoadOptionsFunctions,
|
||||
);
|
||||
|
||||
expect(bitbucketApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'values',
|
||||
'GET',
|
||||
'/hook_events/repository',
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'repo:push',
|
||||
value: 'repo:push',
|
||||
description: 'Repository push',
|
||||
},
|
||||
{
|
||||
name: 'pullrequest:created',
|
||||
value: 'pullrequest:created',
|
||||
description: 'Pull request created',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRepositories', () => {
|
||||
it('should return repositories for workspace', async () => {
|
||||
const mockRepositories = [
|
||||
{
|
||||
slug: 'repo1',
|
||||
description: 'First repository',
|
||||
},
|
||||
{
|
||||
slug: 'repo2',
|
||||
description: 'Second repository',
|
||||
},
|
||||
];
|
||||
|
||||
mockLoadOptionsFunctions.getCurrentNodeParameter.mockReturnValue('test-workspace');
|
||||
bitbucketApiRequestAllItemsSpy.mockResolvedValue(mockRepositories);
|
||||
|
||||
const result =
|
||||
await bitbucketTrigger.methods.loadOptions.getRepositories.call(mockLoadOptionsFunctions);
|
||||
|
||||
expect(mockLoadOptionsFunctions.getCurrentNodeParameter).toHaveBeenCalledWith('workspace');
|
||||
expect(bitbucketApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'values',
|
||||
'GET',
|
||||
'/repositories/test-workspace',
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
name: 'repo1',
|
||||
value: 'repo1',
|
||||
description: 'First repository',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
name: 'repo2',
|
||||
value: 'repo2',
|
||||
description: 'Second repository',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWorkspaces', () => {
|
||||
it('should return workspaces', async () => {
|
||||
const mockWorkspaces = [
|
||||
{
|
||||
name: 'Workspace 1',
|
||||
slug: 'workspace1',
|
||||
},
|
||||
{
|
||||
name: 'Workspace 2',
|
||||
slug: 'workspace2',
|
||||
},
|
||||
];
|
||||
|
||||
bitbucketApiRequestAllItemsSpy.mockResolvedValue(mockWorkspaces);
|
||||
|
||||
const result =
|
||||
await bitbucketTrigger.methods.loadOptions.getWorkspaces.call(mockLoadOptionsFunctions);
|
||||
|
||||
expect(bitbucketApiRequestAllItemsSpy).toHaveBeenCalledWith('values', 'GET', '/workspaces');
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'Workspace 1',
|
||||
value: 'workspace1',
|
||||
},
|
||||
{
|
||||
name: 'Workspace 2',
|
||||
value: 'workspace2',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('webhook methods', () => {
|
||||
const mockHookFunctions = mockDeep<IHookFunctions>();
|
||||
|
||||
beforeEach(() => {
|
||||
mockHookFunctions.getNode.mockReturnValue(mockNode);
|
||||
mockHookFunctions.getNodeWebhookUrl.mockReturnValue('https://test.n8n.io/webhook/test');
|
||||
mockHookFunctions.getWorkflowStaticData.mockReturnValue({});
|
||||
});
|
||||
|
||||
describe('checkExists', () => {
|
||||
it('should return true if webhook exists for workspace', async () => {
|
||||
const mockHooks = {
|
||||
values: [
|
||||
{
|
||||
uuid: '{12345678-1234-1234-1234-123456789012}',
|
||||
url: 'https://test.n8n.io/webhook/test',
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
uuid: '{87654321-4321-4321-4321-210987654321}',
|
||||
url: 'https://other.webhook.url',
|
||||
active: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockHookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'resource') return 'workspace';
|
||||
if (paramName === 'workspace') return 'test-workspace';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
bitbucketApiRequestSpy.mockResolvedValue(mockHooks);
|
||||
|
||||
const result =
|
||||
await bitbucketTrigger.webhookMethods.default.checkExists.call(mockHookFunctions);
|
||||
|
||||
expect(bitbucketApiRequestSpy).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'/workspaces/test-workspace/hooks',
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Check that webhook ID is stored
|
||||
const staticData = mockHookFunctions.getWorkflowStaticData('node');
|
||||
expect(staticData.webhookId).toBe('12345678-1234-1234-1234-123456789012');
|
||||
});
|
||||
|
||||
it('should return true if webhook exists for repository', async () => {
|
||||
const mockHooks = {
|
||||
values: [
|
||||
{
|
||||
uuid: '{12345678-1234-1234-1234-123456789012}',
|
||||
url: 'https://test.n8n.io/webhook/test',
|
||||
active: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockHookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'resource') return 'repository';
|
||||
if (paramName === 'workspace') return 'test-workspace';
|
||||
if (paramName === 'repository') return 'test-repo';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
bitbucketApiRequestSpy.mockResolvedValue(mockHooks);
|
||||
|
||||
const result =
|
||||
await bitbucketTrigger.webhookMethods.default.checkExists.call(mockHookFunctions);
|
||||
|
||||
expect(bitbucketApiRequestSpy).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'/repositories/test-workspace/test-repo/hooks',
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if webhook does not exist', async () => {
|
||||
const mockHooks = {
|
||||
values: [
|
||||
{
|
||||
uuid: '{87654321-4321-4321-4321-210987654321}',
|
||||
url: 'https://other.webhook.url',
|
||||
active: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockHookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'resource') return 'workspace';
|
||||
if (paramName === 'workspace') return 'test-workspace';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
bitbucketApiRequestSpy.mockResolvedValue(mockHooks);
|
||||
|
||||
const result =
|
||||
await bitbucketTrigger.webhookMethods.default.checkExists.call(mockHookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if webhook exists but is inactive', async () => {
|
||||
const mockHooks = {
|
||||
values: [
|
||||
{
|
||||
uuid: '{12345678-1234-1234-1234-123456789012}',
|
||||
url: 'https://test.n8n.io/webhook/test',
|
||||
active: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockHookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'resource') return 'workspace';
|
||||
if (paramName === 'workspace') return 'test-workspace';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
bitbucketApiRequestSpy.mockResolvedValue(mockHooks);
|
||||
|
||||
const result =
|
||||
await bitbucketTrigger.webhookMethods.default.checkExists.call(mockHookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create webhook for workspace', async () => {
|
||||
const mockResponse = {
|
||||
uuid: '{12345678-1234-1234-1234-123456789012}',
|
||||
url: 'https://test.n8n.io/webhook/test',
|
||||
active: true,
|
||||
};
|
||||
|
||||
mockHookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'resource') return 'workspace';
|
||||
if (paramName === 'workspace') return 'test-workspace';
|
||||
if (paramName === 'events') return ['repo:push', 'repo:fork'];
|
||||
return undefined;
|
||||
});
|
||||
|
||||
bitbucketApiRequestSpy.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await bitbucketTrigger.webhookMethods.default.create.call(mockHookFunctions);
|
||||
|
||||
expect(bitbucketApiRequestSpy).toHaveBeenCalledWith(
|
||||
'POST',
|
||||
'/workspaces/test-workspace/hooks',
|
||||
{
|
||||
description: 'n8n webhook',
|
||||
url: 'https://test.n8n.io/webhook/test',
|
||||
active: true,
|
||||
events: ['repo:push', 'repo:fork'],
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Check that webhook ID is stored
|
||||
const staticData = mockHookFunctions.getWorkflowStaticData('node');
|
||||
expect(staticData.webhookId).toBe('12345678-1234-1234-1234-123456789012');
|
||||
});
|
||||
|
||||
it('should create webhook for repository', async () => {
|
||||
const mockResponse = {
|
||||
uuid: '{12345678-1234-1234-1234-123456789012}',
|
||||
url: 'https://test.n8n.io/webhook/test',
|
||||
active: true,
|
||||
};
|
||||
|
||||
mockHookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'resource') return 'repository';
|
||||
if (paramName === 'workspace') return 'test-workspace';
|
||||
if (paramName === 'repository') return 'test-repo';
|
||||
if (paramName === 'events') return ['pullrequest:created'];
|
||||
return undefined;
|
||||
});
|
||||
|
||||
bitbucketApiRequestSpy.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await bitbucketTrigger.webhookMethods.default.create.call(mockHookFunctions);
|
||||
|
||||
expect(bitbucketApiRequestSpy).toHaveBeenCalledWith(
|
||||
'POST',
|
||||
'/repositories/test-workspace/test-repo/hooks',
|
||||
{
|
||||
description: 'n8n webhook',
|
||||
url: 'https://test.n8n.io/webhook/test',
|
||||
active: true,
|
||||
events: ['pullrequest:created'],
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete webhook for workspace', async () => {
|
||||
const staticData = { webhookId: '12345678-1234-1234-1234-123456789012' };
|
||||
mockHookFunctions.getWorkflowStaticData.mockReturnValue(staticData);
|
||||
|
||||
mockHookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'resource') return 'workspace';
|
||||
if (paramName === 'workspace') return 'test-workspace';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
bitbucketApiRequestSpy.mockResolvedValue({});
|
||||
|
||||
const result = await bitbucketTrigger.webhookMethods.default.delete.call(mockHookFunctions);
|
||||
|
||||
expect(bitbucketApiRequestSpy).toHaveBeenCalledWith(
|
||||
'DELETE',
|
||||
'/workspaces/test-workspace/hooks/12345678-1234-1234-1234-123456789012',
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(staticData.webhookId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should delete webhook for repository', async () => {
|
||||
const staticData = { webhookId: '12345678-1234-1234-1234-123456789012' };
|
||||
mockHookFunctions.getWorkflowStaticData.mockReturnValue(staticData);
|
||||
|
||||
mockHookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'resource') return 'repository';
|
||||
if (paramName === 'workspace') return 'test-workspace';
|
||||
if (paramName === 'repository') return 'test-repo';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
bitbucketApiRequestSpy.mockResolvedValue({});
|
||||
|
||||
const result = await bitbucketTrigger.webhookMethods.default.delete.call(mockHookFunctions);
|
||||
|
||||
expect(bitbucketApiRequestSpy).toHaveBeenCalledWith(
|
||||
'DELETE',
|
||||
'/repositories/test-workspace/test-repo/hooks/12345678-1234-1234-1234-123456789012',
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(staticData.webhookId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return false if delete fails', async () => {
|
||||
const staticData = { webhookId: '12345678-1234-1234-1234-123456789012' };
|
||||
mockHookFunctions.getWorkflowStaticData.mockReturnValue(staticData);
|
||||
|
||||
mockHookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'resource') return 'workspace';
|
||||
if (paramName === 'workspace') return 'test-workspace';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
bitbucketApiRequestSpy.mockRejectedValue(new Error('Delete failed'));
|
||||
|
||||
const result = await bitbucketTrigger.webhookMethods.default.delete.call(mockHookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
// Webhook ID should still be present since delete failed
|
||||
expect(staticData.webhookId).toBe('12345678-1234-1234-1234-123456789012');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('webhook', () => {
|
||||
const mockWebhookFunctions = mockDeep<IWebhookFunctions>();
|
||||
|
||||
beforeEach(() => {
|
||||
mockWebhookFunctions.getNode.mockReturnValue(mockNode);
|
||||
});
|
||||
|
||||
it('should process webhook with matching UUID', async () => {
|
||||
const webhookId = '12345678-1234-1234-1234-123456789012';
|
||||
const staticData = { webhookId };
|
||||
const mockRequestBody = {
|
||||
repository: {
|
||||
name: 'test-repo',
|
||||
},
|
||||
push: {
|
||||
changes: [
|
||||
{
|
||||
new: {
|
||||
name: 'main',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
mockWebhookFunctions.getWorkflowStaticData.mockReturnValue(staticData);
|
||||
mockWebhookFunctions.getRequestObject.mockReturnValue({
|
||||
body: mockRequestBody,
|
||||
} as any);
|
||||
mockWebhookFunctions.getHeaderData.mockReturnValue({
|
||||
'x-hook-uuid': webhookId,
|
||||
});
|
||||
mockWebhookFunctions.helpers.returnJsonArray.mockReturnValue([{ json: mockRequestBody }]);
|
||||
|
||||
const result = await bitbucketTrigger.webhook.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toEqual({
|
||||
workflowData: [[{ json: mockRequestBody }]],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty object for non-matching UUID', async () => {
|
||||
const webhookId = '12345678-1234-1234-1234-123456789012';
|
||||
const staticData = { webhookId };
|
||||
|
||||
mockWebhookFunctions.getWorkflowStaticData.mockReturnValue(staticData);
|
||||
mockWebhookFunctions.getHeaderData.mockReturnValue({
|
||||
'x-hook-uuid': 'different-uuid',
|
||||
});
|
||||
|
||||
const result = await bitbucketTrigger.webhook.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle array request body', async () => {
|
||||
const webhookId = '12345678-1234-1234-1234-123456789012';
|
||||
const staticData = { webhookId };
|
||||
const mockRequestBody = [
|
||||
{
|
||||
repository: { name: 'repo1' },
|
||||
},
|
||||
{
|
||||
repository: { name: 'repo2' },
|
||||
},
|
||||
];
|
||||
|
||||
mockWebhookFunctions.getWorkflowStaticData.mockReturnValue(staticData);
|
||||
mockWebhookFunctions.getRequestObject.mockReturnValue({
|
||||
body: mockRequestBody,
|
||||
} as any);
|
||||
mockWebhookFunctions.getHeaderData.mockReturnValue({
|
||||
'x-hook-uuid': webhookId,
|
||||
});
|
||||
mockWebhookFunctions.helpers.returnJsonArray.mockReturnValue([
|
||||
{ json: mockRequestBody[0] },
|
||||
{ json: mockRequestBody[1] },
|
||||
]);
|
||||
|
||||
const result = await bitbucketTrigger.webhook.call(mockWebhookFunctions);
|
||||
|
||||
expect(mockWebhookFunctions.helpers.returnJsonArray).toHaveBeenCalledWith(mockRequestBody);
|
||||
expect(result).toEqual({
|
||||
workflowData: [[{ json: mockRequestBody[0] }, { json: mockRequestBody[1] }]],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,503 @@
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
INode,
|
||||
IHttpRequestOptions,
|
||||
IRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { bitbucketApiRequest, bitbucketApiRequestAllItems } from '../GenericFunctions';
|
||||
|
||||
describe('Bitbucket GenericFunctions', () => {
|
||||
describe('bitbucketApiRequest', () => {
|
||||
const mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
const mockHookFunctions = mockDeep<IHookFunctions>();
|
||||
const mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
|
||||
|
||||
const mockNode: INode = {
|
||||
id: 'test-node-id',
|
||||
name: 'Bitbucket Test',
|
||||
type: 'n8n-nodes-base.bitbucket',
|
||||
typeVersion: 1.1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
|
||||
mockHookFunctions.getNode.mockReturnValue(mockNode);
|
||||
mockLoadOptionsFunctions.getNode.mockReturnValue(mockNode);
|
||||
});
|
||||
|
||||
describe('with access token authentication', () => {
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValue('accessToken');
|
||||
mockHookFunctions.getNodeParameter.mockReturnValue('accessToken');
|
||||
mockLoadOptionsFunctions.getNodeParameter.mockReturnValue('accessToken');
|
||||
});
|
||||
|
||||
it('should make successful API request with access token', async () => {
|
||||
const mockResponse = { data: 'test response' };
|
||||
mockExecuteFunctions.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await bitbucketApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/repositories',
|
||||
{},
|
||||
{},
|
||||
);
|
||||
|
||||
expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'bitbucketAccessTokenApi',
|
||||
{
|
||||
method: 'GET',
|
||||
qs: {},
|
||||
url: 'https://api.bitbucket.org/2.0/repositories',
|
||||
json: true,
|
||||
},
|
||||
);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should handle custom URI with access token', async () => {
|
||||
const mockResponse = { data: 'test response' };
|
||||
const customUri = 'https://custom.api.bitbucket.org/2.0/custom';
|
||||
mockExecuteFunctions.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
|
||||
|
||||
await bitbucketApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'POST',
|
||||
'/repositories',
|
||||
{ name: 'test' },
|
||||
{ page: 1 },
|
||||
customUri,
|
||||
);
|
||||
|
||||
expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'bitbucketAccessTokenApi',
|
||||
{
|
||||
method: 'POST',
|
||||
qs: { page: 1 },
|
||||
body: { name: 'test' },
|
||||
url: customUri,
|
||||
json: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove empty body with access token', async () => {
|
||||
const mockResponse = { data: 'test response' };
|
||||
mockExecuteFunctions.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
|
||||
|
||||
await bitbucketApiRequest.call(mockExecuteFunctions, 'GET', '/repositories');
|
||||
|
||||
const expectedOptions: IHttpRequestOptions = {
|
||||
method: 'GET',
|
||||
qs: {},
|
||||
url: 'https://api.bitbucket.org/2.0/repositories',
|
||||
json: true,
|
||||
};
|
||||
|
||||
expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'bitbucketAccessTokenApi',
|
||||
expectedOptions,
|
||||
);
|
||||
});
|
||||
|
||||
it('should work with hook functions', async () => {
|
||||
const mockResponse = { values: [] };
|
||||
mockHookFunctions.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await bitbucketApiRequest.call(mockHookFunctions, 'GET', '/workspaces');
|
||||
|
||||
expect(mockHookFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'bitbucketAccessTokenApi',
|
||||
{
|
||||
method: 'GET',
|
||||
qs: {},
|
||||
url: 'https://api.bitbucket.org/2.0/workspaces',
|
||||
json: true,
|
||||
},
|
||||
);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should work with load options functions', async () => {
|
||||
const mockResponse = { values: [] };
|
||||
mockLoadOptionsFunctions.helpers.httpRequestWithAuthentication.mockResolvedValue(
|
||||
mockResponse,
|
||||
);
|
||||
|
||||
const result = await bitbucketApiRequest.call(
|
||||
mockLoadOptionsFunctions,
|
||||
'GET',
|
||||
'/hook_events/workspace',
|
||||
);
|
||||
|
||||
expect(mockLoadOptionsFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'bitbucketAccessTokenApi',
|
||||
{
|
||||
method: 'GET',
|
||||
qs: {},
|
||||
url: 'https://api.bitbucket.org/2.0/hook_events/workspace',
|
||||
json: true,
|
||||
},
|
||||
);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with password authentication', () => {
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValue('password');
|
||||
mockHookFunctions.getNodeParameter.mockReturnValue('password');
|
||||
mockLoadOptionsFunctions.getNodeParameter.mockReturnValue('password');
|
||||
});
|
||||
|
||||
it('should make successful API request with password', async () => {
|
||||
const mockCredentials = {
|
||||
username: 'testuser',
|
||||
appPassword: 'testpassword',
|
||||
};
|
||||
const mockResponse = { data: 'test response' };
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
mockExecuteFunctions.helpers.request.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await bitbucketApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
'/repositories',
|
||||
{},
|
||||
{},
|
||||
);
|
||||
|
||||
expect(mockExecuteFunctions.getCredentials).toHaveBeenCalledWith('bitbucketApi');
|
||||
expect(mockExecuteFunctions.helpers.request).toHaveBeenCalledWith({
|
||||
method: 'GET',
|
||||
auth: {
|
||||
user: 'testuser',
|
||||
password: 'testpassword',
|
||||
},
|
||||
qs: {},
|
||||
uri: 'https://api.bitbucket.org/2.0/repositories',
|
||||
json: true,
|
||||
});
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should handle custom URI with password', async () => {
|
||||
const mockCredentials = {
|
||||
username: 'testuser',
|
||||
appPassword: 'testpassword',
|
||||
};
|
||||
const mockResponse = { data: 'test response' };
|
||||
const customUri = 'https://custom.api.bitbucket.org/2.0/custom';
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
mockExecuteFunctions.helpers.request.mockResolvedValue(mockResponse);
|
||||
|
||||
await bitbucketApiRequest.call(
|
||||
mockExecuteFunctions,
|
||||
'POST',
|
||||
'/repositories',
|
||||
{ name: 'test' },
|
||||
{ page: 1 },
|
||||
customUri,
|
||||
);
|
||||
|
||||
expect(mockExecuteFunctions.helpers.request).toHaveBeenCalledWith({
|
||||
method: 'POST',
|
||||
auth: {
|
||||
user: 'testuser',
|
||||
password: 'testpassword',
|
||||
},
|
||||
qs: { page: 1 },
|
||||
body: { name: 'test' },
|
||||
uri: customUri,
|
||||
json: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove empty body with password', async () => {
|
||||
const mockCredentials = {
|
||||
username: 'testuser',
|
||||
appPassword: 'testpassword',
|
||||
};
|
||||
const mockResponse = { data: 'test response' };
|
||||
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
|
||||
mockExecuteFunctions.helpers.request.mockResolvedValue(mockResponse);
|
||||
|
||||
await bitbucketApiRequest.call(mockExecuteFunctions, 'GET', '/repositories');
|
||||
|
||||
const expectedOptions: IRequestOptions = {
|
||||
method: 'GET',
|
||||
auth: {
|
||||
user: 'testuser',
|
||||
password: 'testpassword',
|
||||
},
|
||||
qs: {},
|
||||
uri: 'https://api.bitbucket.org/2.0/repositories',
|
||||
json: true,
|
||||
};
|
||||
|
||||
expect(mockExecuteFunctions.helpers.request).toHaveBeenCalledWith(expectedOptions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValue('accessToken');
|
||||
});
|
||||
|
||||
it('should throw NodeApiError when request fails', async () => {
|
||||
const mockError = new Error('API request failed');
|
||||
mockExecuteFunctions.helpers.httpRequestWithAuthentication.mockRejectedValue(mockError);
|
||||
|
||||
await expect(
|
||||
bitbucketApiRequest.call(mockExecuteFunctions, 'GET', '/repositories'),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
|
||||
expect(mockExecuteFunctions.getNode).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should preserve original error details in NodeApiError', async () => {
|
||||
const mockError = {
|
||||
message: 'Unauthorized',
|
||||
statusCode: 401,
|
||||
response: { body: { error: 'Invalid credentials' } },
|
||||
};
|
||||
mockExecuteFunctions.helpers.httpRequestWithAuthentication.mockRejectedValue(mockError);
|
||||
|
||||
await expect(
|
||||
bitbucketApiRequest.call(mockExecuteFunctions, 'GET', '/repositories'),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('bitbucketApiRequestAllItems', () => {
|
||||
const mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
const mockNode: INode = {
|
||||
id: 'test-node-id',
|
||||
name: 'Bitbucket Test',
|
||||
type: 'n8n-nodes-base.bitbucket',
|
||||
typeVersion: 1.1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValue('accessToken');
|
||||
});
|
||||
|
||||
it('should handle single page response', async () => {
|
||||
const mockResponse = {
|
||||
values: [{ id: 1 }, { id: 2 }],
|
||||
// no 'next' property means single page
|
||||
};
|
||||
|
||||
mockExecuteFunctions.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await bitbucketApiRequestAllItems.call(
|
||||
mockExecuteFunctions,
|
||||
'values',
|
||||
'GET',
|
||||
'/repositories',
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ id: 1 }, { id: 2 }]);
|
||||
expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle multiple pages', async () => {
|
||||
const firstPageResponse = {
|
||||
values: [{ id: 1 }, { id: 2 }],
|
||||
next: 'https://api.bitbucket.org/2.0/repositories?page=2',
|
||||
};
|
||||
|
||||
const secondPageResponse = {
|
||||
values: [{ id: 3 }, { id: 4 }],
|
||||
next: 'https://api.bitbucket.org/2.0/repositories?page=3',
|
||||
};
|
||||
|
||||
const thirdPageResponse = {
|
||||
values: [{ id: 5 }],
|
||||
// no 'next' property means last page
|
||||
};
|
||||
|
||||
mockExecuteFunctions.helpers.httpRequestWithAuthentication
|
||||
.mockResolvedValueOnce(firstPageResponse)
|
||||
.mockResolvedValueOnce(secondPageResponse)
|
||||
.mockResolvedValueOnce(thirdPageResponse);
|
||||
|
||||
const result = await bitbucketApiRequestAllItems.call(
|
||||
mockExecuteFunctions,
|
||||
'values',
|
||||
'GET',
|
||||
'/repositories',
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }]);
|
||||
expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Check that subsequent calls use the 'next' URL
|
||||
expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'bitbucketAccessTokenApi',
|
||||
{
|
||||
method: 'GET',
|
||||
qs: {},
|
||||
url: 'https://api.bitbucket.org/2.0/repositories?page=2',
|
||||
json: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'bitbucketAccessTokenApi',
|
||||
{
|
||||
method: 'GET',
|
||||
qs: {},
|
||||
url: 'https://api.bitbucket.org/2.0/repositories?page=3',
|
||||
json: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty pages', async () => {
|
||||
const mockResponse = {
|
||||
values: [],
|
||||
};
|
||||
|
||||
mockExecuteFunctions.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await bitbucketApiRequestAllItems.call(
|
||||
mockExecuteFunctions,
|
||||
'values',
|
||||
'GET',
|
||||
'/repositories',
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should pass through body and query parameters', async () => {
|
||||
const mockResponse = {
|
||||
values: [{ id: 1 }],
|
||||
};
|
||||
|
||||
const body = { name: 'test-repo' };
|
||||
const query = { role: 'admin' };
|
||||
|
||||
mockExecuteFunctions.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
|
||||
|
||||
await bitbucketApiRequestAllItems.call(
|
||||
mockExecuteFunctions,
|
||||
'values',
|
||||
'POST',
|
||||
'/repositories',
|
||||
body,
|
||||
query,
|
||||
);
|
||||
|
||||
expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'bitbucketAccessTokenApi',
|
||||
{
|
||||
method: 'POST',
|
||||
qs: query,
|
||||
body,
|
||||
url: 'https://api.bitbucket.org/2.0/repositories',
|
||||
json: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should work with different property names', async () => {
|
||||
const mockResponse = {
|
||||
items: [{ id: 1 }, { id: 2 }],
|
||||
};
|
||||
|
||||
mockExecuteFunctions.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await bitbucketApiRequestAllItems.call(
|
||||
mockExecuteFunctions,
|
||||
'items',
|
||||
'GET',
|
||||
'/custom-endpoint',
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ id: 1 }, { id: 2 }]);
|
||||
});
|
||||
|
||||
it('should handle errors during pagination', async () => {
|
||||
const firstPageResponse = {
|
||||
values: [{ id: 1 }],
|
||||
next: 'https://api.bitbucket.org/2.0/repositories?page=2',
|
||||
};
|
||||
|
||||
const mockError = new Error('Network error');
|
||||
|
||||
mockExecuteFunctions.helpers.httpRequestWithAuthentication
|
||||
.mockResolvedValueOnce(firstPageResponse)
|
||||
.mockRejectedValueOnce(mockError);
|
||||
|
||||
await expect(
|
||||
bitbucketApiRequestAllItems.call(mockExecuteFunctions, 'values', 'GET', '/repositories'),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
});
|
||||
|
||||
it('should work with hook functions', async () => {
|
||||
const mockHookFunctions = mockDeep<IHookFunctions>();
|
||||
mockHookFunctions.getNode.mockReturnValue(mockNode);
|
||||
mockHookFunctions.getNodeParameter.mockReturnValue('accessToken');
|
||||
|
||||
const mockResponse = {
|
||||
values: [{ event: 'repo:push' }],
|
||||
};
|
||||
|
||||
mockHookFunctions.helpers.httpRequestWithAuthentication.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await bitbucketApiRequestAllItems.call(
|
||||
mockHookFunctions,
|
||||
'values',
|
||||
'GET',
|
||||
'/hook_events/repository',
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ event: 'repo:push' }]);
|
||||
});
|
||||
|
||||
it('should work with load options functions', async () => {
|
||||
const mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
|
||||
mockLoadOptionsFunctions.getNode.mockReturnValue(mockNode);
|
||||
mockLoadOptionsFunctions.getNodeParameter.mockReturnValue('accessToken');
|
||||
|
||||
const mockResponse = {
|
||||
values: [{ slug: 'workspace1' }],
|
||||
};
|
||||
|
||||
mockLoadOptionsFunctions.helpers.httpRequestWithAuthentication.mockResolvedValue(
|
||||
mockResponse,
|
||||
);
|
||||
|
||||
const result = await bitbucketApiRequestAllItems.call(
|
||||
mockLoadOptionsFunctions,
|
||||
'values',
|
||||
'GET',
|
||||
'/workspaces',
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ slug: 'workspace1' }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user