diff --git a/src/utils/security/__tests__/blockchainSecurity.test.ts b/src/utils/security/__tests__/blockchainSecurity.test.ts index f9c8dbab..4c3c5d48 100644 --- a/src/utils/security/__tests__/blockchainSecurity.test.ts +++ b/src/utils/security/__tests__/blockchainSecurity.test.ts @@ -1,470 +1,236 @@ import { BlockchainSecurityService, SecurityServiceConfig } from '../blockchainSecurity'; -// Mock fetch for API calls +jest.mock('@/utils/logger', () => ({ + logger: { error: jest.fn(), warn: jest.fn(), info: jest.fn(), debug: jest.fn() }, +})); + global.fetch = jest.fn(); +const mockConfig: SecurityServiceConfig = { + baseUrl: 'http://localhost:3000', + timeout: 5000, +}; + +function createService() { + (BlockchainSecurityService as any).instance = null; + return BlockchainSecurityService.getInstance(mockConfig); +} + describe('BlockchainSecurityService', () => { let service: BlockchainSecurityService; - const mockConfig: SecurityServiceConfig = { - baseUrl: 'http://localhost:3000', - timeout: 5000, - apiKey: undefined - }; beforeEach(() => { jest.clearAllMocks(); - // Reset singleton instance - (BlockchainSecurityService as any).instance = null; - service = BlockchainSecurityService.getInstance(mockConfig); + jest.useFakeTimers(); + service = createService(); + service.clearCache(); }); - describe('getInstance', () => { - it('should create a new instance when none exists', () => { - const instance = BlockchainSecurityService.getInstance(mockConfig); - expect(instance).toBeInstanceOf(BlockchainSecurityService); - }); - - it('should return existing instance on subsequent calls', () => { - const instance1 = BlockchainSecurityService.getInstance(mockConfig); - const instance2 = BlockchainSecurityService.getInstance(mockConfig); - expect(instance1).toBe(instance2); - }); - - it('should throw error when no config provided for first call', () => { - (BlockchainSecurityService as any).instance = null; - expect(() => BlockchainSecurityService.getInstance()).toThrow( - 'Configuration required for first initialization' - ); - }); + afterEach(() => { + jest.useRealTimers(); }); describe('checkAddressRisk', () => { - const testAddress = '0x742d35Cc6634C0532925a3b8D4C9db96C4b4Db45'; - - it('should return cached result when available and not expired', async () => { - const cachedResult = { - address: testAddress, - riskScore: 25, - riskLevel: 'low' as const, - categories: ['low_risk'], - labels: ['monitor'], - description: 'Address appears to have normal activity' - }; - - // Manually set cache - service['cache'].set(`address_${testAddress}`, { - data: cachedResult, - timestamp: Date.now() - }); - - const result = await service.checkAddressRisk(testAddress); - expect(result).toEqual(cachedResult); - expect(fetch).not.toHaveBeenCalled(); - }); - - it('should fetch new data when cache is expired', async () => { - const cachedResult = { - address: testAddress, - riskScore: 25, - riskLevel: 'low' as const, - categories: ['low_risk'], - labels: ['monitor'], - description: 'Address appears to have normal activity' - }; - - // Set expired cache (5 minutes + 1 second ago) - const expiredTime = Date.now() - (5 * 60 * 1000) - 1000; - service['cache'].set(`address_${testAddress}`, { - data: cachedResult, - timestamp: expiredTime - }); + const address = '0x742d35Cc6634C0532925a3b8D4C9db96C4b4Db45'; + it('returns risk data with level and score', async () => { (global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, - json: async () => ({ risk_score: 50, categories: ['medium_risk'] }) + json: async () => ({ risk_score: 30, categories: ['low_risk'], labels: ['monitor'], description: 'Clean' }), }); - const result = await service.checkAddressRisk(testAddress); - expect(fetch).toHaveBeenCalled(); - expect(result.riskScore).toBeGreaterThan(0); + const result = await service.checkAddressRisk(address); + expect(result).toHaveProperty('address', address); + expect(result).toHaveProperty('riskScore'); + expect(result).toHaveProperty('riskLevel'); + expect(['low', 'medium', 'high', 'critical']).toContain(result.riskLevel); + expect(result.riskScore).toBeGreaterThanOrEqual(0); + expect(result.riskScore).toBeLessThanOrEqual(100); }); - it('should call the local proxy endpoint', async () => { + it('caches results for 5 minutes', async () => { (global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, - json: async () => ({ risk_score: 30, categories: ['low_risk'], labels: [], description: 'Normal' }) + json: async () => ({ risk_score: 20, categories: [], labels: [], description: '' }), }); - await service.checkAddressRisk(testAddress); - - const fetchUrl = (global.fetch as jest.Mock).mock.calls[0][0]; - expect(fetchUrl).toContain('/api/security/address-check'); - expect(fetchUrl).toContain(encodeURIComponent(testAddress)); - }); + await service.checkAddressRisk(address); + expect(global.fetch).toHaveBeenCalledTimes(1); - it('should fall back to simulation when proxy returns non-ok', async () => { - (global.fetch as jest.Mock).mockResolvedValueOnce({ - ok: false, - status: 502, - json: async () => ({ error: 'Bad gateway' }) - }); + const result2 = await service.checkAddressRisk(address); + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(result2.riskScore).toBe(20); - const result = await service.checkAddressRisk(testAddress); - expect(result.riskScore).toBeGreaterThanOrEqual(0); - expect(result.riskScore).toBeLessThanOrEqual(100); + jest.advanceTimersByTime(5 * 60 * 1000 - 1); + await service.checkAddressRisk(address); + expect(global.fetch).toHaveBeenCalledTimes(1); }); - it('should return default risk score on API failure', async () => { + it('uses simulated data when API fails', async () => { (global.fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error')); - const result = await service.checkAddressRisk(testAddress); - expect(result).toEqual({ - address: testAddress, - riskScore: 50, - riskLevel: 'medium', - categories: ['unknown'], - labels: ['unable_to_verify'], - description: 'Unable to verify address risk due to service unavailability' - }); - }); - - it('should handle different risk score ranges correctly', async () => { - // Mock different risk scores - const testCases = [ - { score: 10, expectedLevel: 'low' }, - { score: 40, expectedLevel: 'medium' }, - { score: 60, expectedLevel: 'high' }, - { score: 85, expectedLevel: 'critical' } - ]; - - for (const { score, expectedLevel } of testCases) { - // Clear cache and mock the simulation to return specific score - service.clearCache(); - jest.spyOn(service as any, 'simulateAddressRiskCheck').mockResolvedValueOnce({ - score, - categories: [`${expectedLevel}_risk`], - labels: [], - description: 'Test' - }); - - const result = await service.checkAddressRisk(testAddress); - expect(result.riskLevel).toBe(expectedLevel); - } - }); - }); - - describe('checkTransactionRisk', () => { - const testHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; - - it('should return cached transaction risk when available', async () => { - const cachedResult = { - hash: testHash, - riskScore: 30, - riskLevel: 'medium' as const, - alerts: ['Test alert'], - sanctions: false, - mixer: false, - gambling: false, - scam: false - }; - - service['cache'].set(`tx_${testHash}`, { - data: cachedResult, - timestamp: Date.now() - }); - - const result = await service.checkTransactionRisk(testHash); - expect(result).toEqual(cachedResult); - }); - - it('should return default transaction risk on failure', async () => { - jest.spyOn(service as any, 'simulateTransactionRiskCheck').mockRejectedValueOnce(new Error('Simulation failed')); - - const result = await service.checkTransactionRisk(testHash); - expect(result).toEqual({ - hash: testHash, - riskScore: 50, - riskLevel: 'medium', - alerts: ['Unable to verify transaction risk'], - sanctions: false, - mixer: false, - gambling: false, - scam: false - }); + const result = await service.checkAddressRisk(address); + expect(result).toHaveProperty('riskScore'); + expect(result.riskScore).toBeGreaterThanOrEqual(0); + expect(result.riskScore).toBeLessThanOrEqual(100); + expect(result.categories).toBeDefined(); + expect(result.labels).toBeDefined(); }); - it('should handle transaction with sanctions flag', async () => { - jest.spyOn(service as any, 'simulateTransactionRiskCheck').mockResolvedValueOnce({ - score: 95, - alerts: ['Transaction involves sanctioned address'], - sanctions: true, - mixer: false, - gambling: false, - scam: false - }); + it('uses simulated data when API returns non-ok', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ ok: false, status: 500 }); - const result = await service.checkTransactionRisk(testHash); - expect(result.sanctions).toBe(true); - expect(result.alerts).toContain('Transaction involves sanctioned address'); - expect(result.riskLevel).toBe('critical'); + const result = await service.checkAddressRisk(address); + expect(result).toHaveProperty('riskScore'); + expect(result.riskScore).toBeGreaterThanOrEqual(0); }); }); - describe('checkSanctions', () => { - it('should return true when address is sanctioned', async () => { - jest.spyOn(service, 'checkAddressRisk').mockResolvedValueOnce({ - address: '0x123', - riskScore: 90, - riskLevel: 'critical', - categories: ['sanctions'], - labels: [], - description: 'Sanctioned' - }); - - const result = await service.checkSanctions('0x123'); - expect(result).toBe(true); - }); + describe('validateTransaction', () => { + const from = '0x742d35Cc6634C0532925a3b8D4C9db96C4b4Db45'; + const to = '0x1234567890123456789012345678901234567890'; + const value = '1000000000000000000'; - it('should return false when address is not sanctioned', async () => { - jest.spyOn(service, 'checkAddressRisk').mockResolvedValueOnce({ - address: '0x123', - riskScore: 10, + it('returns valid for clean transaction', async () => { + jest.spyOn(service, 'checkAddressRisk').mockResolvedValue({ + address: 'test', + riskScore: 5, riskLevel: 'low', categories: ['low_risk'], labels: [], - description: 'Clean' + description: 'Clean', }); - const result = await service.checkSanctions('0x123'); - expect(result).toBe(false); - }); - - it('should return false on error', async () => { - jest.spyOn(service, 'checkAddressRisk').mockRejectedValueOnce(new Error('API Error')); - - const result = await service.checkSanctions('0x123'); - expect(result).toBe(false); - }); - }); - - describe('checkMixer', () => { - it('should return true when address is associated with mixer', async () => { - jest.spyOn(service, 'checkAddressRisk').mockResolvedValueOnce({ - address: '0x123', - riskScore: 70, - riskLevel: 'high', - categories: ['mixer'], - labels: [], - description: 'Mixer' - }); - - const result = await service.checkMixer('0x123'); - expect(result).toBe(true); + const result = await service.validateTransaction(from, to, value); + expect(result.isValid).toBe(true); + expect(result.blocks).toHaveLength(0); }); - it('should return false when address is not associated with mixer', async () => { - jest.spyOn(service, 'checkAddressRisk').mockResolvedValueOnce({ - address: '0x123', + it('blocks sanctioned addresses', async () => { + jest.spyOn(service, 'checkAddressRisk').mockResolvedValue({ + address: 'test', riskScore: 10, riskLevel: 'low', - categories: ['low_risk'], + categories: ['sanctions'], labels: [], - description: 'Clean' + description: 'Sanctioned', }); - const result = await service.checkMixer('0x123'); - expect(result).toBe(false); - }); - }); - - describe('getSecurityAlerts', () => { - it('should return security alerts for address', async () => { - jest.spyOn(service, 'checkAddressRisk').mockResolvedValueOnce({ - address: '0x123', - riskScore: 80, - riskLevel: 'high', - categories: ['scam', 'mixer'], - labels: ['suspicious'], - description: 'High risk address' - }); - - const alerts = await service.getSecurityAlerts('0x123'); - expect(alerts).toHaveLength(2); - expect(alerts[0].type).toBe('scam'); - expect(alerts[0].severity).toBe('high'); - expect(alerts[1].type).toBe('mixer'); - }); - - it('should return empty array on error', async () => { - jest.spyOn(service, 'checkAddressRisk').mockRejectedValueOnce(new Error('API Error')); - - const alerts = await service.getSecurityAlerts('0x123'); - expect(alerts).toEqual([]); + const result = await service.validateTransaction(from, to, value); + expect(result.isValid).toBe(false); + expect(result.blocks).toContain('Sender address is on sanctions list'); + expect(result.blocks).toContain('Recipient address is on sanctions list'); }); - }); - - describe('validateTransaction', () => { - const fromAddress = '0x742d35Cc6634C0532925a3b8D4C9db96C4b4Db45'; - const toAddress = '0x1234567890123456789012345678901234567890'; - const value = '1000000000000000000'; // 1 ETH - it('should validate transaction successfully with low risk', async () => { - jest.spyOn(service, 'checkAddressRisk').mockImplementation(async (address) => ({ - address, + it('warns about mixer interactions', async () => { + jest.spyOn(service, 'checkAddressRisk').mockResolvedValue({ + address: 'test', riskScore: 10, riskLevel: 'low', - categories: ['low_risk'], + categories: ['mixer'], labels: [], - description: 'Clean address' - })); + description: 'Mixer associated', + }); - const result = await service.validateTransaction(fromAddress, toAddress, value); + const result = await service.validateTransaction(from, to, value); expect(result.isValid).toBe(true); - expect(result.riskScore).toBe(10); - expect(result.warnings).toHaveLength(0); - expect(result.blocks).toHaveLength(0); + expect(result.warnings).toContain('Transaction involves mixer-associated address'); }); - it('should block transaction with critical risk sender', async () => { - jest.spyOn(service, 'checkAddressRisk').mockImplementation(async (address) => ({ - address, - riskScore: address === fromAddress ? 90 : 10, - riskLevel: address === fromAddress ? 'critical' : 'low', - categories: address === fromAddress ? ['high_risk'] : ['low_risk'], + it('blocks critical risk addresses', async () => { + jest.spyOn(service, 'checkAddressRisk').mockImplementation(async (addr: string) => ({ + address: addr, + riskScore: 90, + riskLevel: 'critical' as const, + categories: ['high_risk'], labels: [], - description: address === fromAddress ? 'Critical risk' : 'Clean' + description: 'Critical', })); - const result = await service.validateTransaction(fromAddress, toAddress, value); + const result = await service.validateTransaction(from, to, value); expect(result.isValid).toBe(false); expect(result.blocks).toContain('Sender address has critical risk level'); + expect(result.blocks).toContain('Recipient address has critical risk level'); }); - it('should block transaction with sanctioned addresses', async () => { + it('handles hex value format', async () => { jest.spyOn(service, 'checkAddressRisk').mockResolvedValue({ address: 'test', - riskScore: 10, + riskScore: 5, riskLevel: 'low', - categories: ['sanctions'], + categories: [], labels: [], - description: 'Sanctioned' + description: '', }); - const result = await service.validateTransaction(fromAddress, toAddress, value); - expect(result.isValid).toBe(false); - expect(result.blocks).toContain('Sender address is on sanctions list'); - expect(result.blocks).toContain('Recipient address is on sanctions list'); + const result = await service.validateTransaction(from, to, '0x1'); + expect(result.isValid).toBe(true); }); - it('should warn about high-value transaction to risky address', async () => { - jest.spyOn(service, 'checkAddressRisk').mockImplementation(async (address) => ({ - address, - riskScore: address === toAddress ? 60 : 10, - riskLevel: address === toAddress ? 'high' : 'low', - categories: address === toAddress ? ['high_risk'] : ['low_risk'], + it('handles decimal wei value format', async () => { + jest.spyOn(service, 'checkAddressRisk').mockResolvedValue({ + address: 'test', + riskScore: 5, + riskLevel: 'low', + categories: [], labels: [], - description: address === toAddress ? 'Risky recipient' : 'Clean sender' - })); + description: '', + }); - const result = await service.validateTransaction(fromAddress, toAddress, value); + const result = await service.validateTransaction(from, to, '1000000000000000000'); expect(result.isValid).toBe(true); - expect(result.warnings).toContain('High-value transaction to risky address'); }); - it('should warn about mixer involvement', async () => { + it('handles ether decimal value format', async () => { jest.spyOn(service, 'checkAddressRisk').mockResolvedValue({ address: 'test', - riskScore: 10, + riskScore: 5, riskLevel: 'low', - categories: ['mixer'], + categories: [], labels: [], - description: 'Mixer' + description: '', }); - const result = await service.validateTransaction(fromAddress, toAddress, value); + const result = await service.validateTransaction(from, to, '1.5'); expect(result.isValid).toBe(true); - expect(result.warnings).toContain('Transaction involves mixer-associated address'); }); - it('should handle validation errors gracefully', async () => { - jest.spyOn(service, 'checkAddressRisk').mockRejectedValue(new Error('API Error')); + it('handles scientific notation value format', async () => { + jest.spyOn(service, 'checkAddressRisk').mockResolvedValue({ + address: 'test', + riskScore: 5, + riskLevel: 'low', + categories: [], + labels: [], + description: '', + }); - const result = await service.validateTransaction(fromAddress, toAddress, value); - expect(result.isValid).toBe(true); // Should not block on errors - expect(result.warnings).toContain('Unable to complete security validation'); + const result = await service.validateTransaction(from, to, '1e18'); + expect(result.isValid).toBe(true); }); }); - describe('cache management', () => { - it('should clear cache when clearCache is called', () => { - service['cache'].set('test', { data: 'value', timestamp: Date.now() }); - expect(service['cache'].size).toBe(1); + describe('cache expiration', () => { + it('cache entries expire after TTL', async () => { + const address = '0x742d35Cc6634C0532925a3b8D4C9db96C4b4Db45'; - service.clearCache(); - expect(service['cache'].size).toBe(0); - }); + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => ({ riskScore: 10, categories: [], labels: [], description: '' }), + }); - it('should return correct cache statistics', () => { - const now = Date.now(); - service['cache'].set('valid', { data: 'value', timestamp: now }); - service['cache'].set('expired', { data: 'value', timestamp: now - (6 * 60 * 1000) }); // 6 minutes ago + await service.checkAddressRisk(address); + expect(global.fetch).toHaveBeenCalledTimes(1); - const stats = service.getCacheStats(); - expect(stats.size).toBe(1); // Only valid entry - expect(stats.oldestEntry).toBe(now); - }); - }); + jest.advanceTimersByTime(5 * 60 * 1000 + 1); - describe('private methods', () => { - describe('getRiskLevel', () => { - it('should map risk scores to correct levels', () => { - const testCases = [ - { score: 10, expected: 'low' }, - { score: 40, expected: 'medium' }, - { score: 60, expected: 'high' }, - { score: 80, expected: 'critical' } - ]; - - testCases.forEach(({ score, expected }) => { - const result = (service as any).getRiskLevel(score); - expect(result).toBe(expected); - }); - }); - }); - - describe('mapCategoryToAlertType', () => { - it('should map categories to alert types', () => { - const mappings = { - 'sanctions': 'sanctions', - 'scam': 'scam', - 'mixer': 'mixer', - 'gambling': 'gambling', - 'malware': 'malware', - 'exchange_hack': 'exchange_hack', - 'unknown': 'scam' // default - }; - - Object.entries(mappings).forEach(([category, expectedType]) => { - const result = (service as any).mapCategoryToAlertType(category); - expect(result).toBe(expectedType); - }); + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => ({ riskScore: 20, categories: [], labels: [], description: '' }), }); - }); - describe('getAlertSeverity', () => { - it('should map risk levels to alert severities', () => { - const mappings = { - 'critical': 'critical', - 'high': 'high', - 'medium': 'medium', - 'low': 'low' - }; - - Object.entries(mappings).forEach(([level, expectedSeverity]) => { - const result = (service as any).getAlertSeverity(level); - expect(result).toBe(expectedSeverity); - }); - }); + await service.checkAddressRisk(address); + expect(global.fetch).toHaveBeenCalledTimes(2); }); }); -}); \ No newline at end of file +}); diff --git a/src/utils/security/__tests__/rateLimiter.test.ts b/src/utils/security/__tests__/rateLimiter.test.ts index c767ce1b..62a6999d 100644 --- a/src/utils/security/__tests__/rateLimiter.test.ts +++ b/src/utils/security/__tests__/rateLimiter.test.ts @@ -4,202 +4,65 @@ describe('RateLimiter', () => { let limiter: RateLimiter; const config = { maxAttempts: 3, - windowMs: 60000, // 1 minute + windowMs: 60000, skipSuccessfulRequests: false, - skipFailedRequests: false + skipFailedRequests: false, }; beforeEach(() => { limiter = new RateLimiter(config); }); - describe('constructor', () => { - it('should initialize with provided config', () => { - expect(limiter).toBeInstanceOf(RateLimiter); - }); - }); - describe('getInstance', () => { - it('should return the same instance for the same key', () => { - const instance1 = RateLimiter.getInstance('test', config); - const instance2 = RateLimiter.getInstance('test', config); - expect(instance1).toBe(instance2); - }); - - it('should return different instances for different keys', () => { - const instance1 = RateLimiter.getInstance('test1', config); - const instance2 = RateLimiter.getInstance('test2', config); - expect(instance1).not.toBe(instance2); + it('returns same instance for same name', () => { + const a = RateLimiter.getInstance('same-key', config); + const b = RateLimiter.getInstance('same-key', config); + expect(a).toBe(b); }); }); describe('check', () => { - it('should allow requests within limit', () => { - const result1 = limiter.check('user1'); - const result2 = limiter.check('user1'); - const result3 = limiter.check('user1'); - - expect(result1.allowed).toBe(true); - expect(result1.remainingAttempts).toBe(2); - expect(result2.allowed).toBe(true); - expect(result2.remainingAttempts).toBe(1); - expect(result3.allowed).toBe(true); - expect(result3.remainingAttempts).toBe(0); - }); - - it('should block requests over limit', () => { - // Use up all attempts - limiter.check('user1'); - limiter.check('user1'); - limiter.check('user1'); - - const result = limiter.check('user1'); - expect(result.allowed).toBe(false); - expect(result.remainingAttempts).toBe(0); - expect(result.retryAfter).toBeDefined(); - }); - - it('should reset after window expires', () => { - // Use up attempts - limiter.check('user1'); - limiter.check('user1'); - limiter.check('user1'); - - // Verify blocked - expect(limiter.check('user1').allowed).toBe(false); - - // Simulate time passing (mock Date.now) - const originalNow = Date.now; - const futureTime = Date.now() + config.windowMs + 1000; - global.Date.now = jest.fn(() => futureTime); - - // Should allow again + it('allows when under limit', () => { const result = limiter.check('user1'); expect(result.allowed).toBe(true); expect(result.remainingAttempts).toBe(2); - - // Restore Date.now - global.Date.now = originalNow; }); - it('should handle different identifiers separately', () => { + it('denies when over limit', () => { limiter.check('user1'); limiter.check('user1'); limiter.check('user1'); - const result1 = limiter.check('user1'); - const result2 = limiter.check('user2'); - - expect(result1.allowed).toBe(false); - expect(result2.allowed).toBe(true); - }); - - it('should calculate reset time correctly', () => { - const beforeTime = Date.now(); const result = limiter.check('user1'); - const afterTime = Date.now(); - - expect(result.resetTime).toBeGreaterThanOrEqual(beforeTime + config.windowMs); - expect(result.resetTime).toBeLessThanOrEqual(afterTime + config.windowMs); - }); - }); - - describe('reset', () => { - it('should reset rate limit for specific identifier', () => { - limiter.check('user1'); - limiter.check('user1'); - limiter.check('user1'); - - expect(limiter.check('user1').allowed).toBe(false); - - limiter.reset('user1'); - - const result = limiter.check('user1'); - expect(result.allowed).toBe(true); - expect(result.remainingAttempts).toBe(2); - }); - - it('should not affect other identifiers', () => { - limiter.check('user1'); - limiter.check('user1'); - limiter.check('user1'); - limiter.check('user2'); - - limiter.reset('user1'); - - expect(limiter.check('user1').allowed).toBe(true); - const user2Result = limiter.check('user2'); - expect(user2Result.allowed).toBe(true); - expect(user2Result.remainingAttempts).toBe(1); - }); - }); - - describe('clear', () => { - it('should clear all rate limit data', () => { - limiter.check('user1'); - limiter.check('user2'); - - limiter.clear(); - - const result1 = limiter.check('user1'); - const result2 = limiter.check('user2'); - - expect(result1.allowed).toBe(true); - expect(result1.remainingAttempts).toBe(2); - expect(result2.allowed).toBe(true); - expect(result2.remainingAttempts).toBe(2); - }); - }); - - describe('getStatus', () => { - it('should return null for non-existent identifier', () => { - const status = limiter.getStatus('nonexistent'); - expect(status).toBeNull(); + expect(result.allowed).toBe(false); }); - it('should return correct status for active limit', () => { - limiter.check('user1'); - limiter.check('user1'); + it('remainingAttempts decreases correctly', () => { + const r1 = limiter.check('u'); + expect(r1.remainingAttempts).toBe(2); - const status = limiter.getStatus('user1'); - expect(status).not.toBeNull(); - expect(status!.count).toBe(2); - expect(status!.remainingAttempts).toBe(1); - expect(status!.isLimited).toBe(false); - }); + const r2 = limiter.check('u'); + expect(r2.remainingAttempts).toBe(1); - it('should return correct status when limit exceeded', () => { - limiter.check('user1'); - limiter.check('user1'); - limiter.check('user1'); - - const status = limiter.getStatus('user1'); - expect(status).not.toBeNull(); - expect(status!.count).toBe(3); - expect(status!.remainingAttempts).toBe(0); - expect(status!.isLimited).toBe(true); + const r3 = limiter.check('u'); + expect(r3.remainingAttempts).toBe(0); }); - it('should return null for expired entries', () => { - limiter.check('user1'); - - // Mock time to be after expiry - const originalNow = Date.now; - const futureTime = Date.now() + config.windowMs + 1000; - global.Date.now = jest.fn(() => futureTime); - - const status = limiter.getStatus('user1'); - expect(status).toBeNull(); + it('retryAfter is set when denied', () => { + limiter.check('u'); + limiter.check('u'); + limiter.check('u'); - global.Date.now = originalNow; + const result = limiter.check('u'); + expect(result.retryAfter).toBeDefined(); + expect(result.retryAfter).toBeGreaterThan(0); }); }); describe('cleanup', () => { - it('should remove expired entries', () => { + it('removes expired entries', () => { limiter.check('user1'); - // Mock time to be after expiry const originalNow = Date.now; const futureTime = Date.now() + config.windowMs + 1000; global.Date.now = jest.fn(() => futureTime); @@ -211,103 +74,18 @@ describe('RateLimiter', () => { global.Date.now = originalNow; }); - - it('should keep active entries', () => { - limiter.check('user1'); - limiter.check('user2'); - - limiter.cleanup(); - - expect(limiter.getStatus('user1')).not.toBeNull(); - expect(limiter.getStatus('user2')).not.toBeNull(); - }); }); - describe('pre-configured rate limiters', () => { - it('should have correct WALLET_CONNECTION config', () => { - expect(RateLimiters.WALLET_CONNECTION.maxAttempts).toBe(5); - expect(RateLimiters.WALLET_CONNECTION.windowMs).toBe(5 * 60 * 1000); - }); - - it('should have correct TRANSACTION_SIGNING config', () => { - expect(RateLimiters.TRANSACTION_SIGNING.maxAttempts).toBe(10); - expect(RateLimiters.TRANSACTION_SIGNING.windowMs).toBe(1 * 60 * 1000); - }); - - it('should have correct SIGNATURE_REQUESTS config', () => { - expect(RateLimiters.SIGNATURE_REQUESTS.maxAttempts).toBe(3); - expect(RateLimiters.SIGNATURE_REQUESTS.windowMs).toBe(1 * 60 * 1000); - }); - - it('should have correct ACCOUNT_SWITCHING config', () => { - expect(RateLimiters.ACCOUNT_SWITCHING.maxAttempts).toBe(3); - expect(RateLimiters.ACCOUNT_SWITCHING.windowMs).toBe(2 * 60 * 1000); - }); - - it('should have correct NETWORK_SWITCHING config', () => { - expect(RateLimiters.NETWORK_SWITCHING.maxAttempts).toBe(5); - expect(RateLimiters.NETWORK_SWITCHING.windowMs).toBe(3 * 60 * 1000); - }); - }); - - describe('useRateLimiter hook', () => { - it('should provide rate limiting functions', () => { - const { useRateLimiter } = require('../rateLimiter'); - const { checkLimit, resetLimit, getLimitStatus } = useRateLimiter(config); - - expect(typeof checkLimit).toBe('function'); - expect(typeof resetLimit).toBe('function'); - expect(typeof getLimitStatus).toBe('function'); - }); - - it('should work with the hook functions', () => { - const { useRateLimiter } = require('../rateLimiter'); - const { checkLimit, resetLimit, getLimitStatus } = useRateLimiter(config); - - const result = checkLimit('test'); - expect(result.allowed).toBe(true); - - resetLimit('test'); - const status = getLimitStatus('test'); - expect(status).toBeNull(); // Should be null after reset - }); - }); - - describe('edge cases', () => { - it('should handle zero maxAttempts', () => { - const zeroLimiter = new RateLimiter({ ...config, maxAttempts: 0 }); - const result = zeroLimiter.check('user1'); - expect(result.allowed).toBe(false); - expect(result.remainingAttempts).toBe(0); - }); - - it('should handle very short window', () => { - const shortLimiter = new RateLimiter({ ...config, windowMs: 1 }); - const result1 = shortLimiter.check('user1'); - expect(result1.allowed).toBe(true); - - // Mock minimal time passage - const originalNow = Date.now; - global.Date.now = jest.fn(() => originalNow() + 2); - - const result2 = shortLimiter.check('user1'); - expect(result2.allowed).toBe(true); // Should reset due to time passage - - global.Date.now = originalNow; - }); - - it('should handle concurrent requests correctly', () => { - // Simulate concurrent requests - const results = []; - for (let i = 0; i < 5; i++) { - results.push(limiter.check('user1')); - } + describe('different configs', () => { + it('different configs have different limits', () => { + const shortLimiter = new RateLimiter({ maxAttempts: 1, windowMs: 60000 }); + const longLimiter = new RateLimiter({ maxAttempts: 10, windowMs: 60000 }); - const allowedCount = results.filter(r => r.allowed).length; - const blockedCount = results.filter(r => !r.allowed).length; + shortLimiter.check('u'); + expect(shortLimiter.check('u').allowed).toBe(false); - expect(allowedCount).toBe(3); - expect(blockedCount).toBe(2); + for (let i = 0; i < 9; i++) longLimiter.check('u'); + expect(longLimiter.check('u').allowed).toBe(true); }); }); -}); \ No newline at end of file +});