Files
frontend/apps/cs/tests/unit/use-session-extend-confirm.test.ts
T

181 lines
6.6 KiB
TypeScript

import { renderHook, waitFor } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { useSessionExtendConfirm } from "../../../../packages/common/components/src/lib/use-session-extend-confirm";
import { getRemainingMs } from "@common/auth";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
// useConfirm 모킹
const mockConfirm = vi.fn();
vi.mock("@frontend-opensource/use-react-hooks", () => ({
useConfirm: () => ({
confirm: mockConfirm,
}),
}));
// @common/auth 모킹
const mockGetRemainingMs = vi.fn();
vi.mock("@common/auth", () => ({
getRemainingMs: (...args: any[]) => mockGetRemainingMs(...args),
SESSION_EXTEND_CONFIRM_BUFFER_MS: 300000,
}));
describe("useSessionExtendConfirm 커스텀 훅 테스트 (Vitest)", () => {
const mockGetAccessToken = vi.fn();
const mockSilentRefresh = vi.fn();
const mockDeclineSessionExtend = vi.fn();
beforeEach(() => {
vi.useFakeTimers();
vi.clearAllMocks();
});
afterEach(() => {
vi.useRealTimers();
});
it("1. enabled가 false일 때는 어떤 타이머나 컨펌도 동작하지 않아야 함", () => {
mockGetAccessToken.mockReturnValue("mock-token-123");
mockGetRemainingMs.mockReturnValue(10000); // 10초 뒤 컨펌 필요
renderHook(() =>
useSessionExtendConfirm({
enabled: false,
getAccessToken: mockGetAccessToken,
silentRefresh: mockSilentRefresh,
declineSessionExtend: mockDeclineSessionExtend,
})
);
// 타이머가 동작하지 않으므로 시간 경과 후에도 confirm이 실행되지 않아야 함
vi.advanceTimersByTime(10000);
expect(mockConfirm).not.toHaveBeenCalled();
});
it("2. 토큰 만료 버퍼 진입 전인 경우, 해당 시간만큼 대기 후 컨펌창이 나타나야 함", async () => {
mockGetAccessToken.mockReturnValue("mock-token-123");
// getRemainingMs(token, buffer) -> 10000ms (10초 후에 버퍼 시점 도달)
// getRemainingMs(token, 0) -> 310000ms (토큰 유효함)
mockGetRemainingMs.mockImplementation((token, buffer) => {
return buffer === 0 ? 310000 : 10000;
});
mockConfirm.mockResolvedValue(true);
renderHook(() =>
useSessionExtendConfirm({
enabled: true,
getAccessToken: mockGetAccessToken,
silentRefresh: mockSilentRefresh,
declineSessionExtend: mockDeclineSessionExtend,
})
);
// 9초 지났을 때는 아직 컨펌이 뜨지 않아야 함
vi.advanceTimersByTime(9000);
expect(mockConfirm).not.toHaveBeenCalled();
// 10초째에 진입했을 때 컨펌창 노출
await vi.advanceTimersByTimeAsync(1000);
expect(mockConfirm).toHaveBeenCalledWith("로그인을 연장하시겠습니까?");
});
it("3. 컨펌창에서 연장을 승인(Accept)하면 silentRefresh가 호출되고 재스케줄링되어야 함", async () => {
let token = "mock-token-123";
mockGetAccessToken.mockImplementation(() => token);
mockGetRemainingMs.mockImplementation((t, buffer) => {
return buffer === 0 ? 310000 : 10000;
});
mockConfirm.mockResolvedValue(true); // 연장 승인
mockSilentRefresh.mockImplementation(async () => {
token = "new-mock-token-456"; // 토큰 갱신 모사
});
renderHook(() =>
useSessionExtendConfirm({
enabled: true,
getAccessToken: mockGetAccessToken,
silentRefresh: mockSilentRefresh,
declineSessionExtend: mockDeclineSessionExtend,
})
);
// 10초 대기하여 컨펌 활성화 및 연장 승인 처리 진행
await vi.advanceTimersByTimeAsync(10000);
expect(mockSilentRefresh).toHaveBeenCalled();
// 신규 발급된 토큰("new-mock-token-456")을 기반으로 getAccessToken이 재차 호출되었는지 확인
expect(mockGetAccessToken).toHaveBeenCalled();
});
it("4. 컨펌창에서 연장을 거절(Decline)하면 declineSessionExtend가 실행되고, 해당 토큰에 대해 재확인하지 않아야 함", async () => {
mockGetAccessToken.mockReturnValue("mock-token-123");
mockGetRemainingMs.mockImplementation((t, buffer) => {
return buffer === 0 ? 310000 : 10000;
});
mockConfirm.mockResolvedValue(false); // 연장 거절
const { rerender } = renderHook(() =>
useSessionExtendConfirm({
enabled: true,
getAccessToken: mockGetAccessToken,
silentRefresh: mockSilentRefresh,
declineSessionExtend: mockDeclineSessionExtend,
})
);
// 10초 대기하여 거절 실행
await vi.advanceTimersByTimeAsync(10000);
expect(mockDeclineSessionExtend).toHaveBeenCalled();
expect(mockSilentRefresh).not.toHaveBeenCalled();
// 거절 이후 훅이 다시 실행되어도(혹은 상태가 업데이트되어 스케줄이 재호출되어도)
// dismissedTokenRef에 동일 토큰이 설정되어 있으므로 더 이상 컨펌창이 호출되지 않아야 함
mockConfirm.mockClear();
rerender();
await vi.advanceTimersByTimeAsync(10000);
expect(mockConfirm).not.toHaveBeenCalled();
});
it("5. 이미 만료 버퍼 이하로 남은 경우, 대기 시간 없이 즉시 컨펌창이 나타나야 함", async () => {
mockGetAccessToken.mockReturnValue("mock-token-123");
// getRemainingMs(token, buffer) -> 0ms (이미 버퍼 시간 이내임)
// getRemainingMs(token, 0) -> 50000ms (토큰 자체는 살아있음)
mockGetRemainingMs.mockImplementation((t, buffer) => {
return buffer === 0 ? 50000 : 0;
});
mockConfirm.mockResolvedValue(false); // 거절 처리하여 무한 루프 방지
renderHook(() =>
useSessionExtendConfirm({
enabled: true,
getAccessToken: mockGetAccessToken,
silentRefresh: mockSilentRefresh,
declineSessionExtend: mockDeclineSessionExtend,
})
);
// 타이머 흐름 대기 없이 즉시 동기/비동기 마이크로태스크 단계에서 컨펌 실행
await vi.advanceTimersByTimeAsync(1);
expect(mockConfirm).toHaveBeenCalled();
});
it("6. 토큰이 아예 유효하지 않거나 이미 만료된 경우 컨펌창이 뜨지 않아야 함", () => {
mockGetAccessToken.mockReturnValue("expired-token");
// getRemainingMs(token, 0) -> 0ms (만료됨)
mockGetRemainingMs.mockReturnValue(0);
renderHook(() =>
useSessionExtendConfirm({
enabled: true,
getAccessToken: mockGetAccessToken,
silentRefresh: mockSilentRefresh,
declineSessionExtend: mockDeclineSessionExtend,
})
);
vi.advanceTimersByTime(10000);
expect(mockConfirm).not.toHaveBeenCalled();
});
});