From 9b9247ef916c528524ab569ad456ed35e4ffc292 Mon Sep 17 00:00:00 2001 From: Mwale Kalenga Date: Tue, 30 Dec 2025 12:15:39 -0500 Subject: [PATCH 1/5] Fix OAuth signature generation in validateRestApiAccess The validateRestApiAccess function was calling getAuthHeaders() without the required URL parameter for OAuth 1.0a signature generation. This caused authentication failures when using HTTP connections (which fall back to OAuth instead of Basic Auth). Changes: - Get baseURL from httpClient.defaults.baseURL - Construct full URL for each endpoint - Pass proper parameters (method, url, params) to getAuthHeaders() This fixes the "REST API validation failed: undefined" error when connecting via HTTP/OAuth. --- src/config/auth.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/config/auth.js b/src/config/auth.js index 1bc236d..95d50a3 100644 --- a/src/config/auth.js +++ b/src/config/auth.js @@ -317,10 +317,15 @@ export async function validateRestApiAccess(httpClient, authManager) { { path: '/feeds', name: 'Feeds' } ]; + // Get baseURL from httpClient for OAuth signature generation + const baseURL = httpClient.defaults.baseURL; + const results = []; for (const endpoint of endpoints) { try { - const headers = authManager.getAuthHeaders(); + // Generate proper OAuth headers with full URL for signature + const fullUrl = `${baseURL}${endpoint.path}`; + const headers = authManager.getAuthHeaders('GET', fullUrl, { per_page: 1 }); await httpClient.get(endpoint.path, { headers, params: { per_page: 1 } From 75dccf13a3efea1118ea6482601ba52045217465 Mon Sep 17 00:00:00 2001 From: Mwale Kalenga Date: Tue, 30 Dec 2025 12:56:44 -0500 Subject: [PATCH 2/5] Add defensive check for httpClient baseURL --- src/config/auth.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/config/auth.js b/src/config/auth.js index 95d50a3..3bf35b8 100644 --- a/src/config/auth.js +++ b/src/config/auth.js @@ -320,6 +320,10 @@ export async function validateRestApiAccess(httpClient, authManager) { // Get baseURL from httpClient for OAuth signature generation const baseURL = httpClient.defaults.baseURL; + if (!baseURL) { + throw new Error('httpClient baseURL is not configured'); + } + const results = []; for (const endpoint of endpoints) { try { From 8911c9f4972662eab62b7b2d5168b1d781f35b3b Mon Sep 17 00:00:00 2001 From: Mwale Kalenga Date: Tue, 30 Dec 2025 13:52:26 -0500 Subject: [PATCH 3/5] Add defaults.baseURL to MockHttpClient for test compatibility --- src/tests/helpers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/helpers.js b/src/tests/helpers.js index 5465874..609fba2 100644 --- a/src/tests/helpers.js +++ b/src/tests/helpers.js @@ -179,6 +179,7 @@ export class MockHttpClient { this.requests = []; this.responses = new Map(); this.defaultResponse = new MockResponse(); + this.defaults = { baseURL: 'https://test.example.com' }; } /** From 8fed60477ffd2688880de1074989a68104e1b45f Mon Sep 17 00:00:00 2001 From: Mwale Kalenga Date: Wed, 18 Feb 2026 06:58:32 +0200 Subject: [PATCH 4/5] Fix confirmations and notifications validation to accept objects The Gravity Forms REST API v2 returns confirmations and notifications as objects keyed by ID, not arrays. The validation was incorrectly using validateArray()/`.array()` which rejected the correct format. Changed both validators to use validateObject()/`.object()` and added missing notifications validation. --- src/config/validation.js | 11 +++++++---- src/config/validators.js | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/config/validation.js b/src/config/validation.js index 88bd898..dc75e1f 100644 --- a/src/config/validation.js +++ b/src/config/validation.js @@ -309,15 +309,18 @@ export class FormsValidator extends BaseValidator { } if (formData.confirmations !== undefined) { - validated.confirmations = this.validateArray(formData.confirmations, 'confirmations'); - validated.confirmations = validated.confirmations.map((conf, index) => { + validated.confirmations = this.validateObject(formData.confirmations, 'confirmations'); + Object.entries(validated.confirmations).forEach(([key, conf]) => { if (conf.type === 'redirect' && conf.url !== undefined) { - conf.url = this.validateURL(conf.url, `confirmations[${index}].url`); + conf.url = this.validateURL(conf.url, `confirmations.${key}.url`); } - return conf; }); } + if (formData.notifications !== undefined) { + validated.notifications = this.validateObject(formData.notifications, 'notifications'); + } + if (formData.schedule_start !== undefined) { validated.schedule_start = this.validateDate(formData.schedule_start, 'schedule_start'); } diff --git a/src/config/validators.js b/src/config/validators.js index fd4e63b..6fbd0bc 100644 --- a/src/config/validators.js +++ b/src/config/validators.js @@ -88,12 +88,12 @@ export class FormsValidator { .array() ) .field('confirmations', validate('confirmations') - .array() + .object() .custom((confirmations) => { - if (Array.isArray(confirmations)) { - confirmations.forEach((conf, index) => { + if (confirmations && typeof confirmations === 'object' && !Array.isArray(confirmations)) { + Object.entries(confirmations).forEach(([key, conf]) => { if (conf.type === 'redirect' && conf.url !== undefined) { - validate(`confirmations[${index}].url`) + validate(`confirmations.${key}.url`) .required() .string() .url() @@ -104,6 +104,9 @@ export class FormsValidator { return true; }) ) + .field('notifications', validate('notifications') + .object() + ) .field('schedule_start', validate('schedule_start') .string() .date() From 5bb0e78b447c2eb265d9a12a1217735a36cd5485 Mon Sep 17 00:00:00 2001 From: Mwale Kalenga Date: Wed, 18 Feb 2026 07:10:10 +0200 Subject: [PATCH 5/5] Address code review feedback - Use optional chaining for httpClient.defaults.baseURL access to prevent TypeError when defaults is undefined (auth.js) - Remove redundant Array.isArray guard in validators.js since .object() already rejects arrays - Convert confirmations from array to keyed-object format in tests and setup scripts to match Gravity Forms API v2 structure --- scripts/setup-test-data.js | 44 +++++++++++++++++++++-------------- src/config/auth.js | 2 +- src/config/validators.js | 2 +- src/tests/integration.test.js | 10 ++++---- src/tests/validation.test.js | 20 +++++++++------- 5 files changed, 46 insertions(+), 32 deletions(-) diff --git a/scripts/setup-test-data.js b/scripts/setup-test-data.js index 08fed04..fc9308c 100644 --- a/scripts/setup-test-data.js +++ b/scripts/setup-test-data.js @@ -138,16 +138,20 @@ const testForms = [ button: { text: 'Send Message' }, - confirmations: [{ - type: 'message', - message: '

