221 lines
6.4 KiB
TypeScript
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);
|
|
});
|
|
});
|