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');
}
});
});