Skip to main content
Glama

1MCP Server

clientSessionRepository.test.ts26.1 kB
import fs from 'fs'; import { tmpdir } from 'os'; import path from 'path'; import { ClientSessionData } from '@src/auth/sessionTypes.js'; import { AUTH_CONFIG } from '@src/constants.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { ClientSessionRepository } from './clientSessionRepository.js'; import { FileStorageService } from './fileStorageService.js'; // Mock logger to avoid console output during tests vi.mock('@src/logger/logger.js', () => ({ default: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn(), }, })); describe('ClientSessionRepository', () => { let repository: ClientSessionRepository; let storage: FileStorageService; let tempDir: string; beforeEach(() => { // Create a temporary directory for testing tempDir = path.join(tmpdir(), `client-session-repo-test-${Date.now()}`); storage = new FileStorageService(tempDir); repository = new ClientSessionRepository(storage); }); afterEach(() => { storage.shutdown(); // Clean up temp directory if (fs.existsSync(tempDir)) { fs.rmSync(tempDir, { recursive: true, force: true }); } }); describe('save', () => { it('should save client session with all fields', () => { const serverName = 'test-server'; const clientSessionData: ClientSessionData = { serverName: 'test-server', clientInfo: JSON.stringify({ client_id: 'test-client-123', client_secret: 'secret-value', redirect_uris: ['https://app.com/callback'], }), tokens: JSON.stringify({ access_token: 'access-token-123', refresh_token: 'refresh-token-456', token_type: 'Bearer', expires_in: 3600, }), codeVerifier: 'test-code-verifier', state: 'test-state', expires: Date.now() + 3600000, createdAt: Date.now(), }; const ttlMs = 3600000; // 1 hour const result = repository.save(serverName, clientSessionData, ttlMs); expect(result).toBe('test-server'); // Server name should be returned const retrieved = repository.get(serverName); expect(retrieved).toBeDefined(); expect(retrieved!.serverName).toBe('test-server'); expect(retrieved!.clientInfo).toBe(clientSessionData.clientInfo); expect(retrieved!.tokens).toBe(clientSessionData.tokens); expect(retrieved!.codeVerifier).toBe(clientSessionData.codeVerifier); expect(retrieved!.state).toBe(clientSessionData.state); expect(retrieved!.expires).toBeGreaterThan(Date.now()); expect(retrieved!.createdAt).toBeLessThanOrEqual(Date.now()); }); it('should save client session with minimal required fields', () => { const serverName = 'minimal-server'; const clientSessionData: ClientSessionData = { serverName: 'minimal-server', expires: Date.now() + 3600000, createdAt: Date.now(), }; const ttlMs = 3600000; repository.save(serverName, clientSessionData, ttlMs); const retrieved = repository.get(serverName); expect(retrieved!.serverName).toBe('minimal-server'); expect(retrieved!.clientInfo).toBeUndefined(); expect(retrieved!.tokens).toBeUndefined(); expect(retrieved!.codeVerifier).toBeUndefined(); expect(retrieved!.state).toBeUndefined(); }); it('should sanitize server names for security', () => { const maliciousServerName = '../../../etc/passwd'; const clientSessionData: ClientSessionData = { serverName: maliciousServerName, expires: Date.now() + 3600000, createdAt: Date.now(), }; const result = repository.save(maliciousServerName, clientSessionData, 3600000); // Should sanitize the server name expect(result).not.toBe(maliciousServerName); expect(result).toMatch(/^[a-zA-Z0-9_-]+$/); }); it('should set correct expiration time based on TTL', () => { const serverName = 'expiry-test-server'; const ttlMs = 30000; // 30 seconds const beforeSave = Date.now(); const clientSessionData: ClientSessionData = { serverName, expires: Date.now() + 3600000, createdAt: Date.now(), }; repository.save(serverName, clientSessionData, ttlMs); const afterSave = Date.now(); const retrieved = repository.get(serverName); const expectedMinExpiry = beforeSave + ttlMs; const expectedMaxExpiry = afterSave + ttlMs; expect(retrieved!.expires).toBeGreaterThanOrEqual(expectedMinExpiry); expect(retrieved!.expires).toBeLessThanOrEqual(expectedMaxExpiry); }); it('should preserve createdAt when provided', () => { const serverName = 'created-at-test'; const pastTimestamp = Date.now() - 60000; // 1 minute ago const clientSessionData: ClientSessionData = { serverName, expires: Date.now() + 3600000, createdAt: pastTimestamp, }; repository.save(serverName, clientSessionData, 3600000); const retrieved = repository.get(serverName); expect(retrieved!.createdAt).toBe(pastTimestamp); }); it('should set createdAt when not provided', () => { const serverName = 'no-created-at-test'; const beforeSave = Date.now(); const clientSessionData: ClientSessionData = { serverName, expires: Date.now() + 3600000, createdAt: Date.now(), }; delete (clientSessionData as any).createdAt; repository.save(serverName, clientSessionData, 3600000); const afterSave = Date.now(); const retrieved = repository.get(serverName); expect(retrieved!.createdAt).toBeGreaterThanOrEqual(beforeSave); expect(retrieved!.createdAt).toBeLessThanOrEqual(afterSave); }); it('should overwrite existing client session', () => { const serverName = 'overwrite-test-server'; const originalData: ClientSessionData = { serverName, clientInfo: JSON.stringify({ client_id: 'original-client' }), expires: Date.now() + 3600000, createdAt: Date.now(), }; const updatedData: ClientSessionData = { serverName, clientInfo: JSON.stringify({ client_id: 'updated-client' }), tokens: JSON.stringify({ access_token: 'new-token' }), expires: Date.now() + 3600000, createdAt: Date.now(), }; repository.save(serverName, originalData, 3600000); repository.save(serverName, updatedData, 3600000); const retrieved = repository.get(serverName); expect(JSON.parse(retrieved!.clientInfo!)).toEqual({ client_id: 'updated-client' }); expect(retrieved!.tokens).toBeDefined(); }); }); describe('get', () => { it('should retrieve existing client session', () => { const serverName = 'get-test-server'; const clientSessionData: ClientSessionData = { serverName, clientInfo: JSON.stringify({ client_id: 'get-test-client' }), tokens: JSON.stringify({ access_token: 'test-token' }), codeVerifier: 'test-verifier', state: 'test-state', expires: Date.now() + 3600000, createdAt: Date.now(), }; repository.save(serverName, clientSessionData, 3600000); const retrieved = repository.get(serverName); expect(retrieved).toBeDefined(); expect(retrieved!.serverName).toBe(serverName); expect(retrieved!.clientInfo).toBe(clientSessionData.clientInfo); expect(retrieved!.tokens).toBe(clientSessionData.tokens); expect(retrieved!.codeVerifier).toBe(clientSessionData.codeVerifier); expect(retrieved!.state).toBe(clientSessionData.state); }); it('should return null for non-existent client session', () => { const result = repository.get('nonexistent-server'); expect(result).toBeNull(); }); it('should return null for expired client session', () => { const serverName = 'expired-test-server'; const expiredData: ClientSessionData = { serverName, expires: Date.now() - 1000, // Expired 1 second ago createdAt: Date.now() - 3600000, }; // Directly save expired data to storage to bypass TTL calculation storage.writeData( AUTH_CONFIG.CLIENT.SESSION.FILE_PREFIX, `${AUTH_CONFIG.CLIENT.SESSION.ID_PREFIX}${serverName}`, expiredData, ); const result = repository.get(serverName); expect(result).toBeNull(); }); it('should handle malformed server names gracefully', () => { const malformedNames = ['', ' ', 'invalid/name', '../../../etc/passwd']; for (const name of malformedNames) { const result = repository.get(name); expect(result).toBeNull(); } }); it('should sanitize server names consistently', () => { const serverName = 'test_server-123'; const clientSessionData: ClientSessionData = { serverName, expires: Date.now() + 3600000, createdAt: Date.now(), }; repository.save(serverName, clientSessionData, 3600000); const retrieved = repository.get(serverName); expect(retrieved).toBeDefined(); expect(retrieved!.serverName).toBe(serverName); }); }); describe('delete', () => { it('should delete existing client session', () => { const serverName = 'delete-test-server'; const clientSessionData: ClientSessionData = { serverName, clientInfo: JSON.stringify({ client_id: 'delete-test' }), expires: Date.now() + 3600000, createdAt: Date.now(), }; repository.save(serverName, clientSessionData, 3600000); // Verify session exists expect(repository.get(serverName)).toBeDefined(); // Delete session const deleted = repository.delete(serverName); expect(deleted).toBe(true); // Verify session is gone expect(repository.get(serverName)).toBeNull(); }); it('should return false when deleting non-existent client session', () => { const deleted = repository.delete('nonexistent-server'); expect(deleted).toBe(false); }); it('should handle multiple deletions of same client session', () => { const serverName = 'multi-delete-server'; const clientSessionData: ClientSessionData = { serverName, expires: Date.now() + 3600000, createdAt: Date.now(), }; repository.save(serverName, clientSessionData, 3600000); const deleted1 = repository.delete(serverName); expect(deleted1).toBe(true); const deleted2 = repository.delete(serverName); expect(deleted2).toBe(false); }); it('should delete only the specified client session', () => { const serverName1 = 'server-1'; const serverName2 = 'server-2'; const sessionData1: ClientSessionData = { serverName: serverName1, expires: Date.now() + 3600000, createdAt: Date.now(), }; const sessionData2: ClientSessionData = { serverName: serverName2, expires: Date.now() + 3600000, createdAt: Date.now(), }; repository.save(serverName1, sessionData1, 3600000); repository.save(serverName2, sessionData2, 3600000); repository.delete(serverName1); expect(repository.get(serverName1)).toBeNull(); expect(repository.get(serverName2)).toBeDefined(); }); }); describe('list', () => { it('should return empty array when no client sessions exist', () => { const result = repository.list(); expect(result).toEqual([]); }); it('should list all client session server names', () => { const serverNames = ['server-1', 'server-2', 'server-3']; for (const serverName of serverNames) { const sessionData: ClientSessionData = { serverName, expires: Date.now() + 3600000, createdAt: Date.now(), }; repository.save(serverName, sessionData, 3600000); } const result = repository.list(); expect(result).toHaveLength(3); expect(result).toEqual(expect.arrayContaining(serverNames)); }); it('should not list expired sessions (they should be cleaned up)', () => { const validServerName = 'valid-server'; const validSessionData: ClientSessionData = { serverName: validServerName, expires: Date.now() + 3600000, createdAt: Date.now(), }; repository.save(validServerName, validSessionData, 3600000); // Manually create an expired session const expiredServerName = 'expired-server'; const expiredData: ClientSessionData = { serverName: expiredServerName, expires: Date.now() - 1000, createdAt: Date.now() - 3600000, }; storage.writeData( AUTH_CONFIG.CLIENT.SESSION.FILE_PREFIX, `${AUTH_CONFIG.CLIENT.SESSION.ID_PREFIX}${expiredServerName}`, expiredData, ); const result = repository.list(); expect(result).toContain(validServerName); // Note: The expired session might still show in list() since it doesn't check expiration // but get() will return null for expired sessions }); it('should handle server names with special characters', () => { // Test server names that contain special characters that need sanitization const testCases = [ { input: 'server-test1', expected: 'server-test1' }, { input: 'server_test2', expected: 'server_test2' }, { input: 'server/test3', expected: 'server_test3' }, { input: 'server@test4', expected: 'server_test4' }, ]; for (const testCase of testCases) { const sessionData: ClientSessionData = { serverName: testCase.input, expires: Date.now() + 3600000, createdAt: Date.now(), }; const sanitizedName = repository.save(testCase.input, sessionData, 3600000); // The sanitized name should be safe for filesystem use expect(sanitizedName).toMatch(/^[a-zA-Z0-9_-]+$/); // Should be able to retrieve the session const retrieved = repository.get(testCase.input); expect(retrieved).toBeDefined(); expect(retrieved!.serverName).toBe(testCase.input); } }); it('should filter out non-client-session files', () => { // Create a client session const serverName = 'test-server'; const sessionData: ClientSessionData = { serverName, expires: Date.now() + 3600000, createdAt: Date.now(), }; repository.save(serverName, sessionData, 3600000); // Create some other files that shouldn't be listed const otherFiles = ['session_other-session.json', 'auth_code_some-code.json', 'random_file.json']; for (const fileName of otherFiles) { const filePath = path.join(tempDir, fileName); fs.writeFileSync(filePath, JSON.stringify({ test: 'data' })); } const result = repository.list(); expect(result).toEqual([serverName]); }); it('should work correctly with non-empty FILE_PREFIX', () => { // Temporarily mock the FILE_PREFIX to test robustness const originalFilePrefix = AUTH_CONFIG.CLIENT.SESSION.FILE_PREFIX; (AUTH_CONFIG.CLIENT.SESSION as any).FILE_PREFIX = 'client_session_'; try { const serverName = 'test-server'; const sessionData: ClientSessionData = { serverName, expires: Date.now() + 3600000, createdAt: Date.now(), }; // Create a new repository instance to use the modified config const newRepository = new ClientSessionRepository(storage); newRepository.save(serverName, sessionData, 3600000); // Create some other files that shouldn't be listed const otherFiles = ['session_other-session.json', 'auth_code_some-code.json', 'random_file.json']; for (const fileName of otherFiles) { const filePath = path.join(tempDir, fileName); fs.writeFileSync(filePath, JSON.stringify({ test: 'data' })); } const result = newRepository.list(); expect(result).toEqual([serverName]); } finally { // Restore original FILE_PREFIX (AUTH_CONFIG.CLIENT.SESSION as any).FILE_PREFIX = originalFilePrefix; } }); }); describe('Client Session Data Structure', () => { it('should preserve all client session fields', () => { const serverName = 'full-data-server'; const fullSessionData: ClientSessionData = { serverName, clientInfo: JSON.stringify({ client_id: 'full-client-123', client_secret: 'full-secret', client_name: 'Full Test Application', redirect_uris: ['https://app.com/callback'], grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], scope: 'openid profile email', }), tokens: JSON.stringify({ access_token: 'access-token-value', refresh_token: 'refresh-token-value', token_type: 'Bearer', expires_in: 3600, scope: 'openid profile email', }), codeVerifier: 'PKCE-code-verifier-value', state: 'OAuth-state-parameter', expires: Date.now() + 3600000, createdAt: Date.now(), }; repository.save(serverName, fullSessionData, 3600000); const retrieved = repository.get(serverName); expect(retrieved!.serverName).toBe(fullSessionData.serverName); expect(retrieved!.clientInfo).toBe(fullSessionData.clientInfo); expect(retrieved!.tokens).toBe(fullSessionData.tokens); expect(retrieved!.codeVerifier).toBe(fullSessionData.codeVerifier); expect(retrieved!.state).toBe(fullSessionData.state); expect(typeof retrieved!.expires).toBe('number'); expect(typeof retrieved!.createdAt).toBe('number'); expect(retrieved!.expires).toBeGreaterThan(retrieved!.createdAt); }); it('should handle JSON stringified client info and tokens', () => { const serverName = 'json-test-server'; const clientInfo = { client_id: 'json-client', client_secret: 'json-secret', redirect_uris: ['https://json.app/callback'], }; const tokens = { access_token: 'json-access-token', refresh_token: 'json-refresh-token', token_type: 'Bearer', }; const sessionData: ClientSessionData = { serverName, clientInfo: JSON.stringify(clientInfo), tokens: JSON.stringify(tokens), expires: Date.now() + 3600000, createdAt: Date.now(), }; repository.save(serverName, sessionData, 3600000); const retrieved = repository.get(serverName); const parsedClientInfo = JSON.parse(retrieved!.clientInfo!); const parsedTokens = JSON.parse(retrieved!.tokens!); expect(parsedClientInfo).toEqual(clientInfo); expect(parsedTokens).toEqual(tokens); }); it('should handle optional fields being undefined', () => { const serverName = 'optional-fields-server'; const minimalSessionData: ClientSessionData = { serverName, expires: Date.now() + 3600000, createdAt: Date.now(), }; repository.save(serverName, minimalSessionData, 3600000); const retrieved = repository.get(serverName); expect(retrieved!.serverName).toBe(serverName); expect(retrieved!.clientInfo).toBeUndefined(); expect(retrieved!.tokens).toBeUndefined(); expect(retrieved!.codeVerifier).toBeUndefined(); expect(retrieved!.state).toBeUndefined(); expect(retrieved!.expires).toBeDefined(); expect(retrieved!.createdAt).toBeDefined(); }); }); describe('Integration with FileStorageService', () => { it('should use correct file prefix', () => { const serverName = 'file-prefix-test'; const sessionData: ClientSessionData = { serverName, expires: Date.now() + 3600000, createdAt: Date.now(), }; repository.save(serverName, sessionData, 3600000); // Check that file was created with correct prefix const sessionId = `${AUTH_CONFIG.CLIENT.SESSION.ID_PREFIX}${serverName}`; const expectedFileName = AUTH_CONFIG.CLIENT.SESSION.FILE_PREFIX + sessionId + '.json'; const filePath = path.join(tempDir, expectedFileName); expect(fs.existsSync(filePath)).toBe(true); }); it('should survive FileStorageService restart', () => { const serverName = 'restart-test-server'; const sessionData: ClientSessionData = { serverName, clientInfo: JSON.stringify({ client_id: 'restart-client' }), expires: Date.now() + 3600000, createdAt: Date.now(), }; repository.save(serverName, sessionData, 3600000); const originalData = repository.get(serverName); // Shutdown and recreate storage service storage.shutdown(); storage = new FileStorageService(tempDir); repository = new ClientSessionRepository(storage); // Data should still be accessible const retrievedData = repository.get(serverName); expect(retrievedData).toEqual(originalData); }); it('should handle storage errors gracefully', () => { // This test would need to mock FileStorageService to simulate errors // For now, we verify that the repository doesn't crash on invalid operations const result = repository.get('invalid-server-name'); expect(result).toBeNull(); }); }); describe('Performance and Edge Cases', () => { it('should handle creating many client sessions', () => { const serverNames: string[] = []; const numSessions = 10; // Reduced for testing performance for (let i = 0; i < numSessions; i++) { const serverName = `server-${i}`; const sessionData: ClientSessionData = { serverName, clientInfo: JSON.stringify({ client_id: `client-${i}` }), expires: Date.now() + 3600000, createdAt: Date.now(), }; repository.save(serverName, sessionData, 3600000); serverNames.push(serverName); } expect(serverNames.length).toBe(numSessions); // Verify all sessions can be retrieved for (const serverName of serverNames) { const retrieved = repository.get(serverName); expect(retrieved).toBeDefined(); } // Verify list includes all servers const listedServers = repository.list(); expect(listedServers.length).toBeGreaterThanOrEqual(numSessions); }); it('should handle very long server names', () => { const longServerName = 'a'.repeat(200); // Very long server name const sessionData: ClientSessionData = { serverName: longServerName, expires: Date.now() + 3600000, createdAt: Date.now(), }; repository.save(longServerName, sessionData, 3600000); const retrieved = repository.get(longServerName); expect(retrieved).toBeDefined(); expect(retrieved!.serverName).toBe(longServerName); }); it('should handle complex JSON data in client info and tokens', () => { const serverName = 'complex-json-server'; const complexClientInfo = { client_id: 'complex-client', client_name: 'Complex Application with 特殊字符 and émojis 🚀', redirect_uris: ['https://app.example.com/callback', 'myapp://oauth/callback', 'http://localhost:3000/auth'], metadata: { version: '2.1', features: ['pkce', 'refresh_tokens'], nested: { deep: { value: 'test', }, }, }, }; const complexTokens = { access_token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...', refresh_token: 'rt_' + 'x'.repeat(100), token_type: 'Bearer', expires_in: 3600, scope: 'openid profile email admin:read admin:write', custom_claims: { user_id: '12345', roles: ['admin', 'user'], }, }; const sessionData: ClientSessionData = { serverName, clientInfo: JSON.stringify(complexClientInfo), tokens: JSON.stringify(complexTokens), codeVerifier: 'a'.repeat(128), // Maximum PKCE verifier length state: JSON.stringify({ returnTo: '/dashboard', userId: 12345 }), expires: Date.now() + 3600000, createdAt: Date.now(), }; repository.save(serverName, sessionData, 3600000); const retrieved = repository.get(serverName); const parsedClientInfo = JSON.parse(retrieved!.clientInfo!); const parsedTokens = JSON.parse(retrieved!.tokens!); const parsedState = JSON.parse(retrieved!.state!); expect(parsedClientInfo).toEqual(complexClientInfo); expect(parsedTokens).toEqual(complexTokens); expect(parsedState).toEqual({ returnTo: '/dashboard', userId: 12345 }); }); it('should handle different TTL values', () => { const shortTtl = 1000; // 1 second const mediumTtl = 60 * 60 * 1000; // 1 hour const longTtl = 30 * 24 * 60 * 60 * 1000; // 30 days const testCases = [ { name: 'short-ttl-server', ttl: shortTtl }, { name: 'medium-ttl-server', ttl: mediumTtl }, { name: 'long-ttl-server', ttl: longTtl }, ]; for (const testCase of testCases) { const sessionData: ClientSessionData = { serverName: testCase.name, expires: Date.now() + testCase.ttl, createdAt: Date.now(), }; const beforeSave = Date.now(); repository.save(testCase.name, sessionData, testCase.ttl); const afterSave = Date.now(); const retrieved = repository.get(testCase.name); expect(retrieved!.expires).toBeGreaterThanOrEqual(beforeSave + testCase.ttl); expect(retrieved!.expires).toBeLessThanOrEqual(afterSave + testCase.ttl); } }); }); });

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