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,610 @@
import { mockDeep } from 'jest-mock-extended';
import moment from 'moment-timezone';
import type { IPollFunctions, INode, ILoadOptionsFunctions, IDataObject } from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import { GoogleDriveTrigger } from '../GoogleDriveTrigger.node';
import * as GenericFunctions from '../v1/GenericFunctions';
import * as listSearch from '../v2/methods/listSearch';
jest.mock('../v1/GenericFunctions', () => ({
extractId: jest.fn(),
googleApiRequest: jest.fn(),
googleApiRequestAllItems: jest.fn(),
}));
jest.mock('../v2/methods/listSearch', () => ({
fileSearch: jest.fn(),
folderSearch: jest.fn(),
}));
describe('GoogleDriveTrigger', () => {
let trigger: GoogleDriveTrigger;
let mockPollFunctions: jest.Mocked<IPollFunctions>;
let mockNode: INode;
const extractIdSpy = jest.spyOn(GenericFunctions, 'extractId');
const googleApiRequestSpy = jest.spyOn(GenericFunctions, 'googleApiRequest');
const googleApiRequestAllItemsSpy = jest.spyOn(GenericFunctions, 'googleApiRequestAllItems');
beforeEach(() => {
trigger = new GoogleDriveTrigger();
mockPollFunctions = mockDeep<IPollFunctions>();
mockNode = {
id: 'test-node-id',
name: 'Google Drive Trigger Test',
type: 'n8n-nodes-base.googleDriveTrigger',
typeVersion: 1,
position: [0, 0],
parameters: {},
};
jest.clearAllMocks();
mockPollFunctions.getNode.mockReturnValue(mockNode);
mockPollFunctions.getWorkflowStaticData.mockReturnValue({});
mockPollFunctions.getMode.mockReturnValue('trigger');
(mockPollFunctions.helpers.returnJsonArray as jest.Mock).mockImplementation((data: unknown[]) =>
data.map((item: unknown, index: number) => ({ json: item, pairedItem: { item: index } })),
);
extractIdSpy.mockImplementation((id) => id);
});
afterEach(() => {
jest.resetAllMocks();
});
describe('Methods', () => {
it('should have correct list search methods', () => {
expect(trigger.methods?.listSearch?.fileSearch).toBe(listSearch.fileSearch);
expect(trigger.methods?.listSearch?.folderSearch).toBe(listSearch.folderSearch);
});
it('should have correct load options methods', () => {
expect(trigger.methods?.loadOptions?.getDrives).toBeDefined();
});
describe('getDrives', () => {
it('should return drives with root option', async () => {
const mockDrives = [
{ id: 'drive1', name: 'My Drive 1' },
{ id: 'drive2', name: 'My Drive 2' },
];
const mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
googleApiRequestAllItemsSpy.mockResolvedValue(mockDrives);
const result = await trigger.methods.loadOptions.getDrives.call(mockLoadOptionsFunctions);
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
'drives',
'GET',
'/drive/v3/drives',
);
expect(result).toEqual([
{ name: 'Root', value: 'root' },
{ name: 'My Drive 1', value: 'drive1' },
{ name: 'My Drive 2', value: 'drive2' },
]);
});
it('should handle empty drives list', async () => {
const mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
googleApiRequestAllItemsSpy.mockResolvedValue([]);
const result = await trigger.methods.loadOptions.getDrives.call(mockLoadOptionsFunctions);
expect(result).toEqual([{ name: 'Root', value: 'root' }]);
});
});
});
describe('Poll Function - Parameter Setup', () => {
beforeEach(() => {
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
triggerOn: 'specificFile',
event: 'fileUpdated',
fileToWatch: 'test-file-id',
folderToWatch: 'test-folder-id',
options: {},
};
return params[paramName] ?? '';
});
});
it('should handle specific file trigger', async () => {
const now = moment().utc();
const webhookData = { lastTimeChecked: now.clone().subtract(1, 'hour').format() };
mockPollFunctions.getWorkflowStaticData.mockReturnValue(webhookData);
const mockFiles = [
{
id: 'test-file-id',
name: 'Test File',
modifiedTime: now.format(),
},
];
extractIdSpy.mockReturnValue('test-file-id');
googleApiRequestAllItemsSpy.mockResolvedValue(mockFiles);
const result = await trigger.poll.call(mockPollFunctions);
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
'files',
'GET',
'/drive/v3/files',
{},
expect.objectContaining({
includeItemsFromAllDrives: true,
supportsAllDrives: true,
spaces: 'appDataFolder, drive',
corpora: 'allDrives',
q: expect.stringContaining('trashed = false'),
fields: 'nextPageToken, files(*)',
}),
);
expect(result).toBeDefined();
expect(result![0]).toHaveLength(1);
expect(result![0][0].json.id).toBe('test-file-id');
});
it('should handle specific folder trigger for file created', async () => {
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
triggerOn: 'specificFolder',
event: 'fileCreated',
folderToWatch: 'test-folder-id',
options: {},
};
return params[paramName] ?? '';
});
const now = moment().utc();
const webhookData = { lastTimeChecked: now.clone().subtract(1, 'hour').format() };
mockPollFunctions.getWorkflowStaticData.mockReturnValue(webhookData);
const mockFiles = [
{
id: 'new-file-id',
name: 'New File',
createdTime: now.format(),
},
];
extractIdSpy.mockReturnValue('test-folder-id');
googleApiRequestAllItemsSpy.mockResolvedValue(mockFiles);
const result = await trigger.poll.call(mockPollFunctions);
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
'files',
'GET',
'/drive/v3/files',
{},
expect.objectContaining({
q: expect.stringContaining("'test-folder-id' in parents"),
}),
);
expect(result).toBeDefined();
expect(result![0]).toHaveLength(1);
});
it('should filter by file type when specified', async () => {
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
triggerOn: 'specificFolder',
event: 'fileCreated',
folderToWatch: 'test-folder-id',
options: { fileType: 'application/vnd.google-apps.document' },
};
return params[paramName] ?? '';
});
googleApiRequestAllItemsSpy.mockResolvedValue([]);
await trigger.poll.call(mockPollFunctions);
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
'files',
'GET',
'/drive/v3/files',
{},
expect.objectContaining({
q: expect.stringContaining("mimeType = 'application/vnd.google-apps.document'"),
}),
);
});
it('should handle folder events with folder mime type filter', async () => {
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
triggerOn: 'specificFolder',
event: 'folderCreated',
folderToWatch: 'test-folder-id',
options: {},
};
return params[paramName] ?? '';
});
googleApiRequestAllItemsSpy.mockResolvedValue([]);
await trigger.poll.call(mockPollFunctions);
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
'files',
'GET',
'/drive/v3/files',
{},
expect.objectContaining({
q: expect.stringContaining("mimeType = 'application/vnd.google-apps.folder'"),
}),
);
});
it('should handle watch folder updated event', async () => {
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
triggerOn: 'specificFolder',
event: 'watchFolderUpdated',
folderToWatch: 'test-folder-id',
options: {},
};
return params[paramName] ?? '';
});
const mockFiles = [
{
id: 'test-folder-id',
name: 'Test Folder',
modifiedTime: moment().utc().format(),
},
];
extractIdSpy.mockReturnValue('test-folder-id');
googleApiRequestAllItemsSpy.mockResolvedValue(mockFiles);
const result = await trigger.poll.call(mockPollFunctions);
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
'files',
'GET',
'/drive/v3/files',
{},
expect.objectContaining({
q: expect.not.stringContaining('in parents'),
}),
);
expect(result).toBeDefined();
});
it('should use createdTime for Created events and modifiedTime for Updated events', async () => {
const now = moment().utc();
const webhookData: IDataObject = {
lastTimeChecked: now.clone().subtract(1, 'hour').format(),
};
mockPollFunctions.getWorkflowStaticData.mockReturnValue(webhookData);
// Test fileCreated event uses createdTime
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, unknown> = {
triggerOn: 'specificFolder',
event: 'fileCreated',
folderToWatch: 'test-folder-id',
options: {},
};
return params[paramName] ?? '';
});
googleApiRequestAllItemsSpy.mockResolvedValue([]);
await trigger.poll.call(mockPollFunctions);
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
'files',
'GET',
'/drive/v3/files',
{},
expect.objectContaining({
q: expect.stringContaining('createdTime >'),
}),
);
// Reset mock
googleApiRequestAllItemsSpy.mockClear();
// Test fileUpdated event uses modifiedTime
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, unknown> = {
triggerOn: 'specificFolder',
event: 'fileUpdated',
folderToWatch: 'test-folder-id',
options: {},
};
return params[paramName] ?? '';
});
await trigger.poll.call(mockPollFunctions);
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
'files',
'GET',
'/drive/v3/files',
{},
expect.objectContaining({
q: expect.stringContaining('modifiedTime >'),
}),
);
});
});
describe('Poll Function - Manual Mode', () => {
beforeEach(() => {
mockPollFunctions.getMode.mockReturnValue('manual');
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
triggerOn: 'specificFile',
event: 'fileUpdated',
fileToWatch: 'test-file-id',
options: {},
};
return params[paramName] ?? '';
});
});
it('should fetch single file in manual mode', async () => {
const mockResponse = {
files: [
{
id: 'test-file-id',
name: 'Test File',
},
],
};
googleApiRequestSpy.mockResolvedValue(mockResponse);
const result = await trigger.poll.call(mockPollFunctions);
expect(googleApiRequestSpy).toHaveBeenCalledWith(
'GET',
'/drive/v3/files',
{},
expect.objectContaining({
pageSize: 1,
}),
);
expect(result).toBeDefined();
expect(result![0]).toHaveLength(1);
expect(result![0][0].json.id).toBe('test-file-id');
});
it('should throw NodeApiError when no data found in manual mode', async () => {
googleApiRequestSpy.mockResolvedValue({ files: [] });
await expect(trigger.poll.call(mockPollFunctions)).rejects.toThrow(NodeApiError);
await expect(trigger.poll.call(mockPollFunctions)).rejects.toThrow(
'No data with the current filter could be found',
);
});
});
describe('Poll Function - State Management', () => {
beforeEach(() => {
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
triggerOn: 'specificFile',
event: 'fileUpdated',
fileToWatch: 'test-file-id',
options: {},
};
return params[paramName] ?? '';
});
});
it('should update lastTimeChecked in webhook data', async () => {
const mockWebhookData = { lastTimeChecked: moment().subtract(1, 'day').format() };
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWebhookData);
googleApiRequestAllItemsSpy.mockResolvedValue([]);
await trigger.poll.call(mockPollFunctions);
expect(mockWebhookData.lastTimeChecked).toBeDefined();
expect(moment(mockWebhookData.lastTimeChecked).isValid()).toBe(true);
});
it('should use current time as startDate when no lastTimeChecked exists', async () => {
const mockWebhookData: IDataObject = {};
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWebhookData);
googleApiRequestAllItemsSpy.mockResolvedValue([]);
await trigger.poll.call(mockPollFunctions);
expect(mockWebhookData.lastTimeChecked).toBeDefined();
expect(moment(mockWebhookData.lastTimeChecked as string).isValid()).toBe(true);
});
});
describe('Poll Function - Error Handling', () => {
beforeEach(() => {
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
triggerOn: 'specificFile',
event: 'fileUpdated',
fileToWatch: 'test-file-id',
options: {},
};
return params[paramName] ?? '';
});
});
it('should handle API request errors', async () => {
const apiError = new Error('API Error');
googleApiRequestAllItemsSpy.mockRejectedValue(apiError);
await expect(trigger.poll.call(mockPollFunctions)).rejects.toThrow('API Error');
});
it('should handle invalid extractId results', async () => {
extractIdSpy.mockImplementation(() => {
throw new Error('Invalid ID');
});
await expect(trigger.poll.call(mockPollFunctions)).rejects.toThrow('Invalid ID');
});
});
describe('Poll Function - Edge Cases', () => {
it('should return null when no files found in trigger mode', async () => {
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
triggerOn: 'specificFile',
event: 'fileUpdated',
fileToWatch: 'test-file-id',
options: {},
};
return params[paramName] ?? '';
});
googleApiRequestAllItemsSpy.mockResolvedValue([]);
const result = await trigger.poll.call(mockPollFunctions);
expect(result).toBeNull();
});
it('should handle empty files array', async () => {
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
triggerOn: 'specificFolder',
event: 'fileCreated',
folderToWatch: 'test-folder-id',
options: {},
};
return params[paramName] ?? '';
});
googleApiRequestAllItemsSpy.mockResolvedValue([]);
const result = await trigger.poll.call(mockPollFunctions);
expect(result).toBeNull();
});
it('should handle files without required fields gracefully', async () => {
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
triggerOn: 'specificFile',
event: 'fileUpdated',
fileToWatch: 'test-file-id',
options: {},
};
return params[paramName] ?? '';
});
const mockFiles = [
{
id: 'test-file-id',
// Missing name and other fields
},
];
googleApiRequestAllItemsSpy.mockResolvedValue(mockFiles);
const result = await trigger.poll.call(mockPollFunctions);
expect(result).toBeDefined();
expect(result![0]).toHaveLength(1);
expect(result![0][0].json.id).toBe('test-file-id');
});
it('should skip time filtering in manual mode', async () => {
mockPollFunctions.getMode.mockReturnValue('manual');
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
triggerOn: 'specificFolder',
event: 'fileCreated',
folderToWatch: 'test-folder-id',
options: {},
};
return params[paramName] ?? '';
});
const mockResponse = { files: [{ id: 'test-file', name: 'Test' }] };
googleApiRequestSpy.mockResolvedValue(mockResponse);
await trigger.poll.call(mockPollFunctions);
expect(googleApiRequestSpy).toHaveBeenCalledWith(
'GET',
'/drive/v3/files',
{},
expect.objectContaining({
q: expect.not.stringMatching(/createdTime|modifiedTime/),
}),
);
});
});
describe('Poll Function - File Filtering', () => {
it('should filter specific file results correctly', async () => {
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
triggerOn: 'specificFile',
event: 'fileUpdated',
fileToWatch: 'target-file-id',
options: {},
};
return params[paramName] ?? '';
});
const mockFiles = [
{ id: 'target-file-id', name: 'Target File' },
{ id: 'other-file-id', name: 'Other File' },
];
extractIdSpy.mockReturnValue('target-file-id');
googleApiRequestAllItemsSpy.mockResolvedValue(mockFiles);
const result = await trigger.poll.call(mockPollFunctions);
expect(result).toBeDefined();
expect(result![0]).toHaveLength(1);
expect(result![0][0].json.id).toBe('target-file-id');
});
it('should filter specific folder results correctly for watchFolderUpdated', async () => {
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
triggerOn: 'specificFolder',
event: 'watchFolderUpdated',
folderToWatch: 'target-folder-id',
options: {},
};
return params[paramName] ?? '';
});
const mockFiles = [
{ id: 'target-folder-id', name: 'Target Folder' },
{ id: 'other-folder-id', name: 'Other Folder' },
];
extractIdSpy.mockReturnValue('target-folder-id');
googleApiRequestAllItemsSpy.mockResolvedValue(mockFiles);
const result = await trigger.poll.call(mockPollFunctions);
expect(result).toBeDefined();
expect(result![0]).toHaveLength(1);
expect(result![0][0].json.id).toBe('target-folder-id');
});
});
});
@@ -0,0 +1,62 @@
import * as create from '../../../../v2/actions/drive/create.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
jest.mock('uuid', () => {
const originalModule = jest.requireActual('uuid');
return {
...originalModule,
v4: jest.fn(function () {
return '430c0ca1-2498-472c-9d43-da0163839823';
}),
};
});
describe('test GoogleDriveV2: drive create', () => {
it('should be called with', async () => {
const nodeParameters = {
resource: 'drive',
name: 'newDrive',
options: {
capabilities: {
canComment: true,
canRename: true,
canTrashChildren: true,
},
colorRgb: '#451AD3',
hidden: false,
restrictions: {
driveMembersOnly: true,
},
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await create.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'POST',
'/drive/v3/drives',
{
capabilities: { canComment: true, canRename: true, canTrashChildren: true },
colorRgb: '#451AD3',
hidden: false,
name: 'newDrive',
restrictions: { driveMembersOnly: true },
},
{ requestId: '430c0ca1-2498-472c-9d43-da0163839823' },
);
});
});
@@ -0,0 +1,37 @@
import * as deleteDrive from '../../../../v2/actions/drive/deleteDrive.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: drive deleteDrive', () => {
it('should be called with', async () => {
const nodeParameters = {
resource: 'drive',
operation: 'deleteDrive',
driveId: {
__rl: true,
value: 'driveIDxxxxxx',
mode: 'id',
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await deleteDrive.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'DELETE',
'/drive/v3/drives/driveIDxxxxxx',
);
});
});
@@ -0,0 +1,42 @@
import * as get from '../../../../v2/actions/drive/get.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: drive get', () => {
it('should be called with', async () => {
const nodeParameters = {
resource: 'drive',
operation: 'get',
driveId: {
__rl: true,
value: 'driveIDxxxxxx',
mode: 'id',
},
options: {
useDomainAdminAccess: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await get.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'GET',
'/drive/v3/drives/driveIDxxxxxx',
{},
{ useDomainAdminAccess: true },
);
});
});
@@ -0,0 +1,67 @@
import type { IHttpRequestMethods } from 'n8n-workflow';
import * as list from '../../../../v2/actions/drive/list.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function (method: IHttpRequestMethods) {
if (method === 'GET') {
return {};
}
}),
googleApiRequestAllItems: jest.fn(async function (method: IHttpRequestMethods) {
if (method === 'GET') {
return {};
}
}),
};
});
describe('test GoogleDriveV2: drive list', () => {
it('should be called with limit', async () => {
const nodeParameters = {
resource: 'drive',
operation: 'list',
limit: 20,
options: {},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await list.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'GET',
'/drive/v3/drives',
{},
{ pageSize: 20 },
);
});
it('should be called with returnAll true', async () => {
const nodeParameters = {
resource: 'drive',
operation: 'list',
returnAll: true,
options: {},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await list.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequestAllItems).toBeCalledTimes(1);
expect(transport.googleApiRequestAllItems).toHaveBeenCalledWith(
'GET',
'drives',
'/drive/v3/drives',
{},
{},
);
});
});
@@ -0,0 +1,45 @@
import * as update from '../../../../v2/actions/drive/update.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: drive update', () => {
it('should be called with', async () => {
const nodeParameters = {
resource: 'drive',
operation: 'update',
driveId: {
__rl: true,
value: 'sharedDriveIDxxxxx',
mode: 'id',
},
options: {
colorRgb: '#F4BEBE',
name: 'newName',
restrictions: {
driveMembersOnly: true,
},
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await update.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'PATCH',
'/drive/v3/drives/sharedDriveIDxxxxx',
{ colorRgb: '#F4BEBE', name: 'newName', restrictions: { driveMembersOnly: true } },
);
});
});
@@ -0,0 +1,63 @@
import * as copy from '../../../../v2/actions/file/copy.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: file copy', () => {
it('should be called with', async () => {
const nodeParameters = {
operation: 'copy',
fileId: {
__rl: true,
value: 'fileIDxxxxxx',
mode: 'list',
cachedResultName: 'test01.png',
cachedResultUrl: 'https://drive.google.com/file/d/fileIDxxxxxx/view?usp=drivesdk',
},
name: 'copyImage.png',
sameFolder: false,
folderId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder 3',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
options: {
copyRequiresWriterPermission: true,
description: 'image copy',
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await copy.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toBeCalledWith(
'POST',
'/drive/v3/files/fileIDxxxxxx/copy',
{
copyRequiresWriterPermission: true,
description: 'image copy',
name: 'copyImage.png',
parents: ['folderIDxxxxxx'],
},
{
supportsAllDrives: true,
corpora: 'allDrives',
includeItemsFromAllDrives: true,
spaces: 'appDataFolder, drive',
},
);
});
});
@@ -0,0 +1,94 @@
import * as createFromText from '../../../../v2/actions/file/createFromText.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return { id: 42 };
}),
};
});
describe('test GoogleDriveV2: file createFromText', () => {
it('should be called with', async () => {
const nodeParameters = {
operation: 'createFromText',
content: 'hello drive!',
name: 'helloDrive.txt',
folderId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder 3',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
options: {
appPropertiesUi: {
appPropertyValues: [
{
key: 'appKey1',
value: 'appValue1',
},
],
},
propertiesUi: {
propertyValues: [
{
key: 'prop1',
value: 'value1',
},
{
key: 'prop2',
value: 'value2',
},
],
},
keepRevisionForever: true,
ocrLanguage: 'en',
useContentAsIndexableText: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await createFromText.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(2);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'POST',
'/upload/drive/v3/files',
expect.anything(), // Buffer of content goes here
{ uploadType: 'multipart', supportsAllDrives: true },
undefined,
{
headers: {
'Content-Length': 503,
'Content-Type': expect.stringMatching(/^multipart\/related; boundary=(\\S)*/),
},
},
);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'PATCH',
'/drive/v3/files/42',
{
appProperties: { appKey1: 'appValue1' },
mimeType: 'text/plain',
name: 'helloDrive.txt',
properties: { prop1: 'value1', prop2: 'value2' },
},
{
addParents: 'folderIDxxxxxx',
corpora: 'allDrives',
includeItemsFromAllDrives: true,
keepRevisionForever: true,
ocrLanguage: 'en',
spaces: 'appDataFolder, drive',
supportsAllDrives: true,
useContentAsIndexableText: true,
},
);
});
});
@@ -0,0 +1,43 @@
import * as deleteFile from '../../../../v2/actions/file/deleteFile.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: file deleteFile', () => {
it('should be called with', async () => {
const nodeParameters = {
operation: 'deleteFile',
fileId: {
__rl: true,
value: 'fileIDxxxxxx',
mode: 'list',
cachedResultName: 'test.txt',
cachedResultUrl: 'https://drive.google.com/file/d/fileIDxxxxxx/view?usp=drivesdk',
},
options: {
deletePermanently: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await deleteFile.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'DELETE',
'/drive/v3/files/fileIDxxxxxx',
undefined,
{ supportsAllDrives: true },
);
});
});
@@ -0,0 +1,51 @@
import * as download from '../../../../v2/actions/file/download.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: file download', () => {
it('should be called with', async () => {
const nodeParameters = {
operation: 'deleteFile',
fileId: {
__rl: true,
value: 'fileIDxxxxxx',
mode: 'list',
cachedResultName: 'test.txt',
cachedResultUrl: 'https://drive.google.com/file/d/fileIDxxxxxx/view?usp=drivesdk',
},
options: {
deletePermanently: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await download.execute.call(fakeExecuteFunction, 0, { json: {} });
expect(transport.googleApiRequest).toBeCalledTimes(2);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'GET',
'/drive/v3/files/fileIDxxxxxx',
{},
{ fields: 'mimeType,name', supportsTeamDrives: true, supportsAllDrives: true },
);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'GET',
'/drive/v3/files/fileIDxxxxxx',
{},
{ alt: 'media', supportsAllDrives: true },
undefined,
{ encoding: 'arraybuffer', json: false, returnFullResponse: true, useStream: true },
);
});
});
@@ -0,0 +1,73 @@
import type { IHttpRequestMethods } from 'n8n-workflow';
import * as move from '../../../../v2/actions/file/move.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function (method: IHttpRequestMethods) {
if (method === 'GET') {
return {
parents: ['parentFolderIDxxxxxx'],
};
}
return {};
}),
};
});
describe('test GoogleDriveV2: file move', () => {
it('should be called with', async () => {
const nodeParameters = {
operation: 'move',
fileId: {
__rl: true,
value: 'fileIDxxxxxx',
mode: 'list',
cachedResultName: 'test.txt',
cachedResultUrl: 'https://drive.google.com/file/d/fileIDxxxxxx/view?usp=drivesdk',
},
folderId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder1',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await move.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(2);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'GET',
'/drive/v3/files/fileIDxxxxxx',
undefined,
{
corpora: 'allDrives',
fields: 'parents',
includeItemsFromAllDrives: true,
spaces: 'appDataFolder, drive',
supportsAllDrives: true,
},
);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'PATCH',
'/drive/v3/files/fileIDxxxxxx',
undefined,
{
addParents: 'folderIDxxxxxx',
removeParents: 'parentFolderIDxxxxxx',
corpora: 'allDrives',
includeItemsFromAllDrives: true,
spaces: 'appDataFolder, drive',
supportsAllDrives: true,
},
);
});
});
@@ -0,0 +1,61 @@
import * as share from '../../../../v2/actions/file/share.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: file share', () => {
it('should be called with', async () => {
const nodeParameters = {
operation: 'share',
fileId: {
__rl: true,
value: 'fileIDxxxxxx',
mode: 'list',
cachedResultName: 'test.txt',
cachedResultUrl: 'https://drive.google.com/file/d/fileIDxxxxxx/view?usp=drivesdk',
},
permissionsUi: {
permissionsValues: {
role: 'owner',
type: 'user',
emailAddress: 'user@gmail.com',
},
},
options: {
emailMessage: 'some message',
moveToNewOwnersRoot: true,
sendNotificationEmail: true,
transferOwnership: true,
useDomainAdminAccess: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await share.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'POST',
'/drive/v3/files/fileIDxxxxxx/permissions',
{ emailAddress: 'user@gmail.com', role: 'owner', type: 'user' },
{
emailMessage: 'some message',
moveToNewOwnersRoot: true,
sendNotificationEmail: true,
supportsAllDrives: true,
transferOwnership: true,
useDomainAdminAccess: true,
},
);
});
});
@@ -0,0 +1,53 @@
import * as update from '../../../../v2/actions/file/update.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: file update', () => {
it('should be called with', async () => {
const nodeParameters = {
operation: 'update',
fileId: {
__rl: true,
value: 'fileIDxxxxxx',
mode: 'list',
cachedResultName: 'test.txt',
cachedResultUrl: 'https://drive.google.com/file/d/fileIDxxxxxx/view?usp=drivesdk',
},
newUpdatedFileName: 'test2.txt',
options: {
keepRevisionForever: true,
ocrLanguage: 'en',
useContentAsIndexableText: true,
fields: ['hasThumbnail', 'starred'],
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await update.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'PATCH',
'/drive/v3/files/fileIDxxxxxx',
{ name: 'test2.txt' },
{
fields: 'hasThumbnail, starred',
keepRevisionForever: true,
ocrLanguage: 'en',
supportsAllDrives: true,
useContentAsIndexableText: true,
},
);
});
});
@@ -0,0 +1,160 @@
import type { IHttpRequestMethods } from 'n8n-workflow';
import * as upload from '../../../../v2/actions/file/upload.operation';
import * as utils from '../../../../v2/helpers/utils';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, createTestStream, driveNode } from '../helpers';
const fileContent = Buffer.from('Hello Drive!');
const originalFilename = 'original.txt';
const contentLength = 123;
const mimeType = 'text/plain';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function (method: IHttpRequestMethods) {
if (method === 'POST') {
return {
headers: { location: 'someLocation' },
};
}
return {};
}),
};
});
jest.mock('../../../../v2/helpers/utils', () => {
const originalModule = jest.requireActual('../../../../v2/helpers/utils');
return {
...originalModule,
getItemBinaryData: jest.fn(async function () {
return {
contentLength,
fileContent,
originalFilename,
mimeType,
};
}),
};
});
describe('test GoogleDriveV2: file upload', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should upload buffers', async () => {
const name = 'newFile.txt';
const parent = 'folderIDxxxxxx';
const nodeParameters = {
name,
folderId: {
__rl: true,
value: parent,
mode: 'list',
cachedResultName: 'testFolder 3',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
options: {
simplifyOutput: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await upload.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(2);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'POST',
'/upload/drive/v3/files',
expect.any(Buffer),
{ uploadType: 'multipart', supportsAllDrives: true },
undefined,
{
headers: {
'Content-Length': 498,
'Content-Type': expect.stringMatching(/^multipart\/related; boundary=(\\S)*/),
},
},
);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'PATCH',
'/drive/v3/files/undefined',
{ mimeType, name, originalFilename },
{
addParents: parent,
supportsAllDrives: true,
corpora: 'allDrives',
includeItemsFromAllDrives: true,
spaces: 'appDataFolder, drive',
},
);
expect(utils.getItemBinaryData).toBeCalledTimes(1);
expect(utils.getItemBinaryData).toHaveBeenCalled();
});
it('should stream large files in 2MB chunks', async () => {
const name = 'newFile.jpg';
const parent = 'folderIDxxxxxx';
const originalFilename = 'test.jpg';
const mimeType = 'image/jpg';
const nodeParameters = {
name,
folderId: {
__rl: true,
value: parent,
mode: 'list',
cachedResultName: 'testFolder 3',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
options: {
simplifyOutput: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
const httpRequestSpy = jest.spyOn(fakeExecuteFunction.helpers, 'httpRequest');
const fileSize = 7 * 1024 * 1024; // 7MB
jest.mocked(utils.getItemBinaryData).mockResolvedValue({
mimeType,
originalFilename,
contentLength: fileSize,
fileContent: createTestStream(fileSize),
});
await upload.execute.call(fakeExecuteFunction, 0);
// 4 chunks: 7MB = 3x2MB + 1x1MB
expect(httpRequestSpy).toHaveBeenCalledTimes(4);
expect(httpRequestSpy).toHaveBeenCalledWith(
expect.objectContaining({ body: expect.any(Buffer) }),
);
expect(transport.googleApiRequest).toBeCalledTimes(2);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'POST',
'/upload/drive/v3/files',
{ name, parents: [parent] },
{ uploadType: 'resumable', supportsAllDrives: true },
undefined,
{ returnFullResponse: true, headers: { 'X-Upload-Content-Type': 'image/jpg' } },
);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'PATCH',
'/drive/v3/files/undefined',
{ mimeType, name, originalFilename },
{
addParents: parent,
supportsAllDrives: true,
corpora: 'allDrives',
includeItemsFromAllDrives: true,
spaces: 'appDataFolder, drive',
},
);
});
});
@@ -0,0 +1,108 @@
import type { IHttpRequestMethods } from 'n8n-workflow';
import * as search from '../../../../v2/actions/fileFolder/search.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function (method: IHttpRequestMethods) {
if (method === 'GET') {
return {};
}
}),
googleApiRequestAllItems: jest.fn(async function (method: IHttpRequestMethods) {
if (method === 'GET') {
return {};
}
}),
};
});
describe('test GoogleDriveV2: fileFolder search', () => {
it('returnAll = false', async () => {
const nodeParameters = {
searchMethod: 'name',
resource: 'fileFolder',
queryString: 'test',
returnAll: false,
limit: 2,
filter: {
whatToSearch: 'files',
fileTypes: ['application/vnd.google-apps.document'],
},
options: {
fields: ['id', 'name', 'starred', 'version'],
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await search.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toBeCalledWith('GET', '/drive/v3/files', undefined, {
corpora: 'allDrives',
fields: 'nextPageToken, files(id, name, starred, version)',
includeItemsFromAllDrives: true,
pageSize: 2,
q: "name contains 'test' and mimeType != 'application/vnd.google-apps.folder' and trashed = false and (mimeType = 'application/vnd.google-apps.document')",
spaces: 'appDataFolder, drive',
supportsAllDrives: true,
});
});
it('returnAll = true', async () => {
const nodeParameters = {
resource: 'fileFolder',
searchMethod: 'query',
queryString: 'test',
returnAll: true,
filter: {
driveId: {
__rl: true,
value: 'driveID000000123',
mode: 'list',
cachedResultName: 'sharedDrive',
cachedResultUrl: 'https://drive.google.com/drive/folders/driveID000000123',
},
folderId: {
__rl: true,
value: 'folderID000000123',
mode: 'list',
cachedResultName: 'testFolder 3',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderID000000123',
},
whatToSearch: 'all',
fileTypes: ['*'],
includeTrashed: true,
},
options: {
fields: ['permissions', 'mimeType'],
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await search.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequestAllItems).toBeCalledTimes(1);
expect(transport.googleApiRequestAllItems).toBeCalledWith(
'GET',
'files',
'/drive/v3/files',
{},
{
corpora: 'drive',
driveId: 'driveID000000123',
fields: 'nextPageToken, files(permissions, mimeType)',
includeItemsFromAllDrives: true,
q: "test and 'folderID000000123' in parents",
spaces: 'appDataFolder, drive',
supportsAllDrives: true,
},
);
});
});
@@ -0,0 +1,55 @@
import * as create from '../../../../v2/actions/folder/create.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: folder create', () => {
it('should be called with', async () => {
const nodeParameters = {
resource: 'folder',
name: 'testFolder 2',
folderId: {
__rl: true,
value: 'root',
mode: 'list',
cachedResultName: 'root',
cachedResultUrl: 'https://drive.google.com/drive',
},
options: {
folderColorRgb: '#167D08',
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await create.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'POST',
'/drive/v3/files',
{
folderColorRgb: '#167D08',
mimeType: 'application/vnd.google-apps.folder',
name: 'testFolder 2',
parents: ['root'],
},
{
fields: undefined,
includeItemsFromAllDrives: true,
supportsAllDrives: true,
spaces: 'appDataFolder, drive',
corpora: 'allDrives',
},
);
});
});
@@ -0,0 +1,67 @@
import * as deleteFolder from '../../../../v2/actions/folder/deleteFolder.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: folder deleteFolder', () => {
it('should be called with PATCH', async () => {
const nodeParameters = {
resource: 'folder',
operation: 'deleteFolder',
folderNoRootId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder 2',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
options: {},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await deleteFolder.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'PATCH',
'/drive/v3/files/folderIDxxxxxx',
{ trashed: true },
{ supportsAllDrives: true },
);
});
it('should be called with DELETE', async () => {
const nodeParameters = {
resource: 'folder',
operation: 'deleteFolder',
folderNoRootId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder 2',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
options: { deletePermanently: true },
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await deleteFolder.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'DELETE',
'/drive/v3/files/folderIDxxxxxx',
undefined,
{ supportsAllDrives: true },
);
});
});
@@ -0,0 +1,51 @@
import * as share from '../../../../v2/actions/folder/share.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: folder share', () => {
it('should be called with', async () => {
const nodeParameters = {
resource: 'folder',
operation: 'share',
folderNoRootId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder 2',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
permissionsUi: {
permissionsValues: {
role: 'reader',
type: 'anyone',
allowFileDiscovery: true,
},
},
options: {
moveToNewOwnersRoot: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await share.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'POST',
'/drive/v3/files/folderIDxxxxxx/permissions',
{ allowFileDiscovery: true, role: 'reader', type: 'anyone' },
{ moveToNewOwnersRoot: true, supportsAllDrives: true },
);
});
});
@@ -0,0 +1,64 @@
import get from 'lodash/get';
import { constructExecutionMetaData } from 'n8n-core';
import type { IDataObject, IExecuteFunctions, IGetNodeParameterOptions, INode } from 'n8n-workflow';
import { Readable } from 'stream';
export const driveNode: INode = {
id: '11',
name: 'Google Drive node',
typeVersion: 3,
type: 'n8n-nodes-base.googleDrive',
position: [42, 42],
parameters: {},
};
export const createMockExecuteFunction = (
nodeParameters: IDataObject,
node: INode,
continueOnFail = false,
) => {
const fakeExecuteFunction = {
getNodeParameter(
parameterName: string,
_itemIndex: number,
fallbackValue?: IDataObject,
options?: IGetNodeParameterOptions,
) {
const parameter = options?.extractValue ? `${parameterName}.value` : parameterName;
return get(nodeParameters, parameter, fallbackValue);
},
getNode() {
return node;
},
helpers: {
constructExecutionMetaData,
returnJsonArray: () => [],
prepareBinaryData: () => {},
httpRequest: () => {},
},
continueOnFail: () => continueOnFail,
} as unknown as IExecuteFunctions;
return fakeExecuteFunction;
};
export function createTestStream(byteSize: number) {
let bytesSent = 0;
const CHUNK_SIZE = 64 * 1024; // 64kB chunks (default NodeJS highWaterMark)
return new Readable({
read() {
const remainingBytes = byteSize - bytesSent;
if (remainingBytes <= 0) {
this.push(null);
return;
}
const chunkSize = Math.min(CHUNK_SIZE, remainingBytes);
const chunk = Buffer.alloc(chunkSize, 'A'); // Test data just a string of "A"
bytesSent += chunkSize;
this.push(chunk);
},
});
}
@@ -0,0 +1,125 @@
import {
prepareQueryString,
setFileProperties,
setUpdateCommonParams,
} from '../../v2/helpers/utils';
describe('test GoogleDriveV2, prepareQueryString', () => {
it('should return id, name', () => {
const fields = undefined;
const result = prepareQueryString(fields);
expect(result).toEqual('id, name');
});
it('should return *', () => {
const fields = ['*'];
const result = prepareQueryString(fields);
expect(result).toEqual('*');
});
it('should return string joined by ,', () => {
const fields = ['id', 'name', 'mimeType'];
const result = prepareQueryString(fields);
expect(result).toEqual('id, name, mimeType');
});
});
describe('test GoogleDriveV2, setFileProperties', () => {
it('should return empty object', () => {
const body = {};
const options = {};
const result = setFileProperties(body, options);
expect(result).toEqual({});
});
it('should return object with properties', () => {
const body = {};
const options = {
propertiesUi: {
propertyValues: [
{
key: 'propertyKey1',
value: 'propertyValue1',
},
{
key: 'propertyKey2',
value: 'propertyValue2',
},
],
},
};
const result = setFileProperties(body, options);
expect(result).toEqual({
properties: {
propertyKey1: 'propertyValue1',
propertyKey2: 'propertyValue2',
},
});
});
it('should return object with appProperties', () => {
const body = {};
const options = {
appPropertiesUi: {
appPropertyValues: [
{
key: 'appPropertyKey1',
value: 'appPropertyValue1',
},
{
key: 'appPropertyKey2',
value: 'appPropertyValue2',
},
],
},
};
const result = setFileProperties(body, options);
expect(result).toEqual({
appProperties: {
appPropertyKey1: 'appPropertyValue1',
appPropertyKey2: 'appPropertyValue2',
},
});
});
});
describe('test GoogleDriveV2, setUpdateCommonParams', () => {
it('should return empty object', () => {
const qs = {};
const options = {};
const result = setUpdateCommonParams(qs, options);
expect(result).toEqual({});
});
it('should return qs with params', () => {
const options = {
useContentAsIndexableText: true,
keepRevisionForever: true,
ocrLanguage: 'en',
trashed: true,
includePermissionsForView: 'published',
};
const qs = setUpdateCommonParams({}, options);
expect(qs).toEqual({
useContentAsIndexableText: true,
keepRevisionForever: true,
ocrLanguage: 'en',
});
});
});