Files
formbricks/apps/web/modules/projects/settings/lib/project.test.ts
2025-05-07 16:34:06 +00:00

227 lines
8.5 KiB
TypeScript

import { environmentCache } from "@/lib/environment/cache";
import { createEnvironment } from "@/lib/environment/service";
import { projectCache } from "@/lib/project/cache";
import { deleteLocalFilesByEnvironmentId, deleteS3FilesByEnvironmentId } from "@/lib/storage/service";
import { Prisma } from "@prisma/client";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { logger } from "@formbricks/logger";
import { TEnvironment } from "@formbricks/types/environment";
import { DatabaseError, InvalidInputError, ValidationError } from "@formbricks/types/errors";
import { ZProject } from "@formbricks/types/project";
import { createProject, deleteProject, updateProject } from "./project";
const baseProject = {
id: "p1",
createdAt: new Date(),
updatedAt: new Date(),
name: "Project 1",
organizationId: "org1",
languages: [],
recontactDays: 0,
linkSurveyBranding: false,
inAppSurveyBranding: false,
config: { channel: null, industry: null },
placement: "bottomRight",
clickOutsideClose: false,
darkOverlay: false,
environments: [
{
id: "prodenv",
createdAt: new Date(),
updatedAt: new Date(),
type: "production" as TEnvironment["type"],
projectId: "p1",
appSetupCompleted: false,
},
{
id: "devenv",
createdAt: new Date(),
updatedAt: new Date(),
type: "development" as TEnvironment["type"],
projectId: "p1",
appSetupCompleted: false,
},
],
styling: { allowStyleOverwrite: true },
logo: null,
};
vi.mock("@formbricks/database", () => ({
prisma: {
project: {
update: vi.fn(),
create: vi.fn(),
delete: vi.fn(),
},
projectTeam: {
createMany: vi.fn(),
},
},
}));
vi.mock("@formbricks/logger", () => ({
logger: {
error: vi.fn(),
},
}));
vi.mock("@/lib/project/cache", () => ({
projectCache: {
revalidate: vi.fn(),
},
}));
vi.mock("@/lib/environment/cache", () => ({
environmentCache: {
revalidate: vi.fn(),
},
}));
vi.mock("@/lib/storage/service", () => ({
deleteLocalFilesByEnvironmentId: vi.fn(),
deleteS3FilesByEnvironmentId: vi.fn(),
}));
vi.mock("@/lib/environment/service", () => ({
createEnvironment: vi.fn(),
}));
let mockIsS3Configured = true;
vi.mock("@/lib/constants", () => ({
isS3Configured: () => {
return mockIsS3Configured;
},
}));
describe("project lib", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("updateProject", () => {
test("updates project and revalidates cache", async () => {
vi.mocked(prisma.project.update).mockResolvedValueOnce(baseProject as any);
vi.mocked(projectCache.revalidate).mockImplementation(() => {});
const result = await updateProject("p1", { name: "Project 1", environments: baseProject.environments });
expect(result).toEqual(ZProject.parse(baseProject));
expect(prisma.project.update).toHaveBeenCalled();
expect(projectCache.revalidate).toHaveBeenCalledWith({ id: "p1", organizationId: "org1" });
});
test("throws DatabaseError on Prisma error", async () => {
vi.mocked(prisma.project.update).mockRejectedValueOnce(
new (class extends Error {
constructor() {
super();
this.message = "fail";
}
})()
);
await expect(updateProject("p1", { name: "Project 1" })).rejects.toThrow();
});
test("throws ValidationError on Zod error", async () => {
vi.mocked(prisma.project.update).mockResolvedValueOnce({ ...baseProject, id: 123 } as any);
await expect(
updateProject("p1", { name: "Project 1", environments: baseProject.environments })
).rejects.toThrow(ValidationError);
});
});
describe("createProject", () => {
test("creates project, environments, and revalidates cache", async () => {
vi.mocked(prisma.project.create).mockResolvedValueOnce({ ...baseProject, id: "p2" } as any);
vi.mocked(prisma.projectTeam.createMany).mockResolvedValueOnce({} as any);
vi.mocked(createEnvironment).mockResolvedValueOnce(baseProject.environments[0] as any);
vi.mocked(createEnvironment).mockResolvedValueOnce(baseProject.environments[1] as any);
vi.mocked(prisma.project.update).mockResolvedValueOnce(baseProject as any);
vi.mocked(projectCache.revalidate).mockImplementation(() => {});
const result = await createProject("org1", { name: "Project 1", teamIds: ["t1"] });
expect(result).toEqual(baseProject);
expect(prisma.project.create).toHaveBeenCalled();
expect(prisma.projectTeam.createMany).toHaveBeenCalled();
expect(createEnvironment).toHaveBeenCalled();
expect(projectCache.revalidate).toHaveBeenCalledWith({ id: "p2", organizationId: "org1" });
});
test("throws ValidationError if name is missing", async () => {
await expect(createProject("org1", {})).rejects.toThrow(ValidationError);
});
test("throws InvalidInputError on unique constraint", async () => {
const prismaError = new Prisma.PrismaClientKnownRequestError("Test Prisma Error", {
code: "P2002",
clientVersion: "5.0.0",
});
vi.mocked(prisma.project.create).mockRejectedValueOnce(prismaError);
await expect(createProject("org1", { name: "Project 1" })).rejects.toThrow(InvalidInputError);
});
test("throws DatabaseError on Prisma error", async () => {
const prismaError = new Prisma.PrismaClientKnownRequestError("Test Prisma Error", {
code: "P2001",
clientVersion: "5.0.0",
});
vi.mocked(prisma.project.create).mockRejectedValueOnce(prismaError);
await expect(createProject("org1", { name: "Project 1" })).rejects.toThrow(DatabaseError);
});
test("throws unknown error", async () => {
vi.mocked(prisma.project.create).mockRejectedValueOnce(new Error("fail"));
await expect(createProject("org1", { name: "Project 1" })).rejects.toThrow("fail");
});
});
describe("deleteProject", () => {
test("deletes project, deletes files, and revalidates cache (S3)", async () => {
vi.mocked(prisma.project.delete).mockResolvedValueOnce(baseProject as any);
vi.mocked(deleteS3FilesByEnvironmentId).mockResolvedValue(undefined);
vi.mocked(projectCache.revalidate).mockImplementation(() => {});
vi.mocked(environmentCache.revalidate).mockImplementation(() => {});
const result = await deleteProject("p1");
expect(result).toEqual(baseProject);
expect(deleteS3FilesByEnvironmentId).toHaveBeenCalledWith("prodenv");
expect(projectCache.revalidate).toHaveBeenCalledWith({ id: "p1", organizationId: "org1" });
expect(environmentCache.revalidate).toHaveBeenCalledWith({ projectId: "p1" });
});
test("deletes project, deletes files, and revalidates cache (local)", async () => {
vi.mocked(prisma.project.delete).mockResolvedValueOnce(baseProject as any);
mockIsS3Configured = false;
vi.mocked(deleteLocalFilesByEnvironmentId).mockResolvedValue(undefined);
vi.mocked(projectCache.revalidate).mockImplementation(() => {});
vi.mocked(environmentCache.revalidate).mockImplementation(() => {});
const result = await deleteProject("p1");
expect(result).toEqual(baseProject);
expect(deleteLocalFilesByEnvironmentId).toHaveBeenCalledWith("prodenv");
expect(projectCache.revalidate).toHaveBeenCalledWith({ id: "p1", organizationId: "org1" });
expect(environmentCache.revalidate).toHaveBeenCalledWith({ projectId: "p1" });
});
test("logs error if file deletion fails", async () => {
vi.mocked(prisma.project.delete).mockResolvedValueOnce(baseProject as any);
mockIsS3Configured = true;
vi.mocked(deleteS3FilesByEnvironmentId).mockRejectedValueOnce(new Error("fail"));
vi.mocked(logger.error).mockImplementation(() => {});
vi.mocked(projectCache.revalidate).mockImplementation(() => {});
vi.mocked(environmentCache.revalidate).mockImplementation(() => {});
await deleteProject("p1");
expect(logger.error).toHaveBeenCalled();
});
test("throws DatabaseError on Prisma error", async () => {
const err = new Prisma.PrismaClientKnownRequestError("Test Prisma Error", {
code: "P2001",
clientVersion: "5.0.0",
});
vi.mocked(prisma.project.delete).mockRejectedValueOnce(err as any);
await expect(deleteProject("p1")).rejects.toThrow(DatabaseError);
});
test("throws unknown error", async () => {
vi.mocked(prisma.project.delete).mockRejectedValueOnce(new Error("fail"));
await expect(deleteProject("p1")).rejects.toThrow("fail");
});
});
});