297 lines
8.9 KiB
TypeScript
297 lines
8.9 KiB
TypeScript
import "dotenv/config";
|
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import { signAccessToken } from "@/features/auth/authCrypto";
|
|
|
|
const BASE_URL = "http://localhost";
|
|
|
|
const inMemoryProjects: any[] = [];
|
|
const inMemoryFormSubmissions: any[] = [];
|
|
|
|
vi.mock("@prisma/client", () => {
|
|
class PrismaClientMock {
|
|
project = {
|
|
findUnique: async ({ where: { slug } }: any) => {
|
|
return inMemoryProjects.find((p) => p.slug === slug) ?? null;
|
|
},
|
|
};
|
|
|
|
formSubmission = {
|
|
findMany: async (args: any = {}) => {
|
|
const where = args.where ?? {};
|
|
const orderBy = args.orderBy;
|
|
const take = args.take;
|
|
|
|
let items = [...inMemoryFormSubmissions];
|
|
|
|
if (where.projectId) {
|
|
items = items.filter((s) => s.projectId === where.projectId);
|
|
}
|
|
|
|
if (where.userId) {
|
|
items = items.filter((s) => s.userId === where.userId);
|
|
}
|
|
|
|
if (orderBy?.createdAt === "desc") {
|
|
items = items.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
}
|
|
|
|
if (typeof take === "number") {
|
|
items = items.slice(0, take);
|
|
}
|
|
|
|
return items;
|
|
},
|
|
};
|
|
}
|
|
|
|
return { PrismaClient: PrismaClientMock };
|
|
});
|
|
|
|
const TEST_USER = { id: "user-1", email: "submissions@example.com", tokenVersion: 1 };
|
|
const OTHER_USER = { id: "user-2", email: "submissions2@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-submissions";
|
|
process.env.AUTH_ENCRYPTION_KEY = "0123456789abcdef0123456789abcdef";
|
|
inMemoryProjects.length = 0;
|
|
inMemoryFormSubmissions.length = 0;
|
|
});
|
|
|
|
describe("/api/projects/[slug]/submissions", () => {
|
|
it("로그인한 소유자가 자신의 프로젝트 제출 내역을 조회할 수 있어야 한다", async () => {
|
|
const project = {
|
|
id: "proj-1",
|
|
slug: "project-with-submissions",
|
|
userId: TEST_USER.id,
|
|
};
|
|
inMemoryProjects.push(project);
|
|
|
|
const createdAt = new Date();
|
|
inMemoryFormSubmissions.push({
|
|
id: "sub-1",
|
|
projectId: project.id,
|
|
projectSlug: project.slug,
|
|
userId: TEST_USER.id,
|
|
createdAt,
|
|
payloadJson: { name: "홍길동", message: "문의" },
|
|
sensitiveEnc: undefined,
|
|
metaJson: null,
|
|
});
|
|
|
|
const { GET: getSubmissions } = await import("@/app/api/projects/[slug]/submissions/route");
|
|
|
|
const headers = await buildAuthHeaders();
|
|
|
|
const res = await getSubmissions(
|
|
new Request(`${BASE_URL}/api/projects/${project.slug}/submissions`, {
|
|
headers,
|
|
}),
|
|
{ params: { slug: project.slug } } as any,
|
|
);
|
|
|
|
expect(res.status).toBe(200);
|
|
const json = (await res.json()) as any[];
|
|
expect(Array.isArray(json)).toBe(true);
|
|
expect(json.length).toBe(1);
|
|
expect(json[0].id).toBe("sub-1");
|
|
expect(json[0].projectSlug).toBe(project.slug);
|
|
expect(json[0].payload.name).toBe("홍길동");
|
|
expect(json[0].payload.message).toBe("문의");
|
|
});
|
|
|
|
it("민감정보는 암호화되어 저장되더라도 조회 시 복호화된 값으로 응답 payload 에 포함되어야 한다", async () => {
|
|
const project = {
|
|
id: "proj-2",
|
|
slug: "project-sensitive",
|
|
userId: TEST_USER.id,
|
|
};
|
|
inMemoryProjects.push(project);
|
|
|
|
const { encryptJson } = await import("@/features/auth/authCrypto");
|
|
const sensitive = { email: "test@example.com", phone: "010-0000-0000" };
|
|
const sensitiveEnc = await encryptJson(sensitive);
|
|
|
|
const createdAt = new Date();
|
|
inMemoryFormSubmissions.push({
|
|
id: "sub-2",
|
|
projectId: project.id,
|
|
projectSlug: project.slug,
|
|
userId: TEST_USER.id,
|
|
createdAt,
|
|
payloadJson: { name: "김철수" },
|
|
sensitiveEnc,
|
|
metaJson: null,
|
|
});
|
|
|
|
const { GET: getSubmissions } = await import("@/app/api/projects/[slug]/submissions/route");
|
|
|
|
const headers = await buildAuthHeaders();
|
|
|
|
const res = await getSubmissions(
|
|
new Request(`${BASE_URL}/api/projects/${project.slug}/submissions`, {
|
|
headers,
|
|
}),
|
|
{ params: { slug: project.slug } } as any,
|
|
);
|
|
|
|
expect(res.status).toBe(200);
|
|
const json = (await res.json()) as any[];
|
|
expect(json.length).toBe(1);
|
|
expect(json[0].payload.name).toBe("김철수");
|
|
expect(json[0].payload.email).toBe("test@example.com");
|
|
expect(json[0].payload.phone).toBe("010-0000-0000");
|
|
});
|
|
|
|
it("로그인하지 않은 경우 401 을 반환해야 한다", async () => {
|
|
const { GET: getSubmissions } = await import("@/app/api/projects/[slug]/submissions/route");
|
|
|
|
const res = await getSubmissions(
|
|
new Request(`${BASE_URL}/api/projects/any/submissions`),
|
|
{ params: { slug: "any" } } as any,
|
|
);
|
|
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it("다른 사용자가 소유한 프로젝트 제출 내역에 접근하면 404 를 반환해야 한다", async () => {
|
|
const project = {
|
|
id: "proj-3",
|
|
slug: "owner-only-project",
|
|
userId: OTHER_USER.id,
|
|
};
|
|
inMemoryProjects.push(project);
|
|
|
|
const { GET: getSubmissions } = await import("@/app/api/projects/[slug]/submissions/route");
|
|
|
|
const headers = await buildAuthHeaders();
|
|
|
|
const res = await getSubmissions(
|
|
new Request(`${BASE_URL}/api/projects/${project.slug}/submissions`, {
|
|
headers,
|
|
}),
|
|
{ params: { slug: project.slug } } as any,
|
|
);
|
|
|
|
expect(res.status).toBe(404);
|
|
});
|
|
});
|
|
|
|
describe("/api/projects/submissions", () => {
|
|
it("로그인한 사용자가 자신의 모든 프로젝트 제출 내역을 조회할 수 있어야 한다", async () => {
|
|
const project1 = {
|
|
id: "proj-1",
|
|
slug: "proj-1-slug",
|
|
userId: TEST_USER.id,
|
|
};
|
|
const project2 = {
|
|
id: "proj-2",
|
|
slug: "proj-2-slug",
|
|
userId: OTHER_USER.id,
|
|
};
|
|
inMemoryProjects.push(project1, project2);
|
|
|
|
const createdAt = new Date();
|
|
inMemoryFormSubmissions.push(
|
|
{
|
|
id: "sub-1",
|
|
projectId: project1.id,
|
|
projectSlug: project1.slug,
|
|
userId: TEST_USER.id,
|
|
createdAt,
|
|
payloadJson: { name: "홍길동" },
|
|
sensitiveEnc: undefined,
|
|
metaJson: null,
|
|
},
|
|
{
|
|
id: "sub-2",
|
|
projectId: project2.id,
|
|
projectSlug: project2.slug,
|
|
userId: OTHER_USER.id,
|
|
createdAt,
|
|
payloadJson: { name: "다른 유저" },
|
|
sensitiveEnc: undefined,
|
|
metaJson: null,
|
|
},
|
|
);
|
|
|
|
const { GET: getAllSubmissions } = await import("@/app/api/projects/submissions/route");
|
|
|
|
const headers = await buildAuthHeaders();
|
|
|
|
const res = await getAllSubmissions(
|
|
new Request(`${BASE_URL}/api/projects/submissions`, {
|
|
headers,
|
|
}),
|
|
);
|
|
|
|
expect(res.status).toBe(200);
|
|
const json = (await res.json()) as any[];
|
|
expect(Array.isArray(json)).toBe(true);
|
|
expect(json.length).toBe(1);
|
|
expect(json[0].id).toBe("sub-1");
|
|
expect(json[0].projectSlug).toBe(project1.slug);
|
|
expect(json[0].payload.name).toBe("홍길동");
|
|
});
|
|
|
|
it("민감정보는 전체 조회에서도 복호화된 값으로 payload 에 합쳐져야 한다", async () => {
|
|
const project = {
|
|
id: "proj-sensitive",
|
|
slug: "proj-sensitive",
|
|
userId: TEST_USER.id,
|
|
};
|
|
inMemoryProjects.push(project);
|
|
|
|
const { encryptJson } = await import("@/features/auth/authCrypto");
|
|
const sensitive = { email: "all@example.com", phone: "010-9999-8888" };
|
|
const sensitiveEnc = await encryptJson(sensitive);
|
|
|
|
const createdAt = new Date();
|
|
inMemoryFormSubmissions.push({
|
|
id: "sub-sensitive",
|
|
projectId: project.id,
|
|
projectSlug: project.slug,
|
|
userId: TEST_USER.id,
|
|
createdAt,
|
|
payloadJson: { name: "전체조회", message: "테스트" },
|
|
sensitiveEnc,
|
|
metaJson: null,
|
|
});
|
|
|
|
const { GET: getAllSubmissions } = await import("@/app/api/projects/submissions/route");
|
|
const headers = await buildAuthHeaders();
|
|
|
|
const res = await getAllSubmissions(
|
|
new Request(`${BASE_URL}/api/projects/submissions`, {
|
|
headers,
|
|
}),
|
|
);
|
|
|
|
expect(res.status).toBe(200);
|
|
const json = (await res.json()) as any[];
|
|
expect(json.length).toBe(1);
|
|
expect(json[0].payload.name).toBe("전체조회");
|
|
expect(json[0].payload.message).toBe("테스트");
|
|
expect(json[0].payload.email).toBe("all@example.com");
|
|
expect(json[0].payload.phone).toBe("010-9999-8888");
|
|
});
|
|
|
|
it("로그인하지 않은 경우 401 을 반환해야 한다", async () => {
|
|
const { GET: getAllSubmissions } = await import("@/app/api/projects/submissions/route");
|
|
|
|
const res = await getAllSubmissions(
|
|
new Request(`${BASE_URL}/api/projects/submissions`),
|
|
);
|
|
|
|
expect(res.status).toBe(401);
|
|
});
|
|
});
|