Files
page-builder/tests/api/dashboard.spec.ts
T
jaybe 9d8c4538c7
CI / test (push) Successful in 4m58s
CI / e2e (push) Has been skipped
CI / pr_and_merge (push) Successful in 1m45s
대시보드, GNB 추가
2025-12-08 06:59:52 +09:00

221 lines
6.4 KiB
TypeScript

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<string, number>, item: any) => {
acc[item.date] = item.count;
return acc;
}, {} as Record<string, number>);
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);
});
});