session timeout
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useSessionTimeout } from '../src/hooks/useSessionTimeout';
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {};
|
||||
return {
|
||||
getItem: (key: string) => store[key] || null,
|
||||
setItem: (key: string, value: string) => { store[key] = value; },
|
||||
removeItem: (key: string) => { delete store[key]; },
|
||||
clear: () => { store = {}; },
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(global, 'localStorage', {
|
||||
value: localStorageMock,
|
||||
});
|
||||
|
||||
// Mock fetch
|
||||
const fetchMock = vi.fn();
|
||||
global.fetch = fetchMock;
|
||||
|
||||
describe('useSessionTimeout', () => {
|
||||
const mockLogout = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
localStorage.clear();
|
||||
fetchMock.mockReset();
|
||||
mockLogout.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should not show warning when user is active', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSessionTimeout(true, mockLogout)
|
||||
);
|
||||
|
||||
expect(result.current.isWarningOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('should show warning after idle period approaches timeout', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSessionTimeout(true, mockLogout)
|
||||
);
|
||||
|
||||
// Fast-forward 12 minutes (warning threshold is 3 min before 15 min timeout)
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(12 * 60 * 1000);
|
||||
});
|
||||
|
||||
// Fast-forward past the activity check interval (5 seconds)
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(10 * 1000);
|
||||
});
|
||||
|
||||
expect(result.current.isWarningOpen).toBe(true);
|
||||
expect(result.current.countdownSeconds).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should call logout when session expires', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSessionTimeout(true, mockLogout)
|
||||
);
|
||||
|
||||
// Fast-forward past the 15-minute timeout
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(16 * 60 * 1000);
|
||||
});
|
||||
|
||||
// Wait for async logout
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(10 * 1000);
|
||||
});
|
||||
|
||||
expect(mockLogout).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should extend session when extendSession is called', async () => {
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: 'Session refreshed',
|
||||
data: { expires_at: '2026-04-26T20:00:00Z' },
|
||||
}),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSessionTimeout(true, mockLogout)
|
||||
);
|
||||
|
||||
// Trigger warning
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(12 * 60 * 1000);
|
||||
});
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(10 * 1000);
|
||||
});
|
||||
|
||||
expect(result.current.isWarningOpen).toBe(true);
|
||||
|
||||
// Extend session
|
||||
await act(async () => {
|
||||
await result.current.extendSession();
|
||||
});
|
||||
|
||||
expect(result.current.isWarningOpen).toBe(false);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/auth/sessions/refresh'),
|
||||
expect.objectContaining({ method: 'POST' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should send heartbeat periodically', () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: 'OK',
|
||||
data: { user: { id: '1', email: 'test@example.com' } },
|
||||
}),
|
||||
});
|
||||
|
||||
renderHook(() => useSessionTimeout(true, mockLogout));
|
||||
|
||||
// Fast-forward past heartbeat interval (5 minutes)
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(5 * 60 * 1000 + 1000);
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/users/me'),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should logout on 401 heartbeat response', async () => {
|
||||
fetchMock.mockRejectedValueOnce({ code: 401, message: 'Unauthorized' });
|
||||
|
||||
renderHook(() => useSessionTimeout(true, mockLogout));
|
||||
|
||||
// Fast-forward past heartbeat interval
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(5 * 60 * 1000 + 1000);
|
||||
});
|
||||
|
||||
// Wait for the async catch block
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(mockLogout).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user