Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions __tests__/components/logout-postcondition.property.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,29 @@ const userArbitrary: fc.Arbitrary<User> = fc.record({
});

describe("Feature: auth-flow-improvements, Property 12: Logout post-condition invariant", () => {
let originalLocation: Location;

beforeEach(() => {
mockSignout.mockReset();
mockReplace.mockReset();
sessionStorage.clear();
// 원본 location 보관 후, href 기록 가능한 스텁으로 교체
originalLocation = window.location;
Object.defineProperty(window, "location", {
configurable: true,
writable: true,
value: { ...originalLocation, href: "" },
});
});

afterEach(() => {
cleanup();
// 원본 location getter 완전 복원
Object.defineProperty(window, "location", {
configurable: true,
writable: true,
value: originalLocation,
});
});

it(
Expand All @@ -108,12 +123,12 @@ describe("Feature: auth-flow-improvements, Property 12: Logout post-condition in
render(<UserSetting />);

// Find and click the logout button
const logoutButton = screen.getByText("로그아웃");
const logoutButton = screen.getByRole("button", { name: /로그아웃/ });
fireEvent.click(logoutButton);

// Wait for the handler to complete
// Wait for the handler to complete (window.location.href 설정 확인)
await waitFor(() => {
expect(mockReplace).toHaveBeenCalledWith("/");
expect(window.location.href).toBe("/");
});

// Post-condition: store.user SHALL be null
Expand All @@ -123,7 +138,7 @@ describe("Feature: auth-flow-improvements, Property 12: Logout post-condition in
expect(sessionStorage.getItem(SESSION_CACHE_KEY)).toBeNull();

cleanup();
mockReplace.mockReset();
window.location.href = "";
}
),
{ numRuns: 100 }
Expand Down
11 changes: 7 additions & 4 deletions components/pages/config/user-setting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ const UserSetting = () => {
setUser(null);
clearSessionCache();

// 2. 서버 로그아웃 API 호출 (5초 타임아웃, 실패해도 진행)
// 2. 서버 로그아웃 API 호출
// signout은 keepalive:true라서 이후 하드 네비게이션으로 페이지가
// 언로드되어도 요청이 취소되지 않고 서버 세션 무효화가 보장된다.
// UX 지연 방지를 위해 최대 5초까지만 대기한다.
try {
await Promise.race([
signout(),
Expand All @@ -28,11 +31,11 @@ const UserSetting = () => {
),
]);
} catch {
// API 실패 또는 타임아웃: 무시하고 진행
// API 실패 또는 타임아웃: keepalive 요청은 백그라운드에서 계속 진행
}

// 3. 홈으로 이동
router.replace("/");
// 3. 홈으로 이동 (하드 네비게이션 — Next.js Router Cache 무효화)
window.location.href = "/";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

const handleResign = () => {
Expand Down
6 changes: 3 additions & 3 deletions components/pages/signin/signin-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ const SigninForm = ({ returnUrl }: SinginFormProps) => {
returnUrl && returnUrl.startsWith("/") && !returnUrl.startsWith("//")
? returnUrl
: "/";
router.replace(targetUrl);
router.refresh();
// 하드 네비게이션: 미들웨어가 새 쿠키를 확실히 인식하도록
window.location.href = targetUrl;
} catch (error) {
// FetchError: 로그인 실패(401/400)도 여기로 옴 (fetchData가 non-OK에서 throw)
if (error instanceof FetchError) {
Expand All @@ -125,7 +125,7 @@ const SigninForm = ({ returnUrl }: SinginFormProps) => {
}
setLoading(false);
}
}, [emailValue.value, passwordValue.value, loading, errors, router, returnUrl, setUser, toast]);
}, [emailValue.value, passwordValue.value, loading, errors, returnUrl, setUser, toast]);

// Enter 키 핸들러
useEffect(() => {
Expand Down
2 changes: 2 additions & 0 deletions lib/api/auth/signout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const signout = async () => {
const response = await fetchData(`/api/v1/auth/logout`, {
method: "POST",
credentials: "include",
// 페이지가 언로드(하드 네비게이션)되어도 요청이 취소되지 않도록 보장
keepalive: true,
});

const data = response.json();
Expand Down
Loading