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:
+594
@@ -0,0 +1,594 @@
|
||||
import { mock, mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../../../../v2/actions/sheet/append.operation';
|
||||
import type { GoogleSheet } from '../../../../v2/helpers/GoogleSheet';
|
||||
import * as GoogleSheetsUtils from '../../../../v2/helpers/GoogleSheets.utils';
|
||||
|
||||
jest.mock('../../../../v2/helpers/GoogleSheets.utils', () => ({
|
||||
autoMapInputData: jest.fn(),
|
||||
mapFields: jest.fn(),
|
||||
checkForSchemaChanges: jest.fn(),
|
||||
cellFormatDefault: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Google Sheets Append Operation', () => {
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
let mockSheet: jest.Mocked<GoogleSheet>;
|
||||
let mockNode: jest.Mocked<INode>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
|
||||
mockNode = mock<INode>({
|
||||
id: 'test-node',
|
||||
name: 'Google Sheets Append',
|
||||
type: 'n8n-nodes-base.googleSheets',
|
||||
typeVersion: 3,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
});
|
||||
|
||||
mockSheet = mock<GoogleSheet>();
|
||||
mockSheet.getData = jest.fn();
|
||||
mockSheet.appendSheetData = jest.fn();
|
||||
mockSheet.appendEmptyRowsOrColumns = jest.fn();
|
||||
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
|
||||
mockExecuteFunctions.getInputData.mockReturnValue([
|
||||
{ json: { name: 'John', email: 'john@example.com' } },
|
||||
{ json: { name: 'Jane', email: 'jane@example.com' } },
|
||||
]);
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const mockParams: Record<string, any> = {
|
||||
dataMode: 'defineBelow',
|
||||
'columns.mappingMode': 'defineBelow',
|
||||
options: {},
|
||||
'columns.schema': [],
|
||||
};
|
||||
return mockParams[paramName];
|
||||
});
|
||||
|
||||
(GoogleSheetsUtils.autoMapInputData as jest.Mock).mockResolvedValue([
|
||||
{ name: 'John', email: 'john@example.com' },
|
||||
{ name: 'Jane', email: 'jane@example.com' },
|
||||
]);
|
||||
(GoogleSheetsUtils.mapFields as jest.Mock).mockReturnValue([
|
||||
{ name: 'John', email: 'john@example.com' },
|
||||
{ name: 'Jane', email: 'jane@example.com' },
|
||||
]);
|
||||
(GoogleSheetsUtils.cellFormatDefault as jest.Mock).mockReturnValue('USER_ENTERED');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('Basic Execution', () => {
|
||||
it('should execute successfully with valid parameters', async () => {
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions,
|
||||
mockSheet,
|
||||
'Sheet1!A1:B2',
|
||||
'sheet123',
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({
|
||||
json: { name: 'John', email: 'john@example.com' },
|
||||
pairedItem: { item: 0 },
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
json: { name: 'Jane', email: 'jane@example.com' },
|
||||
pairedItem: { item: 1 },
|
||||
});
|
||||
expect(mockSheet.appendSheetData).toHaveBeenCalledWith({
|
||||
inputData: [
|
||||
{ name: 'John', email: 'john@example.com' },
|
||||
{ name: 'Jane', email: 'jane@example.com' },
|
||||
],
|
||||
range: 'Sheet1!A1:B2',
|
||||
keyRowIndex: 1,
|
||||
valueInputMode: 'USER_ENTERED',
|
||||
lastRow: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty array when no input data', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValue([]);
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions,
|
||||
mockSheet,
|
||||
'Sheet1!A1:B2',
|
||||
'sheet123',
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockSheet.appendSheetData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return empty array when dataMode is nothing', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'dataMode' || paramName === 'columns.mappingMode') {
|
||||
return 'nothing';
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions,
|
||||
mockSheet,
|
||||
'Sheet1!A1:B2',
|
||||
'sheet123',
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockSheet.appendSheetData).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Data Mode Handling', () => {
|
||||
it('should use autoMapInputData mode when sheet is empty', async () => {
|
||||
mockSheet.getData.mockResolvedValue([]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockSheet, 'Sheet1!A1:B2', 'sheet123');
|
||||
|
||||
expect(GoogleSheetsUtils.autoMapInputData).toHaveBeenCalledWith(
|
||||
'Sheet1!A1:B2',
|
||||
mockSheet,
|
||||
[
|
||||
{ json: { name: 'John', email: 'john@example.com' }, pairedItem: { item: 0 } },
|
||||
{ json: { name: 'Jane', email: 'jane@example.com' }, pairedItem: { item: 1 } },
|
||||
],
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it('should use defineBelow mode when sheet has data', async () => {
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockSheet, 'Sheet1!A1:B2', 'sheet123');
|
||||
|
||||
expect(GoogleSheetsUtils.mapFields).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it('should handle autoMapInputData mode explicitly', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'dataMode' || paramName === 'columns.mappingMode') {
|
||||
return 'autoMapInputData';
|
||||
}
|
||||
return {};
|
||||
});
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockSheet, 'Sheet1!A1:B2', 'sheet123');
|
||||
|
||||
expect(GoogleSheetsUtils.autoMapInputData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Node Version Handling', () => {
|
||||
it('should use dataMode parameter for node version < 4', async () => {
|
||||
mockNode.typeVersion = 3;
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'dataMode') return 'autoMapInputData';
|
||||
return {};
|
||||
});
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockSheet, 'Sheet1!A1:B2', 'sheet123');
|
||||
|
||||
expect(GoogleSheetsUtils.autoMapInputData).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use columns.mappingMode parameter for node version >= 4', async () => {
|
||||
mockNode.typeVersion = 4;
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'columns.mappingMode') return 'autoMapInputData';
|
||||
return {};
|
||||
});
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockSheet, 'Sheet1!A1:B2', 'sheet123');
|
||||
|
||||
expect(GoogleSheetsUtils.autoMapInputData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Options and Configuration', () => {
|
||||
it('should handle custom header row', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options') {
|
||||
return {
|
||||
locationDefine: {
|
||||
values: { headerRow: 2 },
|
||||
},
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Header1', 'Header2'],
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockSheet, 'Sheet1!A1:B2', 'sheet123');
|
||||
|
||||
expect(mockSheet.getData).toHaveBeenCalledWith('Sheet1!A1:B2', 'FORMATTED_VALUE');
|
||||
});
|
||||
|
||||
it('should handle useAppend option', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options') {
|
||||
return { useAppend: true };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockSheet, 'Sheet1!A1:B2', 'sheet123');
|
||||
|
||||
expect(mockSheet.appendSheetData).toHaveBeenCalledWith({
|
||||
inputData: [
|
||||
{ name: 'John', email: 'john@example.com' },
|
||||
{ name: 'Jane', email: 'jane@example.com' },
|
||||
],
|
||||
range: 'Sheet1!A1:B2',
|
||||
keyRowIndex: 1,
|
||||
valueInputMode: 'USER_ENTERED',
|
||||
useAppend: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle cell format option', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options') {
|
||||
return { cellFormat: 'RAW' };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockSheet, 'Sheet1!A1:B2', 'sheet123');
|
||||
|
||||
expect(mockSheet.appendSheetData).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
valueInputMode: 'RAW',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should throw error when column names cannot be retrieved (node version >= 4.4)', async () => {
|
||||
mockNode.typeVersion = 4.4;
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'columns.mappingMode') return 'defineBelow';
|
||||
if (paramName === 'columns.schema') return [{ id: 'name' }, { id: 'email' }];
|
||||
return {};
|
||||
});
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
]);
|
||||
|
||||
// Mock checkForSchemaChanges to throw error
|
||||
(GoogleSheetsUtils.checkForSchemaChanges as jest.Mock).mockImplementation(() => {
|
||||
throw new NodeOperationError(mockNode, 'Column names were updated');
|
||||
});
|
||||
|
||||
await expect(
|
||||
execute.call(mockExecuteFunctions, mockSheet, 'Sheet1!A1:B2', 'sheet123'),
|
||||
).rejects.toThrow(NodeOperationError);
|
||||
});
|
||||
|
||||
it('should throw error when header row is out of bounds', async () => {
|
||||
mockNode.typeVersion = 4.4;
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'columns.mappingMode') return 'defineBelow';
|
||||
if (paramName === 'columns.schema') return [{ id: 'name' }, { id: 'email' }];
|
||||
if (paramName === 'options') {
|
||||
return {
|
||||
locationDefine: {
|
||||
values: { headerRow: 5 },
|
||||
},
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
]);
|
||||
|
||||
await expect(
|
||||
execute.call(mockExecuteFunctions, mockSheet, 'Sheet1!A1:B2', 'sheet123'),
|
||||
).rejects.toThrow('Could not retrieve the column names from row 5');
|
||||
});
|
||||
|
||||
it('should handle empty input data gracefully', async () => {
|
||||
(GoogleSheetsUtils.mapFields as jest.Mock).mockReturnValue([]);
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
]);
|
||||
|
||||
(GoogleSheetsUtils.autoMapInputData as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions,
|
||||
mockSheet,
|
||||
'Sheet1!A1:B2',
|
||||
'sheet123',
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockSheet.appendSheetData).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Return Data Formatting', () => {
|
||||
it('should return original items for node version < 4', async () => {
|
||||
mockNode.typeVersion = 3;
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions,
|
||||
mockSheet,
|
||||
'Sheet1!A1:B2',
|
||||
'sheet123',
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].json).toEqual({ name: 'John', email: 'john@example.com' });
|
||||
expect(result[0].pairedItem).toEqual({ item: 0 });
|
||||
});
|
||||
|
||||
it('should return mapped data for node version >= 4 with defineBelow mode', async () => {
|
||||
mockNode.typeVersion = 4;
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'columns.mappingMode') return 'defineBelow';
|
||||
return {};
|
||||
});
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions,
|
||||
mockSheet,
|
||||
'Sheet1!A1:B2',
|
||||
'sheet123',
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].json).toEqual({ name: 'John', email: 'john@example.com' });
|
||||
expect(result[0].pairedItem).toEqual({ item: 0 });
|
||||
});
|
||||
|
||||
it('should return original items for autoMapInputData mode regardless of node version', async () => {
|
||||
mockNode.typeVersion = 4;
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'columns.mappingMode') return 'autoMapInputData';
|
||||
return {};
|
||||
});
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions,
|
||||
mockSheet,
|
||||
'Sheet1!A1:B2',
|
||||
'sheet123',
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].json).toEqual({ name: 'John', email: 'john@example.com' });
|
||||
expect(result[0].pairedItem).toEqual({ item: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sheet Data Processing', () => {
|
||||
it('should calculate lastRow correctly when sheet has data', async () => {
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
['Jane', 'jane@example.com'],
|
||||
]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockSheet, 'Sheet1!A1:B2', 'sheet123');
|
||||
|
||||
expect(mockSheet.appendEmptyRowsOrColumns).toHaveBeenCalledWith('sheet123', 1, 0);
|
||||
expect(mockSheet.appendSheetData).toHaveBeenCalledWith({
|
||||
inputData: [
|
||||
{ name: 'John', email: 'john@example.com' },
|
||||
{ name: 'Jane', email: 'jane@example.com' },
|
||||
],
|
||||
range: 'Sheet1!A1:B2',
|
||||
keyRowIndex: 1,
|
||||
valueInputMode: 'USER_ENTERED',
|
||||
lastRow: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty sheet data for lastRow calculation', async () => {
|
||||
mockSheet.getData.mockResolvedValue([]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockSheet, 'Sheet1!A1:B2', 'sheet123');
|
||||
|
||||
expect(mockSheet.appendEmptyRowsOrColumns).toHaveBeenCalledWith('sheet123', 1, 0);
|
||||
expect(mockSheet.appendSheetData).toHaveBeenCalledWith({
|
||||
inputData: [
|
||||
{ name: 'John', email: 'john@example.com' },
|
||||
{ name: 'Jane', email: 'jane@example.com' },
|
||||
],
|
||||
range: 'Sheet1!A1:B2',
|
||||
keyRowIndex: 1,
|
||||
valueInputMode: 'USER_ENTERED',
|
||||
lastRow: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration with GoogleSheets Utils', () => {
|
||||
it('should call autoMapInputData with correct parameters', async () => {
|
||||
mockSheet.getData.mockResolvedValue([]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockSheet, 'Sheet1!A1:B2', 'sheet123');
|
||||
|
||||
expect(GoogleSheetsUtils.autoMapInputData).toHaveBeenCalledWith(
|
||||
'Sheet1!A1:B2',
|
||||
mockSheet,
|
||||
[
|
||||
{ json: { name: 'John', email: 'john@example.com' }, pairedItem: { item: 0 } },
|
||||
{ json: { name: 'Jane', email: 'jane@example.com' }, pairedItem: { item: 1 } },
|
||||
],
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it('should call mapFields with correct input size', async () => {
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockSheet, 'Sheet1!A1:B2', 'sheet123');
|
||||
|
||||
expect(GoogleSheetsUtils.mapFields).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it('should call checkForSchemaChanges for node version >= 4.4', async () => {
|
||||
mockNode.typeVersion = 4.4;
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'columns.mappingMode') return 'defineBelow';
|
||||
if (paramName === 'columns.schema') return [{ id: 'name' }, { id: 'email' }];
|
||||
return {};
|
||||
});
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
await execute.call(mockExecuteFunctions, mockSheet, 'Sheet1!A1:B2', 'sheet123');
|
||||
|
||||
expect(GoogleSheetsUtils.checkForSchemaChanges).toHaveBeenCalledWith(
|
||||
mockNode,
|
||||
['Name', 'Email'],
|
||||
[{ id: 'name' }, { id: 'email' }],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle single item input', async () => {
|
||||
mockExecuteFunctions.getInputData.mockReturnValue([
|
||||
{ json: { name: 'John', email: 'john@example.com' } },
|
||||
]);
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions,
|
||||
mockSheet,
|
||||
'Sheet1!A1:B2',
|
||||
'sheet123',
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].json).toEqual({ name: 'John', email: 'john@example.com' });
|
||||
});
|
||||
|
||||
it('should handle large input data', async () => {
|
||||
const largeInputData = Array.from({ length: 100 }, (_, i) => ({
|
||||
json: { name: `User${i}`, email: `user${i}@example.com` },
|
||||
}));
|
||||
mockExecuteFunctions.getInputData.mockReturnValue(largeInputData);
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions,
|
||||
mockSheet,
|
||||
'Sheet1!A1:B2',
|
||||
'sheet123',
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(100);
|
||||
expect(mockSheet.appendSheetData).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle undefined options gracefully', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options') return {};
|
||||
return {};
|
||||
});
|
||||
mockSheet.getData.mockResolvedValue([
|
||||
['Name', 'Email'],
|
||||
['John', 'john@example.com'],
|
||||
]);
|
||||
mockSheet.appendSheetData.mockResolvedValue([]);
|
||||
|
||||
await expect(
|
||||
execute.call(mockExecuteFunctions, mockSheet, 'Sheet1!A1:B2', 'sheet123'),
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../../../../v2/actions/spreadsheet/create.operation';
|
||||
import { apiRequest } from '../../../../v2/transport';
|
||||
|
||||
jest.mock('../../../../v2/transport', () => ({
|
||||
apiRequest: {
|
||||
call: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Spreadsheet Create Operation', () => {
|
||||
let mockExecuteFunctions: IExecuteFunctions;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = {
|
||||
getInputData: jest.fn().mockReturnValue([{}]),
|
||||
getNodeParameter: jest.fn(),
|
||||
helpers: {
|
||||
constructExecutionMetaData: jest.fn().mockImplementation((data) => data),
|
||||
},
|
||||
} as unknown as IExecuteFunctions;
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should create a basic spreadsheet with title only', async () => {
|
||||
const mockTitle = 'Test Spreadsheet';
|
||||
const mockResponse = { spreadsheetId: '1234', title: mockTitle };
|
||||
|
||||
mockExecuteFunctions.getNodeParameter = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(mockTitle)
|
||||
.mockReturnValueOnce({})
|
||||
.mockReturnValueOnce({});
|
||||
|
||||
(apiRequest.call as jest.Mock).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(apiRequest.call).toHaveBeenCalledWith(
|
||||
mockExecuteFunctions,
|
||||
'POST',
|
||||
'/v4/spreadsheets',
|
||||
{
|
||||
properties: {
|
||||
title: mockTitle,
|
||||
autoRecalc: undefined,
|
||||
locale: undefined,
|
||||
},
|
||||
sheets: [],
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({ json: mockResponse });
|
||||
});
|
||||
|
||||
it('should create spreadsheet with multiple sheets', async () => {
|
||||
const mockSheets = {
|
||||
sheetValues: [
|
||||
{ title: 'Sheet1', hidden: false },
|
||||
{ title: 'Sheet2', hidden: true },
|
||||
],
|
||||
};
|
||||
|
||||
mockExecuteFunctions.getNodeParameter = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce('Test Spreadsheet')
|
||||
.mockReturnValueOnce(mockSheets)
|
||||
.mockReturnValueOnce({});
|
||||
|
||||
const mockResponse = { spreadsheetId: '1234' };
|
||||
(apiRequest.call as jest.Mock).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
await execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(apiRequest.call).toHaveBeenCalledWith(
|
||||
mockExecuteFunctions,
|
||||
'POST',
|
||||
'/v4/spreadsheets',
|
||||
{
|
||||
properties: {
|
||||
title: 'Test Spreadsheet',
|
||||
autoRecalc: undefined,
|
||||
locale: undefined,
|
||||
},
|
||||
sheets: [
|
||||
{ properties: { title: 'Sheet1', hidden: false } },
|
||||
{ properties: { title: 'Sheet2', hidden: true } },
|
||||
],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle all options when creating spreadsheet', async () => {
|
||||
const mockOptions = {
|
||||
locale: 'en_US',
|
||||
autoRecalc: 'ON_CHANGE',
|
||||
};
|
||||
|
||||
mockExecuteFunctions.getNodeParameter = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce('Test Spreadsheet')
|
||||
.mockReturnValueOnce({})
|
||||
.mockReturnValueOnce(mockOptions);
|
||||
|
||||
const mockResponse = { spreadsheetId: '1234' };
|
||||
(apiRequest.call as jest.Mock).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
await execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(apiRequest.call).toHaveBeenCalledWith(
|
||||
mockExecuteFunctions,
|
||||
'POST',
|
||||
'/v4/spreadsheets',
|
||||
{
|
||||
properties: {
|
||||
title: 'Test Spreadsheet',
|
||||
autoRecalc: 'ON_CHANGE',
|
||||
locale: 'en_US',
|
||||
},
|
||||
sheets: [],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle multiple input items', async () => {
|
||||
mockExecuteFunctions.getInputData = jest.fn().mockReturnValue([{}, {}]);
|
||||
|
||||
const mockResponse1 = { spreadsheetId: '1234' };
|
||||
const mockResponse2 = { spreadsheetId: '5678' };
|
||||
|
||||
mockExecuteFunctions.getNodeParameter = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce('Spreadsheet 1')
|
||||
.mockReturnValueOnce({})
|
||||
.mockReturnValueOnce({})
|
||||
.mockReturnValueOnce('Spreadsheet 2')
|
||||
.mockReturnValueOnce({})
|
||||
.mockReturnValueOnce({});
|
||||
|
||||
(apiRequest.call as jest.Mock)
|
||||
.mockResolvedValueOnce(mockResponse1)
|
||||
.mockResolvedValueOnce(mockResponse2);
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(apiRequest.call).toHaveBeenCalledTimes(2);
|
||||
expect(result[0]).toEqual({ json: mockResponse1 });
|
||||
expect(result[1]).toEqual({ json: mockResponse2 });
|
||||
});
|
||||
|
||||
it('should handle empty sheet properties', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce('Test Spreadsheet')
|
||||
.mockReturnValueOnce({ sheetValues: [] })
|
||||
.mockReturnValueOnce({});
|
||||
|
||||
const mockResponse = { spreadsheetId: '1234' };
|
||||
(apiRequest.call as jest.Mock).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
await execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(apiRequest.call).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'POST',
|
||||
'/v4/spreadsheets',
|
||||
expect.objectContaining({
|
||||
sheets: [],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should preserve undefined values for optional properties', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce('Test Spreadsheet')
|
||||
.mockReturnValueOnce({})
|
||||
.mockReturnValueOnce({});
|
||||
|
||||
const mockResponse = { spreadsheetId: '1234' };
|
||||
(apiRequest.call as jest.Mock).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
await execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(apiRequest.call).toHaveBeenCalledWith(expect.anything(), 'POST', '/v4/spreadsheets', {
|
||||
properties: {
|
||||
title: 'Test Spreadsheet',
|
||||
autoRecalc: undefined,
|
||||
locale: undefined,
|
||||
},
|
||||
sheets: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../../../../v2/actions/spreadsheet/delete.operation';
|
||||
import { apiRequest } from '../../../../v2/transport';
|
||||
|
||||
jest.mock('../../../../v2/transport', () => ({
|
||||
apiRequest: {
|
||||
call: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('GoogleSheetsDeleteSpreadsheet', () => {
|
||||
let mockExecuteFunction: IExecuteFunctions;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunction = {
|
||||
getInputData: jest.fn().mockReturnValue([{}]),
|
||||
getNodeParameter: jest.fn(),
|
||||
helpers: {
|
||||
constructExecutionMetaData: jest.fn().mockImplementation((data) => [data]),
|
||||
},
|
||||
} as unknown as IExecuteFunctions;
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should successfully delete a spreadsheet', async () => {
|
||||
const documentId = '1234567890';
|
||||
const expectedUrl = `https://www.googleapis.com/drive/v3/files/${documentId}`;
|
||||
|
||||
mockExecuteFunction.getNodeParameter = jest.fn().mockReturnValue(documentId);
|
||||
(apiRequest.call as jest.Mock).mockResolvedValue({});
|
||||
|
||||
const result = await execute.call(mockExecuteFunction);
|
||||
|
||||
expect(apiRequest.call).toHaveBeenCalledWith(
|
||||
mockExecuteFunction,
|
||||
'DELETE',
|
||||
'',
|
||||
{},
|
||||
{},
|
||||
expectedUrl,
|
||||
);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toEqual([[{ json: { success: true } }]]);
|
||||
});
|
||||
|
||||
it('should handle multiple input items', async () => {
|
||||
const documentIds = ['doc1', 'doc2', 'doc3'];
|
||||
mockExecuteFunction.getInputData = jest.fn().mockReturnValue([{}, {}, {}]);
|
||||
mockExecuteFunction.getNodeParameter = jest
|
||||
.fn()
|
||||
.mockImplementation((_, index) => documentIds[index]);
|
||||
(apiRequest.call as jest.Mock).mockResolvedValue({});
|
||||
|
||||
const result = await execute.call(mockExecuteFunction);
|
||||
|
||||
expect(apiRequest.call).toHaveBeenCalledTimes(3);
|
||||
expect(result).toHaveLength(3);
|
||||
result.forEach((item) => {
|
||||
expect(item).toEqual([{ json: { success: true } }]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle API errors gracefully', async () => {
|
||||
const documentId = '1234567890';
|
||||
const errorMessage = 'File not found';
|
||||
mockExecuteFunction.getNodeParameter = jest.fn().mockReturnValue(documentId);
|
||||
(apiRequest.call as jest.Mock).mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
await expect(execute.call(mockExecuteFunction)).rejects.toThrow(Error);
|
||||
});
|
||||
|
||||
it('should validate document ID parameter', async () => {
|
||||
mockExecuteFunction.getNodeParameter = jest.fn().mockReturnValue(undefined);
|
||||
await expect(execute.call(mockExecuteFunction)).rejects.toThrow();
|
||||
});
|
||||
|
||||
describe('Resource Locator Modes', () => {
|
||||
it('should handle list mode correctly', async () => {
|
||||
const documentId = '1234567890';
|
||||
mockExecuteFunction.getNodeParameter = jest.fn().mockReturnValue({
|
||||
mode: 'list',
|
||||
value: documentId,
|
||||
});
|
||||
(apiRequest.call as jest.Mock).mockResolvedValue({});
|
||||
|
||||
const result = await execute.call(mockExecuteFunction);
|
||||
|
||||
expect(result).toEqual([[{ json: { success: true } }]]);
|
||||
});
|
||||
|
||||
it('should handle URL mode correctly', async () => {
|
||||
const documentUrl = 'https://docs.google.com/spreadsheets/d/1234567890/edit';
|
||||
mockExecuteFunction.getNodeParameter = jest.fn().mockReturnValue({
|
||||
mode: 'url',
|
||||
value: documentUrl,
|
||||
});
|
||||
(apiRequest.call as jest.Mock).mockResolvedValue({});
|
||||
|
||||
const result = await execute.call(mockExecuteFunction);
|
||||
|
||||
expect(result).toEqual([[{ json: { success: true } }]]);
|
||||
});
|
||||
|
||||
it('should handle ID mode correctly', async () => {
|
||||
const documentId = '1234567890';
|
||||
mockExecuteFunction.getNodeParameter = jest.fn().mockReturnValue({
|
||||
mode: 'id',
|
||||
value: documentId,
|
||||
});
|
||||
(apiRequest.call as jest.Mock).mockResolvedValue({});
|
||||
|
||||
const result = await execute.call(mockExecuteFunction);
|
||||
|
||||
expect(result).toEqual([[{ json: { success: true } }]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user