Files
formbricks/apps/web/lib/project/service.test.ts
2025-06-20 16:46:43 +00:00

684 lines
18 KiB
TypeScript

import { createId } from "@paralleldrive/cuid2";
import { OrganizationRole, Prisma, WidgetPlacement } from "@prisma/client";
import { afterEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { DatabaseError, ValidationError } from "@formbricks/types/errors";
import { ITEMS_PER_PAGE } from "../constants";
import {
getProject,
getProjectByEnvironmentId,
getProjects,
getUserProjectEnvironmentsByOrganizationIds,
getUserProjects,
} from "./service";
vi.mock("@formbricks/database", () => ({
prisma: {
project: {
findUnique: vi.fn(),
findFirst: vi.fn(),
findMany: vi.fn(),
},
membership: {
findFirst: vi.fn(),
findMany: vi.fn(),
},
},
}));
describe("Project Service", () => {
afterEach(() => {
vi.clearAllMocks();
});
test("getProject should return a project when it exists", async () => {
const mockProject = {
id: createId(),
name: "Test Project",
organizationId: createId(),
createdAt: new Date(),
updatedAt: new Date(),
languages: ["en"],
recontactDays: 0,
linkSurveyBranding: true,
inAppSurveyBranding: true,
config: {
channel: null,
industry: null,
},
placement: WidgetPlacement.bottomRight,
clickOutsideClose: true,
darkOverlay: false,
environments: [],
styling: {
allowStyleOverwrite: true,
},
logo: null,
brandColor: null,
highlightBorderColor: null,
};
vi.mocked(prisma.project.findUnique).mockResolvedValue(mockProject);
const result = await getProject(mockProject.id);
expect(result).toEqual(mockProject);
expect(prisma.project.findUnique).toHaveBeenCalledWith({
where: {
id: mockProject.id,
},
select: expect.any(Object),
});
});
test("getProject should return null when project does not exist", async () => {
vi.mocked(prisma.project.findUnique).mockResolvedValue(null);
const result = await getProject(createId());
expect(result).toBeNull();
});
test("getProject should throw DatabaseError when prisma throws", async () => {
const prismaError = new Prisma.PrismaClientKnownRequestError("Database error", {
code: "P2002",
clientVersion: "5.0.0",
});
vi.mocked(prisma.project.findUnique).mockRejectedValue(prismaError);
await expect(getProject(createId())).rejects.toThrow(DatabaseError);
});
test("getProjectByEnvironmentId should return a project when it exists", async () => {
const mockProject = {
id: createId(),
name: "Test Project",
organizationId: createId(),
createdAt: new Date(),
updatedAt: new Date(),
languages: ["en"],
recontactDays: 0,
linkSurveyBranding: true,
inAppSurveyBranding: true,
config: {
channel: null,
industry: null,
},
placement: WidgetPlacement.bottomRight,
clickOutsideClose: true,
darkOverlay: false,
environments: [],
styling: {
allowStyleOverwrite: true,
},
logo: null,
brandColor: null,
highlightBorderColor: null,
};
vi.mocked(prisma.project.findFirst).mockResolvedValue(mockProject);
const result = await getProjectByEnvironmentId(createId());
expect(result).toEqual(mockProject);
expect(prisma.project.findFirst).toHaveBeenCalledWith({
where: {
environments: {
some: {
id: expect.any(String),
},
},
},
select: expect.any(Object),
});
});
test("getProjectByEnvironmentId should return null when project does not exist", async () => {
vi.mocked(prisma.project.findFirst).mockResolvedValue(null);
const result = await getProjectByEnvironmentId(createId());
expect(result).toBeNull();
});
test("getProjectByEnvironmentId should throw DatabaseError when prisma throws", async () => {
const prismaError = new Prisma.PrismaClientKnownRequestError("Database error", {
code: "P2002",
clientVersion: "5.0.0",
});
vi.mocked(prisma.project.findFirst).mockRejectedValue(prismaError);
await expect(getProjectByEnvironmentId(createId())).rejects.toThrow(DatabaseError);
});
test("getUserProjects should return projects for admin user", async () => {
const userId = createId();
const organizationId = createId();
const mockProjects = [
{
id: createId(),
name: "Test Project 1",
organizationId,
createdAt: new Date(),
updatedAt: new Date(),
languages: ["en"],
recontactDays: 0,
linkSurveyBranding: true,
inAppSurveyBranding: true,
config: {
channel: null,
industry: null,
},
placement: WidgetPlacement.bottomRight,
clickOutsideClose: true,
darkOverlay: false,
environments: [],
styling: {
allowStyleOverwrite: true,
},
logo: null,
brandColor: null,
highlightBorderColor: null,
},
{
id: createId(),
name: "Test Project 2",
organizationId,
createdAt: new Date(),
updatedAt: new Date(),
languages: ["en"],
recontactDays: 0,
linkSurveyBranding: true,
inAppSurveyBranding: true,
config: {
channel: null,
industry: null,
},
placement: WidgetPlacement.bottomRight,
clickOutsideClose: true,
darkOverlay: false,
environments: [],
styling: {
allowStyleOverwrite: true,
},
logo: null,
brandColor: null,
highlightBorderColor: null,
},
];
vi.mocked(prisma.membership.findFirst).mockResolvedValue({
userId,
organizationId,
role: OrganizationRole.owner,
accepted: true,
deprecatedRole: null,
});
vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects);
const result = await getUserProjects(userId, organizationId);
expect(result).toEqual(mockProjects);
expect(prisma.project.findMany).toHaveBeenCalledWith({
where: {
organizationId,
},
select: expect.any(Object),
take: undefined,
skip: undefined,
});
});
test("getUserProjects should return projects for member user", async () => {
const userId = createId();
const organizationId = createId();
const mockProjects = [
{
id: createId(),
name: "Test Project 1",
organizationId,
createdAt: new Date(),
updatedAt: new Date(),
languages: ["en"],
recontactDays: 0,
linkSurveyBranding: true,
inAppSurveyBranding: true,
config: {
channel: null,
industry: null,
},
placement: WidgetPlacement.bottomRight,
clickOutsideClose: true,
darkOverlay: false,
environments: [],
styling: {
allowStyleOverwrite: true,
},
logo: null,
brandColor: null,
highlightBorderColor: null,
},
];
vi.mocked(prisma.membership.findFirst).mockResolvedValue({
userId,
organizationId,
role: OrganizationRole.member,
accepted: true,
deprecatedRole: null,
});
vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects);
const result = await getUserProjects(userId, organizationId);
expect(result).toEqual(mockProjects);
expect(prisma.project.findMany).toHaveBeenCalledWith({
where: {
organizationId,
projectTeams: {
some: {
team: {
teamUsers: {
some: {
userId,
},
},
},
},
},
},
select: expect.any(Object),
take: undefined,
skip: undefined,
});
});
test("getUserProjects should throw ValidationError when user is not a member of organization", async () => {
const userId = createId();
const organizationId = createId();
vi.mocked(prisma.membership.findFirst).mockResolvedValue(null);
await expect(getUserProjects(userId, organizationId)).rejects.toThrow(ValidationError);
});
test("getUserProjects should handle pagination", async () => {
const userId = createId();
const organizationId = createId();
const mockProjects = [
{
id: createId(),
name: "Test Project 1",
organizationId,
createdAt: new Date(),
updatedAt: new Date(),
languages: ["en"],
recontactDays: 0,
linkSurveyBranding: true,
inAppSurveyBranding: true,
config: {
channel: null,
industry: null,
},
placement: WidgetPlacement.bottomRight,
clickOutsideClose: true,
darkOverlay: false,
environments: [],
styling: {
allowStyleOverwrite: true,
},
logo: null,
brandColor: null,
highlightBorderColor: null,
},
];
vi.mocked(prisma.membership.findFirst).mockResolvedValue({
userId,
organizationId,
role: OrganizationRole.owner,
accepted: true,
deprecatedRole: null,
});
vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects);
const page = 2;
const result = await getUserProjects(userId, organizationId, page);
expect(result).toEqual(mockProjects);
expect(prisma.project.findMany).toHaveBeenCalledWith({
where: {
organizationId,
},
select: expect.any(Object),
take: ITEMS_PER_PAGE,
skip: ITEMS_PER_PAGE * (page - 1),
});
});
test("getProjects should return all projects for an organization", async () => {
const organizationId = createId();
const mockProjects = [
{
id: createId(),
name: "Test Project 1",
organizationId,
createdAt: new Date(),
updatedAt: new Date(),
languages: ["en"],
recontactDays: 0,
linkSurveyBranding: true,
inAppSurveyBranding: true,
config: {
channel: null,
industry: null,
},
placement: WidgetPlacement.bottomRight,
clickOutsideClose: true,
darkOverlay: false,
environments: [],
styling: {
allowStyleOverwrite: true,
},
logo: null,
brandColor: null,
highlightBorderColor: null,
},
{
id: createId(),
name: "Test Project 2",
organizationId,
createdAt: new Date(),
updatedAt: new Date(),
languages: ["en"],
recontactDays: 0,
linkSurveyBranding: true,
inAppSurveyBranding: true,
config: {
channel: null,
industry: null,
},
placement: WidgetPlacement.bottomRight,
clickOutsideClose: true,
darkOverlay: false,
environments: [],
styling: {
allowStyleOverwrite: true,
},
logo: null,
brandColor: null,
highlightBorderColor: null,
},
];
vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects);
const result = await getProjects(organizationId);
expect(result).toEqual(mockProjects);
expect(prisma.project.findMany).toHaveBeenCalledWith({
where: {
organizationId,
},
select: expect.any(Object),
take: undefined,
skip: undefined,
});
});
test("getProjects should handle pagination", async () => {
const organizationId = createId();
const mockProjects = [
{
id: createId(),
name: "Test Project 1",
organizationId,
createdAt: new Date(),
updatedAt: new Date(),
languages: ["en"],
recontactDays: 0,
linkSurveyBranding: true,
inAppSurveyBranding: true,
config: {
channel: null,
industry: null,
},
placement: WidgetPlacement.bottomRight,
clickOutsideClose: true,
darkOverlay: false,
environments: [],
styling: {
allowStyleOverwrite: true,
},
logo: null,
brandColor: null,
highlightBorderColor: null,
},
];
vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects);
const page = 2;
const result = await getProjects(organizationId, page);
expect(result).toEqual(mockProjects);
expect(prisma.project.findMany).toHaveBeenCalledWith({
where: {
organizationId,
},
select: expect.any(Object),
take: ITEMS_PER_PAGE,
skip: ITEMS_PER_PAGE * (page - 1),
});
});
test("getProjects should throw DatabaseError when prisma throws", async () => {
const organizationId = createId();
const prismaError = new Prisma.PrismaClientKnownRequestError("Database error", {
code: "P2002",
clientVersion: "5.0.0",
});
vi.mocked(prisma.project.findMany).mockRejectedValue(prismaError);
await expect(getProjects(organizationId)).rejects.toThrow(DatabaseError);
});
test("getProjectsByOrganizationIds should return projects for given organization IDs", async () => {
const organizationId1 = createId();
const organizationId2 = createId();
const userId = createId();
const mockProjects = [
{
environments: [],
},
{
environments: [],
},
];
vi.mocked(prisma.membership.findMany).mockResolvedValue([
{
userId,
organizationId: organizationId1,
role: "owner" as any,
accepted: true,
deprecatedRole: null,
},
{
userId,
organizationId: organizationId2,
role: "owner" as any,
accepted: true,
deprecatedRole: null,
},
]);
vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects as any);
const result = await getUserProjectEnvironmentsByOrganizationIds(
[organizationId1, organizationId2],
userId
);
expect(result).toEqual(mockProjects);
expect(prisma.project.findMany).toHaveBeenCalledWith({
where: {
OR: [{ organizationId: organizationId1 }, { organizationId: organizationId2 }],
},
select: { environments: true },
});
});
test("getProjectsByOrganizationIds should return empty array when no projects are found", async () => {
const organizationId1 = createId();
const organizationId2 = createId();
const userId = createId();
vi.mocked(prisma.membership.findMany).mockResolvedValue([
{
userId,
organizationId: organizationId1,
role: "owner" as any,
accepted: true,
deprecatedRole: null,
},
{
userId,
organizationId: organizationId2,
role: "owner" as any,
accepted: true,
deprecatedRole: null,
},
]);
vi.mocked(prisma.project.findMany).mockResolvedValue([]);
const result = await getUserProjectEnvironmentsByOrganizationIds(
[organizationId1, organizationId2],
userId
);
expect(result).toEqual([]);
expect(prisma.project.findMany).toHaveBeenCalledWith({
where: {
OR: [{ organizationId: organizationId1 }, { organizationId: organizationId2 }],
},
select: { environments: true },
});
});
test("getProjectsByOrganizationIds should throw DatabaseError when prisma throws", async () => {
const organizationId1 = createId();
const organizationId2 = createId();
const userId = createId();
const prismaError = new Prisma.PrismaClientKnownRequestError("Database error", {
code: "P2002",
clientVersion: "5.0.0",
});
vi.mocked(prisma.membership.findMany).mockResolvedValue([
{
userId,
organizationId: organizationId1,
role: "owner" as any,
accepted: true,
deprecatedRole: null,
},
]);
vi.mocked(prisma.project.findMany).mockRejectedValue(prismaError);
await expect(
getUserProjectEnvironmentsByOrganizationIds([organizationId1, organizationId2], userId)
).rejects.toThrow(DatabaseError);
});
test("getProjectsByOrganizationIds should throw ValidationError with wrong input", async () => {
const userId = createId();
await expect(getUserProjectEnvironmentsByOrganizationIds(["wrong-id"], userId)).rejects.toThrow(
ValidationError
);
});
test("getProjectsByOrganizationIds should return empty array when user has no memberships", async () => {
const organizationId1 = createId();
const organizationId2 = createId();
const userId = createId();
// Mock no memberships found
vi.mocked(prisma.membership.findMany).mockResolvedValue([]);
const result = await getUserProjectEnvironmentsByOrganizationIds(
[organizationId1, organizationId2],
userId
);
expect(result).toEqual([]);
expect(prisma.membership.findMany).toHaveBeenCalledWith({
where: {
userId,
organizationId: {
in: [organizationId1, organizationId2],
},
},
});
// Should not call project.findMany when no memberships
expect(prisma.project.findMany).not.toHaveBeenCalled();
});
test("getProjectsByOrganizationIds should handle member role with team access", async () => {
const organizationId1 = createId();
const organizationId2 = createId();
const userId = createId();
const mockProjects = [
{
environments: [],
},
];
// Mock membership where user is a member
vi.mocked(prisma.membership.findMany).mockResolvedValue([
{
userId,
organizationId: organizationId1,
role: "member" as any,
accepted: true,
deprecatedRole: null,
},
]);
vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects as any);
const result = await getUserProjectEnvironmentsByOrganizationIds(
[organizationId1, organizationId2],
userId
);
expect(result).toEqual(mockProjects);
expect(prisma.project.findMany).toHaveBeenCalledWith({
where: {
OR: [
{
organizationId: organizationId1,
projectTeams: {
some: {
team: {
teamUsers: {
some: {
userId,
},
},
},
},
},
},
],
},
select: { environments: true },
});
});
});