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

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,101 @@
import { CurrentsApi } from '../../../credentials/CurrentsApi.credentials';
import { Currents } from '../Currents.node';
import { actionOperations, actionFields } from '../descriptions/ActionDescription';
import { projectRLC } from '../descriptions/common.descriptions';
import { instanceOperations, instanceFields } from '../descriptions/InstanceDescription';
import { projectOperations, projectFields } from '../descriptions/ProjectDescription';
import { runOperations, runFields } from '../descriptions/RunDescription';
import { signatureOperations, signatureFields } from '../descriptions/SignatureDescription';
import { specFileOperations, specFileFields } from '../descriptions/SpecFileDescription';
import { testOperations, testFields } from '../descriptions/TestDescription';
import { testResultOperations, testResultFields } from '../descriptions/TestResultDescription';
import { listSearch } from '../methods';
describe('Currents Node Structure', () => {
describe('Currents class', () => {
it('should be a valid node class', () => {
const node = new Currents();
expect(node.description).toBeDefined();
expect(node.description.name).toBe('currents');
expect(node.description.displayName).toBe('Currents');
});
it('should have correct credentials', () => {
const node = new Currents();
expect(node.description.credentials).toContainEqual(
expect.objectContaining({ name: 'currentsApi' }),
);
});
it('should have all resource types', () => {
const node = new Currents();
const resourceProperty = node.description.properties.find((p) => p.name === 'resource');
expect(resourceProperty).toBeDefined();
const options = resourceProperty?.options as Array<{ value: string }>;
const resourceValues = options?.map((o) => o.value) ?? [];
expect(resourceValues).toContain('action');
expect(resourceValues).toContain('instance');
expect(resourceValues).toContain('project');
expect(resourceValues).toContain('run');
expect(resourceValues).toContain('signature');
expect(resourceValues).toContain('specFile');
expect(resourceValues).toContain('test');
expect(resourceValues).toContain('testResult');
});
it('should use brackets array format for query params (tags[], authors[], etc.)', () => {
const node = new Currents();
const defaults = node.description.requestDefaults as { arrayFormat?: string } | undefined;
expect(defaults).toBeDefined();
expect(defaults?.arrayFormat).toBe('brackets');
});
});
describe('Credentials', () => {
it('should be a valid credential class', () => {
const cred = new CurrentsApi();
expect(cred.name).toBe('currentsApi');
expect(cred.displayName).toBe('Currents API');
expect(cred.properties).toBeDefined();
expect(Array.isArray(cred.properties)).toBe(true);
});
});
describe('Methods', () => {
it('should export listSearch with getProjects', () => {
expect(listSearch).toBeDefined();
expect(listSearch.getProjects).toBeDefined();
expect(typeof listSearch.getProjects).toBe('function');
});
});
describe('Description exports', () => {
const descriptionPairs = [
{ name: 'action', operations: actionOperations, fields: actionFields },
{ name: 'instance', operations: instanceOperations, fields: instanceFields },
{ name: 'project', operations: projectOperations, fields: projectFields },
{ name: 'run', operations: runOperations, fields: runFields },
{ name: 'signature', operations: signatureOperations, fields: signatureFields },
{ name: 'specFile', operations: specFileOperations, fields: specFileFields },
{ name: 'test', operations: testOperations, fields: testFields },
{ name: 'testResult', operations: testResultOperations, fields: testResultFields },
];
it.each(descriptionPairs)(
'$name should export valid operations and fields arrays',
({ operations, fields }) => {
expect(Array.isArray(operations)).toBe(true);
expect(operations.length).toBeGreaterThan(0);
expect(Array.isArray(fields)).toBe(true);
},
);
it('should export projectRLC as a valid resource locator', () => {
expect(projectRLC).toBeDefined();
expect(projectRLC.name).toBe('projectId');
expect(projectRLC.type).toBe('resourceLocator');
});
});
});
@@ -0,0 +1,147 @@
import type { IDataObject, IWebhookFunctions } from 'n8n-workflow';
import { CurrentsTrigger } from '../CurrentsTrigger.node';
// Mock the helper module
jest.mock('../CurrentsTriggerHelpers', () => ({
verifyWebhook: jest.fn(),
}));
import { verifyWebhook } from '../CurrentsTriggerHelpers';
describe('CurrentsTrigger', () => {
let trigger: CurrentsTrigger;
let mockWebhookFunctions: Partial<IWebhookFunctions>;
let mockResponse: { status: jest.Mock; send: jest.Mock; end: jest.Mock };
beforeEach(() => {
trigger = new CurrentsTrigger();
mockResponse = {
status: jest.fn().mockReturnThis(),
send: jest.fn().mockReturnThis(),
end: jest.fn().mockReturnThis(),
};
mockWebhookFunctions = {
getBodyData: jest.fn(),
getNodeParameter: jest.fn(),
getResponseObject: jest.fn().mockReturnValue(mockResponse),
helpers: {
returnJsonArray: jest.fn((data) => data),
} as unknown as IWebhookFunctions['helpers'],
};
(verifyWebhook as jest.Mock).mockReturnValue(true);
});
describe('webhook', () => {
it('should return 401 when verification fails', async () => {
(verifyWebhook as jest.Mock).mockReturnValue(false);
const result = await trigger.webhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(mockResponse.status).toHaveBeenCalledWith(401);
expect(mockResponse.send).toHaveBeenCalledWith('Unauthorized');
expect(result).toEqual({ noWebhookResponse: true });
});
it('should trigger workflow when event matches selected events', async () => {
const bodyData: IDataObject = {
event: 'RUN_FINISH',
runUrl: 'https://app.currents.dev/run/123',
buildId: 'build-456',
};
(mockWebhookFunctions.getBodyData as jest.Mock).mockReturnValue(bodyData);
(mockWebhookFunctions.getNodeParameter as jest.Mock).mockReturnValue([
'RUN_FINISH',
'RUN_START',
]);
const result = await trigger.webhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(result.workflowData).toBeDefined();
expect(mockWebhookFunctions.helpers!.returnJsonArray).toHaveBeenCalledWith([bodyData]);
});
it('should acknowledge but not trigger when event does not match', async () => {
const bodyData: IDataObject = {
event: 'RUN_TIMEOUT',
runUrl: 'https://app.currents.dev/run/123',
};
(mockWebhookFunctions.getBodyData as jest.Mock).mockReturnValue(bodyData);
(mockWebhookFunctions.getNodeParameter as jest.Mock).mockReturnValue([
'RUN_FINISH',
'RUN_START',
]);
const result = await trigger.webhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(result).toEqual({ webhookResponse: 'OK' });
expect(result.workflowData).toBeUndefined();
});
it('should trigger workflow for all events when no filter is set', async () => {
const bodyData: IDataObject = {
event: 'RUN_CANCELED',
runUrl: 'https://app.currents.dev/run/123',
};
(mockWebhookFunctions.getBodyData as jest.Mock).mockReturnValue(bodyData);
(mockWebhookFunctions.getNodeParameter as jest.Mock).mockReturnValue([]);
const result = await trigger.webhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(result.workflowData).toBeDefined();
});
it('should pass full webhook payload to workflow', async () => {
const bodyData: IDataObject = {
event: 'RUN_FINISH',
runUrl: 'https://app.currents.dev/run/123',
buildId: 'build-456',
groupId: 'group-1',
tags: ['smoke', 'regression'],
commit: {
sha: 'abc123',
branch: 'main',
authorName: 'Test Author',
},
failures: 0,
passes: 42,
flaky: 2,
};
(mockWebhookFunctions.getBodyData as jest.Mock).mockReturnValue(bodyData);
(mockWebhookFunctions.getNodeParameter as jest.Mock).mockReturnValue(['RUN_FINISH']);
const result = await trigger.webhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(mockWebhookFunctions.helpers!.returnJsonArray).toHaveBeenCalledWith([bodyData]);
expect(result.workflowData).toBeDefined();
});
});
describe('description', () => {
it('should have correct node metadata', () => {
expect(trigger.description.displayName).toBe('Currents Trigger');
expect(trigger.description.name).toBe('currentsTrigger');
expect(trigger.description.group).toContain('trigger');
});
it('should have all webhook event options', () => {
const eventsProperty = trigger.description.properties.find((p) => p.name === 'events');
expect(eventsProperty).toBeDefined();
expect(eventsProperty?.type).toBe('multiOptions');
const options = (eventsProperty as { options?: Array<{ value: string }> })?.options ?? [];
const eventValues = options.map((o) => o.value);
expect(eventValues).toContain('RUN_START');
expect(eventValues).toContain('RUN_FINISH');
expect(eventValues).toContain('RUN_TIMEOUT');
expect(eventValues).toContain('RUN_CANCELED');
});
});
});
@@ -0,0 +1,469 @@
import type { IDataObject, IHookFunctions, IWebhookFunctions } from 'n8n-workflow';
import {
createWebhook,
deleteWebhook,
findWebhookByUrl,
generateWebhookSecret,
listWebhooks,
updateWebhook,
verifyWebhook,
} from '../CurrentsTriggerHelpers';
describe('CurrentsTriggerHelpers', () => {
describe('generateWebhookSecret', () => {
it('should generate a 64-character hex string', () => {
const secret = generateWebhookSecret();
expect(secret).toHaveLength(64);
expect(/^[0-9a-f]+$/.test(secret)).toBe(true);
});
it('should generate unique secrets', () => {
const secret1 = generateWebhookSecret();
const secret2 = generateWebhookSecret();
expect(secret1).not.toBe(secret2);
});
});
describe('verifyWebhook', () => {
let mockWebhookFunctions: Partial<IWebhookFunctions>;
beforeEach(() => {
mockWebhookFunctions = {
getRequestObject: jest.fn(),
getHeaderData: jest.fn(),
getWorkflowStaticData: jest.fn(),
};
});
it('should return true when no secret in static data (no verification)', () => {
const nowMs = Date.now();
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
headers: { 'x-timestamp': String(nowMs) },
});
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({});
(mockWebhookFunctions.getWorkflowStaticData as jest.Mock).mockReturnValue({});
const result = verifyWebhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(result).toBe(true);
});
it('should return false when timestamp is stale', () => {
const tenMinutesAgoMs = Date.now() - 600 * 1000;
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
headers: { 'x-timestamp': String(tenMinutesAgoMs) },
});
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({});
(mockWebhookFunctions.getWorkflowStaticData as jest.Mock).mockReturnValue({});
const result = verifyWebhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(result).toBe(false);
});
it('should return false when timestamp is invalid/non-numeric', () => {
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
headers: { 'x-timestamp': 'not-a-number' },
});
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({});
(mockWebhookFunctions.getWorkflowStaticData as jest.Mock).mockReturnValue({});
const result = verifyWebhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(result).toBe(false);
});
it('should return true when secret matches from static data', () => {
const nowMs = Date.now();
const secret = 'auto-generated-secret';
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
headers: { 'x-timestamp': String(nowMs) },
});
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
'x-webhook-secret': secret,
});
(mockWebhookFunctions.getWorkflowStaticData as jest.Mock).mockReturnValue({
webhookSecret: secret,
} as IDataObject);
const result = verifyWebhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(result).toBe(true);
});
it('should return false when secret does not match (different length)', () => {
const nowMs = Date.now();
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
headers: { 'x-timestamp': String(nowMs) },
});
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
'x-webhook-secret': 'wrong-secret',
});
(mockWebhookFunctions.getWorkflowStaticData as jest.Mock).mockReturnValue({
webhookSecret: 'correct-secret',
} as IDataObject);
const result = verifyWebhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(result).toBe(false);
});
it('should return false when secret does not match (same length)', () => {
const nowMs = Date.now();
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
headers: { 'x-timestamp': String(nowMs) },
});
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
'x-webhook-secret': 'wrong-secret-aa',
});
(mockWebhookFunctions.getWorkflowStaticData as jest.Mock).mockReturnValue({
webhookSecret: 'correct-secret',
} as IDataObject);
const result = verifyWebhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(result).toBe(false);
});
it('should return false when secret header is missing but expected', () => {
const nowMs = Date.now();
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
headers: { 'x-timestamp': String(nowMs) },
});
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({});
(mockWebhookFunctions.getWorkflowStaticData as jest.Mock).mockReturnValue({
webhookSecret: 'expected-secret',
} as IDataObject);
const result = verifyWebhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(result).toBe(false);
});
it('should handle missing timestamp header gracefully', () => {
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
headers: {},
});
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({});
(mockWebhookFunctions.getWorkflowStaticData as jest.Mock).mockReturnValue({});
const result = verifyWebhook.call(mockWebhookFunctions as IWebhookFunctions);
expect(result).toBe(true);
});
});
describe('listWebhooks', () => {
let mockHookFunctions: Partial<IHookFunctions>;
beforeEach(() => {
mockHookFunctions = {
helpers: {
httpRequestWithAuthentication: jest.fn(),
} as unknown as IHookFunctions['helpers'],
};
});
it('should return webhooks from API response', async () => {
const mockWebhooks = [
{
hookId: 'hook-1',
projectId: 'project-123',
url: 'https://example.com/webhook1',
hookEvents: ['RUN_FINISH'],
},
{
hookId: 'hook-2',
projectId: 'project-123',
url: 'https://example.com/webhook2',
hookEvents: ['RUN_START', 'RUN_FINISH'],
},
];
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({
data: mockWebhooks,
});
const result = await listWebhooks.call(mockHookFunctions as IHookFunctions, 'project-123');
expect(mockHookFunctions.helpers!.httpRequestWithAuthentication).toHaveBeenCalledWith(
'currentsApi',
{
method: 'GET',
url: 'https://api.currents.dev/v1/webhooks',
qs: { projectId: 'project-123' },
},
);
expect(result).toEqual(mockWebhooks);
});
it('should return empty array when no webhooks exist', async () => {
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({
data: null,
});
const result = await listWebhooks.call(mockHookFunctions as IHookFunctions, 'project-123');
expect(result).toEqual([]);
});
});
describe('findWebhookByUrl', () => {
let mockHookFunctions: Partial<IHookFunctions>;
beforeEach(() => {
mockHookFunctions = {
helpers: {
httpRequestWithAuthentication: jest.fn(),
} as unknown as IHookFunctions['helpers'],
};
});
it('should find webhook matching URL', async () => {
const targetUrl = 'https://example.com/webhook2';
const mockWebhooks = [
{
hookId: 'hook-1',
projectId: 'project-123',
url: 'https://example.com/webhook1',
hookEvents: ['RUN_FINISH'],
},
{
hookId: 'hook-2',
projectId: 'project-123',
url: targetUrl,
hookEvents: ['RUN_START'],
},
];
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({
data: mockWebhooks,
});
const result = await findWebhookByUrl.call(
mockHookFunctions as IHookFunctions,
'project-123',
targetUrl,
);
expect(result).toEqual(mockWebhooks[1]);
});
it('should return undefined when no webhook matches URL', async () => {
const mockWebhooks = [
{
hookId: 'hook-1',
projectId: 'project-123',
url: 'https://example.com/webhook1',
hookEvents: ['RUN_FINISH'],
},
];
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({
data: mockWebhooks,
});
const result = await findWebhookByUrl.call(
mockHookFunctions as IHookFunctions,
'project-123',
'https://example.com/nonexistent',
);
expect(result).toBeUndefined();
});
});
describe('createWebhook', () => {
let mockHookFunctions: Partial<IHookFunctions>;
beforeEach(() => {
mockHookFunctions = {
helpers: {
httpRequestWithAuthentication: jest.fn(),
} as unknown as IHookFunctions['helpers'],
};
});
it('should create webhook with all options', async () => {
const createdWebhook = {
hookId: 'new-hook-id',
projectId: 'project-123',
url: 'https://example.com/webhook',
hookEvents: ['RUN_FINISH', 'RUN_START'],
headers: '{"x-webhook-secret":"secret123"}',
label: 'n8n workflow 456',
};
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({
data: createdWebhook,
});
const result = await createWebhook.call(mockHookFunctions as IHookFunctions, 'project-123', {
url: 'https://example.com/webhook',
hookEvents: ['RUN_FINISH', 'RUN_START'],
headers: '{"x-webhook-secret":"secret123"}',
label: 'n8n workflow 456',
});
expect(mockHookFunctions.helpers!.httpRequestWithAuthentication).toHaveBeenCalledWith(
'currentsApi',
{
method: 'POST',
url: 'https://api.currents.dev/v1/webhooks',
qs: { projectId: 'project-123' },
body: {
url: 'https://example.com/webhook',
hookEvents: ['RUN_FINISH', 'RUN_START'],
headers: '{"x-webhook-secret":"secret123"}',
label: 'n8n workflow 456',
},
},
);
expect(result).toEqual(createdWebhook);
});
it('should create webhook with minimal options', async () => {
const createdWebhook = {
hookId: 'new-hook-id',
projectId: 'project-123',
url: 'https://example.com/webhook',
hookEvents: [],
};
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({
data: createdWebhook,
});
const result = await createWebhook.call(mockHookFunctions as IHookFunctions, 'project-123', {
url: 'https://example.com/webhook',
});
expect(mockHookFunctions.helpers!.httpRequestWithAuthentication).toHaveBeenCalledWith(
'currentsApi',
{
method: 'POST',
url: 'https://api.currents.dev/v1/webhooks',
qs: { projectId: 'project-123' },
body: {
url: 'https://example.com/webhook',
hookEvents: [],
headers: undefined,
label: undefined,
},
},
);
expect(result).toEqual(createdWebhook);
});
});
describe('updateWebhook', () => {
let mockHookFunctions: Partial<IHookFunctions>;
beforeEach(() => {
mockHookFunctions = {
helpers: {
httpRequestWithAuthentication: jest.fn(),
} as unknown as IHookFunctions['helpers'],
};
});
it('should update webhook with new events', async () => {
const updatedWebhook = {
hookId: 'hook-123',
projectId: 'project-123',
url: 'https://example.com/webhook',
hookEvents: ['RUN_FINISH', 'RUN_TIMEOUT'],
};
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({
data: updatedWebhook,
});
const result = await updateWebhook.call(mockHookFunctions as IHookFunctions, 'hook-123', {
hookEvents: ['RUN_FINISH', 'RUN_TIMEOUT'],
});
expect(mockHookFunctions.helpers!.httpRequestWithAuthentication).toHaveBeenCalledWith(
'currentsApi',
{
method: 'PUT',
url: 'https://api.currents.dev/v1/webhooks/hook-123',
body: {
hookEvents: ['RUN_FINISH', 'RUN_TIMEOUT'],
},
},
);
expect(result).toEqual(updatedWebhook);
});
it('should update webhook with multiple fields', async () => {
const updatedWebhook = {
hookId: 'hook-123',
projectId: 'project-123',
url: 'https://example.com/new-webhook',
hookEvents: ['RUN_START'],
headers: '{"x-webhook-secret":"newsecret"}',
label: 'updated label',
};
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({
data: updatedWebhook,
});
const result = await updateWebhook.call(mockHookFunctions as IHookFunctions, 'hook-123', {
url: 'https://example.com/new-webhook',
hookEvents: ['RUN_START'],
headers: '{"x-webhook-secret":"newsecret"}',
label: 'updated label',
});
expect(mockHookFunctions.helpers!.httpRequestWithAuthentication).toHaveBeenCalledWith(
'currentsApi',
{
method: 'PUT',
url: 'https://api.currents.dev/v1/webhooks/hook-123',
body: {
url: 'https://example.com/new-webhook',
hookEvents: ['RUN_START'],
headers: '{"x-webhook-secret":"newsecret"}',
label: 'updated label',
},
},
);
expect(result).toEqual(updatedWebhook);
});
});
describe('deleteWebhook', () => {
let mockHookFunctions: Partial<IHookFunctions>;
beforeEach(() => {
mockHookFunctions = {
helpers: {
httpRequestWithAuthentication: jest.fn(),
} as unknown as IHookFunctions['helpers'],
};
});
it('should delete webhook by hookId', async () => {
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({});
await deleteWebhook.call(mockHookFunctions as IHookFunctions, 'hook-123');
expect(mockHookFunctions.helpers!.httpRequestWithAuthentication).toHaveBeenCalledWith(
'currentsApi',
{
method: 'DELETE',
url: 'https://api.currents.dev/v1/webhooks/hook-123',
},
);
});
it('should not throw on successful deletion', async () => {
(mockHookFunctions.helpers!.httpRequestWithAuthentication as jest.Mock).mockResolvedValue({});
await expect(
deleteWebhook.call(mockHookFunctions as IHookFunctions, 'hook-123'),
).resolves.toBeUndefined();
});
});
});
@@ -0,0 +1,116 @@
import type { IDataObject, ILoadOptionsFunctions } from 'n8n-workflow';
import { getProjects } from '../methods/listSearch';
describe('Currents listSearch', () => {
describe('getProjects', () => {
let mockContext: Partial<ILoadOptionsFunctions>;
let mockHttpRequest: jest.Mock;
beforeEach(() => {
mockHttpRequest = jest.fn();
mockContext = {
helpers: {
httpRequestWithAuthentication: mockHttpRequest,
} as unknown as ILoadOptionsFunctions['helpers'],
};
});
it('should return projects sorted by name', async () => {
const mockProjects: IDataObject[] = [
{ projectId: 'proj2', name: 'Zebra Project' },
{ projectId: 'proj1', name: 'Alpha Project' },
{ projectId: 'proj3', name: 'Beta Project' },
];
mockHttpRequest.mockResolvedValue({ data: mockProjects });
const result = await getProjects.call(mockContext as ILoadOptionsFunctions);
expect(result.results).toEqual([
{ name: 'Alpha Project', value: 'proj1' },
{ name: 'Beta Project', value: 'proj3' },
{ name: 'Zebra Project', value: 'proj2' },
]);
});
it('should filter projects by name (case-insensitive)', async () => {
const mockProjects: IDataObject[] = [
{ projectId: 'proj1', name: 'Test Project' },
{ projectId: 'proj2', name: 'Production' },
{ projectId: 'proj3', name: 'Testing Environment' },
];
mockHttpRequest.mockResolvedValue({ data: mockProjects });
const result = await getProjects.call(mockContext as ILoadOptionsFunctions, 'test');
expect(result.results).toEqual([
{ name: 'Test Project', value: 'proj1' },
{ name: 'Testing Environment', value: 'proj3' },
]);
});
it('should filter projects by projectId (case-insensitive)', async () => {
const mockProjects: IDataObject[] = [
{ projectId: 'ABC123', name: 'Project A' },
{ projectId: 'DEF456', name: 'Project B' },
{ projectId: 'abc789', name: 'Project C' },
];
mockHttpRequest.mockResolvedValue({ data: mockProjects });
const result = await getProjects.call(mockContext as ILoadOptionsFunctions, 'abc');
expect(result.results).toEqual([
{ name: 'Project A', value: 'ABC123' },
{ name: 'Project C', value: 'abc789' },
]);
});
it('should handle empty project list', async () => {
mockHttpRequest.mockResolvedValue({ data: [] });
const result = await getProjects.call(mockContext as ILoadOptionsFunctions);
expect(result.results).toEqual([]);
});
it('should handle missing data property', async () => {
mockHttpRequest.mockResolvedValue({});
const result = await getProjects.call(mockContext as ILoadOptionsFunctions);
expect(result.results).toEqual([]);
});
it('should call API with correct parameters', async () => {
mockHttpRequest.mockResolvedValue({ data: [] });
await getProjects.call(mockContext as ILoadOptionsFunctions);
expect(mockHttpRequest).toHaveBeenCalledWith('currentsApi', {
method: 'GET',
url: 'https://api.currents.dev/v1/projects',
});
});
it('should handle projects with missing name gracefully', async () => {
const mockProjects: IDataObject[] = [
{ projectId: 'proj1', name: 'Valid Project' },
{ projectId: 'proj2' }, // missing name - should use projectId as fallback
];
mockHttpRequest.mockResolvedValue({ data: mockProjects });
const result = await getProjects.call(mockContext as ILoadOptionsFunctions);
// Should use projectId as name fallback when name is missing
expect(result.results).toEqual([
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
{ name: 'proj2', value: 'proj2' }, // projectId used as name
{ name: 'Valid Project', value: 'proj1' },
]);
});
});
});