378 lines
10 KiB
TypeScript
378 lines
10 KiB
TypeScript
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<string, any> = {};
|
|
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<string, { projects: number; submissions: number }> = {};
|
|
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);
|
|
});
|
|
});
|