port-nimara-client-portal/tests/auth/session-manager.test.ts

269 lines
8.2 KiB
TypeScript

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')
})
})
})