269 lines
8.2 KiB
TypeScript
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')
|
|
})
|
|
})
|
|
})
|