import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { SessionRefreshTimerImpl } from "./session-refresh-timer"; const createJwt = (exp: number): string => { const payload = btoa(JSON.stringify({ exp })) .replace(/=/g, "") .replace(/\+/g, "-") .replace(/\//g, "_"); return `header.${payload}.sig`; }; const createMockTokenManager = (overrides: Record = {}) => ({ getToken: vi.fn().mockReturnValue(createJwt(Math.floor(Date.now() / 1000) + 3600)), setInitialTokens: vi.fn(), refreshToken: vi.fn().mockResolvedValue("new_token"), ...overrides, }); const createMockActivityMonitor = (lastActivity = Date.now()) => ({ start: vi.fn(), stop: vi.fn(), getLastActivityTimestamp: vi.fn().mockReturnValue(lastActivity), }); describe("SessionRefreshTimer", () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); vi.clearAllMocks(); }); it("should skip tick when there is no recent activity", async () => { const tokenManager = createMockTokenManager({ getToken: vi.fn().mockReturnValue(createJwt(Math.floor(Date.now() / 1000) + 60)), }); const timer = new SessionRefreshTimerImpl(tokenManager, createMockActivityMonitor(0)); timer.start(); await vi.advanceTimersByTimeAsync(30_000); expect(tokenManager.refreshToken).not.toHaveBeenCalled(); }); it("should skip tick when token is not near expiry", async () => { const tokenManager = createMockTokenManager({ getToken: vi.fn().mockReturnValue(createJwt(Math.floor(Date.now() / 1000) + 3600)), }); const timer = new SessionRefreshTimerImpl(tokenManager, createMockActivityMonitor()); timer.start(); await vi.advanceTimersByTimeAsync(30_000); expect(tokenManager.refreshToken).not.toHaveBeenCalled(); }); it("should refresh when token is near expiry and user is active", async () => { const tokenManager = createMockTokenManager({ getToken: vi.fn().mockReturnValue(createJwt(Math.floor(Date.now() / 1000) + 60)), }); const timer = new SessionRefreshTimerImpl(tokenManager, createMockActivityMonitor()); timer.start(); await vi.advanceTimersByTimeAsync(30_000); expect(tokenManager.refreshToken).toHaveBeenCalledTimes(1); }); it("should refresh when token is already expired and user is active", async () => { const tokenManager = createMockTokenManager({ getToken: vi.fn().mockReturnValue(createJwt(Math.floor(Date.now() / 1000) - 60)), }); const timer = new SessionRefreshTimerImpl(tokenManager, createMockActivityMonitor()); timer.start(); await vi.advanceTimersByTimeAsync(30_000); expect(tokenManager.refreshToken).toHaveBeenCalledTimes(1); }); it("should not trigger concurrent refreshes", async () => { let resolveRefresh!: () => void; const tokenManager = createMockTokenManager({ getToken: vi.fn().mockReturnValue(createJwt(Math.floor(Date.now() / 1000) + 60)), refreshToken: vi.fn().mockImplementation( () => new Promise((resolve) => { resolveRefresh = () => resolve("new_token"); }), ), }); const timer = new SessionRefreshTimerImpl(tokenManager, createMockActivityMonitor()); timer.start(); await vi.advanceTimersByTimeAsync(30_000); await vi.advanceTimersByTimeAsync(30_000); resolveRefresh(); await Promise.resolve(); expect(tokenManager.refreshToken).toHaveBeenCalledTimes(1); }); });