Skip to main content
Glama

ClinicalTrials.gov MCP Server

StorageService.test.ts•12.8 kB
/** * @fileoverview Tests for StorageService tenant ID validation and security. * @module tests/storage/StorageService.test */ import { beforeEach, describe, expect, it } from 'vitest'; import { container } from 'tsyringe'; import { StorageService } from '@/storage/core/StorageService.js'; import { InMemoryProvider } from '@/storage/providers/inMemory/inMemoryProvider.js'; import { StorageProvider } from '@/container/tokens.js'; import { JsonRpcErrorCode, McpError } from '@/types-global/errors.js'; import { requestContextService } from '@/utils/index.js'; import type { RequestContext } from '@/utils/index.js'; describe('StorageService - Tenant ID Validation', () => { let storageService: StorageService; let baseContext: RequestContext; beforeEach(() => { // Register a fresh in-memory provider for each test container.clearInstances(); container.registerSingleton(StorageProvider, InMemoryProvider); storageService = container.resolve(StorageService); baseContext = requestContextService.createRequestContext({ operation: 'test-storage-service', }); }); describe('Valid Tenant IDs', () => { it('should accept simple alphanumeric tenant ID', async () => { const context = { ...baseContext, tenantId: 'tenant123' }; await expect( storageService.set('test-key', 'test-value', context), ).resolves.toBeUndefined(); }); it('should accept tenant ID with hyphens', async () => { const context = { ...baseContext, tenantId: 'tenant-123' }; await expect( storageService.set('test-key', 'test-value', context), ).resolves.toBeUndefined(); }); it('should accept tenant ID with underscores', async () => { const context = { ...baseContext, tenantId: 'tenant_123' }; await expect( storageService.set('test-key', 'test-value', context), ).resolves.toBeUndefined(); }); it('should accept tenant ID with dots', async () => { const context = { ...baseContext, tenantId: 'tenant.123' }; await expect( storageService.set('test-key', 'test-value', context), ).resolves.toBeUndefined(); }); it('should accept tenant ID with mixed valid characters', async () => { const context = { ...baseContext, tenantId: 'tenant-123_abc.xyz' }; await expect( storageService.set('test-key', 'test-value', context), ).resolves.toBeUndefined(); }); it('should accept maximum length tenant ID (128 characters)', async () => { const context = { ...baseContext, tenantId: 'a'.repeat(128), }; await expect( storageService.set('test-key', 'test-value', context), ).resolves.toBeUndefined(); }); it('should accept single character tenant ID', async () => { const context = { ...baseContext, tenantId: 'a' }; await expect( storageService.set('test-key', 'test-value', context), ).resolves.toBeUndefined(); }); it('should accept two character tenant ID', async () => { const context = { ...baseContext, tenantId: 'ab' }; await expect( storageService.set('test-key', 'test-value', context), ).resolves.toBeUndefined(); }); it('should trim and accept tenant ID with whitespace', async () => { const context = { ...baseContext, tenantId: ' tenant123 ' }; await expect( storageService.set('test-key', 'test-value', context), ).resolves.toBeUndefined(); // Verify the trimmed value was used const result = await storageService.get<string>('test-key', { ...baseContext, tenantId: 'tenant123', }); expect(result).toBe('test-value'); }); }); describe('Invalid Tenant IDs - Missing or Empty', () => { it('should reject missing tenant ID', async () => { const context = { ...baseContext }; // No tenantId let thrown: Error | null = null; try { await storageService.set('test-key', 'test-value', context); } catch (error) { thrown = error as Error; } expect(thrown).toBeInstanceOf(McpError); const mcpError = thrown as McpError; expect(mcpError.code).toBe(JsonRpcErrorCode.InternalError); expect(mcpError.message).toContain('Tenant ID is required'); }); it('should reject empty string tenant ID', async () => { const context = { ...baseContext, tenantId: '' }; let thrown: Error | null = null; try { await storageService.set('test-key', 'test-value', context); } catch (error) { thrown = error as Error; } expect(thrown).toBeInstanceOf(McpError); const mcpError = thrown as McpError; expect(mcpError.code).toBe(JsonRpcErrorCode.InvalidParams); expect(mcpError.message).toContain('cannot be an empty string'); }); it('should reject whitespace-only tenant ID', async () => { const context = { ...baseContext, tenantId: ' ' }; let thrown: Error | null = null; try { await storageService.set('test-key', 'test-value', context); } catch (error) { thrown = error as Error; } expect(thrown).toBeInstanceOf(McpError); const mcpError = thrown as McpError; expect(mcpError.code).toBe(JsonRpcErrorCode.InvalidParams); expect(mcpError.message).toContain('cannot be an empty string'); }); }); describe('Invalid Tenant IDs - Length Constraints', () => { it('should reject tenant ID exceeding 128 characters', async () => { const context = { ...baseContext, tenantId: 'a'.repeat(129), }; let thrown: Error | null = null; try { await storageService.set('test-key', 'test-value', context); } catch (error) { thrown = error as Error; } expect(thrown).toBeInstanceOf(McpError); const mcpError = thrown as McpError; expect(mcpError.code).toBe(JsonRpcErrorCode.InvalidParams); expect(mcpError.message).toContain('exceeds maximum length'); }); }); describe('Invalid Tenant IDs - Path Traversal Attacks', () => { it('should reject tenant ID with ../ path traversal', async () => { const context = { ...baseContext, tenantId: '../etc/passwd' }; let thrown: Error | null = null; try { await storageService.set('test-key', 'test-value', context); } catch (error) { thrown = error as Error; } expect(thrown).toBeInstanceOf(McpError); const mcpError = thrown as McpError; expect(mcpError.code).toBe(JsonRpcErrorCode.InvalidParams); expect(mcpError.message).toMatch(/invalid characters|path traversal/i); }); it('should reject tenant ID with ..\\ path traversal', async () => { const context = { ...baseContext, tenantId: '..\\windows\\system32' }; let thrown: Error | null = null; try { await storageService.set('test-key', 'test-value', context); } catch (error) { thrown = error as Error; } expect(thrown).toBeInstanceOf(McpError); const mcpError = thrown as McpError; expect(mcpError.code).toBe(JsonRpcErrorCode.InvalidParams); expect(mcpError.message).toMatch(/invalid characters|path traversal/i); }); it('should reject tenant ID with consecutive dots', async () => { const context = { ...baseContext, tenantId: 'tenant..id' }; let thrown: Error | null = null; try { await storageService.set('test-key', 'test-value', context); } catch (error) { thrown = error as Error; } expect(thrown).toBeInstanceOf(McpError); const mcpError = thrown as McpError; expect(mcpError.code).toBe(JsonRpcErrorCode.InvalidParams); expect(mcpError.message).toContain('consecutive dots'); }); }); describe('Invalid Tenant IDs - Special Characters', () => { it('should reject tenant ID with forward slash', async () => { const context = { ...baseContext, tenantId: 'tenant/123' }; let thrown: Error | null = null; try { await storageService.set('test-key', 'test-value', context); } catch (error) { thrown = error as Error; } expect(thrown).toBeInstanceOf(McpError); const mcpError = thrown as McpError; expect(mcpError.code).toBe(JsonRpcErrorCode.InvalidParams); expect(mcpError.message).toContain('invalid characters'); }); it('should reject tenant ID with backslash', async () => { const context = { ...baseContext, tenantId: 'tenant\\123' }; let thrown: Error | null = null; try { await storageService.set('test-key', 'test-value', context); } catch (error) { thrown = error as Error; } expect(thrown).toBeInstanceOf(McpError); const mcpError = thrown as McpError; expect(mcpError.code).toBe(JsonRpcErrorCode.InvalidParams); expect(mcpError.message).toContain('invalid characters'); }); it('should reject tenant ID with special characters (!@#$%)', async () => { const invalidChars = ['!', '@', '#', '$', '%']; for (const char of invalidChars) { const context = { ...baseContext, tenantId: `tenant${char}123` }; let thrown: Error | null = null; try { await storageService.set('test-key', 'test-value', context); } catch (error) { thrown = error as Error; } expect(thrown).toBeInstanceOf(McpError); const mcpError = thrown as McpError; expect(mcpError.code).toBe(JsonRpcErrorCode.InvalidParams); expect(mcpError.message).toContain('invalid characters'); } }); it('should reject tenant ID starting with hyphen', async () => { const context = { ...baseContext, tenantId: '-tenant123' }; let thrown: Error | null = null; try { await storageService.set('test-key', 'test-value', context); } catch (error) { thrown = error as Error; } expect(thrown).toBeInstanceOf(McpError); const mcpError = thrown as McpError; expect(mcpError.code).toBe(JsonRpcErrorCode.InvalidParams); expect(mcpError.message).toContain('invalid characters'); }); it('should reject tenant ID ending with dot', async () => { const context = { ...baseContext, tenantId: 'tenant123.' }; let thrown: Error | null = null; try { await storageService.set('test-key', 'test-value', context); } catch (error) { thrown = error as Error; } expect(thrown).toBeInstanceOf(McpError); const mcpError = thrown as McpError; expect(mcpError.code).toBe(JsonRpcErrorCode.InvalidParams); expect(mcpError.message).toContain('invalid characters'); }); }); describe('All Storage Operations', () => { it('should validate tenant ID in all methods', async () => { const invalidContext = { ...baseContext, tenantId: '../invalid', }; // Test each method const methods = [ () => storageService.get('key', invalidContext), () => storageService.set('key', 'value', invalidContext), () => storageService.delete('key', invalidContext), () => storageService.list('prefix', invalidContext), () => storageService.getMany(['key1'], invalidContext), () => storageService.setMany(new Map([['k', 'v']]), invalidContext), () => storageService.deleteMany(['key1'], invalidContext), () => storageService.clear(invalidContext), ]; for (const method of methods) { let thrown: Error | null = null; try { await method(); } catch (error) { thrown = error as Error; } expect(thrown).toBeInstanceOf(McpError); const mcpError = thrown as McpError; expect(mcpError.code).toBe(JsonRpcErrorCode.InvalidParams); } }); }); describe('Tenant Isolation', () => { it('should isolate data between tenants', async () => { const tenant1Context = { ...baseContext, tenantId: 'tenant1', }; const tenant2Context = { ...baseContext, tenantId: 'tenant2', }; // Set value for tenant1 await storageService.set('shared-key', 'tenant1-value', tenant1Context); // Set value for tenant2 await storageService.set('shared-key', 'tenant2-value', tenant2Context); // Verify isolation const tenant1Value = await storageService.get<string>( 'shared-key', tenant1Context, ); const tenant2Value = await storageService.get<string>( 'shared-key', tenant2Context, ); expect(tenant1Value).toBe('tenant1-value'); expect(tenant2Value).toBe('tenant2-value'); }); }); });

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/cyanheads/clinicaltrialsgov-mcp-server'

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