Skip to main content
Glama
tableIntegration.test.ts20 kB
import { describe, it, expect, beforeAll } from '@jest/globals'; /** * LIVE INTEGRATION TESTS FOR SERVICENOW TABLE API * * These tests make real API calls to ServiceNow using actual credentials. * They will ONLY run if SERVICENOW_ACE_* environment variables are set. * * Purpose: Validate real-world table operations and data manipulation * Run with: npm run test:integration * * ⚠️ WARNING: These tests create, modify, and delete real data in ServiceNow! * Only run against sandbox/dev instances with appropriate permissions. */ const skipIfNoCredentials = () => { const hasCredentials = process.env.SERVICENOW_ACE_INSTANCE && process.env.SERVICENOW_ACE_USERNAME && process.env.SERVICENOW_ACE_PASSWORD; if (!hasCredentials) { console.log('\n⚠️ Skipping live integration tests - ServiceNow credentials not found'); console.log(' Set SERVICENOW_ACE_INSTANCE, SERVICENOW_ACE_USERNAME, SERVICENOW_ACE_PASSWORD to run'); } return hasCredentials; }; // Only run these tests if we have credentials const testSuite = skipIfNoCredentials() ? describe : describe.skip; testSuite('Live ServiceNow Table API Integration Tests', () => { let tableClient: any; let createdRecords: string[] = []; // Track created records for cleanup beforeAll(async () => { try { // Dynamic import to avoid Jest parsing issues const { ServiceNowTableClient } = await import('../servicenow/tableClient.js'); tableClient = new ServiceNowTableClient(); } catch (error) { console.error('Failed to initialize ServiceNow table client:', error); throw error; } }); describe('GET Operations - Query Records', () => { it('should query records from sys_user table', async () => { const result = await tableClient.executeTableOperation({ operation: 'GET', table: 'sys_user', limit: 5, }); expect(result.success).toBe(true); expect(result.data).toBeDefined(); expect(result.data?.records).toBeDefined(); expect(Array.isArray(result.data?.records)).toBe(true); expect(result.data?.records.length).toBeLessThanOrEqual(5); expect(result.metadata.operation).toBe('GET'); expect(result.metadata.table).toBe('sys_user'); expect(result.metadata.recordCount).toBeGreaterThanOrEqual(0); }); it('should query records with encoded query syntax', async () => { const result = await tableClient.executeTableOperation({ operation: 'GET', table: 'sys_user', query: 'active=true', fields: 'sys_id,user_name,first_name,last_name', limit: 3, }); expect(result.success).toBe(true); expect(result.data?.records).toBeDefined(); expect(Array.isArray(result.data?.records)).toBe(true); // Verify all returned records have the expected fields if (result.data?.records.length > 0) { const record = result.data.records[0]; expect(record).toHaveProperty('sys_id'); expect(record).toHaveProperty('user_name'); } }); it('should query records with display values', async () => { const result = await tableClient.executeTableOperation({ operation: 'GET', table: 'sys_user', fields: 'sys_id,user_name,manager', display_value: 'true', limit: 2, }); expect(result.success).toBe(true); expect(result.data?.records).toBeDefined(); }); it('should handle pagination with offset', async () => { const result = await tableClient.executeTableOperation({ operation: 'GET', table: 'sys_user', limit: 2, offset: 1, }); expect(result.success).toBe(true); expect(result.data?.records).toBeDefined(); expect(result.data?.records.length).toBeLessThanOrEqual(2); }); it('should get single record by sys_id', async () => { // First get a record to use its sys_id const queryResult = await tableClient.executeTableOperation({ operation: 'GET', table: 'sys_user', limit: 1, }); if (queryResult.data?.records.length > 0) { const sysId = queryResult.data.records[0].sys_id; const result = await tableClient.executeTableOperation({ operation: 'GET', table: 'sys_user', sys_id: sysId, }); expect(result.success).toBe(true); expect(result.data?.records).toBeDefined(); expect(result.data?.records.length).toBe(1); expect(result.data?.records[0].sys_id).toBe(sysId); } }); }); describe('POST Operations - Create Records', () => { it('should create a single record', async () => { const testData = { short_description: `Test incident created by MCP ACE at ${new Date().toISOString()}`, description: 'This is a test incident created by the MCP ACE Table API integration test', priority: '3', category: 'inquiry', }; const result = await tableClient.executeTableOperation({ operation: 'POST', table: 'incident', data: testData, }); expect(result.success).toBe(true); expect(result.data?.records).toBeDefined(); expect(result.data?.records.length).toBe(1); expect(result.data?.records[0]).toHaveProperty('sys_id'); expect(result.data?.records[0].short_description).toBe(testData.short_description); // Track for cleanup createdRecords.push(result.data?.records[0].sys_id); }); it('should create multiple records in batch', async () => { const testData = [ { short_description: `Batch test incident 1 at ${new Date().toISOString()}`, priority: '3', category: 'inquiry', }, { short_description: `Batch test incident 2 at ${new Date().toISOString()}`, priority: '4', category: 'inquiry', }, ]; const result = await tableClient.executeTableOperation({ operation: 'POST', table: 'incident', data: testData, batch: true, }); expect(result.success).toBe(true); expect(result.data?.records).toBeDefined(); expect(result.data?.records.length).toBe(2); // Track for cleanup result.data?.records.forEach((record: any) => { createdRecords.push(record.sys_id); }); }); it('should handle creation with all required fields', async () => { const testData = { short_description: `Complete test incident at ${new Date().toISOString()}`, description: 'Complete test with all fields', priority: '2', category: 'inquiry', urgency: '2', impact: '2', }; const result = await tableClient.executeTableOperation({ operation: 'POST', table: 'incident', data: testData, }); expect(result.success).toBe(true); expect(result.data?.records).toBeDefined(); expect(result.data?.records.length).toBe(1); // Track for cleanup createdRecords.push(result.data?.records[0].sys_id); }); }); describe('PUT/PATCH Operations - Update Records', () => { let testRecordId: string; beforeAll(async () => { // Create a test record for update operations const createResult = await tableClient.executeTableOperation({ operation: 'POST', table: 'incident', data: { short_description: `Update test incident at ${new Date().toISOString()}`, priority: '3', category: 'inquiry', }, }); if (createResult.success && createResult.data?.records.length > 0) { testRecordId = createResult.data.records[0].sys_id; createdRecords.push(testRecordId); } }); it('should update a record with PUT', async () => { if (!testRecordId) { console.log('Skipping PUT test - no test record available'); return; } const updateData = { short_description: `Updated incident at ${new Date().toISOString()}`, priority: '2', description: 'This incident has been updated via PUT operation', }; const result = await tableClient.executeTableOperation({ operation: 'PUT', table: 'incident', sys_id: testRecordId, data: updateData, }); expect(result.success).toBe(true); expect(result.data?.records).toBeDefined(); expect(result.data?.records.length).toBe(1); expect(result.data?.records[0].sys_id).toBe(testRecordId); expect(result.data?.records[0].short_description).toBe(updateData.short_description); }); it('should update a record with PATCH', async () => { if (!testRecordId) { console.log('Skipping PATCH test - no test record available'); return; } const updateData = { priority: '1', description: 'This incident has been patched via PATCH operation', }; const result = await tableClient.executeTableOperation({ operation: 'PATCH', table: 'incident', sys_id: testRecordId, data: updateData, }); expect(result.success).toBe(true); expect(result.data?.records).toBeDefined(); expect(result.data?.records.length).toBe(1); expect(result.data?.records[0].sys_id).toBe(testRecordId); }); it('should update multiple records in batch', async () => { // Create multiple test records for batch update const createResult = await tableClient.executeTableOperation({ operation: 'POST', table: 'incident', data: [ { short_description: `Batch update test 1 at ${new Date().toISOString()}`, priority: '3', }, { short_description: `Batch update test 2 at ${new Date().toISOString()}`, priority: '3', }, ], batch: true, }); if (createResult.success && createResult.data?.records.length >= 2) { const recordIds = createResult.data.records.map((r: any) => r.sys_id); createdRecords.push(...recordIds); const updateData = [ { sys_id: recordIds[0], priority: '2', description: 'Batch updated 1' }, { sys_id: recordIds[1], priority: '2', description: 'Batch updated 2' }, ]; const result = await tableClient.executeTableOperation({ operation: 'PUT', table: 'incident', data: updateData, batch: true, }); expect(result.success).toBe(true); expect(result.data?.records).toBeDefined(); expect(result.data?.records.length).toBe(2); } }); }); describe('DELETE Operations - Delete Records', () => { let testRecordId: string; beforeAll(async () => { // Create a test record for delete operations const createResult = await tableClient.executeTableOperation({ operation: 'POST', table: 'incident', data: { short_description: `Delete test incident at ${new Date().toISOString()}`, priority: '3', category: 'inquiry', }, }); if (createResult.success && createResult.data?.records.length > 0) { testRecordId = createResult.data.records[0].sys_id; // Don't add to createdRecords since we'll delete it } }); it('should delete a single record', async () => { if (!testRecordId) { console.log('Skipping DELETE test - no test record available'); return; } const result = await tableClient.executeTableOperation({ operation: 'DELETE', table: 'incident', sys_id: testRecordId, }); expect(result.success).toBe(true); expect(result.data?.records).toBeDefined(); expect(result.data?.records.length).toBe(1); expect(result.data?.records[0].sys_id).toBe(testRecordId); // Verify the record is actually deleted try { await tableClient.executeTableOperation({ operation: 'GET', table: 'incident', sys_id: testRecordId, }); fail('Record should have been deleted'); } catch (error) { expect(error).toBeDefined(); } }); it('should delete multiple records in batch', async () => { // Create multiple test records for batch delete const createResult = await tableClient.executeTableOperation({ operation: 'POST', table: 'incident', data: [ { short_description: `Batch delete test 1 at ${new Date().toISOString()}`, priority: '3', }, { short_description: `Batch delete test 2 at ${new Date().toISOString()}`, priority: '3', }, ], batch: true, }); if (createResult.success && createResult.data?.records.length >= 2) { const recordIds = createResult.data.records.map((r: any) => r.sys_id); const result = await tableClient.executeTableOperation({ operation: 'DELETE', table: 'incident', sys_ids: recordIds, batch: true, }); expect(result.success).toBe(true); expect(result.data?.records).toBeDefined(); expect(result.data?.records.length).toBe(2); } }); }); describe('Complex Query Operations', () => { it('should handle complex encoded queries', async () => { const result = await tableClient.executeTableOperation({ operation: 'GET', table: 'sys_user', query: 'active=true^user_name!=admin', fields: 'sys_id,user_name,first_name,last_name,email', limit: 5, }); expect(result.success).toBe(true); expect(result.data?.records).toBeDefined(); }); it('should handle queries with operators', async () => { const result = await tableClient.executeTableOperation({ operation: 'GET', table: 'sys_user', query: 'active=true^OR^user_name=admin', limit: 3, }); expect(result.success).toBe(true); expect(result.data?.records).toBeDefined(); }); it('should handle field filtering', async () => { const result = await tableClient.executeTableOperation({ operation: 'GET', table: 'sys_user', fields: 'sys_id,user_name,active', limit: 3, }); expect(result.success).toBe(true); expect(result.data?.records).toBeDefined(); if (result.data?.records.length > 0) { const record = result.data.records[0]; // Should only have the requested fields const fields = Object.keys(record); expect(fields.length).toBeLessThanOrEqual(3); } }); }); describe('Error Handling - Real Scenarios', () => { it('should handle invalid table names', async () => { try { await tableClient.executeTableOperation({ operation: 'GET', table: 'invalid_table_name_that_does_not_exist', }); fail('Should have thrown an error for invalid table'); } catch (error: any) { expect(error.code).toBeDefined(); expect(error.message).toBeDefined(); } }); it('should handle invalid sys_id', async () => { try { await tableClient.executeTableOperation({ operation: 'GET', table: 'incident', sys_id: 'invalid-sys-id-that-does-not-exist', }); fail('Should have thrown an error for invalid sys_id'); } catch (error: any) { expect(error.code).toBeDefined(); expect(error.message).toBeDefined(); } }); it('should handle invalid query syntax', async () => { try { await tableClient.executeTableOperation({ operation: 'GET', table: 'incident', query: 'invalid^query^syntax', }); // Some instances might not validate query syntax strictly } catch (error: any) { expect(error.code).toBeDefined(); expect(error.message).toBeDefined(); } }); it('should handle missing required fields for creation', async () => { try { await tableClient.executeTableOperation({ operation: 'POST', table: 'incident', data: { // Missing required fields priority: '3', }, }); // Some instances might be lenient with required fields } catch (error: any) { expect(error.code).toBeDefined(); expect(error.message).toBeDefined(); } }); }); describe('Performance and Limits', () => { it('should handle large result sets', async () => { const result = await tableClient.executeTableOperation({ operation: 'GET', table: 'sys_user', limit: 100, }); expect(result.success).toBe(true); expect(result.data?.records).toBeDefined(); expect(result.data?.records.length).toBeLessThanOrEqual(100); }); it('should handle concurrent operations', async () => { const promises = [ tableClient.executeTableOperation({ operation: 'GET', table: 'sys_user', limit: 5, }), tableClient.executeTableOperation({ operation: 'GET', table: 'incident', limit: 5, }), tableClient.executeTableOperation({ operation: 'GET', table: 'sys_user', query: 'active=true', limit: 3, }), ]; const results = await Promise.all(promises); results.forEach((result) => { expect(result.success).toBe(true); expect(result.data?.records).toBeDefined(); }); }); }); describe('Metadata Validation', () => { it('should include accurate execution metadata', async () => { const startTime = Date.now(); const result = await tableClient.executeTableOperation({ operation: 'GET', table: 'sys_user', limit: 5, }); const endTime = Date.now(); expect(result.metadata).toBeDefined(); expect(result.metadata.operation).toBe('GET'); expect(result.metadata.table).toBe('sys_user'); expect(result.metadata.executionTime).toBeGreaterThan(0); expect(result.metadata.executionTime).toBeLessThan(endTime - startTime + 1000); // Allow 1 second buffer expect(result.metadata.recordCount).toBeGreaterThanOrEqual(0); expect(result.metadata.timestamp).toBeDefined(); expect(result.metadata.batch).toBe(false); }); it('should track batch operations correctly', async () => { const result = await tableClient.executeTableOperation({ operation: 'POST', table: 'incident', data: [ { short_description: 'Batch metadata test 1' }, { short_description: 'Batch metadata test 2' }, ], batch: true, }); expect(result.metadata.batch).toBe(true); expect(result.metadata.recordCount).toBe(2); // Track for cleanup if (result.success && result.data?.records) { result.data.records.forEach((record: any) => { createdRecords.push(record.sys_id); }); } }); }); // Cleanup: Delete all test records created during tests afterAll(async () => { if (createdRecords.length > 0) { console.log(`\n🧹 Cleaning up ${createdRecords.length} test records...`); for (const sysId of createdRecords) { try { await tableClient.executeTableOperation({ operation: 'DELETE', table: 'incident', sys_id: sysId, }); } catch (error) { console.log(`Failed to delete record ${sysId}:`, error); } } console.log('✅ Cleanup completed'); } }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ClearSkye/SkyeNet-MCP-ACE'

If you have feedback or need assistance with the MCP directory API, please join our Discord server