사용자 로그인, 가입 처리
This commit is contained in:
@@ -2,6 +2,16 @@ import { describe, it, expect, afterEach, vi } from "vitest";
|
||||
import { render, screen, cleanup, waitFor, fireEvent } from "@testing-library/react";
|
||||
import ProjectsPage from "@/app/projects/page";
|
||||
|
||||
// next/navigation 의 useRouter 를 목으로 대체해 인증 실패 시 리다이렉트 동작을 검증한다.
|
||||
export const pushMock = vi.fn();
|
||||
|
||||
vi.mock("next/navigation", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
useRouter: () => ({ push: pushMock }),
|
||||
};
|
||||
});
|
||||
|
||||
// ProjectsPage 프로젝트 목록 TDD
|
||||
// - 마운트 시 /api/projects 로 GET 요청을 보내고, 반환된 프로젝트 목록을 테이블로 렌더링해야 한다.
|
||||
// - 각 프로젝트 행에는 /editor?slug=..., /preview?slug=... 로 이동하는 "편집" / "미리보기" 링크가 있어야 한다.
|
||||
@@ -228,4 +238,229 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
|
||||
confirmSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("/api/projects 가 401 을 반환하면 로그인 페이지로 리다이렉트해야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ message: "프로젝트 목록을 조회하려면 로그인이 필요합니다." }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
render(<ProjectsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(pushMock).toHaveBeenCalledWith("/login");
|
||||
});
|
||||
});
|
||||
|
||||
it("헤더의 '로그아웃' 버튼을 클릭하면 /api/auth/logout 으로 POST 요청을 보내고 /login 으로 이동해야 한다", async () => {
|
||||
const projects = [
|
||||
{
|
||||
id: "1",
|
||||
title: "테스트 프로젝트 A",
|
||||
slug: "test-project-a",
|
||||
status: "DRAFT",
|
||||
createdAt: "2025-01-01T00:00:00.000Z",
|
||||
updatedAt: "2025-01-01T00:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
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/projects" && method === "GET") {
|
||||
return Promise.resolve(
|
||||
new Response(JSON.stringify(projects), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (url === "/api/auth/logout" && method === "POST") {
|
||||
return Promise.resolve(new Response(JSON.stringify({ message: "ok" }), { status: 200 }));
|
||||
}
|
||||
|
||||
return Promise.resolve(new Response(null, { status: 500 }));
|
||||
});
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
render(<ProjectsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const logoutButton = await screen.findByRole("button", { name: "로그아웃" });
|
||||
fireEvent.click(logoutButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const logoutCall = fetchMock.mock.calls.find(([url, options]) => {
|
||||
return url === "/api/auth/logout" && options?.method === "POST";
|
||||
});
|
||||
|
||||
expect(logoutCall).toBeDefined();
|
||||
expect(pushMock).toHaveBeenCalledWith("/login");
|
||||
});
|
||||
});
|
||||
|
||||
it("/api/projects 가 500 을 반환하면 에러 메시지를 표시해야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response("server error", {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "text/plain" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
render(<ProjectsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
"프로젝트 목록을 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.",
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("단일 프로젝트 삭제가 실패하면 에러 메시지를 표시해야 한다", async () => {
|
||||
const projects = [
|
||||
{
|
||||
id: "1",
|
||||
title: "삭제 실패 프로젝트",
|
||||
slug: "delete-fail-project",
|
||||
status: "DRAFT",
|
||||
createdAt: "2025-01-01T00:00:00.000Z",
|
||||
updatedAt: "2025-01-01T00:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
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/projects" && method === "GET") {
|
||||
return Promise.resolve(
|
||||
new Response(JSON.stringify(projects), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (url === "/api/projects/delete-fail-project" && method === "DELETE") {
|
||||
return Promise.resolve(new Response("fail", { status: 500 }));
|
||||
}
|
||||
|
||||
return Promise.resolve(new Response(null, { status: 500 }));
|
||||
});
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true as any);
|
||||
|
||||
render(<ProjectsPage />);
|
||||
|
||||
expect(await screen.findByText("삭제 실패 프로젝트")).toBeTruthy();
|
||||
|
||||
const deleteButton = screen.getByText("삭제");
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
expect(
|
||||
await screen.findByText("프로젝트 삭제 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."),
|
||||
).toBeTruthy();
|
||||
|
||||
// 삭제 실패이므로 여전히 목록에 남아 있어야 한다.
|
||||
expect(screen.getByText("삭제 실패 프로젝트")).toBeTruthy();
|
||||
|
||||
confirmSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("일괄 삭제에서 일부만 성공하면 에러 메시지를 표시해야 한다", async () => {
|
||||
const projects = [
|
||||
{
|
||||
id: "1",
|
||||
title: "성공 프로젝트",
|
||||
slug: "success-project",
|
||||
status: "DRAFT",
|
||||
createdAt: "2025-01-01T00:00:00.000Z",
|
||||
updatedAt: "2025-01-01T00:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "실패 프로젝트",
|
||||
slug: "fail-project",
|
||||
status: "DRAFT",
|
||||
createdAt: "2025-01-02T00:00:00.000Z",
|
||||
updatedAt: "2025-01-02T00:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
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/projects" && method === "GET") {
|
||||
return Promise.resolve(
|
||||
new Response(JSON.stringify(projects), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (url === "/api/projects/success-project" && method === "DELETE") {
|
||||
return Promise.resolve(new Response(null, { status: 200 }));
|
||||
}
|
||||
|
||||
if (url === "/api/projects/fail-project" && method === "DELETE") {
|
||||
return Promise.resolve(new Response("fail", { status: 500 }));
|
||||
}
|
||||
|
||||
return Promise.resolve(new Response(null, { status: 500 }));
|
||||
});
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true as any);
|
||||
|
||||
render(<ProjectsPage />);
|
||||
|
||||
expect(await screen.findByText("성공 프로젝트")).toBeTruthy();
|
||||
expect(await screen.findByText("실패 프로젝트")).toBeTruthy();
|
||||
|
||||
const checkboxSuccess = screen.getByLabelText("성공 프로젝트 선택");
|
||||
const checkboxFail = screen.getByLabelText("실패 프로젝트 선택");
|
||||
|
||||
fireEvent.click(checkboxSuccess);
|
||||
fireEvent.click(checkboxFail);
|
||||
|
||||
const bulkDeleteButton = screen.getByText("선택 삭제");
|
||||
fireEvent.click(bulkDeleteButton);
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
"일부 프로젝트 삭제에 실패했습니다. 페이지를 새로고침한 뒤 목록을 확인해 주세요.",
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
// 성공 프로젝트는 목록에서 사라지고, 실패 프로젝트는 남아 있어야 한다.
|
||||
expect(screen.queryByText("성공 프로젝트")).toBeNull();
|
||||
expect(screen.getByText("실패 프로젝트")).toBeTruthy();
|
||||
|
||||
confirmSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user