대시보드, GNB 추가
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user