Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
tokenManager.rateLimit.test.tsโ€ข12 kB
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'; import { TokenManager } from '../../../../src/security/tokenManager.js'; import { SecurityError } from '../../../../src/security/errors.js'; // Mock the logger to avoid console output during tests jest.mock('../../../../src/utils/logger.js', () => ({ logger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() } })); // Mock fetch for API calls const mockFetch = jest.fn() as jest.MockedFunction<typeof fetch>; global.fetch = mockFetch; describe('TokenManager Rate Limiting', () => { beforeEach(() => { jest.clearAllMocks(); TokenManager.resetTokenValidationLimiter(); // Mock successful GitHub API response by default mockFetch.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', headers: new Map([ ['x-ratelimit-remaining', '4999'], ['x-ratelimit-reset', String(Math.floor(Date.now() / 1000) + 3600)], ['x-oauth-scopes', 'public_repo, user:email'] ]) as any, json: async () => ({ login: 'testuser', id: 123 }) } as Response); }); afterEach(() => { TokenManager.resetTokenValidationLimiter(); }); describe('Rate Limiter Factory', () => { it('should create rate limiter with appropriate limits', () => { const rateLimiter = TokenManager.createTokenValidationLimiter(); const status = rateLimiter.getStatus(); expect(status.remainingTokens).toBe(10); expect(status.allowed).toBe(true); }); it('should enforce minimum delay between requests', () => { const rateLimiter = TokenManager.createTokenValidationLimiter(); // First request should be allowed expect(rateLimiter.checkLimit().allowed).toBe(true); rateLimiter.consumeToken(); // Immediate second request should be denied due to 5-second minimum delay const status = rateLimiter.checkLimit(); expect(status.allowed).toBe(false); expect(status.retryAfterMs).toBeGreaterThan(4000); expect(status.retryAfterMs).toBeLessThanOrEqual(5000); }); }); describe('Rate Limit Integration in validateTokenScopes', () => { const validToken = 'ghp_' + 'x'.repeat(36); const requiredScopes = { required: ['public_repo'] }; it('should allow token validation when under rate limit', async () => { const result = await TokenManager.validateTokenScopes(validToken, requiredScopes); expect(result.isValid).toBe(true); expect(result.rateLimitExceeded).toBeUndefined(); expect(mockFetch).toHaveBeenCalledTimes(1); }); it('should throw SecurityError when rate limit exceeded', async () => { // Exhaust the rate limit by making multiple validation calls // The first call will succeed, subsequent ones should fail due to minimum delay await TokenManager.validateTokenScopes(validToken, requiredScopes); // Immediate second validation should fail due to minimum delay (5 seconds) await expect( TokenManager.validateTokenScopes(validToken, requiredScopes) ).rejects.toThrow(SecurityError); try { await TokenManager.validateTokenScopes(validToken, requiredScopes); } catch (error) { expect(error).toBeInstanceOf(SecurityError); if (error instanceof SecurityError) { expect(error.code).toBe('RATE_LIMIT_EXCEEDED'); expect(error.message).toContain('rate limit exceeded'); expect(error.message).toContain('retry in'); } } }); it('should respect minimum delay between validation attempts', async () => { // First validation should succeed const result1 = await TokenManager.validateTokenScopes(validToken, requiredScopes); expect(result1.isValid).toBe(true); // Immediate second validation should fail due to minimum delay await expect( TokenManager.validateTokenScopes(validToken, requiredScopes) ).rejects.toThrow(SecurityError); }); it('should handle rate limit errors properly', async () => { // Make first call to trigger rate limiting await TokenManager.validateTokenScopes(validToken, requiredScopes); try { // Immediate second call should fail await TokenManager.validateTokenScopes(validToken, requiredScopes); fail('Expected SecurityError to be thrown'); } catch (error) { expect(error).toBeInstanceOf(SecurityError); // The first call already made a fetch, second call should be blocked expect(mockFetch).toHaveBeenCalledTimes(1); } }); it('should include rate limit information in error response', async () => { // Make first call await TokenManager.validateTokenScopes(validToken, requiredScopes); // Try to make immediate second request try { await TokenManager.validateTokenScopes(validToken, requiredScopes); fail('Expected SecurityError to be thrown'); } catch (error) { if (error instanceof SecurityError) { expect(error.message).toContain('retry in'); expect(error.code).toBe('RATE_LIMIT_EXCEEDED'); } } }); }); describe('Rate Limit Integration in ensureTokenPermissions', () => { beforeEach(() => { // Mock environment variable process.env.GITHUB_TOKEN = 'ghp_' + 'x'.repeat(36); }); afterEach(() => { delete process.env.GITHUB_TOKEN; }); it('should allow permission check when under rate limit', async () => { const result = await TokenManager.ensureTokenPermissions('read'); expect(result.isValid).toBe(true); expect(result.rateLimitExceeded).toBeUndefined(); }); it('should propagate rate limit errors from validateTokenScopes', async () => { // Make first call to trigger rate limiting await TokenManager.ensureTokenPermissions('read'); // Immediate second call should fail due to rate limit await expect( TokenManager.ensureTokenPermissions('read') ).rejects.toThrow(SecurityError); }); it('should return appropriate error when no token available', async () => { delete process.env.GITHUB_TOKEN; const result = await TokenManager.ensureTokenPermissions('read'); expect(result.isValid).toBe(false); expect(result.error).toBe('No GitHub token available'); expect(result.rateLimitExceeded).toBeUndefined(); }); }); describe('Rate Limit Recovery', () => { it('should allow requests after rate limit window expires', async () => { jest.useFakeTimers(); const validToken = 'ghp_' + 'x'.repeat(36); const requiredScopes = { required: ['public_repo'] }; // Exhaust rate limit with minimum delay for (let i = 0; i < 10; i++) { jest.advanceTimersByTime(6000); // 6 seconds to avoid min delay await TokenManager.validateTokenScopes(validToken, requiredScopes); } // Next request should fail await expect( TokenManager.validateTokenScopes(validToken, requiredScopes) ).rejects.toThrow(SecurityError); // Advance time by 1 hour (rate limit window) jest.advanceTimersByTime(60 * 60 * 1000); // Request should now succeed const result = await TokenManager.validateTokenScopes(validToken, requiredScopes); expect(result.isValid).toBe(true); jest.useRealTimers(); }); it('should reset rate limiter when explicitly requested', async () => { const validToken = 'ghp_' + 'x'.repeat(36); const requiredScopes = { required: ['public_repo'] }; // Make first call to trigger rate limiting await TokenManager.validateTokenScopes(validToken, requiredScopes); // Verify immediate second call fails due to rate limit await expect( TokenManager.validateTokenScopes(validToken, requiredScopes) ).rejects.toThrow(SecurityError); // Reset rate limiter TokenManager.resetTokenValidationLimiter(); // Request should now succeed const result = await TokenManager.validateTokenScopes(validToken, requiredScopes); expect(result.isValid).toBe(true); }); }); describe('Rate Limit Error Handling', () => { it('should log rate limit violations appropriately', async () => { const validToken = 'ghp_' + 'x'.repeat(36); const requiredScopes = { required: ['public_repo'] }; // Make first call await TokenManager.validateTokenScopes(validToken, requiredScopes); try { // Immediate second call should fail and trigger logging await TokenManager.validateTokenScopes(validToken, requiredScopes); } catch (error) { // Expected to throw } // The test is more about ensuring rate limiting works than testing specific log calls // since the logger is mocked and the exact call may vary expect(true).toBe(true); // Rate limiting worked (exception was thrown) }); it('should not leak token information in error messages', async () => { const secretToken = 'ghp_' + 'secretvalue'.repeat(4) + 'abcd'; const requiredScopes = { required: ['public_repo'] }; // Exhaust rate limit const rateLimiter = TokenManager.createTokenValidationLimiter(); for (let i = 0; i < 10; i++) { rateLimiter.consumeToken(); } try { await TokenManager.validateTokenScopes(secretToken, requiredScopes); fail('Expected SecurityError to be thrown'); } catch (error) { if (error instanceof SecurityError) { expect(error.message).not.toContain(secretToken); expect(error.message).not.toContain('secretvalue'); } } }); }); describe('Rate Limit Configuration', () => { it('should use conservative limits for security', () => { const rateLimiter = TokenManager.createTokenValidationLimiter(); const status = rateLimiter.getStatus(); // Should be conservative: only 10 requests per hour expect(status.remainingTokens).toBe(10); // Should have minimum delay of 5 seconds rateLimiter.consumeToken(); const nextStatus = rateLimiter.checkLimit(); expect(nextStatus.allowed).toBe(false); expect(nextStatus.retryAfterMs).toBeGreaterThan(4000); }); it('should prevent rapid successive validation attempts', () => { const rateLimiter = TokenManager.createTokenValidationLimiter(); // First request expect(rateLimiter.checkLimit().allowed).toBe(true); rateLimiter.consumeToken(); // Rapid successive requests should be blocked for (let i = 0; i < 5; i++) { const status = rateLimiter.checkLimit(); expect(status.allowed).toBe(false); expect(status.retryAfterMs).toBeGreaterThan(0); } }); }); describe('Integration with GitHub API Rate Limits', () => { it('should track both local and GitHub API rate limits', async () => { const validToken = 'ghp_' + 'x'.repeat(36); const requiredScopes = { required: ['public_repo'] }; // Mock GitHub API response with rate limit info mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', headers: new Map([ ['x-ratelimit-remaining', '59'], ['x-ratelimit-reset', String(Math.floor(Date.now() / 1000) + 3600)], ['x-oauth-scopes', 'public_repo, user:email'] ]) as any, json: async () => ({ login: 'testuser', id: 123 }) } as Response); const result = await TokenManager.validateTokenScopes(validToken, requiredScopes); expect(result.isValid).toBe(true); expect(result.rateLimit?.remaining).toBe(59); expect(result.rateLimit?.resetTime).toBeInstanceOf(Date); }); }); });

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