From 54c8eab2e4784025cd49aedb91f038e243deeb64 Mon Sep 17 00:00:00 2001 From: Alexander Gekov <40495748+alexander-gekov@users.noreply.github.com> Date: Fri, 22 May 2026 09:20:57 +0300 Subject: [PATCH] fix(Facebook Graph API Node): Clarify endpoints that accept binary uploads (#30903) Co-authored-by: Claude Opus 4.7 --- .../nodes/Facebook/FacebookGraphApi.node.ts | 5 +- .../__tests__/FacebookGraphApi.node.test.ts | 109 ++++++++++++++++++ 2 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 packages/nodes-base/nodes/Facebook/__tests__/FacebookGraphApi.node.test.ts diff --git a/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts b/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts index 902f2e29c78..0d9b297306b 100644 --- a/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts +++ b/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts @@ -258,7 +258,8 @@ export class FacebookGraphApi implements INodeType { }, default: false, required: true, - description: 'Whether binary data should be sent as body', + hint: 'Page /photos and /videos edges accept binary uploads. Instagram container endpoints (e.g. /media) require image_url or video_url as Query Parameters instead.', + description: 'Whether to upload binary data as multipart/form-data', }, { displayName: 'Input Binary Field', @@ -276,7 +277,7 @@ export class FacebookGraphApi implements INodeType { }, hint: 'The name of the input binary field containing the file to be uploaded', description: - 'For Form-Data Multipart, they can be provided in the format: "sendKey1:binaryProperty1,sendKey2:binaryProperty2', + 'For Form-Data Multipart, multiple files can be provided in the format: sendKey1:binaryProperty1,sendKey2:binaryProperty2', }, { displayName: 'Options', diff --git a/packages/nodes-base/nodes/Facebook/__tests__/FacebookGraphApi.node.test.ts b/packages/nodes-base/nodes/Facebook/__tests__/FacebookGraphApi.node.test.ts new file mode 100644 index 00000000000..cb7967aa679 --- /dev/null +++ b/packages/nodes-base/nodes/Facebook/__tests__/FacebookGraphApi.node.test.ts @@ -0,0 +1,109 @@ +import type { MockProxy } from 'jest-mock-extended'; +import { mock } from 'jest-mock-extended'; +import type { IBinaryData, IExecuteFunctions } from 'n8n-workflow'; + +import { FacebookGraphApi } from '../FacebookGraphApi.node'; + +describe('FacebookGraphApi node — binary upload', () => { + let mockExecuteFunctions: MockProxy; + let node: FacebookGraphApi; + + const binaryDataBuffer = Buffer.from('fake-image-bytes'); + const binaryDescriptor: IBinaryData = { + data: 'base64data', + mimeType: 'image/png', + fileName: 'photo.png', + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockExecuteFunctions = mock(); + node = new FacebookGraphApi(); + + mockExecuteFunctions.getInputData.mockReturnValue([ + { json: {}, binary: { data: binaryDescriptor } }, + ]); + mockExecuteFunctions.getNode.mockReturnValue({ + id: 'test-node-id', + name: 'Facebook Graph API', + type: 'n8n-nodes-base.facebookGraphApi', + typeVersion: 1, + position: [0, 0], + parameters: {}, + }); + mockExecuteFunctions.getCredentials.mockResolvedValue({ accessToken: 'TOKEN' }); + mockExecuteFunctions.continueOnFail.mockReturnValue(false); + mockExecuteFunctions.helpers = { + request: jest.fn().mockResolvedValue({ id: 'photo-id' }), + requestWithAuthentication: jest.fn(), + assertBinaryData: jest.fn().mockReturnValue(binaryDescriptor), + getBinaryDataBuffer: jest.fn().mockResolvedValue(binaryDataBuffer), + } as any; + }); + + const setParams = (overrides: Record = {}) => { + const defaults: Record = { + authType: 'accessToken', + hostUrl: 'graph.facebook.com', + httpRequestMethod: 'POST', + graphApiVersion: 'v23.0', + node: '123456', + edge: 'photos', + options: {}, + sendBinaryData: true, + binaryPropertyName: 'data', + allowUnauthorizedCerts: false, + }; + const params = { ...defaults, ...overrides }; + mockExecuteFunctions.getNodeParameter.mockImplementation( + (name: string) => params[name] as never, + ); + }; + + it('builds a multipart/form-data request with the binary buffer when Send Binary File is enabled', async () => { + setParams(); + + await node.execute.call(mockExecuteFunctions); + + const requestMock = mockExecuteFunctions.helpers.request as jest.Mock; + expect(requestMock).toHaveBeenCalledTimes(1); + + const requestArg = requestMock.mock.calls[0][0]; + expect(requestArg.method).toBe('POST'); + expect(requestArg.uri).toBe('https://graph.facebook.com/v23.0/123456/photos'); + expect(requestArg.formData).toEqual({ + file: { + value: binaryDataBuffer, + options: { + filename: 'photo.png', + contentType: 'image/png', + }, + }, + }); + // Buffer must NOT be JSON-serialized into the body. + expect(requestArg.body).toBeUndefined(); + expect(requestArg.json).toBe(true); + }); + + it('respects the ":" syntax for the form field name', async () => { + setParams({ binaryPropertyName: 'source:data' }); + + await node.execute.call(mockExecuteFunctions); + + const requestArg = (mockExecuteFunctions.helpers.request as jest.Mock).mock.calls[0][0]; + expect(Object.keys(requestArg.formData)).toEqual(['source']); + expect(requestArg.formData.source.value).toBe(binaryDataBuffer); + expect(mockExecuteFunctions.helpers.getBinaryDataBuffer).toHaveBeenCalledWith(0, 'data'); + }); + + it('does not attach formData when Send Binary File is disabled', async () => { + setParams({ sendBinaryData: false }); + + await node.execute.call(mockExecuteFunctions); + + const requestArg = (mockExecuteFunctions.helpers.request as jest.Mock).mock.calls[0][0]; + expect(requestArg.formData).toBeUndefined(); + expect(mockExecuteFunctions.helpers.assertBinaryData).not.toHaveBeenCalled(); + expect(mockExecuteFunctions.helpers.getBinaryDataBuffer).not.toHaveBeenCalled(); + }); +});