From c0f1867429a64a3a27ae585084270d220972e673 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Jul 2025 09:43:46 +0100 Subject: [PATCH] feat(Facebook Graph API Node): Add support for api v23 (#17240) --- .../nodes/Facebook/FacebookGraphApi.node.ts | 4 + .../nodes/Facebook/GenericFunctions.ts | 6 +- .../__tests__/GenericFunctions.test.ts | 152 ++++++++++++++++++ 3 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 packages/nodes-base/nodes/Facebook/__tests__/GenericFunctions.test.ts diff --git a/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts b/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts index ccd9053df9..a84801f2a1 100644 --- a/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts +++ b/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts @@ -81,6 +81,10 @@ export class FacebookGraphApi implements INodeType { name: 'Default', value: '', }, + { + name: 'v23.0', + value: 'v23.0', + }, { name: 'v22.0', value: 'v22.0', diff --git a/packages/nodes-base/nodes/Facebook/GenericFunctions.ts b/packages/nodes-base/nodes/Facebook/GenericFunctions.ts index f1a4e72359..9698e6b86d 100644 --- a/packages/nodes-base/nodes/Facebook/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Facebook/GenericFunctions.ts @@ -39,7 +39,7 @@ export async function facebookApiRequest( qs, body, gzip: true, - uri: uri || `https://graph.facebook.com/v8.0${resource}`, + uri: uri || `https://graph.facebook.com/v23.0${resource}`, json: true, }; @@ -63,7 +63,7 @@ export function getFields(object: string) { page: [ { value: 'affiliation', - description: "Describes changes to a page's Affliation profile field", + description: "Describes changes to a page's affiliation profile field", }, { value: 'attire', @@ -125,7 +125,7 @@ export function getFields(object: string) { }, { value: 'hometown', - description: "Describes changes to a page's Homewtown profile field", + description: "Describes changes to a page's Hometown profile field", }, { value: 'hours', diff --git a/packages/nodes-base/nodes/Facebook/__tests__/GenericFunctions.test.ts b/packages/nodes-base/nodes/Facebook/__tests__/GenericFunctions.test.ts new file mode 100644 index 0000000000..7ea9db4c7c --- /dev/null +++ b/packages/nodes-base/nodes/Facebook/__tests__/GenericFunctions.test.ts @@ -0,0 +1,152 @@ +import * as utils from '../GenericFunctions'; + +jest.mock('n8n-workflow', () => { + const original = jest.requireActual('n8n-workflow'); + return { + ...original, + NodeApiError: jest.fn().mockImplementation(function ( + this: { node: unknown; error: unknown }, + node: unknown, + error: unknown, + ): never { + const err = new Error('Mock NodeApiError'); + (err as any).node = node; + (err as any).error = error; + return Promise.reject(err) as never; + }), + }; +}); + +describe('Facebook GenericFunctions', () => { + let mockExecuteFunctions: any; + + beforeEach(() => { + jest.clearAllMocks(); + + mockExecuteFunctions = { + getNode: jest.fn().mockReturnValue({ + name: 'Facebook', + typeVersion: 1, + }), + getCredentials: jest.fn().mockImplementation(async (type) => { + if (type === 'facebookGraphApi') { + return { accessToken: 'test-access-token' }; + } else if (type === 'facebookGraphAppApi') { + return { accessToken: 'test-app-access-token' }; + } + }), + helpers: { + request: jest.fn(), + }, + }; + }); + + describe('facebookApiRequest', () => { + it('should use correct credentials for regular nodes', async () => { + mockExecuteFunctions.helpers.request.mockResolvedValue({ success: true }); + + await utils.facebookApiRequest.call( + mockExecuteFunctions, + 'GET', + '/me', + {}, + { fields: 'id,name' }, + ); + + expect(mockExecuteFunctions.getCredentials).toHaveBeenCalledWith('facebookGraphApi'); + expect(mockExecuteFunctions.helpers.request).toHaveBeenCalledWith({ + headers: { accept: 'application/json,text/*;q=0.99' }, + method: 'GET', + qs: { access_token: 'test-access-token', fields: 'id,name' }, + body: {}, + gzip: true, + uri: 'https://graph.facebook.com/v23.0/me', + json: true, + }); + }); + + it('should use app credentials for trigger nodes', async () => { + mockExecuteFunctions.getNode.mockReturnValue({ name: 'Facebook Trigger' }); + mockExecuteFunctions.helpers.request.mockResolvedValue({ success: true }); + + await utils.facebookApiRequest.call(mockExecuteFunctions, 'GET', '/app', {}, {}); + + expect(mockExecuteFunctions.getCredentials).toHaveBeenCalledWith('facebookGraphAppApi'); + expect(mockExecuteFunctions.helpers.request).toHaveBeenCalledWith( + expect.objectContaining({ + qs: { access_token: 'test-app-access-token' }, + }), + ); + }); + + it('should allow custom URI', async () => { + mockExecuteFunctions.helpers.request.mockResolvedValue({ success: true }); + const customUri = 'https://graph.facebook.com/v23.0/me/feed'; + + await utils.facebookApiRequest.call( + mockExecuteFunctions, + 'POST', + '', + { message: 'Hello' }, + {}, + customUri, + ); + + expect(mockExecuteFunctions.helpers.request).toHaveBeenCalledWith( + expect.objectContaining({ uri: customUri }), + ); + }); + }); + + describe('getFields', () => { + it('should return all fields for a given object with * option first', () => { + const fields = utils.getFields('page'); + + expect(fields.length).toBeGreaterThan(0); + expect(fields[0]).toEqual({ name: '*', value: '*' }); + expect(fields.some((field) => field.name === 'Feed')).toBeTruthy(); + }); + + it('should return only * for unknown objects', () => { + const fields = utils.getFields('unknown-object'); + + expect(fields).toEqual([{ name: '*', value: '*' }]); + }); + + it('should format field names in capital case', () => { + const fields = utils.getFields('page'); + const findField = (name: string) => fields.find((field) => field.name === name); + + expect(findField('Feed')).toBeDefined(); + expect(findField('Email')).toBeDefined(); + expect(findField('Company Overview')).toBeDefined(); + }); + }); + + describe('getAllFields', () => { + it('should return all field values without *', () => { + const allFields = utils.getAllFields('page'); + + expect(allFields.length).toBeGreaterThan(0); + expect(allFields.includes('*')).toBeFalsy(); + expect(allFields.includes('feed')).toBeTruthy(); + expect(allFields.includes('email')).toBeTruthy(); + }); + + it('should return empty array for unknown objects', () => { + expect(utils.getAllFields('unknown-object')).toEqual([]); + }); + + it('should return all raw field values', () => { + const pageFields = utils.getAllFields('page'); + const instagramFields = utils.getAllFields('instagram'); + + expect(pageFields).toContain('feed'); + expect(pageFields).toContain('email'); + expect(pageFields).toContain('website'); + + expect(instagramFields).toContain('comments'); + expect(instagramFields).toContain('mentions'); + }); + }); +});