Skip to main content
Glama
fault-injection.test.js11.2 kB
/** * Chaos / Fault Injection Testing * * Tests system behavior under adverse conditions: * - Database unavailability * - File system errors * - Resource exhaustion * - Corrupted data */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import path from 'path' import fs from 'fs' const DB_PATH = path.join(process.env.HOME, '.apple-tools-mcp', 'lance_index') describe('Chaos: Database Unavailability', () => { it('should handle missing database gracefully', async () => { const { connect } = await import('@lancedb/lancedb') const nonExistentPath = '/tmp/non-existent-db-' + Date.now() // LanceDB creates the db if it doesn't exist const db = await connect(nonExistentPath) // Should return empty tables const tables = await db.tableNames() expect(Array.isArray(tables)).toBe(true) expect(tables.length).toBe(0) // Cleanup try { fs.rmSync(nonExistentPath, { recursive: true }) } catch (e) { // Ignore cleanup errors } }) it('should handle invalid database path', async () => { const { connect } = await import('@lancedb/lancedb') // Path with invalid characters or permissions const invalidPath = '/root/no-access-' + Date.now() try { await connect(invalidPath) // If we got here, the system allows creating this path } catch (e) { // Expected to fail with permission error expect(e.message).toMatch(/permission|access|denied|EACCES/i) } }) it('should handle table not found', async () => { const { connect } = await import('@lancedb/lancedb') if (!fs.existsSync(DB_PATH)) { return // Skip if no database } const db = await connect(DB_PATH) try { await db.openTable('non_existent_table_' + Date.now()) expect.fail('Should have thrown error') } catch (e) { // Expected - table doesn't exist expect(e.message).toBeDefined() } }) }) describe('Chaos: File System Errors', () => { it('should handle file read errors for mail_read', async () => { const { validateEmailPath } = await import('../../lib/validators.js') const mailDir = path.join(process.env.HOME, 'Library', 'Mail') // Non-existent file path const fakePath = path.join(mailDir, 'non-existent-file-' + Date.now() + '.emlx') // Validator should reject files outside mail directory structure // but allow valid-looking paths try { validateEmailPath(fakePath, mailDir) // Path validation passed - that's OK, file access would fail later } catch (e) { // Path validation failed expect(e.message).toBeDefined() } }) it('should handle path traversal attempts', async () => { const { validateEmailPath } = await import('../../lib/validators.js') const mailDir = path.join(process.env.HOME, 'Library', 'Mail') const traversalPaths = [ '../../../etc/passwd.emlx', // Path traversal with valid extension '..\\..\\..\\windows\\system32\\x.emlx', '/etc/passwd.emlx', mailDir + '/../../../etc/passwd.emlx' ] for (const badPath of traversalPaths) { try { validateEmailPath(badPath, mailDir) // If validation passes, the path should be sanitized expect.fail(`Should have rejected: ${badPath}`) } catch (e) { // Should either fail on extension or path traversal expect(e.message).toMatch(/denied|extension/i) } } }) it('should handle special characters in paths', async () => { const { validateEmailPath } = await import('../../lib/validators.js') const mailDir = path.join(process.env.HOME, 'Library', 'Mail') const specialPaths = [ mailDir + '/test\x00file.emlx', // Null byte mailDir + '/test\nfile.emlx', // Newline mailDir + '/test\rfile.emlx' // Carriage return ] for (const badPath of specialPaths) { try { validateEmailPath(badPath, mailDir) // If it passes, the special chars should be handled } catch (e) { // Expected for potentially malicious paths expect(e.message).toBeDefined() } } }) }) describe('Chaos: Corrupted Data', () => { it('should handle malformed JSON in results', async () => { const malformedData = [ '{"incomplete": true', '{"nested": {"deep": {"unclosed": true}', '[1, 2, 3,]', 'undefined', 'NaN' ] for (const data of malformedData) { try { JSON.parse(data) expect.fail('Should have thrown') } catch (e) { expect(e).toBeInstanceOf(SyntaxError) } } }) it('should handle circular references safely', () => { const obj = { name: 'test' } obj.self = obj // Circular reference expect(() => JSON.stringify(obj)).toThrow() // Safe stringification with replacer const seen = new WeakSet() const safe = JSON.stringify(obj, (key, value) => { if (typeof value === 'object' && value !== null) { if (seen.has(value)) return '[Circular]' seen.add(value) } return value }) expect(safe).toContain('[Circular]') }) it('should handle binary data in text fields', async () => { const { stripHtmlTags } = await import('../../lib/validators.js') // Binary-like content const binaryish = '\x00\x01\x02\xff\xfe<p>text</p>\x00' const result = stripHtmlTags(binaryish) // Should not crash, output should be safe expect(typeof result).toBe('string') }) it('should handle extremely deep nesting', () => { // Create deeply nested object let deep = { value: 'end' } for (let i = 0; i < 100; i++) { deep = { nested: deep } } // Should handle without stack overflow const str = JSON.stringify(deep) const parsed = JSON.parse(str) expect(parsed.nested.nested.nested).toBeDefined() }) }) describe('Chaos: Resource Exhaustion', () => { it('should handle very large strings', async () => { const { validateSearchQuery } = await import('../../lib/validators.js') // 1MB string const largeString = 'x'.repeat(1024 * 1024) const result = validateSearchQuery(largeString) // Should truncate, not crash expect(result.length).toBeLessThan(largeString.length) }) it('should handle many concurrent validations', async () => { const { validateSearchQuery, validateLimit, escapeSQL } = await import('../../lib/validators.js') const start = performance.now() // 10000 validations for (let i = 0; i < 10000; i++) { validateSearchQuery('query ' + i) validateLimit(i) escapeSQL('text ' + i) } const duration = performance.now() - start console.log(` → 30000 validations in ${duration.toFixed(0)}ms`) // Should complete in reasonable time expect(duration).toBeLessThan(5000) }) it('should handle array with many elements', async () => { const { extractKeywords } = await import('../../search.js') // Generate long query const words = Array(1000).fill('word').map((w, i) => w + i) const longQuery = words.join(' ') const start = performance.now() const keywords = extractKeywords(longQuery) const duration = performance.now() - start console.log(` → Extracted ${keywords.length} keywords from ${words.length} words in ${duration.toFixed(0)}ms`) expect(duration).toBeLessThan(1000) }) }) describe('Chaos: Timing and Timeout', () => { it('should handle slow operations', async () => { // Simulate slow operation with timeout const slowOperation = () => new Promise(resolve => setTimeout(() => resolve('done'), 100) ) const timeout = (ms) => new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms) ) // Should complete before timeout const result = await Promise.race([ slowOperation(), timeout(500) ]) expect(result).toBe('done') }) it('should handle timeout race condition', async () => { const operation = () => new Promise(resolve => setTimeout(() => resolve('completed'), 50) ) const timeout = (ms) => new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms) ) // Operation should timeout try { await Promise.race([ operation(), timeout(10) ]) expect.fail('Should have timed out') } catch (e) { expect(e.message).toBe('Timeout') } }) }) describe('Chaos: Error Propagation', () => { it('should propagate errors through async chain', async () => { const step1 = async () => 'step1' const step2 = async () => { throw new Error('Step 2 failed') } const step3 = async () => 'step3' try { await step1() await step2() await step3() expect.fail('Should have thrown') } catch (e) { expect(e.message).toBe('Step 2 failed') } }) it('should handle errors in Promise.all', async () => { const promises = [ Promise.resolve(1), Promise.reject(new Error('Middle failed')), Promise.resolve(3) ] try { await Promise.all(promises) expect.fail('Should have thrown') } catch (e) { expect(e.message).toBe('Middle failed') } }) it('should capture all errors with Promise.allSettled', async () => { const promises = [ Promise.resolve(1), Promise.reject(new Error('Error 1')), Promise.resolve(3), Promise.reject(new Error('Error 2')) ] const results = await Promise.allSettled(promises) const fulfilled = results.filter(r => r.status === 'fulfilled') const rejected = results.filter(r => r.status === 'rejected') expect(fulfilled.length).toBe(2) expect(rejected.length).toBe(2) }) }) describe('Chaos: Edge Case Inputs', () => { it('should handle undefined/null throughout the system', async () => { const { validateLimit, validateDaysBack } = await import('../../lib/validators.js') const edgeCases = [undefined, null, NaN, Infinity, -Infinity, '', [], {}, () => {}] for (const value of edgeCases) { // Should not throw, should return defaults const limit = validateLimit(value) const days = validateDaysBack(value) expect(typeof limit).toBe('number') expect(typeof days).toBe('number') expect(limit).toBeGreaterThan(0) expect(days).toBeGreaterThanOrEqual(0) } }) it('should handle prototype pollution attempts', async () => { const { escapeSQL } = await import('../../lib/validators.js') const malicious = { __proto__: { isAdmin: true }, constructor: { prototype: { isAdmin: true } } } // These should not affect the global Object prototype const testObj = {} expect(testObj.isAdmin).toBeUndefined() // Escaping should work normally const result = escapeSQL(JSON.stringify(malicious)) expect(typeof result).toBe('string') }) it('should handle Symbol inputs', async () => { const { validateSearchQuery } = await import('../../lib/validators.js') try { validateSearchQuery(Symbol('test')) expect.fail('Should have thrown') } catch (e) { expect(e).toBeDefined() } }) })

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/sfls1397/Apple-Tools-MCP'

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