Skip to main content
Glama

1MCP Server

templateValidator.test.ts21.7 kB
import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { categorizeTemplateError, DANGEROUS_TEMPLATE_PATTERNS, DEFAULT_TEMPLATE_VALIDATION_CONFIG, formatValidationError, getErrorSuggestions, isTemplateContentSafe, isTemplateSizeAcceptable, TemplateErrorType, validateTemplateContent, } from './templateValidator.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); describe('Template Validator', () => { const tempDir = path.join(__dirname, 'temp-test-templates'); beforeEach(() => { // Create temp directory for test files if (!fs.existsSync(tempDir)) { fs.mkdirSync(tempDir, { recursive: true }); } }); afterEach(() => { // Clean up temp files if (fs.existsSync(tempDir)) { fs.rmSync(tempDir, { recursive: true, force: true }); } }); describe('validateTemplateContent', () => { describe('Size Validation', () => { it('should reject templates larger than 1MB by default', () => { const largeContent = 'x'.repeat(1024 * 1024 + 1); // 1MB + 1 byte const result = validateTemplateContent(largeContent, 'test-template.md'); expect(result.valid).toBe(false); expect(result.error).toContain('Template file too large'); expect(result.suggestions).toContain('Consider splitting the template into smaller files'); }); it('should accept templates under size limit', () => { const normalContent = '# Normal Template\\n{{serverCount}} servers available'; const result = validateTemplateContent(normalContent, 'test-template.md'); expect(result.valid).toBe(true); expect(result.error).toBeUndefined(); }); it('should respect custom size limits', () => { const content = 'x'.repeat(100); const result = validateTemplateContent(content, 'test-template.md', { maxSizeBytes: 50 }); expect(result.valid).toBe(false); expect(result.error).toContain('Template file too large'); }); }); describe('Safety Validation', () => { it('should reject templates with script tags', () => { const unsafeContent = ` # Template with script {{serverCount}} servers <script>alert('xss')</script> `; const result = validateTemplateContent(unsafeContent, 'unsafe-template.md'); expect(result.valid).toBe(false); expect(result.error).toContain('potentially unsafe content'); expect(result.suggestions).toContain('Remove script tags and event handlers'); }); it('should reject templates with javascript: URLs', () => { const unsafeContent = ` # Template with javascript URL [Click here](javascript:alert('xss')) {{serverCount}} servers `; const result = validateTemplateContent(unsafeContent, 'unsafe-template.md'); expect(result.valid).toBe(false); expect(result.error).toContain('potentially unsafe content'); }); it('should reject templates with event handlers', () => { const unsafeContent = ` # Template with event handlers <div onclick="alert('xss')">{{serverCount}} servers</div> `; const result = validateTemplateContent(unsafeContent, 'unsafe-template.md'); expect(result.valid).toBe(false); expect(result.error).toContain('potentially unsafe content'); }); it('should accept safe templates', () => { const safeContent = ` # Safe Template Available servers: {{serverCount}} {{#each serverNames}} - {{this}} {{/each}} {{#if hasServers}} Instructions available {{else}} No servers found {{/if}} `; const result = validateTemplateContent(safeContent, 'safe-template.md'); expect(result.valid).toBe(true); expect(result.error).toBeUndefined(); }); it('should allow unsafe content when explicitly configured', () => { const unsafeContent = '<script>console.log("test")</script>'; const result = validateTemplateContent(unsafeContent, 'test.md', { allowUnsafeContent: true }); expect(result.valid).toBe(true); }); }); describe('Handlebars Syntax Validation', () => { it('should handle templates with potentially confusing syntax', () => { // Missing closing braces should fail validation const confusingContent = `# Confusing Template {{serverCount`; // Missing closing braces const result = validateTemplateContent(confusingContent, 'confusing-template.md'); expect(result.valid).toBe(false); // Should fail due to missing closing brace expect(result.errorType).toBe(TemplateErrorType.SYNTAX); }); it('should handle templates with mismatched helper tags', () => { // Mismatched tags should fail validation const mismatchedContent = `# Mismatched Helper Template {{#if serverCount}} Content here {{/unless}}`; // Mismatched helper tags const result = validateTemplateContent(mismatchedContent, 'mismatched-template.md'); expect(result.valid).toBe(false); // Should fail due to mismatched tags expect(result.errorType).toBe(TemplateErrorType.SYNTAX); }); it('should accept valid Handlebars syntax', () => { const validContent = ` # Valid Handlebars Template Server Count: {{serverCount}} {{#if hasServers}} {{#each serverNames}} - Server: {{this}} {{/each}} {{else}} No servers available {{/if}} {{#each examples}} Example: {{name}} - {{description}} {{/each}} `; const result = validateTemplateContent(validContent, 'valid-template.md'); expect(result.valid).toBe(true); expect(result.error).toBeUndefined(); }); it('should handle templates with undefined variables gracefully', () => { const templateContent = 'Server: {{nonexistentVariable}}'; const result = validateTemplateContent(templateContent, 'test-template.md'); expect(result.valid).toBe(true); // Undefined variables don't cause compilation errors }); }); describe('Edge Cases', () => { it('should handle empty templates', () => { const result = validateTemplateContent('', 'empty-template.md'); expect(result.valid).toBe(true); }); it('should handle whitespace-only templates', () => { const result = validateTemplateContent(' \\n\\t\\n ', 'whitespace-template.md'); expect(result.valid).toBe(true); }); it('should handle complex real-world templates', () => { const complexContent = `# {{title}} You are interacting with {{serverCount}} MCP {{pluralServers}}. ## Currently Connected Servers {{#if hasServers}} The following {{serverCount}} MCP {{pluralServers}} {{isAre}} currently available: {{#each servers}} ### {{name}} {{#if hasInstructions}} {{instructions}} {{else}} *No specific instructions provided* {{/if}} {{/each}} ## Tool Usage All tools from connected servers are accessible using the format: \`{server}_1mcp_{tool}\` Examples: {{#each examples}} - \`{{name}}\` - {{description}} {{/each}} {{filterContext}} {{else}} No MCP servers are currently connected. {{/if}} --- *Generated by 1MCP*`; const result = validateTemplateContent(complexContent, 'complex-template.md'); expect(result.valid).toBe(true); expect(result.error).toBeUndefined(); }); }); }); describe('File-based Template Testing', () => { describe('Template Size Validation', () => { it('should handle large template files', () => { const largeTempPath = path.join(tempDir, 'large-template.md'); const largeContent = 'x'.repeat(1024 * 1024 + 1); // 1MB + 1 byte fs.writeFileSync(largeTempPath, largeContent); const result = validateTemplateContent(largeContent, largeTempPath); expect(result.valid).toBe(false); expect(result.error).toContain('Template file too large'); expect(result.suggestions).toContain('Consider splitting the template into smaller files'); }); it('should accept normal sized template files', () => { const normalTempPath = path.join(tempDir, 'normal-template.md'); const normalContent = '# Normal Template\n{{serverCount}} servers available'; fs.writeFileSync(normalTempPath, normalContent); const result = validateTemplateContent(normalContent, normalTempPath); expect(result.valid).toBe(true); expect(result.error).toBeUndefined(); }); }); describe('Template Error Handling', () => { it('should handle empty template files', () => { const emptyPath = path.join(tempDir, 'empty-template.md'); const emptyContent = ''; fs.writeFileSync(emptyPath, emptyContent); const result = validateTemplateContent(emptyContent, emptyPath); expect(result.valid).toBe(true); // Empty template is valid }); it('should handle templates with only whitespace', () => { const whitespacePath = path.join(tempDir, 'whitespace-template.md'); const whitespaceContent = ' \n\t\n '; fs.writeFileSync(whitespacePath, whitespaceContent); const result = validateTemplateContent(whitespaceContent, whitespacePath); expect(result.valid).toBe(true); // Whitespace-only template is valid }); it('should handle permission issues gracefully', () => { const restrictedPath = path.join(tempDir, 'restricted-template.md'); fs.writeFileSync(restrictedPath, '# Test template'); // Change permissions to be unreadable (skip on Windows) if (process.platform !== 'win32') { fs.chmodSync(restrictedPath, 0o000); expect(() => fs.readFileSync(restrictedPath)).toThrow(); // Restore permissions for cleanup fs.chmodSync(restrictedPath, 0o644); } }); }); describe('Real-world Template Scenarios', () => { it('should handle complex production-like templates', () => { const complexPath = path.join(tempDir, 'complex-template.md'); const complexContent = `# {{title}} You are interacting with {{serverCount}} MCP {{pluralServers}}. ## Currently Connected Servers {{#if hasServers}} The following {{serverCount}} MCP {{pluralServers}} {{isAre}} currently available: {{#each servers}} ### {{name}} {{#if hasInstructions}} {{instructions}} {{else}} *No specific instructions provided* {{/if}} {{/each}} ## Tool Usage All tools from connected servers are accessible using the format: \`{server}_1mcp_{tool}\` Examples: {{#each examples}} - \`{{name}}\` - {{description}} {{/each}} {{filterContext}} {{else}} No MCP servers are currently connected. {{/if}} --- *Generated by 1MCP*`; fs.writeFileSync(complexPath, complexContent); const result = validateTemplateContent(complexContent, complexPath); expect(result.valid).toBe(true); expect(result.error).toBeUndefined(); }); it('should handle templates with mixed content types', () => { const mixedPath = path.join(tempDir, 'mixed-template.md'); const mixedContent = `# Mixed Content Template ## Markdown Features - Lists - **Bold text** - [Links](https://example.com) - \`code blocks\` ## Handlebars Features {{#if hasServers}} {{#each serverNames}} - Server: {{this}} {{/each}} {{/if}} ## Safe HTML <div class="info"> <strong>Server Count:</strong> {{serverCount}} </div> {{#unless hasServers}} <em>No servers available</em> {{/unless}}`; fs.writeFileSync(mixedPath, mixedContent); const result = validateTemplateContent(mixedContent, mixedPath); expect(result.valid).toBe(true); expect(result.error).toBeUndefined(); }); }); describe('Memory Pressure Scenarios', () => { it('should handle rapid template validations', () => { // Simulate validating many templates quickly for (let i = 0; i < 100; i++) { const templateContent = `# Template ${i}\n{{serverCount}} servers available`; const result = validateTemplateContent(templateContent, `template-${i}.md`); expect(result.valid).toBe(true); } }); it('should handle templates with large variable content', () => { const largeVariableTemplate = `# Large Variable Template Server list: {{#each serverNames}} - {{this}}: ${'x'.repeat(1000)} // Large content per iteration {{/each}} Instructions: {{{instructions}}} End of template`; const result = validateTemplateContent(largeVariableTemplate, 'large-var-template.md'); expect(result.valid).toBe(true); }); }); }); describe('Utility Functions', () => { describe('formatValidationError', () => { it('should format validation errors properly', () => { const result = { valid: false, error: 'Test error message', suggestions: ['Suggestion 1', 'Suggestion 2'], }; const formatted = formatValidationError(result); expect(formatted).toContain('Test error message'); expect(formatted).toContain('Suggestions:'); expect(formatted).toContain('1. Suggestion 1'); expect(formatted).toContain('2. Suggestion 2'); }); it('should return empty string for valid templates', () => { const result = { valid: true }; const formatted = formatValidationError(result); expect(formatted).toBe(''); }); it('should handle missing suggestions', () => { const result = { valid: false, error: 'Test error without suggestions', }; const formatted = formatValidationError(result); expect(formatted).toBe('Test error without suggestions'); }); }); describe('isTemplateSizeAcceptable', () => { it('should return false for non-existent files', () => { expect(isTemplateSizeAcceptable('/non/existent/file.md')).toBe(false); }); it('should use default size limit when not specified', () => { // This test verifies the function works with defaults, actual file testing in integration tests expect(typeof isTemplateSizeAcceptable('/any/path')).toBe('boolean'); }); }); describe('isTemplateContentSafe', () => { it('should detect safe content', () => { expect(isTemplateContentSafe('Safe template {{serverCount}}')).toBe(true); expect(isTemplateContentSafe('# Title\\n{{#if hasServers}}content{{/if}}')).toBe(true); }); it('should detect unsafe content', () => { expect(isTemplateContentSafe('<script>alert("xss")</script>')).toBe(false); expect(isTemplateContentSafe('<div onclick="hack()">content</div>')).toBe(false); expect(isTemplateContentSafe('[link](javascript:alert("xss"))')).toBe(false); }); it('should allow unsafe content when configured', () => { const unsafeContent = '<script>console.log("test")</script>'; expect(isTemplateContentSafe(unsafeContent)).toBe(false); expect(isTemplateContentSafe(unsafeContent, { allowUnsafeContent: true })).toBe(true); }); it('should handle custom dangerous patterns', () => { const customPattern = /custom-dangerous-pattern/i; const content = 'This contains custom-dangerous-pattern'; expect(isTemplateContentSafe(content)).toBe(true); expect(isTemplateContentSafe(content, { customDangerousPatterns: [customPattern] })).toBe(false); }); }); describe('Constants', () => { it('should have properly defined dangerous patterns', () => { expect(DANGEROUS_TEMPLATE_PATTERNS).toHaveLength(3); expect(DANGEROUS_TEMPLATE_PATTERNS[0].test('<script></script>')).toBe(true); expect(DANGEROUS_TEMPLATE_PATTERNS[1].test('javascript:alert(1)')).toBe(true); expect(DANGEROUS_TEMPLATE_PATTERNS[2].test('onclick="test()"')).toBe(true); }); it('should have sensible default configuration', () => { expect(DEFAULT_TEMPLATE_VALIDATION_CONFIG.maxSizeBytes).toBe(1024 * 1024); expect(DEFAULT_TEMPLATE_VALIDATION_CONFIG.allowUnsafeContent).toBe(false); expect(DEFAULT_TEMPLATE_VALIDATION_CONFIG.customDangerousPatterns).toEqual([]); }); }); describe('Error Categorization', () => { describe('categorizeTemplateError', () => { it('should categorize syntax errors correctly', () => { const syntaxErrors = [ 'Parse error on line 1', 'Expecting token CLOSE', 'Unexpected token', 'Unterminated string', 'Unmatched brace', ]; syntaxErrors.forEach((errorMessage) => { expect(categorizeTemplateError(errorMessage)).toBe(TemplateErrorType.SYNTAX); }); }); it('should categorize compilation errors correctly', () => { const compilationErrors = [ 'Missing helper: invalidHelper', 'Must pass iterator to #each', 'Helper not found: customHelper', 'Invalid helper usage', ]; compilationErrors.forEach((errorMessage) => { expect(categorizeTemplateError(errorMessage)).toBe(TemplateErrorType.COMPILATION); }); }); it('should default to compilation error for unknown errors', () => { expect(categorizeTemplateError('Unknown error message')).toBe(TemplateErrorType.COMPILATION); }); }); describe('getErrorSuggestions', () => { it('should provide specific suggestions for syntax errors', () => { const suggestions = getErrorSuggestions(TemplateErrorType.SYNTAX, 'Parse error'); expect(suggestions).toContain('Check for unmatched braces {{ }}'); expect(suggestions).toContain('Ensure all Handlebars expressions are properly closed'); }); it('should provide specific suggestions for compilation errors', () => { const suggestions = getErrorSuggestions(TemplateErrorType.COMPILATION, 'Must pass iterator'); expect(suggestions).toContain('Use {{#each serverNames}} instead of {{#each}}'); expect(suggestions).toContain('Ensure iterator variable is provided for #each helpers'); }); it('should provide size limit suggestions', () => { const suggestions = getErrorSuggestions(TemplateErrorType.SIZE_LIMIT, ''); expect(suggestions).toContain('Consider splitting the template into smaller files'); expect(suggestions).toContain('Increase template size limit if necessary'); }); it('should provide unsafe content suggestions', () => { const suggestions = getErrorSuggestions(TemplateErrorType.UNSAFE_CONTENT, ''); expect(suggestions).toContain('Remove script tags and event handlers'); expect(suggestions).toContain('Use safe template variables only'); }); it('should provide runtime error suggestions', () => { const suggestions = getErrorSuggestions(TemplateErrorType.RUNTIME, ''); expect(suggestions).toContain('Check template variables are correctly defined'); expect(suggestions).toContain('Verify data passed to template matches expected structure'); }); }); describe('validateTemplateContent with error categorization', () => { it('should include error type in validation result for size limit', () => { const largeContent = 'x'.repeat(1024 * 1024 + 1); // 1MB + 1 byte const result = validateTemplateContent(largeContent); expect(result.valid).toBe(false); expect(result.errorType).toBe(TemplateErrorType.SIZE_LIMIT); expect(result.error).toContain('Template file too large'); }); it('should include error type in validation result for unsafe content', () => { const unsafeContent = '<script>alert("xss")</script>{{serverCount}}'; const result = validateTemplateContent(unsafeContent); expect(result.valid).toBe(false); expect(result.errorType).toBe(TemplateErrorType.UNSAFE_CONTENT); expect(result.error).toContain('potentially unsafe content'); }); it('should include error type in validation result for syntax errors', () => { const syntaxError = '{{#each items}}{{name}}{{/if}}'; // Mismatched tags const result = validateTemplateContent(syntaxError); expect(result.valid).toBe(false); expect(result.errorType).toBe(TemplateErrorType.SYNTAX); expect(result.error).toContain('Template syntax error'); }); it('should include error type in validation result for compilation errors', () => { const compilationError = '{{#each}}content{{/each}}'; // Missing iterator // This actually fails at compile time due to missing iterator // Let's test during template rendering by creating a custom test const result = validateTemplateContent(compilationError); expect(result.valid).toBe(false); expect(result.errorType).toBe(TemplateErrorType.COMPILATION); expect(result.error).toContain('Template syntax error'); }); }); }); }); });

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/1mcp-app/agent'

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