mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
test(Google Sheets Node): Add tests for google sheet (no-changelog) (#13253)
This commit is contained in:
parent
c90d0d9161
commit
e3b7243377
|
@ -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: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 } }]]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,236 @@
|
|||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { GoogleSheet } from '../../../v2/helpers/GoogleSheet';
|
||||
import { apiRequest } from '../../../v2/transport';
|
||||
|
||||
jest.mock('../../../v2/transport', () => ({
|
||||
apiRequest: {
|
||||
call: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('GoogleSheet', () => {
|
||||
let googleSheet: GoogleSheet;
|
||||
const mockExecuteFunctions: Partial<IExecuteFunctions> = {
|
||||
getNode: jest.fn(),
|
||||
};
|
||||
const spreadsheetId = 'test-spreadsheet-id';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
googleSheet = new GoogleSheet(spreadsheetId, mockExecuteFunctions as IExecuteFunctions);
|
||||
});
|
||||
|
||||
describe('clearData', () => {
|
||||
it('should make correct API call to clear data', async () => {
|
||||
const range = 'Sheet1!A1:B2';
|
||||
await googleSheet.clearData(range);
|
||||
|
||||
expect(apiRequest.call).toHaveBeenCalledWith(
|
||||
mockExecuteFunctions,
|
||||
'POST',
|
||||
`/v4/spreadsheets/${spreadsheetId}/values/${range}:clear`,
|
||||
{ spreadsheetId, range },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getData', () => {
|
||||
it('should retrieve data with correct parameters', async () => {
|
||||
const range = 'Sheet1!A1:B2';
|
||||
const valueRenderMode = 'UNFORMATTED_VALUE';
|
||||
const mockResponse = {
|
||||
values: [
|
||||
['1', '2'],
|
||||
['3', '4'],
|
||||
],
|
||||
};
|
||||
|
||||
(apiRequest.call as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await googleSheet.getData(range, valueRenderMode);
|
||||
|
||||
expect(apiRequest.call).toHaveBeenCalledWith(
|
||||
mockExecuteFunctions,
|
||||
'GET',
|
||||
`/v4/spreadsheets/${spreadsheetId}/values/${range}`,
|
||||
{},
|
||||
{
|
||||
valueRenderOption: valueRenderMode,
|
||||
dateTimeRenderOption: 'FORMATTED_STRING',
|
||||
},
|
||||
);
|
||||
expect(result).toEqual(mockResponse.values);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertSheetDataArrayToObjectArray', () => {
|
||||
it('should convert sheet data to object array correctly', () => {
|
||||
const data = [
|
||||
['name', 'age'],
|
||||
['John', '30'],
|
||||
['Jane', '25'],
|
||||
];
|
||||
const result = googleSheet.convertSheetDataArrayToObjectArray(data, 1, ['name', 'age']);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ name: 'John', age: '30' },
|
||||
{ name: 'Jane', age: '25' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty rows when addEmpty is false', () => {
|
||||
const data = [
|
||||
['name', 'age'],
|
||||
['John', '30'],
|
||||
['', ''],
|
||||
['Jane', '25'],
|
||||
];
|
||||
const result = googleSheet.convertSheetDataArrayToObjectArray(
|
||||
data,
|
||||
1,
|
||||
['name', 'age'],
|
||||
false,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ name: 'John', age: '30' },
|
||||
// this row should be skipped but the code does not handle it
|
||||
{ name: '', age: '' },
|
||||
{ name: 'Jane', age: '25' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lookupValues', () => {
|
||||
it('should find matching rows with OR combination', async () => {
|
||||
const inputData = [
|
||||
['name', 'age', 'city'],
|
||||
['John', '30', 'NY'],
|
||||
['Jane', '25', 'LA'],
|
||||
['Bob', '30', 'SF'],
|
||||
];
|
||||
|
||||
const lookupValues = [{ lookupColumn: 'age', lookupValue: '30' }];
|
||||
|
||||
const result = await googleSheet.lookupValues({
|
||||
inputData,
|
||||
keyRowIndex: 0,
|
||||
dataStartRowIndex: 1,
|
||||
lookupValues,
|
||||
returnAllMatches: true,
|
||||
combineFilters: 'OR',
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{ name: 'John', age: '30', city: 'NY' },
|
||||
{ name: 'Bob', age: '30', city: 'SF' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should find matching rows with AND combination', async () => {
|
||||
const inputData = [
|
||||
['name', 'age', 'city'],
|
||||
['John', '30', 'NY'],
|
||||
['Jane', '25', 'LA'],
|
||||
['Bob', '30', 'SF'],
|
||||
];
|
||||
|
||||
const lookupValues = [
|
||||
{ lookupColumn: 'age', lookupValue: '30' },
|
||||
{ lookupColumn: 'city', lookupValue: 'NY' },
|
||||
];
|
||||
|
||||
const result = await googleSheet.lookupValues({
|
||||
inputData,
|
||||
keyRowIndex: 0,
|
||||
dataStartRowIndex: 1,
|
||||
lookupValues,
|
||||
returnAllMatches: true,
|
||||
combineFilters: 'AND',
|
||||
});
|
||||
|
||||
expect(result).toEqual([{ name: 'John', age: '30', city: 'NY' }]);
|
||||
});
|
||||
|
||||
it('should throw error for invalid key row', async () => {
|
||||
const inputData = [['name', 'age']];
|
||||
const lookupValues = [{ lookupColumn: 'age', lookupValue: '30' }];
|
||||
|
||||
await expect(
|
||||
googleSheet.lookupValues({
|
||||
inputData,
|
||||
keyRowIndex: -1,
|
||||
dataStartRowIndex: 1,
|
||||
lookupValues,
|
||||
}),
|
||||
).rejects.toThrow('The key row does not exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('appendSheetData', () => {
|
||||
it('should correctly prepare and append data', async () => {
|
||||
const inputData = [
|
||||
{ name: 'John', age: '30' },
|
||||
{ name: 'Jane', age: '25' },
|
||||
];
|
||||
|
||||
const mockAppendResponse = {
|
||||
range: 'Sheet1!A1:B3',
|
||||
majorDimension: 'ROWS',
|
||||
values: [
|
||||
['name', 'age'],
|
||||
['John', '30'],
|
||||
['Jane', '25'],
|
||||
],
|
||||
};
|
||||
(apiRequest.call as jest.Mock).mockResolvedValue(mockAppendResponse);
|
||||
|
||||
await googleSheet.appendSheetData({
|
||||
inputData,
|
||||
range: 'Sheet1!A:B',
|
||||
keyRowIndex: 0,
|
||||
valueInputMode: 'USER_ENTERED',
|
||||
});
|
||||
|
||||
expect(apiRequest.call).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('appendEmptyRowsOrColumns', () => {
|
||||
it('should throw error when no rows or columns specified', async () => {
|
||||
await expect(googleSheet.appendEmptyRowsOrColumns('sheet1', 0, 0)).rejects.toThrow(
|
||||
'Must specify at least one column or row to add',
|
||||
);
|
||||
});
|
||||
|
||||
it('should make correct API call to append rows and columns', async () => {
|
||||
const sheetId = 'sheet1';
|
||||
await googleSheet.appendEmptyRowsOrColumns(sheetId, 2, 3);
|
||||
|
||||
expect(apiRequest.call).toHaveBeenCalledWith(
|
||||
mockExecuteFunctions,
|
||||
'POST',
|
||||
`/v4/spreadsheets/${spreadsheetId}:batchUpdate`,
|
||||
{
|
||||
requests: [
|
||||
{
|
||||
appendDimension: {
|
||||
sheetId,
|
||||
dimension: 'ROWS',
|
||||
length: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
appendDimension: {
|
||||
sheetId,
|
||||
dimension: 'COLUMNS',
|
||||
length: 3,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,274 @@
|
|||
import type { ILoadOptionsFunctions } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { sheetsSearch, spreadSheetsSearch } from '../../../v2/methods/listSearch';
|
||||
import { apiRequest } from '../../../v2/transport';
|
||||
|
||||
jest.mock('../../../v2/transport', () => ({
|
||||
apiRequest: {
|
||||
call: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Google Sheets Search Functions', () => {
|
||||
let mockLoadOptionsFunctions: Partial<ILoadOptionsFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockLoadOptionsFunctions = {
|
||||
getNode: jest.fn(),
|
||||
getNodeParameter: jest.fn(),
|
||||
};
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('spreadSheetsSearch', () => {
|
||||
it('should return search results without filter', async () => {
|
||||
const mockResponse = {
|
||||
files: [
|
||||
{ id: '1', name: 'Sheet1', webViewLink: 'https://sheet1.url' },
|
||||
{ id: '2', name: 'Sheet2', webViewLink: 'https://sheet2.url' },
|
||||
],
|
||||
nextPageToken: 'next-page',
|
||||
};
|
||||
|
||||
(apiRequest.call as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await spreadSheetsSearch.call(
|
||||
mockLoadOptionsFunctions as ILoadOptionsFunctions,
|
||||
);
|
||||
|
||||
expect(apiRequest.call).toHaveBeenCalledWith(
|
||||
mockLoadOptionsFunctions,
|
||||
'GET',
|
||||
'',
|
||||
{},
|
||||
{
|
||||
q: "mimeType = 'application/vnd.google-apps.spreadsheet'",
|
||||
fields: 'nextPageToken, files(id, name, webViewLink)',
|
||||
orderBy: 'modifiedByMeTime desc,name_natural',
|
||||
includeItemsFromAllDrives: true,
|
||||
supportsAllDrives: true,
|
||||
},
|
||||
'https://www.googleapis.com/drive/v3/files',
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
results: [
|
||||
{ name: 'Sheet1', value: '1', url: 'https://sheet1.url' },
|
||||
{ name: 'Sheet2', value: '2', url: 'https://sheet2.url' },
|
||||
],
|
||||
paginationToken: 'next-page',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle search with filter', async () => {
|
||||
const mockResponse = {
|
||||
files: [{ id: '1', name: 'TestSheet', webViewLink: 'https://test.url' }],
|
||||
};
|
||||
|
||||
(apiRequest.call as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await spreadSheetsSearch.call(
|
||||
mockLoadOptionsFunctions as ILoadOptionsFunctions,
|
||||
'Test',
|
||||
);
|
||||
|
||||
expect(apiRequest.call).toHaveBeenCalledWith(
|
||||
mockLoadOptionsFunctions,
|
||||
'GET',
|
||||
'',
|
||||
{},
|
||||
{
|
||||
q: "name contains 'Test' and mimeType = 'application/vnd.google-apps.spreadsheet'",
|
||||
fields: 'nextPageToken, files(id, name, webViewLink)',
|
||||
orderBy: 'modifiedByMeTime desc,name_natural',
|
||||
includeItemsFromAllDrives: true,
|
||||
supportsAllDrives: true,
|
||||
},
|
||||
'https://www.googleapis.com/drive/v3/files',
|
||||
);
|
||||
|
||||
expect(result.results).toHaveLength(1);
|
||||
expect(result.results[0].name).toBe('TestSheet');
|
||||
});
|
||||
|
||||
it('should escape single quotes in filter', async () => {
|
||||
const mockResponse = { files: [] };
|
||||
(apiRequest.call as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
await spreadSheetsSearch.call(
|
||||
mockLoadOptionsFunctions as ILoadOptionsFunctions,
|
||||
"Test's Sheet",
|
||||
);
|
||||
|
||||
expect(apiRequest.call).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'GET',
|
||||
'',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
q: "name contains 'Test\\'s Sheet' and mimeType = 'application/vnd.google-apps.spreadsheet'",
|
||||
}),
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle pagination token', async () => {
|
||||
const mockResponse = { files: [] };
|
||||
(apiRequest.call as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
await spreadSheetsSearch.call(
|
||||
mockLoadOptionsFunctions as ILoadOptionsFunctions,
|
||||
undefined,
|
||||
'page-token',
|
||||
);
|
||||
|
||||
expect(apiRequest.call).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'GET',
|
||||
'',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
pageToken: 'page-token',
|
||||
}),
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sheetsSearch', () => {
|
||||
it('should return empty results when no documentId is provided', async () => {
|
||||
mockLoadOptionsFunctions.getNodeParameter = jest.fn().mockReturnValue(null);
|
||||
|
||||
const result = await sheetsSearch.call(mockLoadOptionsFunctions as ILoadOptionsFunctions);
|
||||
|
||||
expect(result).toEqual({ results: [] });
|
||||
expect(apiRequest.call).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return sheets list for valid spreadsheet', async () => {
|
||||
mockLoadOptionsFunctions.getNodeParameter = jest.fn().mockReturnValue({
|
||||
mode: 'id',
|
||||
value: 'spreadsheet-id',
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
sheets: [
|
||||
{
|
||||
properties: {
|
||||
sheetId: 123,
|
||||
title: 'Sheet1',
|
||||
sheetType: 'GRID',
|
||||
},
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
sheetId: 456,
|
||||
title: 'Sheet2',
|
||||
sheetType: 'GRID',
|
||||
},
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
sheetId: 789,
|
||||
title: 'Chart1',
|
||||
sheetType: 'CHART',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
(apiRequest.call as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await sheetsSearch.call(mockLoadOptionsFunctions as ILoadOptionsFunctions);
|
||||
|
||||
expect(apiRequest.call).toHaveBeenCalledWith(
|
||||
mockLoadOptionsFunctions,
|
||||
'GET',
|
||||
'/v4/spreadsheets/spreadsheet-id',
|
||||
{},
|
||||
{ fields: 'sheets.properties' },
|
||||
);
|
||||
|
||||
expect(result.results).toHaveLength(2); // Only GRID type sheets
|
||||
expect(result.results[0]).toEqual({
|
||||
name: 'Sheet1',
|
||||
value: 123,
|
||||
url: 'https://docs.google.com/spreadsheets/d/spreadsheet-id/edit#gid=123',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle default sheet id when sheetId is not provided', async () => {
|
||||
mockLoadOptionsFunctions.getNodeParameter = jest.fn().mockReturnValue({
|
||||
mode: 'id',
|
||||
value: 'spreadsheet-id',
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
sheets: [
|
||||
{
|
||||
properties: {
|
||||
title: 'Sheet1',
|
||||
sheetType: 'GRID',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
(apiRequest.call as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await sheetsSearch.call(mockLoadOptionsFunctions as ILoadOptionsFunctions);
|
||||
|
||||
expect(result.results[0].value).toBe('gid=0');
|
||||
});
|
||||
|
||||
it('should throw error when no data is returned', async () => {
|
||||
mockLoadOptionsFunctions.getNodeParameter = jest.fn().mockReturnValue({
|
||||
mode: 'id',
|
||||
value: 'spreadsheet-id',
|
||||
});
|
||||
mockLoadOptionsFunctions.getNode = jest.fn().mockReturnValue({});
|
||||
|
||||
(apiRequest.call as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
await expect(
|
||||
sheetsSearch.call(mockLoadOptionsFunctions as ILoadOptionsFunctions),
|
||||
).rejects.toThrow(
|
||||
new NodeOperationError(mockLoadOptionsFunctions.getNode(), 'No data got returned'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter out non-GRID type sheets', async () => {
|
||||
mockLoadOptionsFunctions.getNodeParameter = jest.fn().mockReturnValue({
|
||||
mode: 'id',
|
||||
value: 'spreadsheet-id',
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
sheets: [
|
||||
{
|
||||
properties: {
|
||||
sheetId: 123,
|
||||
title: 'Chart',
|
||||
sheetType: 'CHART',
|
||||
},
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
sheetId: 456,
|
||||
title: 'Grid',
|
||||
sheetType: 'GRID',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
(apiRequest.call as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await sheetsSearch.call(mockLoadOptionsFunctions as ILoadOptionsFunctions);
|
||||
|
||||
expect(result.results).toHaveLength(1);
|
||||
expect(result.results[0].name).toBe('Grid');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,105 @@
|
|||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../../../v2/actions/sheet/clear.operation';
|
||||
import type { GoogleSheet } from '../../../v2/helpers/GoogleSheet';
|
||||
|
||||
describe('Google Sheet - Clear', () => {
|
||||
let mockExecuteFunctions: Partial<IExecuteFunctions>;
|
||||
let mockSheet: Partial<GoogleSheet>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = {
|
||||
getInputData: jest.fn().mockReturnValue([{ json: {} }]),
|
||||
getNodeParameter: jest.fn(),
|
||||
} as Partial<IExecuteFunctions>;
|
||||
|
||||
mockSheet = {
|
||||
clearData: jest.fn(),
|
||||
getData: jest.fn().mockResolvedValue([['Header1', 'Header2']]), // Mock first-row data
|
||||
updateRows: jest.fn(),
|
||||
} as Partial<GoogleSheet>;
|
||||
});
|
||||
|
||||
test('should clear the whole sheet', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'clear') return 'wholeSheet';
|
||||
return false;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
|
||||
expect(mockSheet.clearData).toHaveBeenCalledWith('Sheet1');
|
||||
});
|
||||
|
||||
test('should clear specific rows', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'clear') return 'specificRows';
|
||||
if (param === 'startIndex') return 2;
|
||||
if (param === 'rowsToDelete') return 3;
|
||||
return false;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
|
||||
expect(mockSheet.clearData).toHaveBeenCalledWith('Sheet1!2:4');
|
||||
});
|
||||
|
||||
test('should clear specific columns', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'clear') return 'specificColumns';
|
||||
if (param === 'startIndex') return 'B';
|
||||
if (param === 'columnsToDelete') return 2;
|
||||
return false;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
|
||||
expect(mockSheet.clearData).toHaveBeenCalledWith('Sheet1!B:C');
|
||||
});
|
||||
|
||||
test('should clear a specific range', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'clear') return 'specificRange';
|
||||
if (param === 'range') return 'A1:C5';
|
||||
return false;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
|
||||
expect(mockSheet.clearData).toHaveBeenCalledWith('Sheet1!A1:C5');
|
||||
});
|
||||
|
||||
test('should keep the first row when clearing the whole sheet', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'clear') return 'wholeSheet';
|
||||
if (param === 'keepFirstRow') return true;
|
||||
return false;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
|
||||
expect(mockSheet.getData).toHaveBeenCalledWith('Sheet1!1:1', 'FORMATTED_VALUE');
|
||||
expect(mockSheet.clearData).toHaveBeenCalledWith('Sheet1');
|
||||
expect(mockSheet.updateRows).toHaveBeenCalledWith('Sheet1', [['Header1', 'Header2']], 'RAW', 1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,100 @@
|
|||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../../../v2/actions/sheet/create.operation';
|
||||
import type { GoogleSheet } from '../../../v2/helpers/GoogleSheet';
|
||||
import { getExistingSheetNames, hexToRgb } from '../../../v2/helpers/GoogleSheets.utils';
|
||||
import { apiRequest } from '../../../v2/transport';
|
||||
|
||||
jest.mock('../../../v2/helpers/GoogleSheets.utils', () => ({
|
||||
getExistingSheetNames: jest.fn(),
|
||||
hexToRgb: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../v2/transport', () => ({
|
||||
apiRequest: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Google Sheet - Create', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
const mockExecuteFunctions = {
|
||||
getInputData: jest.fn(),
|
||||
getNodeParameter: jest.fn(),
|
||||
helpers: {
|
||||
constructExecutionMetaData: jest.fn(),
|
||||
},
|
||||
} as unknown as Partial<IExecuteFunctions>;
|
||||
|
||||
const sheet = {} as Partial<GoogleSheet>;
|
||||
const sheetName = 'test-sheet';
|
||||
|
||||
test('should create a new sheet with given title and options', async () => {
|
||||
const items = [{ json: {} }];
|
||||
const existingSheetNames = ['existing-sheet'];
|
||||
const sheetTitle = 'new-sheet';
|
||||
const options = { tabColor: '0aa55c' };
|
||||
const rgbColor = { red: 10, green: 165, blue: 92 };
|
||||
const responseData = {
|
||||
replies: [{ addSheet: { properties: { title: sheetTitle } } }],
|
||||
};
|
||||
|
||||
(mockExecuteFunctions.getInputData as jest.Mock).mockReturnValue(items);
|
||||
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
|
||||
if (paramName === 'title') return sheetTitle;
|
||||
if (paramName === 'options') return options;
|
||||
});
|
||||
(getExistingSheetNames as jest.Mock).mockResolvedValue(existingSheetNames);
|
||||
(hexToRgb as jest.Mock).mockReturnValue(rgbColor);
|
||||
(apiRequest as jest.Mock).mockResolvedValue(responseData);
|
||||
(mockExecuteFunctions as IExecuteFunctions).helpers.constructExecutionMetaData = jest
|
||||
.fn()
|
||||
.mockReturnValue([{ json: responseData }]);
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
sheet as GoogleSheet,
|
||||
sheetName,
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ json: responseData }]);
|
||||
expect(getExistingSheetNames).toHaveBeenCalledWith(sheet);
|
||||
expect(apiRequest).toHaveBeenCalledWith('POST', `/v4/spreadsheets/${sheetName}:batchUpdate`, {
|
||||
requests: [
|
||||
{
|
||||
addSheet: {
|
||||
properties: {
|
||||
title: sheetTitle,
|
||||
tabColor: { red: 10 / 255, green: 165 / 255, blue: 92 / 255 },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('should skip creating a sheet if the title already exists', async () => {
|
||||
const items = [{ json: {} }];
|
||||
const existingSheetNames = ['existing-sheet'];
|
||||
const sheetTitle = 'existing-sheet';
|
||||
|
||||
(mockExecuteFunctions as IExecuteFunctions).getInputData = jest.fn().mockReturnValue(items);
|
||||
(mockExecuteFunctions as IExecuteFunctions).getNodeParameter = jest
|
||||
.fn()
|
||||
.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'title') return sheetTitle;
|
||||
if (paramName === 'options') return {};
|
||||
});
|
||||
(getExistingSheetNames as jest.Mock).mockResolvedValue(existingSheetNames);
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
sheet as GoogleSheet,
|
||||
sheetName,
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(getExistingSheetNames).toHaveBeenCalledWith(sheet);
|
||||
expect(apiRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,148 @@
|
|||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../../../v2/actions/sheet/delete.operation';
|
||||
import type { GoogleSheet } from '../../../v2/helpers/GoogleSheet';
|
||||
|
||||
describe('Google Sheet - Delete', () => {
|
||||
let mockExecuteFunctions: Partial<IExecuteFunctions>;
|
||||
let mockSheet: Partial<GoogleSheet>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = {
|
||||
getInputData: jest.fn().mockReturnValue([{ json: {} }]),
|
||||
getNodeParameter: jest.fn(),
|
||||
} as Partial<IExecuteFunctions>;
|
||||
|
||||
mockSheet = {
|
||||
spreadsheetBatchUpdate: jest.fn(),
|
||||
} as Partial<GoogleSheet>;
|
||||
});
|
||||
|
||||
test('should delete a single row', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'toDelete') return 'rows';
|
||||
if (param === 'startIndex') return 2;
|
||||
if (param === 'numberToDelete') return 1;
|
||||
return null;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
|
||||
expect(mockSheet.spreadsheetBatchUpdate).toHaveBeenCalledWith([
|
||||
{
|
||||
deleteDimension: {
|
||||
range: {
|
||||
sheetId: 'Sheet1',
|
||||
dimension: 'ROWS',
|
||||
startIndex: 1, // Adjusted for zero-based index
|
||||
endIndex: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should delete multiple rows', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'toDelete') return 'rows';
|
||||
if (param === 'startIndex') return 3;
|
||||
if (param === 'numberToDelete') return 2;
|
||||
return null;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
|
||||
expect(mockSheet.spreadsheetBatchUpdate).toHaveBeenCalledWith([
|
||||
{
|
||||
deleteDimension: {
|
||||
range: {
|
||||
sheetId: 'Sheet1',
|
||||
dimension: 'ROWS',
|
||||
startIndex: 2,
|
||||
endIndex: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should delete a single column', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'toDelete') return 'columns';
|
||||
if (param === 'startIndex') return 'B';
|
||||
if (param === 'numberToDelete') return 1;
|
||||
return null;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
|
||||
expect(mockSheet.spreadsheetBatchUpdate).toHaveBeenCalledWith([
|
||||
{
|
||||
deleteDimension: {
|
||||
range: {
|
||||
sheetId: 'Sheet1',
|
||||
dimension: 'COLUMNS',
|
||||
startIndex: 1, // 'B' corresponds to index 1 (zero-based)
|
||||
endIndex: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should delete multiple columns', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'toDelete') return 'columns';
|
||||
if (param === 'startIndex') return 'C';
|
||||
if (param === 'numberToDelete') return 3;
|
||||
return null;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
|
||||
expect(mockSheet.spreadsheetBatchUpdate).toHaveBeenCalledWith([
|
||||
{
|
||||
deleteDimension: {
|
||||
range: {
|
||||
sheetId: 'Sheet1',
|
||||
dimension: 'COLUMNS',
|
||||
startIndex: 2, // 'C' corresponds to index 2 (zero-based)
|
||||
endIndex: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should return wrapped success response', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param: string) => {
|
||||
if (param === 'toDelete') return 'rows';
|
||||
if (param === 'startIndex') return 2;
|
||||
if (param === 'numberToDelete') return 1;
|
||||
return null;
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
expect(result).toEqual([{ json: { success: true } }]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,81 @@
|
|||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../../../v2/actions/sheet/read.operation';
|
||||
import type { GoogleSheet } from '../../../v2/helpers/GoogleSheet';
|
||||
|
||||
describe('Google Sheet - Read', () => {
|
||||
let mockExecuteFunctions: Partial<IExecuteFunctions>;
|
||||
let mockSheet: Partial<GoogleSheet>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = {
|
||||
getInputData: jest.fn().mockReturnValue([{ json: {} }]),
|
||||
getNode: jest.fn().mockReturnValue({ typeVersion: 4.5 }),
|
||||
getNodeParameter: jest.fn((param) => {
|
||||
const mockParams: { [key: string]: unknown } = {
|
||||
options: {},
|
||||
'filtersUI.values': [],
|
||||
combineFilters: 'AND',
|
||||
};
|
||||
return mockParams[param];
|
||||
}),
|
||||
} as Partial<IExecuteFunctions>;
|
||||
|
||||
mockSheet = {
|
||||
getData: jest.fn().mockResolvedValue([
|
||||
['Header1', 'Header2'],
|
||||
['Value1', 'Value2'],
|
||||
]),
|
||||
lookupValues: jest.fn().mockResolvedValue([{ Header1: 'Value1', Header2: 'Value2' }]),
|
||||
structureArrayDataByColumn: jest
|
||||
.fn()
|
||||
.mockReturnValue([{ Header1: 'Value1', Header2: 'Value2' }]),
|
||||
};
|
||||
});
|
||||
|
||||
test('should return structured sheet data when no filters are applied', async () => {
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
expect(mockSheet.getData).toHaveBeenCalled();
|
||||
expect(mockSheet.structureArrayDataByColumn).toHaveBeenCalled();
|
||||
expect(result).toEqual([
|
||||
{
|
||||
json: { Header1: 'Value1', Header2: 'Value2' },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should call lookupValues when filters are provided', async () => {
|
||||
mockExecuteFunctions.getNodeParameter = jest.fn((param) => {
|
||||
if (param === 'filtersUI.values') return [{ lookupColumn: 'Header1', lookupValue: 'Value1' }];
|
||||
return '';
|
||||
}) as unknown as IExecuteFunctions['getNodeParameter'];
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
expect(mockSheet.lookupValues).toHaveBeenCalled();
|
||||
expect(result).toEqual([
|
||||
{
|
||||
json: { Header1: 'Value1', Header2: 'Value2' },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should return an empty array when sheet data is empty', async () => {
|
||||
mockSheet.getData = jest.fn().mockResolvedValue([]);
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
mockSheet as GoogleSheet,
|
||||
'Sheet1',
|
||||
);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,113 @@
|
|||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../../../v2/actions/sheet/remove.operation';
|
||||
import type { GoogleSheet } from '../../../v2/helpers/GoogleSheet';
|
||||
import { apiRequest } from '../../../v2/transport';
|
||||
|
||||
jest.mock('../../../v2/transport', () => ({
|
||||
apiRequest: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Google Sheet - Remove', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
const mockExecuteFunctions = {
|
||||
getInputData: jest.fn(),
|
||||
getNodeParameter: jest.fn(),
|
||||
helpers: {
|
||||
constructExecutionMetaData: jest.fn(),
|
||||
},
|
||||
} as unknown as Partial<IExecuteFunctions>;
|
||||
|
||||
const sheet = {} as Partial<GoogleSheet>;
|
||||
const sheetName = 'spreadsheet123||sheet456';
|
||||
|
||||
test('should process a single item', async () => {
|
||||
const items = [{ json: {} }];
|
||||
((mockExecuteFunctions as IExecuteFunctions).getInputData as jest.Mock).mockReturnValue(items);
|
||||
|
||||
const apiResponse = { replies: [{ some: 'data' }], foo: 'bar' };
|
||||
(apiRequest as jest.Mock).mockResolvedValue(apiResponse);
|
||||
|
||||
const constructedData = [{ json: { foo: 'bar', index: 0 } }];
|
||||
(
|
||||
(mockExecuteFunctions as IExecuteFunctions).helpers.constructExecutionMetaData as jest.Mock
|
||||
).mockReturnValue(constructedData);
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
sheet as GoogleSheet,
|
||||
sheetName,
|
||||
);
|
||||
|
||||
expect(apiRequest).toHaveBeenCalledTimes(1);
|
||||
expect(apiRequest).toHaveBeenCalledWith('POST', '/v4/spreadsheets/spreadsheet123:batchUpdate', {
|
||||
requests: [
|
||||
{
|
||||
deleteSheet: {
|
||||
sheetId: 'sheet456',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).toEqual(constructedData);
|
||||
});
|
||||
|
||||
test('should process multiple items', async () => {
|
||||
const items = [{ json: {} }, { json: {} }];
|
||||
((mockExecuteFunctions as IExecuteFunctions).getInputData as jest.Mock).mockReturnValue(items);
|
||||
|
||||
const apiResponses = [
|
||||
{ replies: [{ some: 'data1' }], foo: 'bar1' },
|
||||
{ replies: [{ some: 'data2' }], foo: 'bar2' },
|
||||
];
|
||||
(apiRequest as jest.Mock)
|
||||
.mockResolvedValueOnce(apiResponses[0])
|
||||
.mockResolvedValueOnce(apiResponses[1]);
|
||||
|
||||
const constructedDataItem0 = [{ json: { foo: 'bar1', index: 0 } }];
|
||||
const constructedDataItem1 = [{ json: { foo: 'bar2', index: 1 } }];
|
||||
((mockExecuteFunctions as IExecuteFunctions).helpers.constructExecutionMetaData as jest.Mock)
|
||||
.mockReturnValueOnce(constructedDataItem0)
|
||||
.mockReturnValueOnce(constructedDataItem1);
|
||||
|
||||
const result = await execute.call(
|
||||
mockExecuteFunctions as IExecuteFunctions,
|
||||
sheet as GoogleSheet,
|
||||
sheetName,
|
||||
);
|
||||
|
||||
expect(apiRequest).toHaveBeenCalledTimes(2);
|
||||
expect(apiRequest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'POST',
|
||||
'/v4/spreadsheets/spreadsheet123:batchUpdate',
|
||||
{
|
||||
requests: [
|
||||
{
|
||||
deleteSheet: {
|
||||
sheetId: 'sheet456',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
expect(apiRequest).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'POST',
|
||||
'/v4/spreadsheets/spreadsheet123:batchUpdate',
|
||||
{
|
||||
requests: [
|
||||
{
|
||||
deleteSheet: {
|
||||
sheetId: 'sheet456',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
expect(result).toEqual([...constructedDataItem0, ...constructedDataItem1]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,179 @@
|
|||
import type { IExecuteFunctions, ILoadOptionsFunctions } from 'n8n-workflow';
|
||||
|
||||
import { getGoogleAccessToken } from '../../../../GenericFunctions';
|
||||
import { apiRequest, apiRequestAllItems } from '../../../v2/transport';
|
||||
|
||||
jest.mock('../../../../GenericFunctions', () => ({
|
||||
getGoogleAccessToken: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Google Sheets Transport', () => {
|
||||
let mockExecuteFunction: IExecuteFunctions;
|
||||
let mockLoadOptionsFunction: ILoadOptionsFunctions;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunction = {
|
||||
getNode: jest.fn(),
|
||||
getNodeParameter: jest.fn(),
|
||||
getCredentials: jest.fn(),
|
||||
helpers: {
|
||||
request: jest.fn(),
|
||||
requestOAuth2: jest.fn(),
|
||||
},
|
||||
} as unknown as IExecuteFunctions;
|
||||
|
||||
mockLoadOptionsFunction = {
|
||||
...mockExecuteFunction,
|
||||
} as unknown as ILoadOptionsFunctions;
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
const mockAccessToken = 'mock-access-token';
|
||||
|
||||
describe('apiRequest', () => {
|
||||
it('should make successful request with service account authentication', async () => {
|
||||
const method = 'GET';
|
||||
const resource = '/v4/spreadsheets';
|
||||
const mockResponse = { data: 'test' };
|
||||
|
||||
mockExecuteFunction.getNodeParameter = jest.fn().mockReturnValue('serviceAccount');
|
||||
mockExecuteFunction.getCredentials = jest.fn().mockResolvedValue({
|
||||
email: 'test@test.com',
|
||||
privateKey: 'private-key',
|
||||
});
|
||||
(getGoogleAccessToken as jest.Mock).mockResolvedValue({ access_token: mockAccessToken });
|
||||
mockExecuteFunction.helpers.request = jest.fn().mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await apiRequest.call(mockExecuteFunction, method, resource);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockExecuteFunction.helpers.request).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: `Bearer ${mockAccessToken}`,
|
||||
}),
|
||||
method,
|
||||
uri: `https://sheets.googleapis.com${resource}`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should make successful request with OAuth2 authentication', async () => {
|
||||
const method = 'GET';
|
||||
const resource = '/v4/spreadsheets';
|
||||
const mockResponse = { data: 'test' };
|
||||
|
||||
mockExecuteFunction.getNodeParameter = jest.fn().mockReturnValue('oAuth2');
|
||||
mockExecuteFunction.helpers.requestOAuth2 = jest.fn().mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await apiRequest.call(mockExecuteFunction, method, resource);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockExecuteFunction.helpers.requestOAuth2).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle custom headers and query parameters', async () => {
|
||||
const method = 'GET';
|
||||
const resource = '/v4/spreadsheets';
|
||||
const headers = { 'Custom-Header': 'value' };
|
||||
const qs = { param: 'value' };
|
||||
|
||||
mockExecuteFunction.getNodeParameter = jest.fn().mockReturnValue('serviceAccount');
|
||||
mockExecuteFunction.getCredentials = jest.fn().mockResolvedValue({});
|
||||
(getGoogleAccessToken as jest.Mock).mockResolvedValue({ access_token: 'token' });
|
||||
|
||||
await apiRequest.call(mockExecuteFunction, method, resource, {}, qs, undefined, headers);
|
||||
|
||||
expect(mockExecuteFunction.helpers.request).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining(headers),
|
||||
qs,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle PERMISSION_DENIED error with custom description', async () => {
|
||||
const method = 'GET';
|
||||
const resource = '/v4/spreadsheets';
|
||||
const error = new Error('PERMISSION_DENIED');
|
||||
error.message = 'PERMISSION_DENIED Some error';
|
||||
|
||||
mockExecuteFunction.getNodeParameter = jest.fn().mockReturnValue('serviceAccount');
|
||||
mockExecuteFunction.getCredentials = jest.fn().mockResolvedValue({});
|
||||
mockExecuteFunction.helpers.request = jest.fn().mockRejectedValue(error);
|
||||
|
||||
await expect(apiRequest.call(mockExecuteFunction, method, resource)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should handle SSL certificate errors', async () => {
|
||||
const method = 'GET';
|
||||
const resource = '/v4/spreadsheets';
|
||||
const error = new Error('ERR_OSSL_PEM_NO_START_LINE');
|
||||
|
||||
mockExecuteFunction.getNodeParameter = jest.fn().mockReturnValue('serviceAccount');
|
||||
mockExecuteFunction.getCredentials = jest.fn().mockResolvedValue({});
|
||||
mockExecuteFunction.helpers.request = jest.fn().mockRejectedValue(error);
|
||||
|
||||
await expect(apiRequest.call(mockExecuteFunction, method, resource)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('apiRequestAllItems', () => {
|
||||
it('should fetch all pages of results', async () => {
|
||||
const propertyName = 'items';
|
||||
const method = 'GET';
|
||||
const endpoint = '/v4/spreadsheets';
|
||||
const firstPage = {
|
||||
items: [{ id: 1 }, { id: 2 }],
|
||||
nextPageToken: 'token1',
|
||||
};
|
||||
const secondPage = {
|
||||
items: [{ id: 3 }, { id: 4 }],
|
||||
nextPageToken: undefined,
|
||||
};
|
||||
|
||||
mockLoadOptionsFunction.getNodeParameter = jest.fn().mockReturnValue('serviceAccount');
|
||||
mockLoadOptionsFunction.getCredentials = jest.fn().mockResolvedValue({});
|
||||
|
||||
(getGoogleAccessToken as jest.Mock).mockResolvedValue({ access_token: mockAccessToken });
|
||||
mockExecuteFunction.helpers.request = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(firstPage)
|
||||
.mockResolvedValueOnce(secondPage);
|
||||
|
||||
const result = await apiRequestAllItems.call(
|
||||
mockLoadOptionsFunction,
|
||||
propertyName,
|
||||
method,
|
||||
endpoint,
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(4);
|
||||
expect(result).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]);
|
||||
});
|
||||
|
||||
it('should handle empty response', async () => {
|
||||
const propertyName = 'items';
|
||||
const method = 'GET';
|
||||
const endpoint = '/v4/spreadsheets';
|
||||
const emptyResponse = {
|
||||
items: [],
|
||||
};
|
||||
|
||||
mockLoadOptionsFunction.getNodeParameter = jest.fn().mockReturnValue('serviceAccount');
|
||||
mockLoadOptionsFunction.getCredentials = jest.fn().mockResolvedValue({});
|
||||
|
||||
(getGoogleAccessToken as jest.Mock).mockResolvedValue({ access_token: mockAccessToken });
|
||||
mockExecuteFunction.helpers.request = jest.fn().mockResolvedValue(emptyResponse);
|
||||
|
||||
const result = await apiRequestAllItems.call(
|
||||
mockLoadOptionsFunction,
|
||||
propertyName,
|
||||
method,
|
||||
endpoint,
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue