From 58e55b58985fccb2b37640edcdc1e84d45753f5e Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Fri, 25 Sep 2026 06:56:53 +0000 Subject: [PATCH] fix(Xero Node): Send tenant header on every page when returning all items (#39431) --- .../nodes-base/nodes/Xero/GenericFunctions.ts | 13 ++--- .../nodes/Xero/test/Xero.node.test.ts | 51 ++++++++++++++++++- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/packages/nodes-base/nodes/Xero/GenericFunctions.ts b/packages/nodes-base/nodes/Xero/GenericFunctions.ts index c1d7d372286..5d12e6cba47 100644 --- a/packages/nodes-base/nodes/Xero/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Xero/GenericFunctions.ts @@ -13,7 +13,7 @@ export async function xeroApiRequest( method: IHttpRequestMethods, resource: string, - body: any = {}, + { organizationId, ...body }: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}, @@ -21,6 +21,7 @@ export async function xeroApiRequest( const options: IRequestOptions = { headers: { 'Content-Type': 'application/json', + ...(organizationId && { 'Xero-tenant-id': organizationId }), }, method, body, @@ -29,10 +30,6 @@ export async function xeroApiRequest( json: true, }; try { - if (body.organizationId) { - options.headers = { ...options.headers, 'Xero-tenant-id': body.organizationId }; - delete body.organizationId; - } if (Object.keys(headers).length !== 0) { options.headers = Object.assign({}, options.headers, headers); } @@ -57,11 +54,11 @@ export async function xeroApiRequestAllItems( const returnData: IDataObject[] = []; let responseData; - query.page = 1; + let page = 1; do { - responseData = await xeroApiRequest.call(this, method, endpoint, body, query); - query.page++; + responseData = await xeroApiRequest.call(this, method, endpoint, body, { ...query, page }); + page++; returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]); } while (responseData[propertyName].length !== 0); diff --git a/packages/nodes-base/nodes/Xero/test/Xero.node.test.ts b/packages/nodes-base/nodes/Xero/test/Xero.node.test.ts index 73d44e3321f..49ebecb45da 100644 --- a/packages/nodes-base/nodes/Xero/test/Xero.node.test.ts +++ b/packages/nodes-base/nodes/Xero/test/Xero.node.test.ts @@ -1,5 +1,11 @@ import { mock, mockDeep } from 'vitest-mock-extended'; -import type { ILoadOptionsFunctions, INode } from 'n8n-workflow'; +import type { + IDataObject, + IExecuteFunctions, + ILoadOptionsFunctions, + INode, + NodeExecutionWithMetadata, +} from 'n8n-workflow'; import { Xero } from '../Xero.node'; @@ -76,4 +82,47 @@ describe('Xero Node', () => { }); }); }); + + describe.each([ + ['invoice', 'Invoices'], + ['contact', 'Contacts'], + ])('%s getAll with returnAll', (resource, propertyName) => { + it('should send the Xero-tenant-id header on every page', async () => { + const xero = new Xero(); + const executeFunctions = mockDeep(); + const params: IDataObject = { + resource, + operation: 'getAll', + organizationId: 'test-org-id', + returnAll: true, + options: {}, + }; + + executeFunctions.getInputData.mockReturnValue([{ json: {} }]); + executeFunctions.getNode.mockReturnValue(mock()); + executeFunctions.getNodeParameter.mockImplementation((name: string) => params[name]); + executeFunctions.helpers.constructExecutionMetaData.mockImplementation( + (data) => data as NodeExecutionWithMetadata[], + ); + executeFunctions.helpers.returnJsonArray.mockImplementation((data) => + (Array.isArray(data) ? data : [data]).map((json) => ({ json })), + ); + executeFunctions.helpers.requestOAuth2 + .mockResolvedValueOnce({ [propertyName]: [{ id: 1 }] }) + .mockResolvedValueOnce({ [propertyName]: [{ id: 2 }] }) + .mockResolvedValueOnce({ [propertyName]: [] }); + + const result = await xero.execute.call(executeFunctions); + + expect(result[0].map(({ json }) => json)).toEqual([{ id: 1 }, { id: 2 }]); + const calls = executeFunctions.helpers.requestOAuth2.mock.calls; + expect(calls).toHaveLength(3); + calls.forEach(([, options], index) => { + expect(options.headers).toEqual( + expect.objectContaining({ 'Xero-tenant-id': 'test-org-id' }), + ); + expect(options.qs).toEqual(expect.objectContaining({ page: index + 1 })); + }); + }); + }); });