import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { join } from 'path';
import { writeFileSync, mkdirSync, rmSync, existsSync } from 'fs';
describe('File Placeholder Integration Tests', () => {
let client: Client;
const testDir = join(process.cwd(), 'test-files');
const testScriptFile = join(testDir, 'test-script.js');
const testTemplateFile = join(testDir, 'test-template.html');
const testConfigFile = join(testDir, 'test-config.json');
const testScopeFile = join(testDir, 'scope.txt');
const testQueryFile = join(testDir, 'query.txt');
const testNameFile = join(testDir, 'name.txt');
const testDescriptionFile = join(testDir, 'description.txt');
const testLargeFile = join(testDir, 'large.txt');
beforeAll(async () => {
// Set up environment variables for ServiceNow (using placeholder values for tests)
process.env.SERVICENOW_ACE_INSTANCE = 'test-instance.service-now.com';
process.env.SERVICENOW_ACE_USERNAME = 'test_user';
process.env.SERVICENOW_ACE_PASSWORD = 'test_password';
// Create test files
if (!existsSync(testDir)) {
mkdirSync(testDir, { recursive: true });
}
writeFileSync(testScriptFile, 'console.log("Hello from file!");\nconst x = 42;');
writeFileSync(testTemplateFile, '<div>Hello World</div>\n<p>This is a template</p>');
writeFileSync(testConfigFile, '{"name": "test", "value": 123}');
writeFileSync(testScopeFile, 'global');
writeFileSync(testQueryFile, 'name=TestScript');
writeFileSync(testNameFile, 'TestUpdateSet');
writeFileSync(testDescriptionFile, 'Test update set description');
writeFileSync(testLargeFile, 'x'.repeat(2 * 1024 * 1024)); // 2MB file
// Create MCP client using the official SDK approach
const serverPath = join(__dirname, '../../build/index.js');
const transport = new StdioClientTransport({
command: 'node',
args: [serverPath],
env: {
NODE_ENV: 'test',
SERVICENOW_ACE_INSTANCE: 'test-instance.service-now.com',
SERVICENOW_ACE_USERNAME: 'test_user',
SERVICENOW_ACE_PASSWORD: 'test_password',
},
});
client = new Client({
name: 'test-client',
version: '1.4.0',
});
await client.connect(transport);
}, 10000); // 10 second timeout for server startup
afterAll(async () => {
if (client) {
await client.close();
}
// Clean up test files
if (existsSync(testDir)) {
rmSync(testDir, { recursive: true, force: true });
}
});
describe('Background Script Tool with File Placeholders', () => {
it('should resolve file placeholder in script parameter', async () => {
const result = await client.callTool({
name: 'execute_background_script',
arguments: {
script: '{{file:${testScriptFile}}}',
scope: 'global',
},
});
// The tool should attempt to execute (may fail due to no real ServiceNow connection)
// but should not fail due to file placeholder resolution
expect(result).toBeDefined();
expect(result.content).toBeDefined();
});
it('should resolve file placeholder in scope parameter', async () => {
// Create a scope file
const scopeFile = join(testDir, 'scope.txt');
writeFileSync(scopeFile, 'x_cls_clear_skye_i');
const result = await client.callTool({
name: 'execute_background_script',
arguments: {
script: 'gs.print("test");',
scope: '{{file:${testScopeFile}}}',
},
});
expect(result).toBeDefined();
expect(result.content).toBeDefined();
});
it('should handle file not found error', async () => {
const result = await client.callTool({
name: 'execute_background_script',
arguments: {
script: '{{file:/nonexistent/file.js}}',
scope: 'global',
},
});
expect(result).toBeDefined();
expect(result.content).toBeDefined();
// Should contain error information
const content = (result.content as any[])[0]?.text;
if (content) {
const parsed = JSON.parse(content);
expect(parsed.success).toBe(false);
expect(parsed.error.code).toBe('FILE_PLACEHOLDER_ERROR');
}
});
});
describe('Table Operation Tool with File Placeholders', () => {
it('should resolve file placeholder in data parameter for insert', async () => {
const result = await client.callTool({
name: 'execute_table_operation',
arguments: {
operation: 'POST',
table: 'sys_script_include',
data: {
name: 'TestScriptInclude',
script: '{{file:${testScriptFile}}}',
active: true,
},
},
});
expect(result).toBeDefined();
expect(result.content).toBeDefined();
});
it('should resolve file placeholder in batch data', async () => {
const result = await client.callTool({
name: 'execute_table_operation',
arguments: {
operation: 'POST',
table: 'sys_script_include',
data: [
{
name: 'Script1',
script: '{{file:${testScriptFile}}}',
active: true,
},
{
name: 'Script2',
script: 'console.log("inline script");',
active: true,
},
],
batch: true,
},
});
expect(result).toBeDefined();
expect(result.content).toBeDefined();
});
it('should resolve file placeholder in query parameter', async () => {
// Create a query file
const queryFile = join(testDir, 'query.txt');
writeFileSync(queryFile, 'active=true^nameLIKEtest');
const result = await client.callTool({
name: 'execute_table_operation',
arguments: {
operation: 'GET',
table: 'sys_script_include',
query: '{{file:${testQueryFile}}}',
},
});
expect(result).toBeDefined();
expect(result.content).toBeDefined();
});
it('should handle file not found error in table operation', async () => {
const result = await client.callTool({
name: 'execute_table_operation',
arguments: {
operation: 'POST',
table: 'sys_script_include',
data: {
name: 'TestScriptInclude',
script: '{{file:/nonexistent/file.js}}',
active: true,
},
},
});
expect(result).toBeDefined();
expect(result.content).toBeDefined();
// Should contain error information
const content = (result.content as any[])[0]?.text;
if (content) {
const parsed = JSON.parse(content);
expect(parsed.success).toBe(false);
expect(parsed.error.code).toBe('FILE_PLACEHOLDER_ERROR');
}
});
});
describe('Update Set Operation Tool with File Placeholders', () => {
it('should resolve file placeholder in data parameter for insert', async () => {
const result = await client.callTool({
name: 'execute_updateset_operation',
arguments: {
operation: 'insert',
table: 'sys_script_include',
data: {
name: 'TestScriptInclude',
script: '{{file:${testScriptFile}}}',
active: true,
},
},
});
expect(result).toBeDefined();
expect(result.content).toBeDefined();
});
it('should resolve file placeholder in name and description', async () => {
// Create name and description files
const nameFile = join(testDir, 'name.txt');
const descFile = join(testDir, 'description.txt');
writeFileSync(nameFile, 'Test Update Set');
writeFileSync(descFile, 'This is a test update set created from file content');
const result = await client.callTool({
name: 'execute_updateset_operation',
arguments: {
operation: 'create',
name: '{{file:${testNameFile}}}',
description: '{{file:${testDescriptionFile}}}',
scope: 'x_cls_clear_skye_i',
},
});
expect(result).toBeDefined();
expect(result.content).toBeDefined();
});
it('should resolve file placeholder in batch data', async () => {
const result = await client.callTool({
name: 'execute_updateset_operation',
arguments: {
operation: 'insert',
table: 'sys_script_include',
data: [
{
name: 'Script1',
script: '{{file:${testScriptFile}}}',
active: true,
},
{
name: 'Script2',
script: 'console.log("inline script");',
active: true,
},
],
batch: true,
},
});
expect(result).toBeDefined();
expect(result.content).toBeDefined();
});
it('should handle file not found error in update set operation', async () => {
const result = await client.callTool({
name: 'execute_updateset_operation',
arguments: {
operation: 'insert',
table: 'sys_script_include',
data: {
name: 'TestScriptInclude',
script: '{{file:/nonexistent/file.js}}',
active: true,
},
},
});
expect(result).toBeDefined();
expect(result.content).toBeDefined();
// Should contain error information
const content = (result.content as any[])[0]?.text;
if (content) {
const parsed = JSON.parse(content);
expect(parsed.success).toBe(false);
expect(parsed.error.code).toBe('FILE_PLACEHOLDER_ERROR');
}
});
});
describe('Multiple Placeholders in Single String', () => {
it('should resolve multiple placeholders in background script', async () => {
const result = await client.callTool({
name: 'execute_background_script',
arguments: {
script: '// Script from file: {{file:${testScriptFile}}}\n// Template: {{file:${testTemplateFile}}}',
scope: 'global',
},
});
expect(result).toBeDefined();
expect(result.content).toBeDefined();
});
it('should resolve multiple placeholders in table operation data', async () => {
const result = await client.callTool({
name: 'execute_table_operation',
arguments: {
operation: 'POST',
table: 'sys_script_include',
data: {
name: 'MultiPlaceholderScript',
script: '{{file:${testScriptFile}}}',
description: 'Template: {{file:${testTemplateFile}}}',
active: true,
},
},
});
expect(result).toBeDefined();
expect(result.content).toBeDefined();
});
});
describe('Error Handling', () => {
it('should handle directory traversal attempts', async () => {
const result = await client.callTool({
name: 'execute_background_script',
arguments: {
script: '{{file:../../../etc/passwd}}',
scope: 'global',
},
});
expect(result).toBeDefined();
expect(result.content).toBeDefined();
// Should contain error information
const content = (result.content as any[])[0]?.text;
if (content) {
const parsed = JSON.parse(content);
expect(parsed.success).toBe(false);
expect(parsed.error.code).toBe('FILE_PLACEHOLDER_ERROR');
}
});
it('should handle absolute paths outside workspace', async () => {
const result = await client.callTool({
name: 'execute_background_script',
arguments: {
script: '{{file:/etc/passwd}}',
scope: 'global',
},
});
expect(result).toBeDefined();
expect(result.content).toBeDefined();
// Should contain error information
const content = (result.content as any[])[0]?.text;
if (content) {
const parsed = JSON.parse(content);
expect(parsed.success).toBe(false);
expect(parsed.error.code).toBe('FILE_PLACEHOLDER_ERROR');
}
});
});
describe('Configuration', () => {
it('should respect file size limits', async () => {
// Create a large file (2MB)
const largeFile = join(testDir, 'large.txt');
const largeContent = 'x'.repeat(2 * 1024 * 1024);
writeFileSync(largeFile, largeContent);
const result = await client.callTool({
name: 'execute_background_script',
arguments: {
script: '{{file:${testLargeFile}}}',
scope: 'global',
},
});
expect(result).toBeDefined();
expect(result.content).toBeDefined();
// Should contain error information about file size
const content = (result.content as any[])[0]?.text;
if (content) {
const parsed = JSON.parse(content);
expect(parsed.success).toBe(false);
expect(parsed.error.code).toBe('FILE_PLACEHOLDER_ERROR');
}
});
});
});