mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-11 18:58:45 -06:00
684 lines
18 KiB
TypeScript
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 },
|
|
});
|
|
});
|
|
});
|