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
405 lines
11 KiB
TypeScript
405 lines
11 KiB
TypeScript
import { mock } from 'jest-mock-extended';
|
|
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
|
|
|
import type { Context } from '../GenericFunctions';
|
|
import { todoistApiGetAllRequest } from '../GenericFunctions';
|
|
|
|
const createMockContext = (typeVersion: number = 2.2) => {
|
|
const mockRequestWithAuth = jest.fn();
|
|
const mockCtx = mock<IExecuteFunctions>({
|
|
getNode: () => mock<INode>({ typeVersion }),
|
|
getNodeParameter: jest.fn((param: string) => {
|
|
if (param === 'authentication') return 'oAuth2';
|
|
return '';
|
|
}) as any,
|
|
helpers: {
|
|
requestWithAuthentication: {
|
|
call: mockRequestWithAuth,
|
|
} as any,
|
|
} as any,
|
|
});
|
|
|
|
return {
|
|
ctx: mockCtx as unknown as Context,
|
|
mockRequestWithAuth,
|
|
};
|
|
};
|
|
|
|
describe('GenericFunctions', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('todoistApiGetAllRequest', () => {
|
|
describe('Legacy mode (node version < 2.2)', () => {
|
|
it('should fetch all items without pagination for version 2.0', async () => {
|
|
const { ctx, mockRequestWithAuth } = createMockContext(2.0);
|
|
const resource = '/tasks';
|
|
const qs = { project_id: '123' };
|
|
|
|
const mockResponse = [
|
|
{ id: '1', content: 'Task 1' },
|
|
{ id: '2', content: 'Task 2' },
|
|
{ id: '3', content: 'Task 3' },
|
|
];
|
|
|
|
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
|
|
|
const result = await todoistApiGetAllRequest(ctx, resource, qs);
|
|
|
|
expect(mockRequestWithAuth).toHaveBeenCalled();
|
|
expect(result).toEqual(mockResponse);
|
|
});
|
|
|
|
it('should fetch all items without pagination for version 2.1', async () => {
|
|
const { ctx, mockRequestWithAuth } = createMockContext(2.1);
|
|
const resource = '/tasks';
|
|
const qs = { project_id: '123' };
|
|
|
|
const mockResponse = [
|
|
{ id: '1', content: 'Task 1' },
|
|
{ id: '2', content: 'Task 2' },
|
|
{ id: '3', content: 'Task 3' },
|
|
];
|
|
|
|
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
|
|
|
const result = await todoistApiGetAllRequest(ctx, resource, qs);
|
|
|
|
expect(mockRequestWithAuth).toHaveBeenCalled();
|
|
expect(result).toEqual(mockResponse);
|
|
});
|
|
|
|
it('should apply limit when specified for old node versions', async () => {
|
|
const { ctx, mockRequestWithAuth } = createMockContext(2.1);
|
|
const resource = '/tasks';
|
|
const qs = { project_id: '123' };
|
|
const limit = 2;
|
|
|
|
const mockResponse = [
|
|
{ id: '1', content: 'Task 1' },
|
|
{ id: '2', content: 'Task 2' },
|
|
{ id: '3', content: 'Task 3' },
|
|
];
|
|
|
|
mockRequestWithAuth.mockResolvedValue([...mockResponse]);
|
|
|
|
const result = await todoistApiGetAllRequest(ctx, resource, qs, limit);
|
|
|
|
expect(mockRequestWithAuth).toHaveBeenCalled();
|
|
expect(result).toEqual([
|
|
{ id: '1', content: 'Task 1' },
|
|
{ id: '2', content: 'Task 2' },
|
|
]);
|
|
});
|
|
|
|
it('should handle limit greater than available items', async () => {
|
|
const { ctx, mockRequestWithAuth } = createMockContext(2.1);
|
|
const resource = '/tasks';
|
|
const limit = 10;
|
|
|
|
const mockResponse = [
|
|
{ id: '1', content: 'Task 1' },
|
|
{ id: '2', content: 'Task 2' },
|
|
];
|
|
|
|
mockRequestWithAuth.mockResolvedValue([...mockResponse]);
|
|
|
|
const result = await todoistApiGetAllRequest(ctx, resource, {}, limit);
|
|
|
|
expect(mockRequestWithAuth).toHaveBeenCalled();
|
|
expect(result).toEqual(mockResponse);
|
|
});
|
|
|
|
it('should handle empty response for old node versions', async () => {
|
|
const { ctx, mockRequestWithAuth } = createMockContext(2.1);
|
|
const resource = '/tasks';
|
|
|
|
mockRequestWithAuth.mockResolvedValue([]);
|
|
|
|
const result = await todoistApiGetAllRequest(ctx, resource);
|
|
|
|
expect(mockRequestWithAuth).toHaveBeenCalled();
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should handle query parameters correctly', async () => {
|
|
const { ctx, mockRequestWithAuth } = createMockContext(2.0);
|
|
const resource = '/tasks';
|
|
const qs = { project_id: '456', filter: 'today' };
|
|
|
|
const mockResponse = [{ id: '1', content: 'Task 1' }];
|
|
|
|
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
|
|
|
const result = await todoistApiGetAllRequest(ctx, resource, qs);
|
|
|
|
expect(mockRequestWithAuth).toHaveBeenCalled();
|
|
expect(result).toEqual(mockResponse);
|
|
});
|
|
|
|
it('should work without query parameters', async () => {
|
|
const { ctx, mockRequestWithAuth } = createMockContext(2.1);
|
|
const resource = '/projects';
|
|
|
|
const mockResponse = [
|
|
{ id: '1', name: 'Project 1' },
|
|
{ id: '2', name: 'Project 2' },
|
|
];
|
|
|
|
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
|
|
|
const result = await todoistApiGetAllRequest(ctx, resource);
|
|
|
|
expect(mockRequestWithAuth).toHaveBeenCalled();
|
|
expect(result).toEqual(mockResponse);
|
|
});
|
|
});
|
|
|
|
describe('Pagination mode (node version >= 2.2)', () => {
|
|
it('should fetch all items with pagination for version 2.2', async () => {
|
|
const { ctx, mockRequestWithAuth } = createMockContext(2.2);
|
|
const resource = '/tasks';
|
|
const qs = { project_id: '123' };
|
|
|
|
const mockResponsePage1 = {
|
|
results: [
|
|
{ id: '1', content: 'Task 1' },
|
|
{ id: '2', content: 'Task 2' },
|
|
],
|
|
next_cursor: 'cursor-123',
|
|
};
|
|
|
|
const mockResponsePage2 = {
|
|
results: [
|
|
{ id: '3', content: 'Task 3' },
|
|
{ id: '4', content: 'Task 4' },
|
|
],
|
|
next_cursor: null,
|
|
};
|
|
|
|
mockRequestWithAuth
|
|
.mockResolvedValueOnce(mockResponsePage1)
|
|
.mockResolvedValueOnce(mockResponsePage2);
|
|
|
|
const result = await todoistApiGetAllRequest(ctx, resource, qs);
|
|
|
|
expect(mockRequestWithAuth).toHaveBeenCalledTimes(2);
|
|
expect(result).toEqual([
|
|
{ id: '1', content: 'Task 1' },
|
|
{ id: '2', content: 'Task 2' },
|
|
{ id: '3', content: 'Task 3' },
|
|
{ id: '4', content: 'Task 4' },
|
|
]);
|
|
});
|
|
|
|
it('should fetch all items with pagination for version greater then 2.2', async () => {
|
|
const { ctx, mockRequestWithAuth } = createMockContext(2.3);
|
|
const resource = '/projects';
|
|
|
|
const mockResponsePage1 = {
|
|
results: [{ id: '1', name: 'Project 1' }],
|
|
next_cursor: 'next-page',
|
|
};
|
|
|
|
const mockResponsePage2 = {
|
|
results: [{ id: '2', name: 'Project 2' }],
|
|
next_cursor: null,
|
|
};
|
|
|
|
mockRequestWithAuth
|
|
.mockResolvedValueOnce(mockResponsePage1)
|
|
.mockResolvedValueOnce(mockResponsePage2);
|
|
|
|
const result = await todoistApiGetAllRequest(ctx, resource);
|
|
|
|
expect(mockRequestWithAuth).toHaveBeenCalledTimes(2);
|
|
expect(result).toHaveLength(2);
|
|
});
|
|
|
|
it('should respect limit and stop fetching when limit is reached', async () => {
|
|
const { ctx, mockRequestWithAuth } = createMockContext(2.2);
|
|
const resource = '/tasks';
|
|
const qs = { project_id: '123' };
|
|
const limit = 3;
|
|
|
|
const mockResponsePage1 = {
|
|
results: [
|
|
{ id: '1', content: 'Task 1' },
|
|
{ id: '2', content: 'Task 2' },
|
|
],
|
|
next_cursor: 'cursor-123',
|
|
};
|
|
|
|
const mockResponsePage2 = {
|
|
results: [
|
|
{ id: '3', content: 'Task 3' },
|
|
{ id: '4', content: 'Task 4' },
|
|
],
|
|
next_cursor: 'cursor-456',
|
|
};
|
|
|
|
mockRequestWithAuth
|
|
.mockResolvedValueOnce(mockResponsePage1)
|
|
.mockResolvedValueOnce(mockResponsePage2);
|
|
|
|
const result = await todoistApiGetAllRequest(ctx, resource, qs, limit);
|
|
|
|
expect(mockRequestWithAuth).toHaveBeenCalledTimes(2);
|
|
expect(result).toEqual([
|
|
{ id: '1', content: 'Task 1' },
|
|
{ id: '2', content: 'Task 2' },
|
|
{ id: '3', content: 'Task 3' },
|
|
]);
|
|
});
|
|
|
|
it('should handle single page response with no next cursor', async () => {
|
|
const { ctx, mockRequestWithAuth } = createMockContext(2.2);
|
|
const resource = '/tasks';
|
|
|
|
const mockResponse = {
|
|
results: [
|
|
{ id: '1', content: 'Task 1' },
|
|
{ id: '2', content: 'Task 2' },
|
|
],
|
|
next_cursor: null,
|
|
};
|
|
|
|
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
|
|
|
const result = await todoistApiGetAllRequest(ctx, resource);
|
|
|
|
expect(mockRequestWithAuth).toHaveBeenCalledTimes(1);
|
|
expect(result).toEqual([
|
|
{ id: '1', content: 'Task 1' },
|
|
{ id: '2', content: 'Task 2' },
|
|
]);
|
|
});
|
|
|
|
it('should handle empty results array', async () => {
|
|
const { ctx, mockRequestWithAuth } = createMockContext(2.2);
|
|
const resource = '/tasks';
|
|
|
|
const mockResponse = {
|
|
results: [],
|
|
next_cursor: null,
|
|
};
|
|
|
|
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
|
|
|
const result = await todoistApiGetAllRequest(ctx, resource);
|
|
|
|
expect(mockRequestWithAuth).toHaveBeenCalledTimes(1);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should handle response without results property', async () => {
|
|
const { ctx, mockRequestWithAuth } = createMockContext(2.2);
|
|
const resource = '/tasks';
|
|
|
|
const mockResponse = {
|
|
next_cursor: null,
|
|
};
|
|
|
|
mockRequestWithAuth.mockResolvedValue(mockResponse);
|
|
|
|
const result = await todoistApiGetAllRequest(ctx, resource);
|
|
|
|
expect(mockRequestWithAuth).toHaveBeenCalledTimes(1);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should respect limit of 200 items per request', async () => {
|
|
const { ctx, mockRequestWithAuth } = createMockContext(2.2);
|
|
const resource = '/tasks';
|
|
const limit = 250;
|
|
|
|
const mockResponsePage1 = {
|
|
results: Array.from({ length: 200 }, (_, i) => ({ id: String(i + 1) })),
|
|
next_cursor: 'cursor-123',
|
|
};
|
|
|
|
const mockResponsePage2 = {
|
|
results: Array.from({ length: 50 }, (_, i) => ({ id: String(i + 201) })),
|
|
next_cursor: null,
|
|
};
|
|
|
|
mockRequestWithAuth
|
|
.mockResolvedValueOnce(mockResponsePage1)
|
|
.mockResolvedValueOnce(mockResponsePage2);
|
|
|
|
const result = await todoistApiGetAllRequest(ctx, resource, {}, limit);
|
|
|
|
expect(mockRequestWithAuth).toHaveBeenCalledTimes(2);
|
|
expect(result).toHaveLength(250);
|
|
});
|
|
|
|
it('should handle multiple pages with different result sizes', async () => {
|
|
const { ctx, mockRequestWithAuth } = createMockContext(2.2);
|
|
const resource = '/tasks';
|
|
|
|
const mockResponsePage1 = {
|
|
results: [{ id: '1' }, { id: '2' }],
|
|
next_cursor: 'cursor-1',
|
|
};
|
|
|
|
const mockResponsePage2 = {
|
|
results: [{ id: '3' }],
|
|
next_cursor: 'cursor-2',
|
|
};
|
|
|
|
const mockResponsePage3 = {
|
|
results: [{ id: '4' }, { id: '5' }, { id: '6' }],
|
|
next_cursor: null,
|
|
};
|
|
|
|
mockRequestWithAuth
|
|
.mockResolvedValueOnce(mockResponsePage1)
|
|
.mockResolvedValueOnce(mockResponsePage2)
|
|
.mockResolvedValueOnce(mockResponsePage3);
|
|
|
|
const result = await todoistApiGetAllRequest(ctx, resource);
|
|
|
|
expect(mockRequestWithAuth).toHaveBeenCalledTimes(3);
|
|
expect(result).toEqual([
|
|
{ id: '1' },
|
|
{ id: '2' },
|
|
{ id: '3' },
|
|
{ id: '4' },
|
|
{ id: '5' },
|
|
{ id: '6' },
|
|
]);
|
|
});
|
|
|
|
it('should stop pagination when limit is exactly reached', async () => {
|
|
const { ctx, mockRequestWithAuth } = createMockContext(2.2);
|
|
const resource = '/tasks';
|
|
const limit = 2;
|
|
|
|
const mockResponsePage1 = {
|
|
results: [{ id: '1' }, { id: '2' }],
|
|
next_cursor: 'cursor-123',
|
|
};
|
|
|
|
mockRequestWithAuth.mockResolvedValueOnce(mockResponsePage1);
|
|
|
|
const result = await todoistApiGetAllRequest(ctx, resource, {}, limit);
|
|
|
|
expect(mockRequestWithAuth).toHaveBeenCalledTimes(1);
|
|
expect(result).toHaveLength(2);
|
|
});
|
|
|
|
it('should handle limit of 0', async () => {
|
|
const { ctx, mockRequestWithAuth } = createMockContext(2.2);
|
|
const resource = '/tasks';
|
|
const limit = 0;
|
|
|
|
const result = await todoistApiGetAllRequest(ctx, resource, {}, limit);
|
|
|
|
expect(mockRequestWithAuth).not.toHaveBeenCalled();
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|
|
});
|
|
});
|