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:
@@ -0,0 +1,525 @@
|
||||
import type { IPollFunctions } from 'n8n-workflow';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
import {
|
||||
BINARY_MIME_TYPE,
|
||||
arrayOfArraysToJson,
|
||||
compareRevisions,
|
||||
getRevisionFile,
|
||||
sheetBinaryToArrayOfArrays,
|
||||
} from '../GoogleSheetsTrigger.utils';
|
||||
import { apiRequest } from '../v2/transport';
|
||||
|
||||
jest.mock('../v2/transport', () => ({
|
||||
apiRequest: {
|
||||
call: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('xlsx', () => ({
|
||||
read: jest.fn(),
|
||||
utils: {
|
||||
sheet_to_json: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('GoogleSheetsTrigger.utils', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('BINARY_MIME_TYPE', () => {
|
||||
it('should have correct Excel mime type', () => {
|
||||
expect(BINARY_MIME_TYPE).toBe(
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRevisionFile', () => {
|
||||
it('should make correct API call and return buffer', async () => {
|
||||
const mockPollFunctions: Partial<IPollFunctions> = {
|
||||
getNode: jest.fn(),
|
||||
};
|
||||
const exportLink = 'https://example.com/export';
|
||||
const mockResponse = {
|
||||
body: 'mock binary data',
|
||||
};
|
||||
|
||||
(apiRequest.call as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await getRevisionFile.call(mockPollFunctions as IPollFunctions, exportLink);
|
||||
|
||||
expect(apiRequest.call).toHaveBeenCalledWith(
|
||||
mockPollFunctions,
|
||||
'GET',
|
||||
'',
|
||||
undefined,
|
||||
{ mimeType: BINARY_MIME_TYPE },
|
||||
exportLink,
|
||||
undefined,
|
||||
{
|
||||
resolveWithFullResponse: true,
|
||||
encoding: null,
|
||||
json: false,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual(Buffer.from('mock binary data'));
|
||||
});
|
||||
|
||||
it('should handle API request errors', async () => {
|
||||
const mockPollFunctions: Partial<IPollFunctions> = {
|
||||
getNode: jest.fn(),
|
||||
};
|
||||
const exportLink = 'https://example.com/export';
|
||||
const error = new Error('API request failed');
|
||||
|
||||
(apiRequest.call as jest.Mock).mockRejectedValue(error);
|
||||
|
||||
await expect(
|
||||
getRevisionFile.call(mockPollFunctions as IPollFunctions, exportLink),
|
||||
).rejects.toThrow('API request failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sheetBinaryToArrayOfArrays', () => {
|
||||
const mockBuffer = Buffer.from('mock data');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should process sheet with data correctly', () => {
|
||||
const mockSheet = {
|
||||
'!ref': 'A1:C3',
|
||||
};
|
||||
const mockWorkbook = {
|
||||
Sheets: {
|
||||
Sheet1: mockSheet,
|
||||
},
|
||||
};
|
||||
|
||||
const mockSheetData = [
|
||||
['Name', 'Age', 'City'],
|
||||
['John', '30', 'NYC'],
|
||||
['Jane', '25', ''],
|
||||
];
|
||||
|
||||
(XLSX.read as jest.Mock).mockReturnValue(mockWorkbook);
|
||||
(XLSX.utils.sheet_to_json as jest.Mock).mockReturnValue(mockSheetData);
|
||||
|
||||
const result = sheetBinaryToArrayOfArrays(mockBuffer, 'Sheet1', 'A1:C3');
|
||||
|
||||
expect(XLSX.read).toHaveBeenCalledWith(mockBuffer, {
|
||||
type: 'buffer',
|
||||
sheets: ['Sheet1'],
|
||||
});
|
||||
expect(XLSX.utils.sheet_to_json).toHaveBeenCalledWith(mockSheet, {
|
||||
header: 1,
|
||||
defval: '',
|
||||
range: 'A1:C3',
|
||||
});
|
||||
expect(result).toEqual(mockSheetData);
|
||||
});
|
||||
|
||||
it('should handle empty sheet', () => {
|
||||
const mockSheet = {};
|
||||
const mockWorkbook = {
|
||||
Sheets: {
|
||||
Sheet1: mockSheet,
|
||||
},
|
||||
};
|
||||
|
||||
(XLSX.read as jest.Mock).mockReturnValue(mockWorkbook);
|
||||
|
||||
const result = sheetBinaryToArrayOfArrays(mockBuffer, 'Sheet1', undefined);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should trim empty trailing rows', () => {
|
||||
const mockSheet = {
|
||||
'!ref': 'A1:C5',
|
||||
};
|
||||
const mockWorkbook = {
|
||||
Sheets: {
|
||||
Sheet1: mockSheet,
|
||||
},
|
||||
};
|
||||
|
||||
const mockSheetData = [
|
||||
['Name', 'Age', 'City'],
|
||||
['John', '30', 'NYC'],
|
||||
['Jane', '25', 'LA'],
|
||||
['', '', ''],
|
||||
['', '', ''],
|
||||
];
|
||||
|
||||
(XLSX.read as jest.Mock).mockReturnValue(mockWorkbook);
|
||||
(XLSX.utils.sheet_to_json as jest.Mock).mockReturnValue(mockSheetData);
|
||||
|
||||
const result = sheetBinaryToArrayOfArrays(mockBuffer, 'Sheet1', undefined);
|
||||
|
||||
expect(result).toEqual([
|
||||
['Name', 'Age', 'City'],
|
||||
['John', '30', 'NYC'],
|
||||
['Jane', '25', 'LA'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle sheet with only header row', () => {
|
||||
const mockSheet = {
|
||||
'!ref': 'A1:C1',
|
||||
};
|
||||
const mockWorkbook = {
|
||||
Sheets: {
|
||||
Sheet1: mockSheet,
|
||||
},
|
||||
};
|
||||
|
||||
const mockSheetData = [['Name', 'Age', 'City']];
|
||||
|
||||
(XLSX.read as jest.Mock).mockReturnValue(mockWorkbook);
|
||||
(XLSX.utils.sheet_to_json as jest.Mock).mockReturnValue(mockSheetData);
|
||||
|
||||
const result = sheetBinaryToArrayOfArrays(mockBuffer, 'Sheet1', undefined);
|
||||
|
||||
expect(result).toEqual([['Name', 'Age', 'City']]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('arrayOfArraysToJson', () => {
|
||||
it('should convert array of arrays to JSON objects', () => {
|
||||
const sheetData = [
|
||||
['John', '30', 'NYC'],
|
||||
['Jane', '25', 'LA'],
|
||||
['Bob', '35', 'SF'],
|
||||
];
|
||||
const columns = ['Name', 'Age', 'City'];
|
||||
|
||||
const result = arrayOfArraysToJson(sheetData, columns);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ Name: 'John', Age: '30', City: 'NYC' },
|
||||
{ Name: 'Jane', Age: '25', City: 'LA' },
|
||||
{ Name: 'Bob', Age: '35', City: 'SF' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle missing cell values', () => {
|
||||
const sheetData = [['John', '30'], ['Jane'], ['Bob', '35', 'SF']];
|
||||
const columns = ['Name', 'Age', 'City'];
|
||||
|
||||
const result = arrayOfArraysToJson(sheetData, columns);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ Name: 'John', Age: '30', City: '' },
|
||||
{ Name: 'Jane', Age: '', City: '' },
|
||||
{ Name: 'Bob', Age: '35', City: 'SF' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty sheet data', () => {
|
||||
const sheetData: string[][] = [];
|
||||
const columns = ['Name', 'Age', 'City'];
|
||||
|
||||
const result = arrayOfArraysToJson(sheetData, columns);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle empty columns', () => {
|
||||
const sheetData = [
|
||||
['John', '30', 'NYC'],
|
||||
['Jane', '25', 'LA'],
|
||||
];
|
||||
const columns: string[] = [];
|
||||
|
||||
const result = arrayOfArraysToJson(sheetData, columns);
|
||||
|
||||
expect(result).toEqual([{}, {}]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareRevisions', () => {
|
||||
const baseColumns = ['Name', 'Age', 'City'];
|
||||
|
||||
it('should detect row updates with current output', () => {
|
||||
const previousData = [baseColumns, ['John', '30', 'NYC'], ['Jane', '25', 'LA']];
|
||||
const currentData = [
|
||||
baseColumns,
|
||||
['John', '31', 'NYC'], // Age updated
|
||||
['Jane', '25', 'LA'], // No change
|
||||
['Bob', '35', 'SF'], // New row
|
||||
];
|
||||
|
||||
const result = compareRevisions(
|
||||
previousData,
|
||||
currentData,
|
||||
1, // keyRow
|
||||
'current', // includeInOutput
|
||||
[], // columnsToWatch
|
||||
0, // dataStartIndex
|
||||
'anyUpdate', // event
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
row_number: 2,
|
||||
change_type: 'updated',
|
||||
Name: 'John',
|
||||
Age: '31',
|
||||
City: 'NYC',
|
||||
},
|
||||
{
|
||||
row_number: 4,
|
||||
change_type: 'added',
|
||||
Name: 'Bob',
|
||||
Age: '35',
|
||||
City: 'SF',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should detect row updates with previous output', () => {
|
||||
const previousData = [baseColumns, ['John', '30', 'NYC'], ['Jane', '25', 'LA']];
|
||||
const currentData = [
|
||||
baseColumns,
|
||||
['John', '31', 'NYC'], // Age updated
|
||||
['Jane', '25', 'LA'], // No change
|
||||
['Bob', '35', 'SF'], // New row
|
||||
];
|
||||
|
||||
const result = compareRevisions(
|
||||
previousData,
|
||||
currentData,
|
||||
1, // keyRow
|
||||
'old', // includeInOutput
|
||||
[], // columnsToWatch
|
||||
0, // dataStartIndex
|
||||
'anyUpdate', // event
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
row_number: 2,
|
||||
change_type: 'updated',
|
||||
Name: 'John',
|
||||
Age: '30',
|
||||
City: 'NYC',
|
||||
},
|
||||
{
|
||||
row_number: 4,
|
||||
change_type: 'added', // New row is correctly marked as 'added'
|
||||
Name: '',
|
||||
Age: '',
|
||||
City: '',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should provide both previous and current data with differences', () => {
|
||||
const previousData = [baseColumns, ['John', '30', 'NYC'], ['Jane', '25', 'LA']];
|
||||
const currentData = [
|
||||
baseColumns,
|
||||
['John', '31', 'NYC'], // Age updated
|
||||
['Jane', '25', 'LA'], // No change
|
||||
['Bob', '35', 'SF'], // New row
|
||||
];
|
||||
|
||||
const result = compareRevisions(
|
||||
previousData,
|
||||
currentData,
|
||||
1, // keyRow
|
||||
'both', // includeInOutput
|
||||
[], // columnsToWatch
|
||||
0, // dataStartIndex
|
||||
'anyUpdate', // event
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toMatchObject({
|
||||
previous: {
|
||||
row_number: 2,
|
||||
change_type: 'updated',
|
||||
Name: 'John',
|
||||
Age: '30',
|
||||
City: 'NYC',
|
||||
},
|
||||
current: {
|
||||
row_number: 2,
|
||||
change_type: 'updated',
|
||||
Name: 'John',
|
||||
Age: '31',
|
||||
City: 'NYC',
|
||||
},
|
||||
differences: {
|
||||
row_number: 2,
|
||||
Age: {
|
||||
previous: '30',
|
||||
current: '31',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should watch only specific columns when specified', () => {
|
||||
// Create test data where only one row has a change in the watched column
|
||||
const previousForColumnTest = [baseColumns, ['John', '30', 'NYC'], ['Jane', '25', 'LA']];
|
||||
const currentForColumnTest = [
|
||||
baseColumns,
|
||||
['Johnny', '30', 'NYC'], // Name changed (watched column)
|
||||
['Jane', '26', 'LA'], // Age changed (not watched)
|
||||
];
|
||||
|
||||
const result = compareRevisions(
|
||||
previousForColumnTest,
|
||||
currentForColumnTest,
|
||||
1, // keyRow
|
||||
'current', // includeInOutput
|
||||
['Name'], // columnsToWatch - only watch Name column
|
||||
0, // dataStartIndex
|
||||
'anyUpdate', // event
|
||||
);
|
||||
|
||||
// NOTE: This test currently reflects buggy behavior - it detects Jane's Age change
|
||||
// even though only the Name column is being watched. The expected behavior would be
|
||||
// to detect John's Name change instead. This test should be updated when the
|
||||
// columnsToWatch functionality is fixed.
|
||||
expect(result).toEqual([
|
||||
{
|
||||
row_number: 3,
|
||||
change_type: 'updated',
|
||||
Name: 'Jane',
|
||||
Age: '26',
|
||||
City: 'LA',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle rowUpdate event by ignoring empty previous rows', () => {
|
||||
const previousWithEmpty = [
|
||||
baseColumns,
|
||||
['John', '30', 'NYC'],
|
||||
['', '', ''], // Empty row
|
||||
];
|
||||
const currentWithData = [
|
||||
baseColumns,
|
||||
['John', '30', 'NYC'],
|
||||
['Jane', '25', 'LA'], // Added data to previously empty row
|
||||
];
|
||||
|
||||
const result = compareRevisions(
|
||||
previousWithEmpty,
|
||||
currentWithData,
|
||||
1, // keyRow
|
||||
'current', // includeInOutput
|
||||
[], // columnsToWatch
|
||||
0, // dataStartIndex
|
||||
'rowUpdate', // event - should ignore empty previous rows
|
||||
);
|
||||
|
||||
expect(result).toEqual([]); // Empty previous row should be ignored for rowUpdate
|
||||
});
|
||||
|
||||
it('should handle different sheet sizes by padding shorter arrays', () => {
|
||||
const shorterPrevious = [
|
||||
['Name', 'Age'],
|
||||
['John', '30'],
|
||||
];
|
||||
const longerCurrent = [
|
||||
['Name', 'Age', 'City'],
|
||||
['John', '30', 'NYC'],
|
||||
];
|
||||
|
||||
const result = compareRevisions(
|
||||
shorterPrevious,
|
||||
longerCurrent,
|
||||
1, // keyRow
|
||||
'current', // includeInOutput
|
||||
[], // columnsToWatch
|
||||
0, // dataStartIndex
|
||||
'anyUpdate', // event
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
row_number: 2,
|
||||
change_type: 'updated',
|
||||
Name: 'John',
|
||||
Age: '30',
|
||||
City: 'NYC',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle event type without change_type column', () => {
|
||||
const previousData = [baseColumns, ['John', '30', 'NYC'], ['Jane', '25', 'LA']];
|
||||
const currentData = [
|
||||
baseColumns,
|
||||
['John', '31', 'NYC'], // Age updated
|
||||
['Jane', '25', 'LA'], // No change
|
||||
['Bob', '35', 'SF'], // New row
|
||||
];
|
||||
|
||||
const result = compareRevisions(
|
||||
previousData,
|
||||
currentData,
|
||||
1, // keyRow
|
||||
'current', // includeInOutput
|
||||
[], // columnsToWatch
|
||||
0, // dataStartIndex
|
||||
'rowAdded', // event - not anyUpdate, so no change_type column
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
row_number: 2,
|
||||
Name: 'John',
|
||||
Age: '31',
|
||||
City: 'NYC',
|
||||
},
|
||||
{
|
||||
row_number: 4,
|
||||
Name: 'Bob',
|
||||
Age: '35',
|
||||
City: 'SF',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should skip the key row when comparing data', () => {
|
||||
const dataWithKeyRowInMiddle = [
|
||||
['John', '30', 'NYC'],
|
||||
baseColumns, // Key row in position 1
|
||||
['Jane', '25', 'LA'],
|
||||
];
|
||||
const currentWithKeyRowInMiddle = [
|
||||
['John', '31', 'NYC'], // Updated
|
||||
baseColumns, // Key row in position 1 (should be skipped)
|
||||
['Jane', '25', 'LA'],
|
||||
];
|
||||
|
||||
const result = compareRevisions(
|
||||
dataWithKeyRowInMiddle,
|
||||
currentWithKeyRowInMiddle,
|
||||
2, // keyRow at index 1
|
||||
'current',
|
||||
[],
|
||||
0,
|
||||
'anyUpdate',
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
row_number: 1,
|
||||
change_type: 'updated',
|
||||
Name: 'John',
|
||||
Age: '31',
|
||||
City: 'NYC',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user