/**
* Forms Endpoint Tests for Gravity MCP
* Tests all 6 forms management tools with happy path, edge cases, and failure modes
*/
import GravityFormsClient from '../gravity-forms-client.js';
import {
TestRunner,
TestAssert,
MockHttpClient,
MockResponse,
setupTestEnvironment,
generateMockForm,
generateId,
generateString
} from './helpers.js';
const suite = new TestRunner('Forms Endpoint Tests');
let client;
let mockHttpClient;
let testEnv;
suite.beforeEach(() => {
testEnv = setupTestEnvironment();
mockHttpClient = new MockHttpClient();
// Create client with mocked HTTP client
client = new GravityFormsClient(testEnv);
client.httpClient = mockHttpClient;
client.allowDelete = true; // Enable delete for testing
// Mock successful initialization
mockHttpClient.setMockResponse('GET', '/forms', new MockResponse({ forms: [] }));
});
// =================================
// LIST FORMS TESTS
// =================================
suite.test('List Forms: Should list all forms as object keyed by ID', async () => {
// The /forms endpoint returns all forms as an object keyed by form ID
const mockFormsResponse = {
"1": { id: "1", title: "Form 1", entries: "10" },
"2": { id: "2", title: "Form 2", entries: "5" }
};
mockHttpClient.setMockResponse('GET', '/forms', new MockResponse(mockFormsResponse));
const result = await client.listForms();
TestAssert.equal(typeof result.forms, 'object');
TestAssert.equal(result.forms["1"].id, "1");
TestAssert.equal(result.forms["2"].title, "Form 2");
});
suite.test('List Forms: Should handle empty results', async () => {
// When no forms exist, returns empty object
mockHttpClient.setMockResponse('GET', '/forms', new MockResponse({}));
const result = await client.listForms();
TestAssert.equal(typeof result.forms, 'object');
TestAssert.equal(Object.keys(result.forms).length, 0);
});
suite.test('List Forms: Should support include parameter for specific forms', async () => {
// When using 'include' parameter, returns full form details for specified IDs
const mockForm = generateMockForm({ id: 5, title: 'Included Form' });
const mockResponse = {
"5": mockForm
};
mockHttpClient.setMockResponse('GET', '/forms', new MockResponse(mockResponse));
const result = await client.listForms({ include: [5] });
TestAssert.equal(result.forms["5"].id, 5);
TestAssert.equal(result.forms["5"].title, 'Included Form');
TestAssert.isNotNull(result.forms["5"].fields);
});
// =================================
// GET FORM TESTS
// =================================
suite.test('Get Form: Should get specific form by ID', async () => {
const mockForm = generateMockForm({ id: 123 });
mockHttpClient.setMockResponse('GET', '/forms/123', new MockResponse(mockForm));
const result = await client.getForm({ id: 123 });
TestAssert.equal(result.form.id, 123);
TestAssert.equal(result.form.title, mockForm.title);
TestAssert.equal(result.field_count, 3);
TestAssert.isTrue(result.is_active);
});
suite.test('Get Form: Should handle form with no fields', async () => {
const emptyForm = generateMockForm({ id: 1, fields: [] });
mockHttpClient.setMockResponse('GET', '/forms/1', new MockResponse(emptyForm));
const result = await client.getForm({ id: 1 });
TestAssert.equal(result.field_count, 0);
});
suite.test('Get Form: Should handle large forms (100+ fields)', async () => {
const fields = Array.from({ length: 150 }, (_, i) => ({
id: i + 1,
type: 'text',
label: `Field ${i + 1}`
}));
const largeForm = generateMockForm({ id: 1, fields });
mockHttpClient.setMockResponse('GET', '/forms/1', new MockResponse(largeForm));
const result = await client.getForm({ id: 1 });
TestAssert.equal(result.field_count, 150);
});
suite.test('Get Form: Should handle non-existent form (404)', async () => {
mockHttpClient.setMockResponse('GET', '/forms/999', new MockResponse(
{ message: 'Form not found' },
404
));
await TestAssert.throwsAsync(
() => client.getForm({ id: 999 }),
'not found',
'Should handle 404 error'
);
});
suite.test('Get Form: Should validate form ID', async () => {
await TestAssert.throwsAsync(
() => client.getForm({ id: 'invalid' }),
'must be a positive integer',
'Should validate ID format'
);
});
// =================================
// CREATE FORM TESTS
// =================================
suite.test('Create Form: Should create new form with fields', async () => {
const newForm = generateMockForm({ id: 5 });
mockHttpClient.setMockResponse('POST', '/forms', new MockResponse(newForm));
const result = await client.createForm({
title: 'New Test Form',
description: 'Test description',
fields: newForm.fields
});
TestAssert.isTrue(result.created);
TestAssert.equal(result.form.id, 5);
TestAssert.equal(result.message, 'Form created successfully');
});
suite.test('Create Form: Should require title', async () => {
await TestAssert.throwsAsync(
() => client.createForm({ description: 'No title' }),
'title is required',
'Should require form title'
);
});
suite.test('Create Form: Should create form with complex conditional logic', async () => {
const complexForm = {
title: 'Complex Form',
fields: [
{
id: 1,
type: 'radio',
label: 'Choice',
choices: [
{ text: 'Option A', value: 'a' },
{ text: 'Option B', value: 'b' }
]
},
{
id: 2,
type: 'text',
label: 'Conditional Field',
conditionalLogic: {
actionType: 'show',
logicType: 'all',
rules: [{
fieldId: 1,
operator: 'is',
value: 'a'
}]
}
}
]
};
mockHttpClient.setMockResponse('POST', '/forms', new MockResponse({
...complexForm,
id: 10
}));
const result = await client.createForm(complexForm);
TestAssert.isTrue(result.created);
TestAssert.equal(result.form.fields[1].conditionalLogic.rules[0].fieldId, 1);
});
suite.test('Create Form: Should handle multi-page forms', async () => {
const multiPageForm = {
title: 'Multi-Page Form',
fields: [
{ id: 1, type: 'text', label: 'Page 1 Field' },
{ id: 2, type: 'page', label: 'Page Break' },
{ id: 3, type: 'text', label: 'Page 2 Field' }
]
};
mockHttpClient.setMockResponse('POST', '/forms', new MockResponse({
...multiPageForm,
id: 20
}));
const result = await client.createForm(multiPageForm);
TestAssert.isTrue(result.created);
TestAssert.equal(result.form.fields.length, 3);
});
suite.test('Create Form: Should handle unicode and special characters', async () => {
const unicodeForm = {
title: 'フォーム 测试 🚀',
description: 'Special chars: <>&"\'',
fields: [
{ id: 1, type: 'text', label: '名前' }
]
};
mockHttpClient.setMockResponse('POST', '/forms', new MockResponse({
...unicodeForm,
id: 30
}));
const result = await client.createForm(unicodeForm);
TestAssert.isTrue(result.created);
TestAssert.equal(result.form.title, unicodeForm.title);
});
suite.test('Create Form: Should handle unknown field type gracefully', async () => {
const invalidForm = {
title: 'Invalid Form',
fields: [
{ id: 1, type: 'custom_field_type', label: 'Custom Field' }
]
};
// Mock should include the _unknown flag that would be added by field validation
mockHttpClient.setMockResponse('POST', '/forms', new MockResponse({
...invalidForm,
id: 40,
fields: [
{ id: 1, type: 'custom_field_type', label: 'Custom Field', _unknown: true }
]
}));
const result = await client.createForm(invalidForm);
TestAssert.isTrue(result.created);
TestAssert.equal(result.form.fields[0]._unknown, true);
});
// =================================
// UPDATE FORM TESTS
// =================================
suite.test('Update Form: Should update existing form', async () => {
// First mock the GET request to fetch existing form
const existingForm = generateMockForm({
id: 1,
title: 'Original Title',
description: 'Original Description',
fields: [
{ id: 1, type: 'text', label: 'Name' },
{ id: 2, type: 'email', label: 'Email' }
],
is_active: true
});
mockHttpClient.setMockResponse('GET', '/forms/1', new MockResponse(existingForm));
// Then mock the PUT request with merged data
const updatedForm = generateMockForm({
id: 1,
title: 'Updated Title',
description: 'Original Description', // Preserved
fields: existingForm.fields, // Preserved
is_active: true // Preserved
});
mockHttpClient.setMockResponse('PUT', '/forms/1', new MockResponse(updatedForm));
const result = await client.updateForm({
id: 1,
title: 'Updated Title'
});
TestAssert.isTrue(result.updated);
TestAssert.equal(result.form.title, 'Updated Title');
TestAssert.equal(result.message, 'Form updated successfully');
});
suite.test('Update Form: Should preserve all form data when updating single property', async () => {
// Mock a complete form with all properties
const existingForm = {
id: 3,
title: 'Third Grade Student Registration',
description: 'Please complete this form to register your child for third grade.',
is_active: true,
fields: [
{
type: 'name',
id: 1,
label: 'Student Name',
isRequired: true
},
{
type: 'email',
id: 2,
label: 'Parent Email Address',
isRequired: true
}
],
button: {
type: 'text',
text: 'Submit Registration'
},
notifications: {
'5f7c31b2e5a23': {
id: '5f7c31b2e5a23',
name: 'Admin Notification',
to: '{admin_email}'
}
},
confirmations: {
'5f7c31b2e5a24': {
id: '5f7c31b2e5a24',
name: 'Default Confirmation',
message: 'Thank you for registering'
}
}
};
mockHttpClient.setMockResponse('GET', '/forms/3', new MockResponse(existingForm));
// Expected merged data (all properties preserved, only is_active updated)
const expectedMergedData = {
...existingForm,
is_active: false
};
mockHttpClient.setMockResponse('PUT', '/forms/3', new MockResponse(expectedMergedData));
// Update only the is_active property
const result = await client.updateForm({
id: 3,
is_active: false
});
// Verify the PUT request was made with ALL data
const putRequest = mockHttpClient.getRequests().find(r => r.method === 'PUT');
TestAssert.exists(putRequest, 'PUT request should be made');
TestAssert.equal(putRequest.config.data.title, 'Third Grade Student Registration', 'Title should be preserved');
TestAssert.equal(putRequest.config.data.description, existingForm.description, 'Description should be preserved');
TestAssert.lengthOf(putRequest.config.data.fields, 2, 'All fields should be preserved');
TestAssert.exists(putRequest.config.data.button, 'Button settings should be preserved');
TestAssert.exists(putRequest.config.data.notifications, 'Notifications should be preserved');
TestAssert.exists(putRequest.config.data.confirmations, 'Confirmations should be preserved');
TestAssert.equal(putRequest.config.data.is_active, false, 'is_active should be updated');
TestAssert.isTrue(result.updated);
TestAssert.equal(result.form.is_active, false, 'Updated property changed');
});
suite.test('Update Form: Should validate form ID is required', async () => {
await TestAssert.throwsAsync(
() => client.updateForm({ title: 'No ID' }),
'id',
'Should require form ID'
);
});
suite.test('Update Form: Should handle permission errors (403)', async () => {
mockHttpClient.setMockResponse('PUT', '/forms/1', new MockResponse(
{ message: 'Insufficient permissions' },
403
));
await TestAssert.throwsAsync(
() => client.updateForm({ id: 1, title: 'Test' }),
'forbidden',
'Should handle permission errors'
);
});
// =================================
// DELETE FORM TESTS
// =================================
suite.test('Delete Form: Should trash form by default', async () => {
mockHttpClient.setMockResponse('DELETE', '/forms/1', new MockResponse({}));
const result = await client.deleteForm({ id: 1 });
TestAssert.isTrue(result.deleted);
TestAssert.isFalse(result.permanently);
TestAssert.equal(result.message, 'Form moved to trash');
});
suite.test('Delete Form: Should permanently delete with force=true', async () => {
mockHttpClient.setMockResponse('DELETE', '/forms/1', new MockResponse({}));
const result = await client.deleteForm({ id: 1, force: true });
TestAssert.isTrue(result.deleted);
TestAssert.isTrue(result.permanently);
TestAssert.equal(result.message, 'Form permanently deleted');
});
suite.test('Delete Form: Should require ALLOW_DELETE=true', async () => {
client.allowDelete = false;
await TestAssert.throwsAsync(
() => client.deleteForm({ id: 1 }),
'Delete operations are disabled',
'Should check delete permission'
);
});
suite.test('Delete Form: Should validate form ID', async () => {
await TestAssert.throwsAsync(
() => client.deleteForm({ id: -1 }),
'positive integer',
'Should validate form ID'
);
});
// =================================
// VALIDATE FORM TESTS
// =================================
suite.test('Validate Form: Should validate form submission data', async () => {
mockHttpClient.setMockResponse('POST', '/forms/1/submissions', new MockResponse({
is_valid: true,
validation_messages: {}
}));
const result = await client.validateForm({
form_id: 1,
input_1: 'John Doe',
input_2: 'john@example.com'
});
TestAssert.isTrue(result.valid);
TestAssert.equal(result.message, 'Form data is valid');
});
suite.test('Validate Form: Should return validation errors', async () => {
mockHttpClient.setMockResponse('POST', '/forms/1/submissions', new MockResponse({
is_valid: false,
validation_messages: {
'2': 'Email is required',
'3': 'Message must be at least 10 characters'
}
}));
const result = await client.validateForm({
form_id: 1,
input_1: 'John'
});
TestAssert.isFalse(result.valid);
TestAssert.equal(result.validation_messages['2'], 'Email is required');
TestAssert.equal(result.message, 'Validation errors found');
});
suite.test('Validate Form: Should require form_id', async () => {
await TestAssert.throwsAsync(
() => client.validateForm({ input_1: 'Test' }),
'form_id is required',
'Should require form_id'
);
});
// =================================
// EDGE CASES AND FAILURE MODES
// =================================
suite.test('Edge Case: Should handle forms with all field types', async () => {
const allFieldsForm = generateMockForm({
id: 1,
fields: [
{ id: 1, type: 'text', label: 'Text' },
{ id: 2, type: 'textarea', label: 'Textarea' },
{ id: 3, type: 'select', label: 'Select' },
{ id: 4, type: 'multiselect', label: 'Multi-Select' },
{ id: 5, type: 'number', label: 'Number' },
{ id: 6, type: 'checkbox', label: 'Checkbox' },
{ id: 7, type: 'radio', label: 'Radio' },
{ id: 8, type: 'hidden', label: 'Hidden' },
{ id: 9, type: 'html', label: 'HTML' },
{ id: 10, type: 'section', label: 'Section' },
{ id: 11, type: 'page', label: 'Page Break' },
{ id: 12, type: 'date', label: 'Date' },
{ id: 13, type: 'time', label: 'Time' },
{ id: 14, type: 'phone', label: 'Phone' },
{ id: 15, type: 'address', label: 'Address' },
{ id: 16, type: 'website', label: 'Website' },
{ id: 17, type: 'email', label: 'Email' },
{ id: 18, type: 'fileupload', label: 'File Upload' }
]
});
mockHttpClient.setMockResponse('GET', '/forms/1', new MockResponse(allFieldsForm));
const result = await client.getForm({ id: 1 });
TestAssert.equal(result.field_count, 18);
TestAssert.equal(result.form.fields[17].type, 'fileupload');
});
suite.test('Failure Mode: Should handle rate limiting', async () => {
mockHttpClient.setMockResponse('GET', '/forms', new MockResponse(
{ message: 'Rate limit exceeded' },
429
));
await TestAssert.throwsAsync(
() => client.listForms(),
'Rate limit',
'Should handle rate limiting'
);
});
suite.test('Failure Mode: Should handle server errors', async () => {
mockHttpClient.setMockResponse('GET', '/forms/1', new MockResponse(
{ message: 'Internal server error' },
500
));
await TestAssert.throwsAsync(
() => client.getForm({ id: 1 }),
'Server error',
'Should handle server errors'
);
});
// Run tests
suite.run().then(results => {
process.exit(results.failed > 0 ? 1 : 0);
});
export default suite;