import { RateLimitTool, setRateLimitLogger, RateLimitLogger } from '../RateLimitTool.js'; import type { User } from '../types.js'; interface RateLimitToolTestable { isDistributedDeployment(): Promise; cleanupExpiredEntries(): void; memoryStore: Map; identifierWindows: Map; } const consoleRateLimitLogger: RateLimitLogger = { info: (message: string, meta?: Record) => meta ? console.info(message, meta) : console.info(message), warn: (message: string, meta?: Record) => meta ? console.warn(message, meta) : console.warn(message) }; describe('RateLimitTool Logging Guards', () => { let rateLimitTool: RateLimitTool; let mockLogger: jest.Mocked; let originalEnv: NodeJS.ProcessEnv; const callRateLimit = async () => { await rateLimitTool.checkRateLimit(null, { ip: '198.51.100.1', fingerprint: 'test-device' }); }; beforeEach(() => { originalEnv = { ...process.env }; mockLogger = { info: jest.fn(), warn: jest.fn() }; setRateLimitLogger(mockLogger); rateLimitTool = new RateLimitTool(); }); afterEach(async () => { process.env = originalEnv; await rateLimitTool.dispose(); }); afterAll(() => { setRateLimitLogger(consoleRateLimitLogger); }); describe('Rate limit configuration logging', () => { it('logs the test-environment fallback only once thanks to caching', async () => { process.env.NODE_ENV = 'test'; await callRateLimit(); await callRateLimit(); const testLogs = mockLogger.info.mock.calls.filter(([message]) => message.includes('Using test rate limits') ); expect(testLogs).toHaveLength(1); }); it('logs development fallback information with hint metadata', async () => { process.env.NODE_ENV = 'development'; delete process.env.GOOGLE_CLOUD_PROJECT; delete process.env.GCP_PROJECT; await callRateLimit(); const devLogs = mockLogger.info.mock.calls.filter(([message, meta]) => message.includes('Using development rate limits') && Boolean(meta?.hint) ); expect(devLogs).toHaveLength(1); }); }); describe('Cleanup retention windows', () => { it('preserves requests newer than their configured window and removes stale entries', () => { const tool = rateLimitTool as unknown as RateLimitToolTestable; const identifier = 'user:test:daily'; const now = Date.now(); const almostDayAgo = now - (23 * 60 * 60 * 1000); const olderThanDay = now - (25 * 60 * 60 * 1000); tool.memoryStore.set(identifier, [almostDayAgo]); tool.identifierWindows.set(identifier, 24 * 60 * 60 * 1000); tool.cleanupExpiredEntries(); expect(tool.memoryStore.has(identifier)).toBe(true); tool.memoryStore.set(identifier, [olderThanDay]); tool.cleanupExpiredEntries(); expect(tool.memoryStore.has(identifier)).toBe(false); expect(tool.identifierWindows.has(identifier)).toBe(false); }); }); }); describe('Admin UID Fallback', () => { let rateLimitTool: RateLimitTool; let originalEnv: NodeJS.ProcessEnv; beforeEach(() => { originalEnv = { ...process.env }; process.env.NODE_ENV = 'test'; process.env.FIREBASE_ADMIN_EMAILS = 'jleechan@gmail.com,jleechantest@gmail.com'; process.env.FIREBASE_ADMIN_UIDS = 'Sr5YzcQNSbM11C7qejg5tjOrOk32,DLJwXoPZSQUzlb6JQHFOmi0HZWB2'; rateLimitTool = new RateLimitTool(); }); afterEach(async () => { process.env = originalEnv; await rateLimitTool.dispose(); }); it('BUG: Admin user with UID but no email gets authenticated rate limit instead of admin', async () => { // This test demonstrates the bug: admin user with missing email claim // should get admin rate limit (1000/hour) but gets authenticated rate limit (50/hour) const adminUserWithoutEmail: User = { id: 'Sr5YzcQNSbM11C7qejg5tjOrOk32', uid: 'Sr5YzcQNSbM11C7qejg5tjOrOk32', // Valid admin UID from FIREBASE_ADMIN_UIDS email: '', // Missing email claim from Firebase token name: 'Jeffrey Lee-Chan', isAuthenticated: true }; const result = await rateLimitTool.checkRateLimit(adminUserWithoutEmail, { ip: '192.168.1.100' }); // BUG: Should get admin limit (1000) but gets authenticated limit (50) expect(result.limit).toBe(1000); // This will FAIL - actual value is 50 expect(result.allowed).toBe(true); }); it('EXPECTED: Admin user with email gets admin rate limit', async () => { // This test passes: admin user with email gets correct admin rate limit const adminUserWithEmail: User = { id: 'Sr5YzcQNSbM11C7qejg5tjOrOk32', uid: 'Sr5YzcQNSbM11C7qejg5tjOrOk32', email: 'jleechan@gmail.com', // Email present name: 'Jeffrey Lee-Chan', isAuthenticated: true }; const result = await rateLimitTool.checkRateLimit(adminUserWithEmail, { ip: '192.168.1.100' }); expect(result.limit).toBe(1000); // Admin limit expect(result.allowed).toBe(true); }); it('EXPECTED: Non-admin user with UID but no email gets authenticated rate limit', async () => { // This test verifies that non-admin users still get authenticated limit const normalUserWithoutEmail: User = { id: 'normal-user-uid-123', uid: 'normal-user-uid-123', // Not in FIREBASE_ADMIN_UIDS email: '', // Missing email name: 'Normal User', isAuthenticated: true }; const result = await rateLimitTool.checkRateLimit(normalUserWithoutEmail, { ip: '192.168.1.100' }); // Test mode uses hourly limit (10/hour) not full authenticated limit (50/hour) expect(result.limit).toBe(10); // Authenticated user hourly limit in test mode expect(result.allowed).toBe(true); }); });