Thank you for contacting us! We\'ll respond within 24 hours.

' - }], - notifications: [{ - name: 'Admin Notification', - to: '{admin_email}', - subject: 'New Contact Form Submission', - message: 'You have a new contact form submission from {Name:1}.' - }] + confirmations: { + conf_1: { + type: 'message', + message: '

Thank you for contacting us! We\'ll respond within 24 hours.

' + } + }, + notifications: { + notif_1: { + name: 'Admin Notification', + to: '{admin_email}', + subject: 'New Contact Form Submission', + message: 'You have a new contact form submission from {Name:1}.' + } + } }, { title: 'Newsletter Signup (Test)', @@ -193,10 +197,12 @@ const testForms = [ button: { text: 'Subscribe' }, - confirmations: [{ - type: 'message', - message: '

Success! Please check your email to confirm your subscription.

' - }] + confirmations: { + conf_1: { + type: 'message', + message: '

Success! Please check your email to confirm your subscription.

' + } + } }, { title: 'Multi-Page Survey (Test)', @@ -263,10 +269,12 @@ const testForms = [ button: { text: 'Submit Survey' }, - confirmations: [{ - type: 'message', - message: '

Thank you for your feedback!

' - }] + confirmations: { + conf_1: { + type: 'message', + message: '

Thank you for your feedback!

' + } + } } ]; diff --git a/src/config/auth.js b/src/config/auth.js index 3bf35b8..8050e8c 100644 --- a/src/config/auth.js +++ b/src/config/auth.js @@ -318,7 +318,7 @@ export async function validateRestApiAccess(httpClient, authManager) { ]; // Get baseURL from httpClient for OAuth signature generation - const baseURL = httpClient.defaults.baseURL; + const baseURL = httpClient?.defaults?.baseURL; if (!baseURL) { throw new Error('httpClient baseURL is not configured'); diff --git a/src/config/validators.js b/src/config/validators.js index 6fbd0bc..af93e0e 100644 --- a/src/config/validators.js +++ b/src/config/validators.js @@ -90,7 +90,7 @@ export class FormsValidator { .field('confirmations', validate('confirmations') .object() .custom((confirmations) => { - if (confirmations && typeof confirmations === 'object' && !Array.isArray(confirmations)) { + if (confirmations && typeof confirmations === 'object') { Object.entries(confirmations).forEach(([key, conf]) => { if (conf.type === 'redirect' && conf.url !== undefined) { validate(`confirmations.${key}.url`) diff --git a/src/tests/integration.test.js b/src/tests/integration.test.js index 811a356..c841b4c 100644 --- a/src/tests/integration.test.js +++ b/src/tests/integration.test.js @@ -154,10 +154,12 @@ suite.test('Integration: Create test form', async () => { button: { text: 'Submit Test' }, - confirmations: [{ - type: 'message', - message: 'Thank you for your test submission!' - }] + confirmations: { + conf_1: { + type: 'message', + message: 'Thank you for your test submission!' + } + } }; const result = await client.createForm(formData); diff --git a/src/tests/validation.test.js b/src/tests/validation.test.js index ade85f0..2fb7d8e 100644 --- a/src/tests/validation.test.js +++ b/src/tests/validation.test.js @@ -411,10 +411,12 @@ suite.test('Format Validation: URLs', async () => { await TestAssert.throwsAsync( () => client.createForm({ title: 'Test', - confirmations: [{ - type: 'redirect', - url: 'not-a-url' - }] + confirmations: { + conf_1: { + type: 'redirect', + url: 'not-a-url' + } + } }), 'valid URL', 'Should validate URL format' @@ -427,10 +429,12 @@ suite.test('Format Validation: URLs', async () => { await client.createForm({ title: 'Test', - confirmations: [{ - type: 'redirect', - url: 'https://example.com' - }] + confirmations: { + conf_1: { + type: 'redirect', + url: 'https://example.com' + } + } }); });