프로젝트 저장 및 목록
This commit is contained in:
+203
-1
@@ -9,13 +9,80 @@ vi.mock("@/generated/prisma/client", () => {
|
||||
class PrismaClientMock {
|
||||
project = {
|
||||
create: async ({ data }: any) => {
|
||||
const project = { id: inMemoryProjects.length + 1, ...data };
|
||||
const now = new Date();
|
||||
const project = {
|
||||
id: String(inMemoryProjects.length + 1),
|
||||
status: "DRAFT",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
...data,
|
||||
};
|
||||
inMemoryProjects.push(project);
|
||||
return project;
|
||||
},
|
||||
findUnique: async ({ where: { slug } }: any) => {
|
||||
return inMemoryProjects.find((p) => p.slug === slug) ?? null;
|
||||
},
|
||||
upsert: async ({ where: { slug }, update, create }: any) => {
|
||||
const index = inMemoryProjects.findIndex((p) => p.slug === slug);
|
||||
if (index >= 0) {
|
||||
const existing = inMemoryProjects[index];
|
||||
const updated = {
|
||||
...existing,
|
||||
...update,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
inMemoryProjects[index] = updated;
|
||||
return updated;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const project = {
|
||||
id: String(inMemoryProjects.length + 1),
|
||||
status: "DRAFT",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
...create,
|
||||
};
|
||||
inMemoryProjects.push(project);
|
||||
return project;
|
||||
},
|
||||
delete: async ({ where: { slug } }: any) => {
|
||||
const index = inMemoryProjects.findIndex((p) => p.slug === slug);
|
||||
if (index === -1) {
|
||||
const error: any = new Error("Project not found");
|
||||
error.code = "P2025";
|
||||
throw error;
|
||||
}
|
||||
|
||||
const [removed] = inMemoryProjects.splice(index, 1);
|
||||
return removed;
|
||||
},
|
||||
findMany: async ({ orderBy, take, select }: any = {}) => {
|
||||
let items = [...inMemoryProjects];
|
||||
|
||||
if (orderBy?.createdAt === "desc") {
|
||||
items.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
}
|
||||
|
||||
if (typeof take === "number") {
|
||||
items = items.slice(0, take);
|
||||
}
|
||||
|
||||
if (select) {
|
||||
return items.map((p) => {
|
||||
const picked: any = {};
|
||||
for (const key of Object.keys(select)) {
|
||||
if (select[key]) {
|
||||
picked[key] = p[key];
|
||||
}
|
||||
}
|
||||
return picked;
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -70,4 +137,139 @@ describe("/api/projects", () => {
|
||||
expect(fetched.slug).toBe(payload.slug);
|
||||
expect(fetched.contentJson).toEqual(payload.contentJson);
|
||||
});
|
||||
|
||||
it("GET /api/projects 는 최근 생성 순으로 프로젝트 목록을 반환해야 한다", async () => {
|
||||
const { POST: createProject } = await import("@/app/api/projects/route");
|
||||
|
||||
const firstPayload = {
|
||||
title: "첫 번째 프로젝트",
|
||||
slug: "first-project",
|
||||
contentJson: [],
|
||||
};
|
||||
|
||||
const secondPayload = {
|
||||
title: "두 번째 프로젝트",
|
||||
slug: "second-project",
|
||||
contentJson: [],
|
||||
};
|
||||
|
||||
await createProject(
|
||||
new Request(`${BASE_URL}/api/projects`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(firstPayload),
|
||||
}),
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
|
||||
await createProject(
|
||||
new Request(`${BASE_URL}/api/projects`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(secondPayload),
|
||||
}),
|
||||
);
|
||||
|
||||
const { GET: listProjects } = await import("@/app/api/projects/route");
|
||||
|
||||
const res = await listProjects(new Request(`${BASE_URL}/api/projects`));
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const list = (await res.json()) as any[];
|
||||
|
||||
expect(list.length).toBeGreaterThanOrEqual(2);
|
||||
expect(list[0].slug).toBe("second-project");
|
||||
expect(list[1].slug).toBe("first-project");
|
||||
});
|
||||
|
||||
it("DELETE /api/projects/[slug] 로 프로젝트를 삭제할 수 있어야 한다", async () => {
|
||||
const { POST: createProject } = await import("@/app/api/projects/route");
|
||||
|
||||
const slug = `delete-slug-${Date.now()}`;
|
||||
const payload = {
|
||||
title: "삭제 테스트 프로젝트",
|
||||
slug,
|
||||
contentJson: [],
|
||||
};
|
||||
|
||||
await createProject(
|
||||
new Request(`${BASE_URL}/api/projects`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
);
|
||||
|
||||
const { DELETE: deleteProject, GET: getProjectBySlug } = await import("@/app/api/projects/[slug]/route");
|
||||
|
||||
const deleteResponse = await deleteProject(
|
||||
new Request(`${BASE_URL}/api/projects/${slug}`, { method: "DELETE" }),
|
||||
{ params: { slug } } as any,
|
||||
);
|
||||
|
||||
expect(deleteResponse.status).toBe(200);
|
||||
|
||||
const getAfterDelete = await getProjectBySlug(
|
||||
new Request(`${BASE_URL}/api/projects/${slug}`),
|
||||
{ params: { slug } } as any,
|
||||
);
|
||||
|
||||
expect(getAfterDelete.status).toBe(404);
|
||||
});
|
||||
|
||||
it("동일 slug 로 두 번 POST 하면 두 번째 요청은 기존 프로젝트를 업데이트해야 한다", async () => {
|
||||
const { POST: handleProject } = await import("@/app/api/projects/route");
|
||||
|
||||
const slug = "duplicate-slug";
|
||||
|
||||
const firstPayload = {
|
||||
title: "첫 번째 제목",
|
||||
slug,
|
||||
contentJson: [
|
||||
{
|
||||
id: "blk_first",
|
||||
type: "text",
|
||||
props: { text: "첫 번째 내용", align: "left", size: "base" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const secondPayload = {
|
||||
title: "두 번째 제목",
|
||||
slug,
|
||||
contentJson: [
|
||||
{
|
||||
id: "blk_second",
|
||||
type: "text",
|
||||
props: { text: "두 번째 내용", align: "left", size: "base" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const firstRes = await handleProject(
|
||||
new Request(`${BASE_URL}/api/projects`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(firstPayload),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(firstRes.status).toBe(201);
|
||||
|
||||
const secondRes = await handleProject(
|
||||
new Request(`${BASE_URL}/api/projects`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(secondPayload),
|
||||
}),
|
||||
);
|
||||
|
||||
expect([200, 201]).toContain(secondRes.status);
|
||||
const updated = (await secondRes.json()) as any;
|
||||
|
||||
expect(updated.slug).toBe(slug);
|
||||
expect(updated.title).toBe("두 번째 제목");
|
||||
expect(updated.contentJson).toEqual(secondPayload.contentJson);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user