import "dotenv/config"; import { describe, it, expect, beforeEach, vi } from "vitest"; import { signAccessToken } from "@/features/auth/authCrypto"; // /api/dashboard/overview TDD // - 로그인한 사용자가 자신의 프로젝트/폼 제출 통계를 조회할 수 있어야 한다. // - 로그인하지 않은 경우 401 을 반환해야 한다. // - 프로젝트/제출 내역이 없는 경우 0 값과 빈 배열을 반환해야 한다. const BASE_URL = "http://localhost"; const inMemoryProjects: any[] = []; const inMemoryFormSubmissions: any[] = []; vi.mock("@prisma/client", () => { class PrismaClientMock { project = { findMany: async (args: any = {}) => { const where = args.where ?? {}; let items = [...inMemoryProjects]; if (where.userId) { items = items.filter((p) => p.userId === where.userId); } return items; }, }; formSubmission = { findMany: async (args: any = {}) => { const where = args.where ?? {}; let items = [...inMemoryFormSubmissions]; if (where.userId) { items = items.filter((s) => s.userId === where.userId); } return items; }, }; } return { PrismaClient: PrismaClientMock }; }); const TEST_USER = { id: "user-1", email: "dashboard@example.com", tokenVersion: 1 }; const OTHER_USER = { id: "user-2", email: "dashboard2@example.com", tokenVersion: 1 }; async function buildAuthHeadersFor(user: { id: string; email: string; tokenVersion: number }) { const token = await signAccessToken(user); return { cookie: `pb_access=${token}` }; } async function buildAuthHeaders() { return buildAuthHeadersFor(TEST_USER); } beforeEach(() => { process.env.AUTH_JWT_SECRET = "test-jwt-secret-dashboard"; inMemoryProjects.length = 0; inMemoryFormSubmissions.length = 0; }); describe("/api/dashboard/overview", () => { it("로그인한 사용자가 자신의 프로젝트/폼 제출 통계를 조회할 수 있어야 한다", async () => { const project1 = { id: "proj-1", userId: TEST_USER.id, title: "테스트 프로젝트 A", slug: "test-project-a", status: "DRAFT", }; const project2 = { id: "proj-2", userId: TEST_USER.id, title: "테스트 프로젝트 B", slug: "test-project-b", status: "PUBLISHED", }; const projectOther = { id: "proj-3", userId: OTHER_USER.id, title: "다른 유저 프로젝트", slug: "other-project", status: "DRAFT", }; inMemoryProjects.push(project1, project2, projectOther); const now = new Date("2025-01-03T12:00:00.000Z"); const yesterday = new Date("2025-01-02T12:00:00.000Z"); const eightDaysAgo = new Date("2024-12-26T12:00:00.000Z"); inMemoryFormSubmissions.push( { id: "sub-1", projectId: project1.id, projectSlug: project1.slug, userId: TEST_USER.id, createdAt: now, payloadJson: { message: "A-1" }, sensitiveEnc: undefined, metaJson: null, }, { id: "sub-2", projectId: project1.id, projectSlug: project1.slug, userId: TEST_USER.id, createdAt: yesterday, payloadJson: { message: "A-2" }, sensitiveEnc: undefined, metaJson: null, }, { id: "sub-3", projectId: project2.id, projectSlug: project2.slug, userId: TEST_USER.id, createdAt: eightDaysAgo, payloadJson: { message: "B-1" }, sensitiveEnc: undefined, metaJson: null, }, { id: "sub-4", projectId: projectOther.id, projectSlug: projectOther.slug, userId: OTHER_USER.id, createdAt: now, payloadJson: { message: "OTHER" }, sensitiveEnc: undefined, metaJson: null, }, ); const { GET: getDashboardOverview } = await import("@/app/api/dashboard/overview/route"); const headers = await buildAuthHeaders(); const res = await getDashboardOverview( new Request(`${BASE_URL}/api/dashboard/overview`, { headers, }), ); expect(res.status).toBe(200); const json = (await res.json()) as any; expect(json.summaryStats.totalProjects).toBe(2); expect(json.summaryStats.totalSubmissions).toBe(3); expect(Array.isArray(json.projectStats)).toBe(true); expect(json.projectStats.length).toBe(2); const proj1Stats = json.projectStats.find((p: any) => p.slug === project1.slug); const proj2Stats = json.projectStats.find((p: any) => p.slug === project2.slug); expect(proj1Stats).toBeTruthy(); expect(proj1Stats.totalSubmissions).toBe(2); expect(proj1Stats.lastSubmissionAt).toBe(now.toISOString()); expect(proj2Stats).toBeTruthy(); expect(proj2Stats.totalSubmissions).toBe(1); expect(proj2Stats.lastSubmissionAt).toBe(eightDaysAgo.toISOString()); const daily = (json as any).dailySubmissions; expect(Array.isArray(daily)).toBe(true); expect(daily.length).toBe(3); const dates = daily.map((item: any) => item.date).sort(); expect(dates).toEqual(["2024-12-26", "2025-01-02", "2025-01-03"]); const countsByDate = (daily as any[]).reduce((acc: Record, item: any) => { acc[item.date] = item.count; return acc; }, {} as Record); expect(countsByDate["2025-01-03"]).toBe(1); expect(countsByDate["2025-01-02"]).toBe(1); expect(countsByDate["2024-12-26"]).toBe(1); }); it("로그인하지 않은 경우 401 을 반환해야 한다", async () => { const { GET: getDashboardOverview } = await import("@/app/api/dashboard/overview/route"); const res = await getDashboardOverview( new Request(`${BASE_URL}/api/dashboard/overview`), ); expect(res.status).toBe(401); }); it("프로젝트와 제출 내역이 없는 경우 0 값과 빈 배열을 반환해야 한다", async () => { const { GET: getDashboardOverview } = await import("@/app/api/dashboard/overview/route"); const headers = await buildAuthHeaders(); const res = await getDashboardOverview( new Request(`${BASE_URL}/api/dashboard/overview`, { headers, }), ); expect(res.status).toBe(200); const json = (await res.json()) as any; expect(json.summaryStats.totalProjects).toBe(0); expect(json.summaryStats.totalSubmissions).toBe(0); expect(Array.isArray(json.projectStats)).toBe(true); expect(json.projectStats.length).toBe(0); }); });