import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { SessionManager } from '~/server/utils/session-manager' describe('SessionManager', () => { let sessionManager: SessionManager let mockFetch: any beforeEach(() => { sessionManager = SessionManager.getInstance() sessionManager.clearCache() mockFetch = vi.fn() vi.clearAllMocks() }) afterEach(() => { sessionManager.clearCache() }) describe('Request Deduplication', () => { it('should deduplicate concurrent requests', async () => { const mockResponse = { authenticated: true, user: { id: '123' }, groups: [] } mockFetch.mockResolvedValue(mockResponse) // Make multiple concurrent requests const promises = [ sessionManager.checkSession({ fetchFn: mockFetch }), sessionManager.checkSession({ fetchFn: mockFetch }), sessionManager.checkSession({ fetchFn: mockFetch }) ] const results = await Promise.all(promises) // Should only call fetch once expect(mockFetch).toHaveBeenCalledTimes(1) expect(results.every(r => r.authenticated)).toBe(true) expect(results.every(r => r.user.id === '123')).toBe(true) }) it('should handle failed requests and not cache errors', async () => { const error = new Error('Network error') mockFetch.mockRejectedValue(error) const result = await sessionManager.checkSession({ fetchFn: mockFetch }) expect(result.authenticated).toBe(false) expect(result.reason).toBe('Network error') expect(mockFetch).toHaveBeenCalledTimes(1) }) it('should rate limit rapid successive requests', async () => { const mockResponse = { authenticated: true, user: { id: '123' }, groups: [] } mockFetch.mockResolvedValue(mockResponse) // First request await sessionManager.checkSession({ fetchFn: mockFetch }) // Immediate second request should use cache const secondResult = await sessionManager.checkSession({ fetchFn: mockFetch }) expect(mockFetch).toHaveBeenCalledTimes(1) expect(secondResult.authenticated).toBe(true) }) }) describe('Caching', () => { it('should cache successful responses', async () => { const mockResponse = { authenticated: true, user: { id: '123' }, groups: [] } mockFetch.mockResolvedValue(mockResponse) // First request const firstResult = await sessionManager.checkSession({ fetchFn: mockFetch, cacheKey: 'test-cache' }) // Second request should use cache const secondResult = await sessionManager.checkSession({ fetchFn: mockFetch, cacheKey: 'test-cache' }) expect(mockFetch).toHaveBeenCalledTimes(1) expect(firstResult.authenticated).toBe(true) expect(secondResult.authenticated).toBe(true) expect(secondResult.fromCache).toBe(true) }) it('should respect cache expiry', async () => { const mockResponse = { authenticated: true, user: { id: '123' }, groups: [] } mockFetch.mockResolvedValue(mockResponse) // First request with very short cache expiry await sessionManager.checkSession({ fetchFn: mockFetch, cacheKey: 'short-cache', cacheExpiry: 100 // 100ms }) // Wait for cache to expire await new Promise(resolve => setTimeout(resolve, 150)) // Second request should make new fetch await sessionManager.checkSession({ fetchFn: mockFetch, cacheKey: 'short-cache', cacheExpiry: 100 }) expect(mockFetch).toHaveBeenCalledTimes(2) }) it('should use grace period for network errors', async () => { const mockResponse = { authenticated: true, user: { id: '123' }, groups: [] } mockFetch.mockResolvedValueOnce(mockResponse) // First successful request await sessionManager.checkSession({ fetchFn: mockFetch, cacheKey: 'grace-test' }) // Mock network error const networkError = new Error('Network error') networkError.code = 'ECONNREFUSED' mockFetch.mockRejectedValue(networkError) // Second request should use cached result due to network error const result = await sessionManager.checkSession({ fetchFn: mockFetch, cacheKey: 'grace-test' }) expect(result.authenticated).toBe(true) expect(result.reason).toBe('NETWORK_ERROR_CACHED') }) }) describe('Session Validation', () => { it('should validate session with fresh check', async () => { const mockResponse = { authenticated: true, user: { id: '123' }, groups: [] } // Mock the fetch API global.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(mockResponse) }) const result = await sessionManager.validateSession() expect(result.authenticated).toBe(true) expect(global.fetch).toHaveBeenCalledWith('/api/auth/session', { headers: { 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' } }) }) it('should handle validation failure', async () => { // Mock failed fetch global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 401 }) const result = await sessionManager.validateSession() expect(result.authenticated).toBe(false) expect(result.reason).toBe('Session validation failed: 401') }) }) describe('Cache Management', () => { it('should clear cache', async () => { const mockResponse = { authenticated: true, user: { id: '123' }, groups: [] } mockFetch.mockResolvedValue(mockResponse) // Cache some data await sessionManager.checkSession({ fetchFn: mockFetch, cacheKey: 'clear-test' }) // Clear cache sessionManager.clearCache() // Next request should make fresh fetch await sessionManager.checkSession({ fetchFn: mockFetch, cacheKey: 'clear-test' }) expect(mockFetch).toHaveBeenCalledTimes(2) }) it('should provide cache statistics', async () => { const mockResponse = { authenticated: true, user: { id: '123' }, groups: [] } mockFetch.mockResolvedValue(mockResponse) const initialStats = sessionManager.getCacheStats() expect(initialStats.entries).toBe(0) // Add some cache entries await sessionManager.checkSession({ fetchFn: mockFetch, cacheKey: 'stats-test-1' }) await sessionManager.checkSession({ fetchFn: mockFetch, cacheKey: 'stats-test-2' }) const finalStats = sessionManager.getCacheStats() expect(finalStats.entries).toBe(2) expect(finalStats.oldestEntry).toBeDefined() expect(finalStats.newestEntry).toBeDefined() }) }) describe('Error Handling', () => { it('should identify network errors correctly', async () => { const networkErrors = [ { code: 'ECONNREFUSED' }, { code: 'ETIMEDOUT' }, { name: 'AbortError' }, { code: 'ENOTFOUND' }, { status: 503 } ] for (const error of networkErrors) { mockFetch.mockRejectedValue(error) const result = await sessionManager.checkSession({ fetchFn: mockFetch, cacheKey: `network-error-${error.code || error.name || error.status}` }) expect(result.authenticated).toBe(false) expect(result.reason).toBe(error.message || 'SESSION_CHECK_FAILED') } }) it('should handle non-network errors without grace period', async () => { const mockResponse = { authenticated: true, user: { id: '123' }, groups: [] } mockFetch.mockResolvedValueOnce(mockResponse) // First successful request await sessionManager.checkSession({ fetchFn: mockFetch, cacheKey: 'non-network-error' }) // Mock non-network error const authError = new Error('Auth error') authError.status = 401 mockFetch.mockRejectedValue(authError) // Second request should not use cached result for auth errors const result = await sessionManager.checkSession({ fetchFn: mockFetch, cacheKey: 'non-network-error' }) expect(result.authenticated).toBe(false) expect(result.reason).toBe('Auth error') }) }) })