import "dotenv/config"; import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { signAccessToken } from "@/features/auth/authCrypto"; const BASE_URL = "http://localhost"; // in-memory Prisma 목 데이터 const inMemoryProjects: any[] = []; const inMemorySubmissions: any[] = []; vi.mock("@prisma/client", () => { class PrismaClientMock { project = { findMany: async ({ where }: any) => { if (!where || !where.userId) return []; return inMemoryProjects.filter((p) => p.userId === where.userId); }, }; formSubmission = { findMany: async ({ where }: any) => { if (!where || !where.userId) return []; return inMemorySubmissions.filter((s) => s.userId === where.userId); }, }; } return { PrismaClient: PrismaClientMock }; }); beforeEach(() => { inMemoryProjects.length = 0; inMemorySubmissions.length = 0; process.env.AUTH_JWT_SECRET = "test-jwt-secret-reports"; process.env.AUTH_ENCRYPTION_KEY = "0123456789abcdef0123456789abcdef"; vi.useFakeTimers(); vi.setSystemTime(new Date("2025-01-10T00:00:00.000Z")); }); afterEach(() => { vi.useRealTimers(); }); describe("/api/reports/overview", () => { it("range=all 에서 전체 프로젝트/제출 기준으로 summary/daily/byProject 를 집계해야 한다", async () => { const { GET: reports } = await import("@/app/api/reports/overview/route"); const userId = "user-reports"; inMemoryProjects.push( { id: "p1", userId, title: "Project A", slug: "project-a", }, { id: "p2", userId, title: "Project B", slug: "project-b", }, ); inMemorySubmissions.push( { id: "s1", userId, projectId: "p1", projectSlug: "project-a", createdAt: new Date("2025-01-09T12:00:00.000Z"), }, { id: "s2", userId, projectId: "p1", projectSlug: "project-a", createdAt: new Date("2025-01-05T09:00:00.000Z"), }, { id: "s3", userId, projectId: "p2", projectSlug: "project-b", createdAt: new Date("2024-12-20T08:00:00.000Z"), }, ); const token = await signAccessToken({ id: userId, email: "reports@example.com", tokenVersion: 1, emailVerified: true, } as any); const res = await reports( new Request(`${BASE_URL}/api/reports/overview?range=all`, { headers: { cookie: `pb_access=${token}` }, }), ); expect(res.status).toBe(200); const json = (await res.json()) as any; expect(json.summary.totalSubmissions).toBe(3); expect(json.summary.uniqueProjects).toBe(2); expect(json.summary.avgPerDay).toBeGreaterThan(0); expect(Array.isArray(json.daily)).toBe(true); expect(json.daily.length).toBe(3); expect(Array.isArray(json.byProject)).toBe(true); expect(json.byProject).toHaveLength(2); const byId: Record = {}; for (const row of json.byProject) { byId[row.projectId] = row; } expect(byId["p1"].submissions).toBe(2); expect(byId["p2"].submissions).toBe(1); expect(byId["p1"].ratio + byId["p2"].ratio).toBeCloseTo(1, 5); }); it("range=7d 에서는 최근 7일간의 제출만 포함해 summary/daily/byProject 를 집계해야 한다", async () => { const { GET: reports } = await import("@/app/api/reports/overview/route"); const userId = "user-reports-7d"; inMemoryProjects.push({ id: "p1", userId, title: "Project A", slug: "project-a" }); inMemorySubmissions.push( { id: "s1", userId, projectId: "p1", projectSlug: "project-a", createdAt: new Date("2025-01-09T12:00:00.000Z"), // within 7d }, { id: "s2", userId, projectId: "p1", projectSlug: "project-a", createdAt: new Date("2025-01-01T12:00:00.000Z"), // older than 7d }, ); const token = await signAccessToken({ id: userId, email: "reports7d@example.com", tokenVersion: 1, emailVerified: true, } as any); const res = await reports( new Request(`${BASE_URL}/api/reports/overview?range=7d`, { headers: { cookie: `pb_access=${token}` }, }), ); expect(res.status).toBe(200); const json = (await res.json()) as any; expect(json.summary.totalSubmissions).toBe(1); expect(json.summary.uniqueProjects).toBe(1); expect(json.daily).toHaveLength(1); expect(json.byProject).toHaveLength(1); expect(json.byProject[0].submissions).toBe(1); }); it("projectId 를 지정하면 해당 프로젝트의 데이터만 포함해야 한다", async () => { const { GET: reports } = await import("@/app/api/reports/overview/route"); const userId = "user-reports-project"; inMemoryProjects.push( { id: "p1", userId, title: "Project A", slug: "project-a" }, { id: "p2", userId, title: "Project B", slug: "project-b" }, ); inMemorySubmissions.push( { id: "s1", userId, projectId: "p1", projectSlug: "project-a", createdAt: new Date("2025-01-09T12:00:00.000Z"), }, { id: "s2", userId, projectId: "p2", projectSlug: "project-b", createdAt: new Date("2025-01-09T15:00:00.000Z"), }, ); const token = await signAccessToken({ id: userId, email: "reports-project@example.com", tokenVersion: 1, emailVerified: true, } as any); const res = await reports( new Request(`${BASE_URL}/api/reports/overview?range=all&projectId=p1`, { headers: { cookie: `pb_access=${token}` }, }), ); expect(res.status).toBe(200); const json = (await res.json()) as any; expect(json.summary.totalSubmissions).toBe(1); expect(json.summary.uniqueProjects).toBe(1); expect(json.byProject).toHaveLength(1); expect(json.byProject[0].projectId).toBe("p1"); }); it("range=all 에서 timePatterns 과 projectStatusSummary 를 함께 집계해야 한다", async () => { const { GET: reports } = await import("@/app/api/reports/overview/route"); const userId = "user-reports-v2"; inMemoryProjects.push( { id: "p1", userId, title: "Published Project", slug: "published-project", status: "PUBLISHED", }, { id: "p2", userId, title: "Draft Project", slug: "draft-project", status: "DRAFT", }, ); // 3개의 제출: p1 에 2개, p2 에 1개, 서로 다른 시간대 inMemorySubmissions.push( { id: "s1", userId, projectId: "p1", projectSlug: "published-project", createdAt: new Date("2025-01-09T01:00:00.000Z"), }, { id: "s2", userId, projectId: "p1", projectSlug: "published-project", createdAt: new Date("2025-01-09T15:00:00.000Z"), }, { id: "s3", userId, projectId: "p2", projectSlug: "draft-project", createdAt: new Date("2025-01-08T09:00:00.000Z"), }, ); const token = await signAccessToken({ id: userId, email: "reports-v2@example.com", tokenVersion: 1, emailVerified: true, } as any); const res = await reports( new Request(`${BASE_URL}/api/reports/overview?range=all`, { headers: { cookie: `pb_access=${token}` }, }), ); expect(res.status).toBe(200); const json = (await res.json()) as any; expect(json.summary.totalSubmissions).toBe(3); // timePatterns expect(json.timePatterns).toBeDefined(); expect(Array.isArray(json.timePatterns.byHour)).toBe(true); const sumByHour = (json.timePatterns.byHour as Array<{ hour: number; count: number }>).reduce( (sum, item) => sum + item.count, 0, ); expect(sumByHour).toBe(3); // projectStatusSummary expect(json.projectStatusSummary).toBeDefined(); expect(Array.isArray(json.projectStatusSummary.byStatus)).toBe(true); const byStatus: Record = {}; for (const row of json.projectStatusSummary.byStatus as Array<{ status: string; projects: number; submissions: number; }>) { byStatus[row.status] = { projects: row.projects, submissions: row.submissions }; } expect(byStatus["PUBLISHED"].projects).toBe(1); expect(byStatus["PUBLISHED"].submissions).toBe(2); expect(byStatus["DRAFT"].projects).toBe(1); expect(byStatus["DRAFT"].submissions).toBe(1); }); it("from/to 쿼리로 커스텀 기간을 필터링해야 한다", async () => { const { GET: reports } = await import("@/app/api/reports/overview/route"); const userId = "user-reports-from-to"; inMemoryProjects.push({ id: "p1", userId, title: "Project A", slug: "project-a" }); inMemorySubmissions.push( { id: "s1", userId, projectId: "p1", projectSlug: "project-a", createdAt: new Date("2025-01-01T12:00:00.000Z"), }, { id: "s2", userId, projectId: "p1", projectSlug: "project-a", createdAt: new Date("2025-01-05T12:00:00.000Z"), }, { id: "s3", userId, projectId: "p1", projectSlug: "project-a", createdAt: new Date("2025-01-09T12:00:00.000Z"), }, ); const token = await signAccessToken({ id: userId, email: "reports-from-to@example.com", tokenVersion: 1, emailVerified: true, } as any); const res = await reports( new Request(`${BASE_URL}/api/reports/overview?from=2025-01-02&to=2025-01-08`, { headers: { cookie: `pb_access=${token}` }, }), ); expect(res.status).toBe(200); const json = (await res.json()) as any; expect(json.summary.totalSubmissions).toBe(1); expect(json.summary.uniqueProjects).toBe(1); expect(json.daily).toHaveLength(1); expect(json.daily[0].date).toBe("2025-01-05"); expect(json.byProject).toHaveLength(1); expect(json.byProject[0].submissions).toBe(1); }); it("JWT 가 없으면 401 을 반환해야 한다", async () => { const { GET: reports } = await import("@/app/api/reports/overview/route"); const res = await reports(new Request(`${BASE_URL}/api/reports/overview?range=all`)); expect(res.status).toBe(401); }); });