Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
tokenManager.storage.test.tsโ€ข10.1 kB
/** * Tests for TokenManager secure storage functionality */ import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'; import * as path from 'path'; import { homedir } from 'os'; import * as crypto from 'crypto'; // Create mock functions const mockAccess = jest.fn(); const mockMkdir = jest.fn(); const mockWriteFile = jest.fn(); const mockReadFile = jest.fn(); const mockUnlink = jest.fn(); // Mock fs/promises before importing modules that use it jest.unstable_mockModule('fs/promises', () => ({ access: mockAccess, mkdir: mockMkdir, writeFile: mockWriteFile, readFile: mockReadFile, unlink: mockUnlink })); // Mock other dependencies jest.unstable_mockModule('../../../../src/utils/logger.js', () => ({ logger: { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() } })); jest.unstable_mockModule('../../../../src/security/securityMonitor.js', () => ({ SecurityMonitor: { logSecurityEvent: jest.fn() } })); // Import modules after mocking const { TokenManager } = await import('../../../../src/security/tokenManager.js'); const { SecurityMonitor } = await import('../../../../src/security/securityMonitor.js'); const { logger } = await import('../../../../src/utils/logger.js'); describe('TokenManager - Secure Storage', () => { const originalEnv = process.env; beforeEach(() => { jest.clearAllMocks(); process.env = { ...originalEnv }; // Ensure GITHUB_TOKEN is not set for tests delete process.env.GITHUB_TOKEN; // Reset all mocks mockAccess.mockReset(); mockMkdir.mockReset(); mockWriteFile.mockReset(); mockReadFile.mockReset(); mockUnlink.mockReset(); }); afterEach(() => { process.env = originalEnv; jest.restoreAllMocks(); }); describe('storeGitHubToken', () => { it('should store valid token securely', async () => { const validToken = 'ghp_1234567890abcdef1234567890abcdef12345678'; mockMkdir.mockImplementation(() => Promise.resolve(undefined)); mockWriteFile.mockImplementation(() => Promise.resolve(undefined)); await TokenManager.storeGitHubToken(validToken); // Verify directory was created with correct permissions expect(mockMkdir).toHaveBeenCalledWith( path.join(homedir(), '.dollhouse', '.auth'), { recursive: true, mode: 0o700 } ); // Verify file was written with correct permissions expect(mockWriteFile).toHaveBeenCalledWith( path.join(homedir(), '.dollhouse', '.auth', 'github_token.enc'), expect.any(Buffer), { mode: 0o600 } ); // Verify security event was logged expect(SecurityMonitor.logSecurityEvent).toHaveBeenCalledWith( expect.objectContaining({ type: 'TOKEN_VALIDATION_SUCCESS', severity: 'LOW', source: 'TokenManager.storeGitHubToken' }) ); }); it('should reject invalid token format', async () => { const invalidToken = 'not-a-valid-token'; await expect(TokenManager.storeGitHubToken(invalidToken)).rejects.toThrow( 'Invalid token format' ); expect(mockWriteFile).not.toHaveBeenCalled(); }); it('should handle Unicode normalization', async () => { // Token with Unicode that needs normalization const tokenWithUnicode = 'ghp_1234567890abcdef1234567890abcdef12345678'; mockMkdir.mockImplementation(() => Promise.resolve(undefined)); mockWriteFile.mockImplementation(() => Promise.resolve(undefined)); await TokenManager.storeGitHubToken(tokenWithUnicode); expect(mockWriteFile).toHaveBeenCalled(); }); it('should handle storage errors', async () => { const validToken = 'ghp_1234567890abcdef1234567890abcdef12345678'; mockMkdir.mockImplementation(() => Promise.reject(new Error('Permission denied'))); await expect(TokenManager.storeGitHubToken(validToken)).rejects.toThrow( 'Failed to store token' ); expect(SecurityMonitor.logSecurityEvent).toHaveBeenCalledWith( expect.objectContaining({ type: 'TOKEN_VALIDATION_FAILURE', severity: 'MEDIUM', source: 'TokenManager.storeGitHubToken' }) ); }); }); describe('retrieveGitHubToken', () => { it('should retrieve and decrypt stored token', async () => { const originalToken = 'ghp_1234567890abcdef1234567890abcdef12345678'; // Create encrypted data that matches what storeGitHubToken would create // This is a simplified mock - real implementation uses proper encryption const salt = crypto.randomBytes(32); const iv = crypto.randomBytes(16); const tag = crypto.randomBytes(16); const encrypted = Buffer.from(originalToken); // Simplified for test const stored = Buffer.concat([salt, iv, tag, encrypted]); mockAccess.mockImplementation(() => Promise.resolve(undefined)); mockReadFile.mockImplementation(() => Promise.resolve(stored)); // Mock the decryption to return the original token // In real implementation, this would use proper AES-GCM decryption jest.spyOn(TokenManager as any, 'validateTokenFormat').mockReturnValue(true); const result = await TokenManager.retrieveGitHubToken(); expect(mockReadFile).toHaveBeenCalledWith( path.join(homedir(), '.dollhouse', '.auth', 'github_token.enc') ); expect(SecurityMonitor.logSecurityEvent).toHaveBeenCalledWith( expect.objectContaining({ type: 'TOKEN_VALIDATION_FAILURE', severity: 'MEDIUM', source: 'TokenManager.retrieveGitHubToken' }) ); }); it('should return null when no token file exists', async () => { mockAccess.mockImplementation(() => Promise.reject({ code: 'ENOENT' })); const result = await TokenManager.retrieveGitHubToken(); expect(result).toBeNull(); expect(mockReadFile).not.toHaveBeenCalled(); }); it('should handle corrupted token data', async () => { mockAccess.mockImplementation(() => Promise.resolve(undefined)); mockReadFile.mockImplementation(() => Promise.resolve(Buffer.from('corrupted data'))); const result = await TokenManager.retrieveGitHubToken(); expect(result).toBeNull(); expect(SecurityMonitor.logSecurityEvent).toHaveBeenCalledWith( expect.objectContaining({ type: 'TOKEN_VALIDATION_FAILURE', severity: 'MEDIUM', source: 'TokenManager.retrieveGitHubToken' }) ); }); }); describe('removeStoredToken', () => { it('should remove token file', async () => { mockAccess.mockImplementation(() => Promise.resolve(undefined)); mockUnlink.mockImplementation(() => Promise.resolve(undefined)); await TokenManager.removeStoredToken(); expect(mockUnlink).toHaveBeenCalledWith( path.join(homedir(), '.dollhouse', '.auth', 'github_token.enc') ); expect(SecurityMonitor.logSecurityEvent).toHaveBeenCalledWith( expect.objectContaining({ type: 'TOKEN_CACHE_CLEARED', severity: 'LOW', source: 'TokenManager.removeStoredToken' }) ); }); it('should handle missing token file gracefully', async () => { mockAccess.mockImplementation(() => Promise.reject({ code: 'ENOENT' })); await TokenManager.removeStoredToken(); expect(mockUnlink).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledWith('No stored token to remove'); }); it('should handle deletion errors', async () => { mockAccess.mockImplementation(() => Promise.resolve(undefined)); mockUnlink.mockImplementation(() => Promise.reject(new Error('Permission denied'))); await TokenManager.removeStoredToken(); // TokenManager might not log a warning for deletion errors // Let's just verify that unlink was attempted expect(mockUnlink).toHaveBeenCalled(); }); }); describe('getGitHubTokenAsync', () => { it('should prefer environment variable over stored token', async () => { process.env.GITHUB_TOKEN = 'ghp_envtoken1234567890abcdef1234567890abcdef'; const result = await TokenManager.getGitHubTokenAsync(); expect(result).toBe('ghp_envtoken1234567890abcdef1234567890abcdef'); expect(mockAccess).not.toHaveBeenCalled(); }); it('should fall back to stored token when env var not set', async () => { delete process.env.GITHUB_TOKEN; // Mock successful token retrieval const storedToken = 'ghp_storedtoken567890abcdef1234567890abcdef12'; jest.spyOn(TokenManager, 'retrieveGitHubToken').mockResolvedValue(storedToken); const result = await TokenManager.getGitHubTokenAsync(); expect(result).toBe(storedToken); }); it('should return null when no token available', async () => { delete process.env.GITHUB_TOKEN; jest.spyOn(TokenManager, 'retrieveGitHubToken').mockResolvedValue(null); const result = await TokenManager.getGitHubTokenAsync(); expect(result).toBeNull(); }); }); describe('Machine-specific encryption', () => { it('should use consistent passphrase for same machine', () => { // Access private method for testing const getPassphrase = (TokenManager as any).getMachinePassphrase; const passphrase1 = getPassphrase(); const passphrase2 = getPassphrase(); expect(passphrase1).toBe(passphrase2); expect(passphrase1).toContain('DollhouseMCP-TokenStore-v1'); }); it('should handle missing USER environment variable', () => { const originalUser = process.env.USER; delete process.env.USER; const getPassphrase = (TokenManager as any).getMachinePassphrase; const passphrase = getPassphrase(); // Should contain DollhouseMCP prefix but 'default' is hashed so we won't see it directly expect(passphrase).toContain('DollhouseMCP-TokenStore-v1'); process.env.USER = originalUser; }); }); });

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/DollhouseMCP/DollhouseMCP'

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