import { describe, it, expect, vi, afterEach, beforeEach } from "vitest"; import { render, screen, fireEvent, cleanup, waitFor } from "@testing-library/react"; import SignupPage from "@/app/signup/page"; // next/navigation 의 useRouter 를 목으로 대체해 리다이렉트 동작을 검증한다. export const pushMock = vi.fn(); vi.mock("next/navigation", () => { return { __esModule: true, useRouter: () => ({ push: pushMock }), }; }); describe("SignupPage", () => { beforeEach(() => { vi.stubGlobal("fetch", vi.fn()); }); afterEach(() => { cleanup(); vi.unstubAllGlobals(); vi.clearAllMocks(); }); it("새 이메일/비밀번호로 회원가입하면 /api/auth/signup 으로 요청을 보내고 /projects 로 이동해야 한다", async () => { const fetchMock = vi.fn().mockImplementation((input: any, init?: any) => { const url = typeof input === "string" ? input : input.url; const method = init?.method ?? "GET"; if (url === "/api/auth/me" && method === "GET") { return Promise.resolve( new Response(JSON.stringify({ message: "인증이 필요합니다." }), { status: 401, headers: { "Content-Type": "application/json" }, }), ); } if (url === "/api/auth/signup" && method === "POST") { return Promise.resolve( new Response(JSON.stringify({ id: "1", email: "new@example.com" }), { status: 201, headers: { "Content-Type": "application/json" }, }), ); } return Promise.resolve(new Response(null, { status: 500 })); }); vi.stubGlobal("fetch", fetchMock as any); render(); const emailInput = screen.getByLabelText("이메일") as HTMLInputElement; const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement; const submitButton = screen.getByRole("button", { name: "회원가입" }); fireEvent.change(emailInput, { target: { value: "new@example.com" } }); fireEvent.change(passwordInput, { target: { value: "securePass1" } }); fireEvent.click(submitButton); await waitFor(() => { const signupCall = fetchMock.mock.calls.find(([url, options]) => { return url === "/api/auth/signup" && options?.method === "POST"; }); expect(signupCall).toBeDefined(); }); const signupCall = fetchMock.mock.calls.find(([url]) => url === "/api/auth/signup") as any; const [url, options] = signupCall; expect(url).toBe("/api/auth/signup"); expect(options.method).toBe("POST"); expect(options.headers["Content-Type"]).toBe("application/json"); const body = JSON.parse(options.body); expect(body.email).toBe("new@example.com"); expect(body.password).toBe("securePass1"); await waitFor(() => { expect(pushMock).toHaveBeenCalledWith("/projects"); }); }); it("회원가입 실패 시 에러 메시지를 화면에 표시해야 한다", async () => { const fetchMock = vi.fn().mockImplementation((input: any, init?: any) => { const url = typeof input === "string" ? input : input.url; const method = init?.method ?? "GET"; if (url === "/api/auth/me" && method === "GET") { return Promise.resolve( new Response(JSON.stringify({ message: "인증이 필요합니다." }), { status: 401, headers: { "Content-Type": "application/json" }, }), ); } if (url === "/api/auth/signup" && method === "POST") { return Promise.resolve( new Response(JSON.stringify({ message: "이미 가입된 이메일입니다." }), { status: 409, headers: { "Content-Type": "application/json" }, }), ); } return Promise.resolve(new Response(null, { status: 500 })); }); vi.stubGlobal("fetch", fetchMock as any); render(); const emailInput = screen.getByLabelText("이메일") as HTMLInputElement; const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement; const submitButton = screen.getByRole("button", { name: "회원가입" }); fireEvent.change(emailInput, { target: { value: "dup@example.com" } }); fireEvent.change(passwordInput, { target: { value: "securePass1" } }); fireEvent.click(submitButton); const errorText = await screen.findByText(/이미 가입된 이메일입니다./); expect(errorText).toBeTruthy(); }); it("이미 로그인된 상태에서 /signup 에 접근하면 /projects 로 리다이렉트해야 한다", async () => { const fetchMock = vi.fn().mockResolvedValue( new Response(JSON.stringify({ id: "1", email: "user@example.com", tokenVersion: 1 }), { status: 200, headers: { "Content-Type": "application/json" }, }), ); vi.stubGlobal("fetch", fetchMock as any); render(); await waitFor(() => { expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock.mock.calls[0][0]).toBe("/api/auth/me"); expect(pushMock).toHaveBeenCalledWith("/projects"); }); }); });