chore: tests for lib/utils and lib/survey (#5676)

This commit is contained in:
Anshuman Pandey
2025-05-07 17:57:48 +05:30
committed by GitHub
parent 928bb3f8bc
commit 07e9a7c007
40 changed files with 5221 additions and 531 deletions

View File

@@ -2,7 +2,7 @@ import { authenticateRequest, handleErrorResponse } from "@/app/api/v1/auth";
import { responses } from "@/app/lib/api/response";
import { getSurveyDomain } from "@/lib/getSurveyUrl";
import { getSurvey } from "@/lib/survey/service";
import { generateSurveySingleUseIds } from "@/lib/utils/singleUseSurveys";
import { generateSurveySingleUseIds } from "@/lib/utils/single-use-surveys";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
import { NextRequest } from "next/server";

View File

@@ -1,4 +1,4 @@
import { mockSurveyLanguages } from "@/lib/survey/tests/__mock__/survey.mock";
import { mockSurveyLanguages } from "@/lib/survey/__mock__/survey.mock";
import {
TSurvey,
TSurveyCTAQuestion,

View File

@@ -22,7 +22,7 @@ import { responseNoteCache } from "../responseNote/cache";
import { getResponseNotes } from "../responseNote/service";
import { deleteFile, putFile } from "../storage/service";
import { getSurvey } from "../survey/service";
import { convertToCsv, convertToXlsxBuffer } from "../utils/fileConversion";
import { convertToCsv, convertToXlsxBuffer } from "../utils/file-conversion";
import { validateInputs } from "../utils/validate";
import { responseCache } from "./cache";
import {

View File

@@ -24,7 +24,7 @@ import {
mockContactAttributeKey,
mockOrganizationOutput,
mockSurveyOutput,
} from "../../survey/tests/__mock__/survey.mock";
} from "../../survey/__mock__/survey.mock";
import {
deleteResponse,
getResponse,

View File

@@ -13,7 +13,7 @@ import {
TSurveyWelcomeCard,
} from "@formbricks/types/surveys/types";
import { TUser } from "@formbricks/types/user";
import { selectSurvey } from "../../service";
import { selectSurvey } from "../service";
const selectContact = {
id: true,

View File

@@ -0,0 +1,113 @@
import { cache } from "@/lib/cache";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { ZId } from "@formbricks/types/common";
import { TSurvey } from "@formbricks/types/surveys/types";
import { hasUserEnvironmentAccess } from "../environment/auth";
import { validateInputs } from "../utils/validate";
import { canUserAccessSurvey } from "./auth";
import { surveyCache } from "./cache";
import { getSurvey } from "./service";
// Mock dependencies
vi.mock("@/lib/cache", () => ({
cache: vi.fn((fn) => fn),
}));
vi.mock("@/lib/utils/validate", () => ({
validateInputs: vi.fn(),
}));
vi.mock("./service", () => ({
getSurvey: vi.fn(),
}));
vi.mock("../environment/auth", () => ({
hasUserEnvironmentAccess: vi.fn(),
}));
vi.mock("./cache", () => ({
surveyCache: {
tag: {
byId: vi.fn().mockReturnValue("survey-tag-id"),
},
},
}));
describe("canUserAccessSurvey", () => {
const userId = "user-123";
const surveyId = "survey-456";
const environmentId = "env-789";
const mockSurvey = {
id: surveyId,
environmentId: environmentId,
} as TSurvey;
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(cache).mockImplementation((fn) => () => fn());
vi.mocked(getSurvey).mockResolvedValue(mockSurvey);
vi.mocked(hasUserEnvironmentAccess).mockResolvedValue(true);
vi.mocked(surveyCache.tag.byId).mockReturnValue(`survey-${surveyId}`);
});
test("validates input parameters", async () => {
await canUserAccessSurvey(userId, surveyId);
expect(validateInputs).toHaveBeenCalledWith([surveyId, ZId], [userId, ZId]);
});
test("returns false if userId is falsy", async () => {
const result = await canUserAccessSurvey("", surveyId);
expect(result).toBe(false);
expect(getSurvey).not.toHaveBeenCalled();
});
test("returns false if survey is not found", async () => {
vi.mocked(getSurvey).mockResolvedValueOnce(null);
await expect(canUserAccessSurvey(userId, surveyId)).rejects.toThrowError("Survey not found");
expect(getSurvey).toHaveBeenCalledWith(surveyId);
expect(hasUserEnvironmentAccess).not.toHaveBeenCalled();
});
test("calls hasUserEnvironmentAccess with userId and survey's environmentId", async () => {
await canUserAccessSurvey(userId, surveyId);
expect(getSurvey).toHaveBeenCalledWith(surveyId);
expect(hasUserEnvironmentAccess).toHaveBeenCalledWith(userId, environmentId);
});
test("returns false if user doesn't have access to the environment", async () => {
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(false);
const result = await canUserAccessSurvey(userId, surveyId);
expect(result).toBe(false);
expect(getSurvey).toHaveBeenCalledWith(surveyId);
expect(hasUserEnvironmentAccess).toHaveBeenCalledWith(userId, environmentId);
});
test("returns true if user has access to the environment", async () => {
const result = await canUserAccessSurvey(userId, surveyId);
expect(result).toBe(true);
expect(getSurvey).toHaveBeenCalledWith(surveyId);
expect(hasUserEnvironmentAccess).toHaveBeenCalledWith(userId, environmentId);
});
test("rethrows errors that occur during execution", async () => {
const error = new Error("Test error");
vi.mocked(getSurvey).mockRejectedValueOnce(error);
await expect(canUserAccessSurvey(userId, surveyId)).rejects.toThrow(error);
});
test("uses cache with correct cache key and tags", async () => {
await canUserAccessSurvey(userId, surveyId);
expect(cache).toHaveBeenCalledWith(expect.any(Function), [`canUserAccessSurvey-${userId}-${surveyId}`], {
tags: [`survey-${surveyId}`],
});
expect(surveyCache.tag.byId).toHaveBeenCalledWith(surveyId);
});
});

View File

@@ -0,0 +1,122 @@
import { cleanup } from "@testing-library/react";
import { revalidateTag } from "next/cache";
import { afterEach, describe, expect, test, vi } from "vitest";
import { surveyCache } from "./cache";
// Mock the revalidateTag function from next/cache
vi.mock("next/cache", () => ({
revalidateTag: vi.fn(),
}));
describe("surveyCache", () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
describe("tag methods", () => {
test("byId returns the correct tag string", () => {
const id = "survey-123";
expect(surveyCache.tag.byId(id)).toBe(`surveys-${id}`);
});
test("byEnvironmentId returns the correct tag string", () => {
const environmentId = "env-456";
expect(surveyCache.tag.byEnvironmentId(environmentId)).toBe(`environments-${environmentId}-surveys`);
});
test("byAttributeClassId returns the correct tag string", () => {
const attributeClassId = "attr-789";
expect(surveyCache.tag.byAttributeClassId(attributeClassId)).toBe(
`attributeFilters-${attributeClassId}-surveys`
);
});
test("byActionClassId returns the correct tag string", () => {
const actionClassId = "action-012";
expect(surveyCache.tag.byActionClassId(actionClassId)).toBe(`actionClasses-${actionClassId}-surveys`);
});
test("bySegmentId returns the correct tag string", () => {
const segmentId = "segment-345";
expect(surveyCache.tag.bySegmentId(segmentId)).toBe(`segments-${segmentId}-surveys`);
});
test("byResultShareKey returns the correct tag string", () => {
const resultShareKey = "share-678";
expect(surveyCache.tag.byResultShareKey(resultShareKey)).toBe(`surveys-resultShare-${resultShareKey}`);
});
});
describe("revalidate method", () => {
test("calls revalidateTag with correct tag when id is provided", () => {
const id = "survey-123";
surveyCache.revalidate({ id });
expect(vi.mocked(revalidateTag)).toHaveBeenCalledWith(`surveys-${id}`);
expect(vi.mocked(revalidateTag)).toHaveBeenCalledTimes(1);
});
test("calls revalidateTag with correct tag when attributeClassId is provided", () => {
const attributeClassId = "attr-789";
surveyCache.revalidate({ attributeClassId });
expect(vi.mocked(revalidateTag)).toHaveBeenCalledWith(`attributeFilters-${attributeClassId}-surveys`);
expect(vi.mocked(revalidateTag)).toHaveBeenCalledTimes(1);
});
test("calls revalidateTag with correct tag when actionClassId is provided", () => {
const actionClassId = "action-012";
surveyCache.revalidate({ actionClassId });
expect(vi.mocked(revalidateTag)).toHaveBeenCalledWith(`actionClasses-${actionClassId}-surveys`);
expect(vi.mocked(revalidateTag)).toHaveBeenCalledTimes(1);
});
test("calls revalidateTag with correct tag when environmentId is provided", () => {
const environmentId = "env-456";
surveyCache.revalidate({ environmentId });
expect(vi.mocked(revalidateTag)).toHaveBeenCalledWith(`environments-${environmentId}-surveys`);
expect(vi.mocked(revalidateTag)).toHaveBeenCalledTimes(1);
});
test("calls revalidateTag with correct tag when segmentId is provided", () => {
const segmentId = "segment-345";
surveyCache.revalidate({ segmentId });
expect(vi.mocked(revalidateTag)).toHaveBeenCalledWith(`segments-${segmentId}-surveys`);
expect(vi.mocked(revalidateTag)).toHaveBeenCalledTimes(1);
});
test("calls revalidateTag with correct tag when resultShareKey is provided", () => {
const resultShareKey = "share-678";
surveyCache.revalidate({ resultShareKey });
expect(vi.mocked(revalidateTag)).toHaveBeenCalledWith(`surveys-resultShare-${resultShareKey}`);
expect(vi.mocked(revalidateTag)).toHaveBeenCalledTimes(1);
});
test("calls revalidateTag multiple times when multiple parameters are provided", () => {
const props = {
id: "survey-123",
environmentId: "env-456",
attributeClassId: "attr-789",
actionClassId: "action-012",
segmentId: "segment-345",
resultShareKey: "share-678",
};
surveyCache.revalidate(props);
expect(vi.mocked(revalidateTag)).toHaveBeenCalledTimes(6);
expect(vi.mocked(revalidateTag)).toHaveBeenCalledWith(`surveys-${props.id}`);
expect(vi.mocked(revalidateTag)).toHaveBeenCalledWith(`environments-${props.environmentId}-surveys`);
expect(vi.mocked(revalidateTag)).toHaveBeenCalledWith(
`attributeFilters-${props.attributeClassId}-surveys`
);
expect(vi.mocked(revalidateTag)).toHaveBeenCalledWith(`actionClasses-${props.actionClassId}-surveys`);
expect(vi.mocked(revalidateTag)).toHaveBeenCalledWith(`segments-${props.segmentId}-surveys`);
expect(vi.mocked(revalidateTag)).toHaveBeenCalledWith(`surveys-resultShare-${props.resultShareKey}`);
});
test("does not call revalidateTag when no parameters are provided", () => {
surveyCache.revalidate({});
expect(vi.mocked(revalidateTag)).not.toHaveBeenCalled();
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -109,7 +109,7 @@ export const selectSurvey = {
followUps: true,
} satisfies Prisma.SurveySelect;
const checkTriggersValidity = (triggers: TSurvey["triggers"], actionClasses: ActionClass[]) => {
export const checkTriggersValidity = (triggers: TSurvey["triggers"], actionClasses: ActionClass[]) => {
if (!triggers) return;
// check if all the triggers are valid

View File

@@ -1,476 +0,0 @@
import { prisma } from "@/lib/__mocks__/database";
import { evaluateLogic } from "@/lib/surveyLogic/utils";
import { Prisma } from "@prisma/client";
import { beforeEach, describe, expect, test } from "vitest";
import { testInputValidation } from "vitestSetup";
import { PrismaErrorType } from "@formbricks/database/types/error";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { getSurvey, getSurveyCount, getSurveys, getSurveysByActionClassId, updateSurvey } from "../service";
import {
mockActionClass,
mockId,
mockOrganizationOutput,
mockSurveyOutput,
mockSurveyWithLogic,
mockTransformedSurveyOutput,
updateSurveyInput,
} from "./__mock__/survey.mock";
beforeEach(() => {
prisma.survey.count.mockResolvedValue(1);
});
describe("evaluateLogic with mockSurveyWithLogic", () => {
test("should return true when q1 answer is blue", () => {
const data = { q1: "blue" };
const variablesData = {};
const result = evaluateLogic(
mockSurveyWithLogic,
data,
variablesData,
mockSurveyWithLogic.questions[0].logic![0].conditions,
"default"
);
expect(result).toBe(true);
});
test("should return false when q1 answer is not blue", () => {
const data = { q1: "red" };
const variablesData = {};
const result = evaluateLogic(
mockSurveyWithLogic,
data,
variablesData,
mockSurveyWithLogic.questions[0].logic![0].conditions,
"default"
);
expect(result).toBe(false);
});
test("should return true when q1 is blue and q2 is pizza", () => {
const data = { q1: "blue", q2: "pizza" };
const variablesData = {};
const result = evaluateLogic(
mockSurveyWithLogic,
data,
variablesData,
mockSurveyWithLogic.questions[1].logic![0].conditions,
"default"
);
expect(result).toBe(true);
});
test("should return false when q1 is blue but q2 is not pizza", () => {
const data = { q1: "blue", q2: "burger" };
const variablesData = {};
const result = evaluateLogic(
mockSurveyWithLogic,
data,
variablesData,
mockSurveyWithLogic.questions[1].logic![0].conditions,
"default"
);
expect(result).toBe(false);
});
test("should return true when q2 is pizza or q3 is Inception", () => {
const data = { q2: "pizza", q3: "Inception" };
const variablesData = {};
const result = evaluateLogic(
mockSurveyWithLogic,
data,
variablesData,
mockSurveyWithLogic.questions[2].logic![0].conditions,
"default"
);
expect(result).toBe(true);
});
test("should return true when var1 is equal to single select question value", () => {
const data = { q4: "lmao" };
const variablesData = { siog1dabtpo3l0a3xoxw2922: "lmao" };
const result = evaluateLogic(
mockSurveyWithLogic,
data,
variablesData,
mockSurveyWithLogic.questions[3].logic![0].conditions,
"default"
);
expect(result).toBe(true);
});
test("should return false when var1 is not equal to single select question value", () => {
const data = { q4: "lol" };
const variablesData = { siog1dabtpo3l0a3xoxw2922: "damn" };
const result = evaluateLogic(
mockSurveyWithLogic,
data,
variablesData,
mockSurveyWithLogic.questions[3].logic![0].conditions,
"default"
);
expect(result).toBe(false);
});
test("should return true when var2 is greater than 30 and less than open text number value", () => {
const data = { q5: "40" };
const variablesData = { km1srr55owtn2r7lkoh5ny1u: 35 };
const result = evaluateLogic(
mockSurveyWithLogic,
data,
variablesData,
mockSurveyWithLogic.questions[4].logic![0].conditions,
"default"
);
expect(result).toBe(true);
});
test("should return false when var2 is not greater than 30 or greater than open text number value", () => {
const data = { q5: "40" };
const variablesData = { km1srr55owtn2r7lkoh5ny1u: 25 };
const result = evaluateLogic(
mockSurveyWithLogic,
data,
variablesData,
mockSurveyWithLogic.questions[4].logic![0].conditions,
"default"
);
expect(result).toBe(false);
});
test("should return for complex condition", () => {
const data = { q6: ["lmao", "XD"], q1: "green", q2: "pizza", q3: "inspection", name: "pizza" };
const variablesData = { siog1dabtpo3l0a3xoxw2922: "tokyo" };
const result = evaluateLogic(
mockSurveyWithLogic,
data,
variablesData,
mockSurveyWithLogic.questions[5].logic![0].conditions,
"default"
);
expect(result).toBe(true);
});
});
describe("Tests for getSurvey", () => {
describe("Happy Path", () => {
test("Returns a survey", async () => {
prisma.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput);
const survey = await getSurvey(mockId);
expect(survey).toEqual(mockTransformedSurveyOutput);
});
test("Returns null if survey is not found", async () => {
prisma.survey.findUnique.mockResolvedValueOnce(null);
const survey = await getSurvey(mockId);
expect(survey).toBeNull();
});
});
describe("Sad Path", () => {
testInputValidation(getSurvey, "123#");
test("should throw a DatabaseError error if there is a PrismaClientKnownRequestError", async () => {
const mockErrorMessage = "Mock error message";
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
code: PrismaErrorType.UniqueConstraintViolation,
clientVersion: "0.0.1",
});
prisma.survey.findUnique.mockRejectedValue(errToThrow);
await expect(getSurvey(mockId)).rejects.toThrow(DatabaseError);
});
test("should throw an error if there is an unknown error", async () => {
const mockErrorMessage = "Mock error message";
prisma.survey.findUnique.mockRejectedValue(new Error(mockErrorMessage));
await expect(getSurvey(mockId)).rejects.toThrow(Error);
});
});
});
describe("Tests for getSurveysByActionClassId", () => {
describe("Happy Path", () => {
test("Returns an array of surveys for a given actionClassId", async () => {
prisma.survey.findMany.mockResolvedValueOnce([mockSurveyOutput]);
const surveys = await getSurveysByActionClassId(mockId);
expect(surveys).toEqual([mockTransformedSurveyOutput]);
});
test("Returns an empty array if no surveys are found", async () => {
prisma.survey.findMany.mockResolvedValueOnce([]);
const surveys = await getSurveysByActionClassId(mockId);
expect(surveys).toEqual([]);
});
});
describe("Sad Path", () => {
testInputValidation(getSurveysByActionClassId, "123#");
test("should throw an error if there is an unknown error", async () => {
const mockErrorMessage = "Unknown error occurred";
prisma.survey.findMany.mockRejectedValue(new Error(mockErrorMessage));
await expect(getSurveysByActionClassId(mockId)).rejects.toThrow(Error);
});
});
});
describe("Tests for getSurveys", () => {
describe("Happy Path", () => {
test("Returns an array of surveys for a given environmentId, limit(optional) and offset(optional)", async () => {
prisma.survey.findMany.mockResolvedValueOnce([mockSurveyOutput]);
const surveys = await getSurveys(mockId);
expect(surveys).toEqual([mockTransformedSurveyOutput]);
});
test("Returns an empty array if no surveys are found", async () => {
prisma.survey.findMany.mockResolvedValueOnce([]);
const surveys = await getSurveys(mockId);
expect(surveys).toEqual([]);
});
});
describe("Sad Path", () => {
testInputValidation(getSurveysByActionClassId, "123#");
test("should throw a DatabaseError error if there is a PrismaClientKnownRequestError", async () => {
const mockErrorMessage = "Mock error message";
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
code: PrismaErrorType.UniqueConstraintViolation,
clientVersion: "0.0.1",
});
prisma.survey.findMany.mockRejectedValue(errToThrow);
await expect(getSurveys(mockId)).rejects.toThrow(DatabaseError);
});
test("should throw an error if there is an unknown error", async () => {
const mockErrorMessage = "Unknown error occurred";
prisma.survey.findMany.mockRejectedValue(new Error(mockErrorMessage));
await expect(getSurveys(mockId)).rejects.toThrow(Error);
});
});
});
describe("Tests for updateSurvey", () => {
beforeEach(() => {
prisma.actionClass.findMany.mockResolvedValueOnce([mockActionClass]);
});
describe("Happy Path", () => {
test("Updates a survey successfully", async () => {
prisma.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput);
prisma.organization.findFirst.mockResolvedValueOnce(mockOrganizationOutput);
prisma.survey.update.mockResolvedValueOnce(mockSurveyOutput);
const updatedSurvey = await updateSurvey(updateSurveyInput);
expect(updatedSurvey).toEqual(mockTransformedSurveyOutput);
});
});
describe("Sad Path", () => {
testInputValidation(updateSurvey, "123#");
test("Throws ResourceNotFoundError if the survey does not exist", async () => {
prisma.survey.findUnique.mockRejectedValueOnce(
new ResourceNotFoundError("Survey", updateSurveyInput.id)
);
await expect(updateSurvey(updateSurveyInput)).rejects.toThrow(ResourceNotFoundError);
});
test("should throw a DatabaseError error if there is a PrismaClientKnownRequestError", async () => {
const mockErrorMessage = "Mock error message";
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
code: PrismaErrorType.UniqueConstraintViolation,
clientVersion: "0.0.1",
});
prisma.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput);
prisma.organization.findFirst.mockResolvedValueOnce(mockOrganizationOutput);
prisma.survey.update.mockRejectedValue(errToThrow);
await expect(updateSurvey(updateSurveyInput)).rejects.toThrow(DatabaseError);
});
test("should throw an error if there is an unknown error", async () => {
const mockErrorMessage = "Unknown error occurred";
prisma.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput);
prisma.survey.update.mockRejectedValue(new Error(mockErrorMessage));
await expect(updateSurvey(updateSurveyInput)).rejects.toThrow(Error);
});
});
});
// describe("Tests for createSurvey", () => {
// beforeEach(() => {
// prisma.actionClass.findMany.mockResolvedValueOnce([mockActionClass]);
// });
// describe("Happy Path", () => {
// test("Creates a survey successfully", async () => {
// prisma.survey.create.mockResolvedValueOnce(mockSurveyOutput);
// prisma.organization.findFirst.mockResolvedValueOnce(mockOrganizationOutput);
// prisma.actionClass.findMany.mockResolvedValue([mockActionClass]);
// prisma.user.findMany.mockResolvedValueOnce([
// {
// ...mockUser,
// twoFactorSecret: null,
// backupCodes: null,
// password: null,
// identityProviderAccountId: null,
// groupId: null,
// role: "engineer",
// },
// ]);
// prisma.user.update.mockResolvedValueOnce({
// ...mockUser,
// twoFactorSecret: null,
// backupCodes: null,
// password: null,
// identityProviderAccountId: null,
// groupId: null,
// role: "engineer",
// });
// const createdSurvey = await createSurvey(mockId, createSurveyInput);
// expect(createdSurvey).toEqual(mockTransformedSurveyOutput);
// });
// });
// describe("Sad Path", () => {
// testInputValidation(createSurvey, "123#", createSurveyInput);
// test("should throw an error if there is an unknown error", async () => {
// const mockErrorMessage = "Unknown error occurred";
// prisma.survey.delete.mockRejectedValue(new Error(mockErrorMessage));
// await expect(createSurvey(mockId, createSurveyInput)).rejects.toThrow(Error);
// });
// });
// });
// describe("Tests for duplicateSurvey", () => {
// beforeEach(() => {
// prisma.actionClass.findMany.mockResolvedValueOnce([mockActionClass]);
// });
// describe("Happy Path", () => {
// test("Duplicates a survey successfully", async () => {
// prisma.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput);
// prisma.survey.create.mockResolvedValueOnce(mockSurveyOutput);
// // @ts-expect-error
// prisma.environment.findUnique.mockResolvedValueOnce(mockEnvironment);
// // @ts-expect-error
// prisma.project.findFirst.mockResolvedValueOnce(mockProject);
// prisma.actionClass.findFirst.mockResolvedValueOnce(mockActionClass);
// prisma.actionClass.create.mockResolvedValueOnce(mockActionClass);
// const createdSurvey = await copySurveyToOtherEnvironment(mockId, mockId, mockId, mockId);
// expect(createdSurvey).toEqual(mockSurveyOutput);
// });
// });
// describe("Sad Path", () => {
// testInputValidation(copySurveyToOtherEnvironment, "123#", "123#", "123#", "123#", "123#");
// test("Throws ResourceNotFoundError if the survey does not exist", async () => {
// prisma.survey.findUnique.mockRejectedValueOnce(new ResourceNotFoundError("Survey", mockId));
// await expect(copySurveyToOtherEnvironment(mockId, mockId, mockId, mockId)).rejects.toThrow(
// ResourceNotFoundError
// );
// });
// test("should throw an error if there is an unknown error", async () => {
// const mockErrorMessage = "Unknown error occurred";
// prisma.survey.create.mockRejectedValue(new Error(mockErrorMessage));
// await expect(copySurveyToOtherEnvironment(mockId, mockId, mockId, mockId)).rejects.toThrow(Error);
// });
// });
// });
// describe("Tests for getSyncSurveys", () => {
// describe("Happy Path", () => {
// beforeEach(() => {
// prisma.project.findFirst.mockResolvedValueOnce({
// ...mockProject,
// brandColor: null,
// highlightBorderColor: null,
// logo: null,
// });
// prisma.display.findMany.mockResolvedValueOnce([mockDisplay]);
// prisma.attributeClass.findMany.mockResolvedValueOnce([mockAttributeClass]);
// });
// test("Returns synced surveys", async () => {
// prisma.survey.findMany.mockResolvedValueOnce([mockSyncSurveyOutput]);
// prisma.person.findUnique.mockResolvedValueOnce(mockPrismaPerson);
// prisma.response.findMany.mockResolvedValue([mockResponseWithMockPerson]);
// prisma.responseNote.findMany.mockResolvedValue([mockResponseNote]);
// const surveys = await getSyncSurveys(mockId, mockPrismaPerson.id, "desktop", {
// version: "1.7.0",
// });
// expect(surveys).toEqual([mockTransformedSyncSurveyOutput]);
// });
// test("Returns an empty array if no surveys are found", async () => {
// prisma.survey.findMany.mockResolvedValueOnce([]);
// prisma.person.findUnique.mockResolvedValueOnce(mockPrismaPerson);
// const surveys = await getSyncSurveys(mockId, mockPrismaPerson.id, "desktop", {
// version: "1.7.0",
// });
// expect(surveys).toEqual([]);
// });
// });
// describe("Sad Path", () => {
// testInputValidation(getSyncSurveys, "123#", {});
// test("does not find a Project", async () => {
// prisma.project.findFirst.mockResolvedValueOnce(null);
// await expect(
// getSyncSurveys(mockId, mockPrismaPerson.id, "desktop", { version: "1.7.0" })
// ).rejects.toThrow(Error);
// });
// test("should throw an error if there is an unknown error", async () => {
// const mockErrorMessage = "Unknown error occurred";
// prisma.actionClass.findMany.mockResolvedValueOnce([mockActionClass]);
// prisma.survey.create.mockRejectedValue(new Error(mockErrorMessage));
// await expect(
// getSyncSurveys(mockId, mockPrismaPerson.id, "desktop", { version: "1.7.0" })
// ).rejects.toThrow(Error);
// });
// });
// });
describe("Tests for getSurveyCount service", () => {
describe("Happy Path", () => {
test("Counts the total number of surveys for a given environment ID", async () => {
const count = await getSurveyCount(mockId);
expect(count).toEqual(1);
});
test("Returns zero count when there are no surveys for a given environment ID", async () => {
prisma.survey.count.mockResolvedValue(0);
const count = await getSurveyCount(mockId);
expect(count).toEqual(0);
});
});
describe("Sad Path", () => {
testInputValidation(getSurveyCount, "123#");
test("Throws a generic Error for other unexpected issues", async () => {
const mockErrorMessage = "Mock error message";
prisma.survey.count.mockRejectedValue(new Error(mockErrorMessage));
await expect(getSurveyCount(mockId)).rejects.toThrow(Error);
});
});
});

View File

@@ -0,0 +1,254 @@
import * as fileValidation from "@/lib/fileValidation";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { InvalidInputError } from "@formbricks/types/errors";
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
import { TSegment } from "@formbricks/types/segment";
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { anySurveyHasFilters, checkForInvalidImagesInQuestions, transformPrismaSurvey } from "./utils";
describe("transformPrismaSurvey", () => {
test("transforms prisma survey without segment", () => {
const surveyPrisma = {
id: "survey1",
name: "Test Survey",
displayPercentage: "30",
segment: null,
};
const result = transformPrismaSurvey(surveyPrisma);
expect(result).toEqual({
id: "survey1",
name: "Test Survey",
displayPercentage: 30,
segment: null,
});
});
test("transforms prisma survey with segment", () => {
const surveyPrisma = {
id: "survey1",
name: "Test Survey",
displayPercentage: "50",
segment: {
id: "segment1",
name: "Test Segment",
filters: [{ id: "filter1", type: "user" }],
surveys: [{ id: "survey1" }, { id: "survey2" }],
},
};
const result = transformPrismaSurvey<TSurvey>(surveyPrisma);
expect(result).toEqual({
id: "survey1",
name: "Test Survey",
displayPercentage: 50,
segment: {
id: "segment1",
name: "Test Segment",
filters: [{ id: "filter1", type: "user" }],
surveys: ["survey1", "survey2"],
},
});
});
test("transforms prisma survey with non-numeric displayPercentage", () => {
const surveyPrisma = {
id: "survey1",
name: "Test Survey",
displayPercentage: "invalid",
};
const result = transformPrismaSurvey<TJsEnvironmentStateSurvey>(surveyPrisma);
expect(result).toEqual({
id: "survey1",
name: "Test Survey",
displayPercentage: null,
segment: null,
});
});
test("transforms prisma survey with undefined displayPercentage", () => {
const surveyPrisma = {
id: "survey1",
name: "Test Survey",
};
const result = transformPrismaSurvey(surveyPrisma);
expect(result).toEqual({
id: "survey1",
name: "Test Survey",
displayPercentage: null,
segment: null,
});
});
});
describe("anySurveyHasFilters", () => {
test("returns false when no surveys have segments", () => {
const surveys = [
{ id: "survey1", name: "Survey 1" },
{ id: "survey2", name: "Survey 2" },
] as TSurvey[];
expect(anySurveyHasFilters(surveys)).toBe(false);
});
test("returns false when surveys have segments but no filters", () => {
const surveys = [
{
id: "survey1",
name: "Survey 1",
segment: {
id: "segment1",
title: "Segment 1",
filters: [],
createdAt: new Date(),
description: "Segment description",
environmentId: "env1",
isPrivate: true,
surveys: ["survey1"],
updatedAt: new Date(),
} as TSegment,
},
{ id: "survey2", name: "Survey 2" },
] as TSurvey[];
expect(anySurveyHasFilters(surveys)).toBe(false);
});
test("returns true when at least one survey has segment with filters", () => {
const surveys = [
{ id: "survey1", name: "Survey 1" },
{
id: "survey2",
name: "Survey 2",
segment: {
id: "segment2",
filters: [
{
id: "filter1",
connector: null,
resource: {
root: { type: "attribute", contactAttributeKey: "attr-1" },
id: "attr-filter-1",
qualifier: { operator: "contains" },
value: "attr",
},
},
],
createdAt: new Date(),
description: "Segment description",
environmentId: "env1",
isPrivate: true,
surveys: ["survey2"],
updatedAt: new Date(),
title: "Segment title",
} as TSegment,
},
] as TSurvey[];
expect(anySurveyHasFilters(surveys)).toBe(true);
});
});
describe("checkForInvalidImagesInQuestions", () => {
beforeEach(() => {
vi.resetAllMocks();
});
test("does not throw error when no images are present", () => {
const questions = [
{ id: "q1", type: TSurveyQuestionTypeEnum.OpenText },
{ id: "q2", type: TSurveyQuestionTypeEnum.MultipleChoiceSingle },
] as TSurveyQuestion[];
expect(() => checkForInvalidImagesInQuestions(questions)).not.toThrow();
});
test("does not throw error when all images are valid", () => {
vi.spyOn(fileValidation, "isValidImageFile").mockReturnValue(true);
const questions = [
{ id: "q1", type: TSurveyQuestionTypeEnum.OpenText, imageUrl: "valid-image.jpg" },
{ id: "q2", type: TSurveyQuestionTypeEnum.MultipleChoiceSingle },
] as TSurveyQuestion[];
expect(() => checkForInvalidImagesInQuestions(questions)).not.toThrow();
expect(fileValidation.isValidImageFile).toHaveBeenCalledWith("valid-image.jpg");
});
test("throws error when question image is invalid", () => {
vi.spyOn(fileValidation, "isValidImageFile").mockReturnValue(false);
const questions = [
{ id: "q1", type: TSurveyQuestionTypeEnum.OpenText, imageUrl: "invalid-image.txt" },
] as TSurveyQuestion[];
expect(() => checkForInvalidImagesInQuestions(questions)).toThrow(
new InvalidInputError("Invalid image file in question 1")
);
expect(fileValidation.isValidImageFile).toHaveBeenCalledWith("invalid-image.txt");
});
test("throws error when picture selection question has no choices", () => {
const questions = [
{
id: "q1",
type: TSurveyQuestionTypeEnum.PictureSelection,
},
] as TSurveyQuestion[];
expect(() => checkForInvalidImagesInQuestions(questions)).toThrow(
new InvalidInputError("Choices missing for question 1")
);
});
test("throws error when picture selection choice has invalid image", () => {
vi.spyOn(fileValidation, "isValidImageFile").mockImplementation((url) => url === "valid-image.jpg");
const questions = [
{
id: "q1",
type: TSurveyQuestionTypeEnum.PictureSelection,
choices: [
{ id: "c1", imageUrl: "valid-image.jpg" },
{ id: "c2", imageUrl: "invalid-image.txt" },
],
},
] as TSurveyQuestion[];
expect(() => checkForInvalidImagesInQuestions(questions)).toThrow(
new InvalidInputError("Invalid image file for choice 2 in question 1")
);
expect(fileValidation.isValidImageFile).toHaveBeenCalledTimes(2);
expect(fileValidation.isValidImageFile).toHaveBeenNthCalledWith(1, "valid-image.jpg");
expect(fileValidation.isValidImageFile).toHaveBeenNthCalledWith(2, "invalid-image.txt");
});
test("validates all choices in picture selection questions", () => {
vi.spyOn(fileValidation, "isValidImageFile").mockReturnValue(true);
const questions = [
{
id: "q1",
type: TSurveyQuestionTypeEnum.PictureSelection,
choices: [
{ id: "c1", imageUrl: "image1.jpg" },
{ id: "c2", imageUrl: "image2.jpg" },
{ id: "c3", imageUrl: "image3.jpg" },
],
},
] as TSurveyQuestion[];
expect(() => checkForInvalidImagesInQuestions(questions)).not.toThrow();
expect(fileValidation.isValidImageFile).toHaveBeenCalledTimes(3);
expect(fileValidation.isValidImageFile).toHaveBeenNthCalledWith(1, "image1.jpg");
expect(fileValidation.isValidImageFile).toHaveBeenNthCalledWith(2, "image2.jpg");
expect(fileValidation.isValidImageFile).toHaveBeenNthCalledWith(3, "image3.jpg");
});
});

View File

@@ -0,0 +1,386 @@
import { getMembershipRole } from "@/lib/membership/hooks/actions";
import { getProjectPermissionByUserId, getTeamRoleByTeamIdUserId } from "@/modules/ee/teams/lib/roles";
import { cleanup } from "@testing-library/react";
import { returnValidationErrors } from "next-safe-action";
import { afterEach, describe, expect, test, vi } from "vitest";
import { ZodIssue, z } from "zod";
import { AuthorizationError } from "@formbricks/types/errors";
import { checkAuthorizationUpdated, formatErrors } from "./action-client-middleware";
vi.mock("@/lib/membership/hooks/actions", () => ({
getMembershipRole: vi.fn(),
}));
vi.mock("@/modules/ee/teams/lib/roles", () => ({
getProjectPermissionByUserId: vi.fn(),
getTeamRoleByTeamIdUserId: vi.fn(),
}));
vi.mock("next-safe-action", () => ({
returnValidationErrors: vi.fn(),
}));
describe("action-client-middleware", () => {
const userId = "user-1";
const organizationId = "org-1";
const projectId = "project-1";
const teamId = "team-1";
afterEach(() => {
cleanup();
vi.resetAllMocks();
});
describe("formatErrors", () => {
// We need to access the private function for testing
// Using any to access the function directly
test("formats simple path ZodIssue", () => {
const issues = [
{
code: "custom",
path: ["name"],
message: "Name is required",
},
] as ZodIssue[];
const result = formatErrors(issues);
expect(result).toEqual({
name: {
_errors: ["Name is required"],
},
});
});
test("formats nested path ZodIssue", () => {
const issues = [
{
code: "custom",
path: ["user", "address", "street"],
message: "Street is required",
},
] as ZodIssue[];
const result = formatErrors(issues);
expect(result).toEqual({
"user.address.street": {
_errors: ["Street is required"],
},
});
});
test("formats multiple ZodIssues", () => {
const issues = [
{
code: "custom",
path: ["name"],
message: "Name is required",
},
{
code: "custom",
path: ["email"],
message: "Invalid email",
},
] as ZodIssue[];
const result = formatErrors(issues);
expect(result).toEqual({
name: {
_errors: ["Name is required"],
},
email: {
_errors: ["Invalid email"],
},
});
});
});
describe("checkAuthorizationUpdated", () => {
test("returns validation errors when schema validation fails", async () => {
vi.mocked(getMembershipRole).mockResolvedValue("owner");
const mockSchema = z.object({
name: z.string(),
});
const mockData = { name: 123 }; // Type error to trigger validation failure
vi.mocked(returnValidationErrors).mockReturnValue("validation-error" as unknown as never);
const access = [
{
type: "organization" as const,
schema: mockSchema,
data: mockData as any,
roles: ["owner" as const],
},
];
const result = await checkAuthorizationUpdated({
userId,
organizationId,
access,
});
expect(returnValidationErrors).toHaveBeenCalledWith(expect.any(Object), expect.any(Object));
expect(result).toBe("validation-error");
});
test("returns true when organization access matches role", async () => {
vi.mocked(getMembershipRole).mockResolvedValue("owner");
const access = [
{
type: "organization" as const,
roles: ["owner" as const],
},
];
const result = await checkAuthorizationUpdated({ userId, organizationId, access });
expect(result).toBe(true);
});
test("continues checking other access items when organization role doesn't match", async () => {
vi.mocked(getMembershipRole).mockResolvedValue("member");
const access = [
{
type: "organization" as const,
roles: ["owner" as const],
},
{
type: "projectTeam" as const,
projectId,
minPermission: "read" as const,
},
];
vi.mocked(getProjectPermissionByUserId).mockResolvedValue("readWrite");
const result = await checkAuthorizationUpdated({ userId, organizationId, access });
expect(result).toBe(true);
expect(getProjectPermissionByUserId).toHaveBeenCalledWith(userId, projectId);
});
test("returns true when projectTeam access matches permission", async () => {
vi.mocked(getMembershipRole).mockResolvedValue("member");
const access = [
{
type: "projectTeam" as const,
projectId,
minPermission: "read" as const,
},
];
vi.mocked(getProjectPermissionByUserId).mockResolvedValue("readWrite");
const result = await checkAuthorizationUpdated({ userId, organizationId, access });
expect(result).toBe(true);
expect(getProjectPermissionByUserId).toHaveBeenCalledWith(userId, projectId);
});
test("continues checking other access items when projectTeam permission is insufficient", async () => {
vi.mocked(getMembershipRole).mockResolvedValue("member");
const access = [
{
type: "projectTeam" as const,
projectId,
minPermission: "manage" as const,
},
{
type: "team" as const,
teamId,
minPermission: "contributor" as const,
},
];
vi.mocked(getProjectPermissionByUserId).mockResolvedValue("read");
vi.mocked(getTeamRoleByTeamIdUserId).mockResolvedValue("admin");
const result = await checkAuthorizationUpdated({ userId, organizationId, access });
expect(result).toBe(true);
expect(getProjectPermissionByUserId).toHaveBeenCalledWith(userId, projectId);
expect(getTeamRoleByTeamIdUserId).toHaveBeenCalledWith(teamId, userId);
});
test("returns true when team access matches role", async () => {
vi.mocked(getMembershipRole).mockResolvedValue("member");
const access = [
{
type: "team" as const,
teamId,
minPermission: "contributor" as const,
},
];
vi.mocked(getTeamRoleByTeamIdUserId).mockResolvedValue("admin");
const result = await checkAuthorizationUpdated({ userId, organizationId, access });
expect(result).toBe(true);
expect(getTeamRoleByTeamIdUserId).toHaveBeenCalledWith(teamId, userId);
});
test("continues checking other access items when team role is insufficient", async () => {
vi.mocked(getMembershipRole).mockResolvedValue("member");
const access = [
{
type: "team" as const,
teamId,
minPermission: "admin" as const,
},
{
type: "organization" as const,
roles: ["member" as const],
},
];
vi.mocked(getTeamRoleByTeamIdUserId).mockResolvedValue("contributor");
const result = await checkAuthorizationUpdated({ userId, organizationId, access });
expect(result).toBe(true);
expect(getTeamRoleByTeamIdUserId).toHaveBeenCalledWith(teamId, userId);
});
test("throws AuthorizationError when no access matches", async () => {
vi.mocked(getMembershipRole).mockResolvedValue("member");
const access = [
{
type: "organization" as const,
roles: ["owner" as const],
},
{
type: "projectTeam" as const,
projectId,
minPermission: "manage" as const,
},
{
type: "team" as const,
teamId,
minPermission: "admin" as const,
},
];
vi.mocked(getProjectPermissionByUserId).mockResolvedValue("read");
vi.mocked(getTeamRoleByTeamIdUserId).mockResolvedValue("contributor");
await expect(checkAuthorizationUpdated({ userId, organizationId, access })).rejects.toThrow(
AuthorizationError
);
await expect(checkAuthorizationUpdated({ userId, organizationId, access })).rejects.toThrow(
"Not authorized"
);
});
test("continues to check when projectPermission is null", async () => {
vi.mocked(getMembershipRole).mockResolvedValue("member");
const access = [
{
type: "projectTeam" as const,
projectId,
minPermission: "read" as const,
},
{
type: "organization" as const,
roles: ["member" as const],
},
];
vi.mocked(getProjectPermissionByUserId).mockResolvedValue(null);
const result = await checkAuthorizationUpdated({ userId, organizationId, access });
expect(result).toBe(true);
});
test("continues to check when teamRole is null", async () => {
vi.mocked(getMembershipRole).mockResolvedValue("member");
const access = [
{
type: "team" as const,
teamId,
minPermission: "contributor" as const,
},
{
type: "organization" as const,
roles: ["member" as const],
},
];
vi.mocked(getTeamRoleByTeamIdUserId).mockResolvedValue(null);
const result = await checkAuthorizationUpdated({ userId, organizationId, access });
expect(result).toBe(true);
});
test("returns true when schema validation passes", async () => {
vi.mocked(getMembershipRole).mockResolvedValue("owner");
const mockSchema = z.object({
name: z.string(),
});
const mockData = { name: "test" };
const access = [
{
type: "organization" as const,
schema: mockSchema,
data: mockData,
roles: ["owner" as const],
},
];
const result = await checkAuthorizationUpdated({ userId, organizationId, access });
expect(result).toBe(true);
});
test("handles projectTeam access without minPermission specified", async () => {
vi.mocked(getMembershipRole).mockResolvedValue("member");
const access = [
{
type: "projectTeam" as const,
projectId,
},
];
vi.mocked(getProjectPermissionByUserId).mockResolvedValue("read");
const result = await checkAuthorizationUpdated({ userId, organizationId, access });
expect(result).toBe(true);
});
test("handles team access without minPermission specified", async () => {
vi.mocked(getMembershipRole).mockResolvedValue("member");
const access = [
{
type: "team" as const,
teamId,
},
];
vi.mocked(getTeamRoleByTeamIdUserId).mockResolvedValue("contributor");
const result = await checkAuthorizationUpdated({ userId, organizationId, access });
expect(result).toBe(true);
});
});
});

View File

@@ -7,7 +7,7 @@ import { ZodIssue, z } from "zod";
import { AuthorizationError } from "@formbricks/types/errors";
import { type TOrganizationRole } from "@formbricks/types/memberships";
const formatErrors = (issues: ZodIssue[]): Record<string, { _errors: string[] }> => {
export const formatErrors = (issues: ZodIssue[]): Record<string, { _errors: string[] }> => {
return {
...issues.reduce((acc, issue) => {
acc[issue.path.join(".")] = {

View File

@@ -0,0 +1,70 @@
import { describe, expect, test, vi } from "vitest";
import { hexToRGBA, isLight, mixColor } from "./colors";
describe("Color utilities", () => {
describe("hexToRGBA", () => {
test("should convert hex to rgba", () => {
expect(hexToRGBA("#000000", 1)).toBe("rgba(0, 0, 0, 1)");
expect(hexToRGBA("#FFFFFF", 0.5)).toBe("rgba(255, 255, 255, 0.5)");
expect(hexToRGBA("#FF0000", 0.8)).toBe("rgba(255, 0, 0, 0.8)");
});
test("should convert shorthand hex to rgba", () => {
expect(hexToRGBA("#000", 1)).toBe("rgba(0, 0, 0, 1)");
expect(hexToRGBA("#FFF", 0.5)).toBe("rgba(255, 255, 255, 0.5)");
expect(hexToRGBA("#F00", 0.8)).toBe("rgba(255, 0, 0, 0.8)");
});
test("should handle hex without # prefix", () => {
expect(hexToRGBA("000000", 1)).toBe("rgba(0, 0, 0, 1)");
expect(hexToRGBA("FFFFFF", 0.5)).toBe("rgba(255, 255, 255, 0.5)");
});
test("should return undefined for undefined or empty input", () => {
expect(hexToRGBA(undefined, 1)).toBeUndefined();
expect(hexToRGBA("", 0.5)).toBeUndefined();
});
test("should return empty string for invalid hex", () => {
expect(hexToRGBA("invalid", 1)).toBe("");
});
});
describe("mixColor", () => {
test("should mix two colors with given weight", () => {
expect(mixColor("#000000", "#FFFFFF", 0.5)).toBe("#808080");
expect(mixColor("#FF0000", "#0000FF", 0.5)).toBe("#800080");
expect(mixColor("#FF0000", "#00FF00", 0.75)).toBe("#40bf00");
});
test("should handle edge cases", () => {
expect(mixColor("#000000", "#FFFFFF", 0)).toBe("#000000");
expect(mixColor("#000000", "#FFFFFF", 1)).toBe("#ffffff");
});
});
describe("isLight", () => {
test("should determine if a color is light", () => {
expect(isLight("#FFFFFF")).toBe(true);
expect(isLight("#EEEEEE")).toBe(true);
expect(isLight("#FFFF00")).toBe(true);
});
test("should determine if a color is dark", () => {
expect(isLight("#000000")).toBe(false);
expect(isLight("#333333")).toBe(false);
expect(isLight("#0000FF")).toBe(false);
});
test("should handle shorthand hex colors", () => {
expect(isLight("#FFF")).toBe(true);
expect(isLight("#000")).toBe(false);
expect(isLight("#F00")).toBe(false);
});
test("should throw error for invalid colors", () => {
expect(() => isLight("invalid-color")).toThrow("Invalid color");
expect(() => isLight("#1")).toThrow("Invalid color");
});
});
});

View File

@@ -1,4 +1,4 @@
const hexToRGBA = (hex: string | undefined, opacity: number): string | undefined => {
export const hexToRGBA = (hex: string | undefined, opacity: number): string | undefined => {
// return undefined if hex is undefined, this is important for adding the default values to the CSS variables
// TODO: find a better way to handle this
if (!hex || hex === "") return undefined;

View File

@@ -0,0 +1,64 @@
import { describe, expect, test } from "vitest";
import { TContactAttributes } from "@formbricks/types/contact-attribute";
import { TResponseContact } from "@formbricks/types/responses";
import { getContactIdentifier } from "./contact";
describe("getContactIdentifier", () => {
test("should return email from contactAttributes when available", () => {
const contactAttributes: TContactAttributes = {
email: "test@example.com",
};
const contact: TResponseContact = {
id: "contact1",
userId: "user123",
};
const result = getContactIdentifier(contact, contactAttributes);
expect(result).toBe("test@example.com");
});
test("should return userId from contact when email is not available", () => {
const contactAttributes: TContactAttributes = {};
const contact: TResponseContact = {
id: "contact2",
userId: "user123",
};
const result = getContactIdentifier(contact, contactAttributes);
expect(result).toBe("user123");
});
test("should return empty string when both email and userId are not available", () => {
const contactAttributes: TContactAttributes = {};
const contact: TResponseContact = {
id: "contact3",
};
const result = getContactIdentifier(contact, contactAttributes);
expect(result).toBe("");
});
test("should return empty string when both contact and contactAttributes are null", () => {
const result = getContactIdentifier(null, null);
expect(result).toBe("");
});
test("should return userId when contactAttributes is null", () => {
const contact: TResponseContact = {
id: "contact4",
userId: "user123",
};
const result = getContactIdentifier(contact, null);
expect(result).toBe("user123");
});
test("should return email when contact is null", () => {
const contactAttributes: TContactAttributes = {
email: "test@example.com",
};
const result = getContactIdentifier(null, contactAttributes);
expect(result).toBe("test@example.com");
});
});

View File

@@ -0,0 +1,27 @@
import { describe, expect, test } from "vitest";
import { diffInDays, formatDateWithOrdinal, getFormattedDateTimeString, isValidDateString } from "./datetime";
describe("datetime utils", () => {
test("diffInDays calculates the difference in days between two dates", () => {
const date1 = new Date("2025-05-01");
const date2 = new Date("2025-05-06");
expect(diffInDays(date1, date2)).toBe(5);
});
test("formatDateWithOrdinal formats a date with ordinal suffix", () => {
const date = new Date("2025-05-06");
expect(formatDateWithOrdinal(date)).toBe("Tuesday, May 6th, 2025");
});
test("isValidDateString validates correct date strings", () => {
expect(isValidDateString("2025-05-06")).toBeTruthy();
expect(isValidDateString("06-05-2025")).toBeTruthy();
expect(isValidDateString("2025/05/06")).toBeFalsy();
expect(isValidDateString("invalid-date")).toBeFalsy();
});
test("getFormattedDateTimeString formats a date-time string correctly", () => {
const date = new Date("2025-05-06T14:30:00");
expect(getFormattedDateTimeString(date)).toBe("2025-05-06 14:30:00");
});
});

View File

@@ -0,0 +1,50 @@
import { describe, expect, test } from "vitest";
import { isValidEmail } from "./email";
describe("isValidEmail", () => {
test("validates correct email formats", () => {
// Valid email addresses
expect(isValidEmail("test@example.com")).toBe(true);
expect(isValidEmail("test.user@example.com")).toBe(true);
expect(isValidEmail("test+user@example.com")).toBe(true);
expect(isValidEmail("test_user@example.com")).toBe(true);
expect(isValidEmail("test-user@example.com")).toBe(true);
expect(isValidEmail("test'user@example.com")).toBe(true);
expect(isValidEmail("test@example.co.uk")).toBe(true);
expect(isValidEmail("test@subdomain.example.com")).toBe(true);
});
test("rejects invalid email formats", () => {
// Missing @ symbol
expect(isValidEmail("testexample.com")).toBe(false);
// Multiple @ symbols
expect(isValidEmail("test@example@com")).toBe(false);
// Invalid characters
expect(isValidEmail("test user@example.com")).toBe(false);
expect(isValidEmail("test<>user@example.com")).toBe(false);
// Missing domain
expect(isValidEmail("test@")).toBe(false);
// Missing local part
expect(isValidEmail("@example.com")).toBe(false);
// Starting or ending with dots in local part
expect(isValidEmail(".test@example.com")).toBe(false);
expect(isValidEmail("test.@example.com")).toBe(false);
// Consecutive dots
expect(isValidEmail("test..user@example.com")).toBe(false);
// Empty string
expect(isValidEmail("")).toBe(false);
// Only whitespace
expect(isValidEmail(" ")).toBe(false);
// TLD too short
expect(isValidEmail("test@example.c")).toBe(false);
});
});

View File

@@ -0,0 +1,63 @@
import { AsyncParser } from "@json2csv/node";
import { describe, expect, test, vi } from "vitest";
import * as xlsx from "xlsx";
import { logger } from "@formbricks/logger";
import { convertToCsv, convertToXlsxBuffer } from "./file-conversion";
// Mock the logger to capture error calls
vi.mock("@formbricks/logger", () => ({
logger: { error: vi.fn() },
}));
describe("convertToCsv", () => {
const fields = ["name", "age"];
const data = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
];
test("should convert JSON array to CSV string with header", async () => {
const csv = await convertToCsv(fields, data);
const lines = csv.trim().split("\n");
// json2csv quotes headers by default
expect(lines[0]).toBe('"name","age"');
expect(lines[1]).toBe('"Alice",30');
expect(lines[2]).toBe('"Bob",25');
});
test("should log an error and throw when conversion fails", async () => {
const parseSpy = vi.spyOn(AsyncParser.prototype, "parse").mockImplementation(
() =>
({
promise: () => Promise.reject(new Error("Test parse error")),
}) as any
);
await expect(convertToCsv(fields, data)).rejects.toThrow("Failed to convert to CSV");
expect(logger.error).toHaveBeenCalledWith(expect.any(Error), "Failed to convert to CSV");
parseSpy.mockRestore();
});
});
describe("convertToXlsxBuffer", () => {
const fields = ["name", "age"];
const data = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
];
test("should convert JSON array to XLSX buffer and preserve data", () => {
const buffer = convertToXlsxBuffer(fields, data);
const wb = xlsx.read(buffer, { type: "buffer" });
const sheet = wb.Sheets["Sheet1"];
// Skip header row (range:1) and remove internal row metadata
const raw = xlsx.utils.sheet_to_json<Record<string, string | number>>(sheet, {
header: fields,
defval: "",
range: 1,
});
const cleaned = raw.map(({ __rowNum__, ...rest }) => rest);
expect(cleaned).toEqual(data);
});
});

View File

@@ -0,0 +1,36 @@
import { describe, expect, test } from "vitest";
import { deviceType } from "./headers";
describe("deviceType", () => {
test("should return 'phone' for mobile user agents", () => {
const mobileUserAgents = [
"Mozilla/5.0 (Linux; Android 10; SM-G960F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Mobile Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (iPad; CPU OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (BlackBerry; U; BlackBerry 9900; en) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.346 Mobile Safari/534.11+",
"Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch)",
"Mozilla/5.0 (iPod; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1",
"Opera/9.80 (Android; Opera Mini/36.2.2254/119.132; U; id) Presto/2.12.423 Version/12.16",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.59 (Edition Campaign WPDesktop)",
];
mobileUserAgents.forEach((userAgent) => {
expect(deviceType(userAgent)).toBe("phone");
});
});
test("should return 'desktop' for non-mobile user agents", () => {
const desktopUserAgents = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0",
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0",
"",
];
desktopUserAgents.forEach((userAgent) => {
expect(deviceType(userAgent)).toBe("desktop");
});
});
});

View File

@@ -0,0 +1,795 @@
import * as services from "@/lib/utils/services";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import {
getEnvironmentIdFromInsightId,
getEnvironmentIdFromResponseId,
getEnvironmentIdFromSegmentId,
getEnvironmentIdFromSurveyId,
getEnvironmentIdFromTagId,
getFormattedErrorMessage,
getOrganizationIdFromActionClassId,
getOrganizationIdFromApiKeyId,
getOrganizationIdFromContactId,
getOrganizationIdFromDocumentId,
getOrganizationIdFromEnvironmentId,
getOrganizationIdFromInsightId,
getOrganizationIdFromIntegrationId,
getOrganizationIdFromInviteId,
getOrganizationIdFromLanguageId,
getOrganizationIdFromProjectId,
getOrganizationIdFromResponseId,
getOrganizationIdFromResponseNoteId,
getOrganizationIdFromSegmentId,
getOrganizationIdFromSurveyId,
getOrganizationIdFromTagId,
getOrganizationIdFromTeamId,
getOrganizationIdFromWebhookId,
getProductIdFromContactId,
getProjectIdFromActionClassId,
getProjectIdFromContactId,
getProjectIdFromDocumentId,
getProjectIdFromEnvironmentId,
getProjectIdFromInsightId,
getProjectIdFromIntegrationId,
getProjectIdFromLanguageId,
getProjectIdFromResponseId,
getProjectIdFromResponseNoteId,
getProjectIdFromSegmentId,
getProjectIdFromSurveyId,
getProjectIdFromTagId,
getProjectIdFromWebhookId,
isStringMatch,
} from "./helper";
// Mock all service functions
vi.mock("@/lib/utils/services", () => ({
getProject: vi.fn(),
getEnvironment: vi.fn(),
getSurvey: vi.fn(),
getResponse: vi.fn(),
getContact: vi.fn(),
getResponseNote: vi.fn(),
getSegment: vi.fn(),
getActionClass: vi.fn(),
getIntegration: vi.fn(),
getWebhook: vi.fn(),
getApiKey: vi.fn(),
getInvite: vi.fn(),
getLanguage: vi.fn(),
getTeam: vi.fn(),
getInsight: vi.fn(),
getDocument: vi.fn(),
getTag: vi.fn(),
}));
describe("Helper Utilities", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("getFormattedErrorMessage", () => {
test("returns server error when present", () => {
const result = {
serverError: "Internal server error occurred",
validationErrors: {},
};
expect(getFormattedErrorMessage(result)).toBe("Internal server error occurred");
});
test("formats validation errors correctly with _errors", () => {
const result = {
validationErrors: {
_errors: ["Invalid input", "Missing required field"],
},
};
expect(getFormattedErrorMessage(result)).toBe("Invalid input, Missing required field");
});
test("formats validation errors for specific fields", () => {
const result = {
validationErrors: {
name: { _errors: ["Name is required"] },
email: { _errors: ["Email is invalid"] },
},
};
expect(getFormattedErrorMessage(result)).toBe("nameName is required\nemailEmail is invalid");
});
test("returns empty string for undefined errors", () => {
const result = { validationErrors: undefined };
expect(getFormattedErrorMessage(result)).toBe("");
});
});
describe("Organization ID retrieval functions", () => {
test("getOrganizationIdFromProjectId returns organization ID when project exists", async () => {
vi.mocked(services.getProject).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromProjectId("project1");
expect(orgId).toBe("org1");
expect(services.getProject).toHaveBeenCalledWith("project1");
});
test("getOrganizationIdFromProjectId throws error when project not found", async () => {
vi.mocked(services.getProject).mockResolvedValueOnce(null);
await expect(getOrganizationIdFromProjectId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
expect(services.getProject).toHaveBeenCalledWith("nonexistent");
});
test("getOrganizationIdFromEnvironmentId returns organization ID through project", async () => {
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
vi.mocked(services.getProject).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromEnvironmentId("env1");
expect(orgId).toBe("org1");
expect(services.getEnvironment).toHaveBeenCalledWith("env1");
expect(services.getProject).toHaveBeenCalledWith("project1");
});
test("getOrganizationIdFromEnvironmentId throws error when environment not found", async () => {
vi.mocked(services.getEnvironment).mockResolvedValueOnce(null);
await expect(getOrganizationIdFromEnvironmentId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getOrganizationIdFromSurveyId returns organization ID through environment and project", async () => {
vi.mocked(services.getSurvey).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
vi.mocked(services.getProject).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromSurveyId("survey1");
expect(orgId).toBe("org1");
expect(services.getSurvey).toHaveBeenCalledWith("survey1");
expect(services.getEnvironment).toHaveBeenCalledWith("env1");
expect(services.getProject).toHaveBeenCalledWith("project1");
});
test("getOrganizationIdFromSurveyId throws error when survey not found", async () => {
vi.mocked(services.getSurvey).mockResolvedValueOnce(null);
await expect(getOrganizationIdFromSurveyId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getOrganizationIdFromResponseId returns organization ID through the response hierarchy", async () => {
vi.mocked(services.getResponse).mockResolvedValueOnce({
surveyId: "survey1",
});
vi.mocked(services.getSurvey).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
vi.mocked(services.getProject).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromResponseId("response1");
expect(orgId).toBe("org1");
});
test("getOrganizationIdFromResponseId throws error when response not found", async () => {
vi.mocked(services.getResponse).mockResolvedValueOnce(null);
await expect(getOrganizationIdFromResponseId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getOrganizationIdFromContactId returns organization ID correctly", async () => {
vi.mocked(services.getContact).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
vi.mocked(services.getProject).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromContactId("contact1");
expect(orgId).toBe("org1");
});
test("getOrganizationIdFromContactId throws error when contact not found", async () => {
vi.mocked(services.getContact).mockResolvedValueOnce(null);
await expect(getOrganizationIdFromContactId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getOrganizationIdFromTagId returns organization ID correctly", async () => {
vi.mocked(services.getTag).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
vi.mocked(services.getProject).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromTagId("tag1");
expect(orgId).toBe("org1");
});
test("getOrganizationIdFromTagId throws error when tag not found", async () => {
vi.mocked(services.getTag).mockResolvedValueOnce(null);
await expect(getOrganizationIdFromTagId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getOrganizationIdFromResponseNoteId returns organization ID correctly", async () => {
vi.mocked(services.getResponseNote).mockResolvedValueOnce({
responseId: "response1",
});
vi.mocked(services.getResponse).mockResolvedValueOnce({
surveyId: "survey1",
});
vi.mocked(services.getSurvey).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
vi.mocked(services.getProject).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromResponseNoteId("note1");
expect(orgId).toBe("org1");
});
test("getOrganizationIdFromResponseNoteId throws error when note not found", async () => {
vi.mocked(services.getResponseNote).mockResolvedValueOnce(null);
await expect(getOrganizationIdFromResponseNoteId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getOrganizationIdFromSegmentId returns organization ID correctly", async () => {
vi.mocked(services.getSegment).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
vi.mocked(services.getProject).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromSegmentId("segment1");
expect(orgId).toBe("org1");
});
test("getOrganizationIdFromSegmentId throws error when segment not found", async () => {
vi.mocked(services.getSegment).mockResolvedValueOnce(null);
await expect(getOrganizationIdFromSegmentId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getOrganizationIdFromActionClassId returns organization ID correctly", async () => {
vi.mocked(services.getActionClass).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
vi.mocked(services.getProject).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromActionClassId("action1");
expect(orgId).toBe("org1");
});
test("getOrganizationIdFromActionClassId throws error when actionClass not found", async () => {
vi.mocked(services.getActionClass).mockResolvedValueOnce(null);
await expect(getOrganizationIdFromActionClassId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getOrganizationIdFromIntegrationId returns organization ID correctly", async () => {
vi.mocked(services.getIntegration).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
vi.mocked(services.getProject).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromIntegrationId("integration1");
expect(orgId).toBe("org1");
});
test("getOrganizationIdFromIntegrationId throws error when integration not found", async () => {
vi.mocked(services.getIntegration).mockResolvedValueOnce(null);
await expect(getOrganizationIdFromIntegrationId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getOrganizationIdFromWebhookId returns organization ID correctly", async () => {
vi.mocked(services.getWebhook).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
vi.mocked(services.getProject).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromWebhookId("webhook1");
expect(orgId).toBe("org1");
});
test("getOrganizationIdFromWebhookId throws error when webhook not found", async () => {
vi.mocked(services.getWebhook).mockResolvedValueOnce(null);
await expect(getOrganizationIdFromWebhookId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getOrganizationIdFromApiKeyId returns organization ID directly", async () => {
vi.mocked(services.getApiKey).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromApiKeyId("apikey1");
expect(orgId).toBe("org1");
});
test("getOrganizationIdFromApiKeyId throws error when apiKey not found", async () => {
vi.mocked(services.getApiKey).mockResolvedValueOnce(null);
await expect(getOrganizationIdFromApiKeyId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getOrganizationIdFromInviteId returns organization ID directly", async () => {
vi.mocked(services.getInvite).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromInviteId("invite1");
expect(orgId).toBe("org1");
});
test("getOrganizationIdFromInviteId throws error when invite not found", async () => {
vi.mocked(services.getInvite).mockResolvedValueOnce(null);
await expect(getOrganizationIdFromInviteId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getOrganizationIdFromLanguageId returns organization ID correctly", async () => {
vi.mocked(services.getLanguage).mockResolvedValueOnce({
projectId: "project1",
});
vi.mocked(services.getProject).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromLanguageId("lang1");
expect(orgId).toBe("org1");
});
test("getOrganizationIdFromLanguageId throws error when language not found", async () => {
vi.mocked(services.getLanguage).mockResolvedValueOnce(undefined as unknown as any);
await expect(getOrganizationIdFromLanguageId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getOrganizationIdFromTeamId returns organization ID directly", async () => {
vi.mocked(services.getTeam).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromTeamId("team1");
expect(orgId).toBe("org1");
});
test("getOrganizationIdFromTeamId throws error when team not found", async () => {
vi.mocked(services.getTeam).mockResolvedValueOnce(null);
await expect(getOrganizationIdFromTeamId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getOrganizationIdFromInsightId returns organization ID correctly", async () => {
vi.mocked(services.getInsight).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
vi.mocked(services.getProject).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromInsightId("insight1");
expect(orgId).toBe("org1");
});
test("getOrganizationIdFromInsightId throws error when insight not found", async () => {
vi.mocked(services.getInsight).mockResolvedValueOnce(null);
await expect(getOrganizationIdFromInsightId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getOrganizationIdFromDocumentId returns organization ID correctly", async () => {
vi.mocked(services.getDocument).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
vi.mocked(services.getProject).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromDocumentId("doc1");
expect(orgId).toBe("org1");
});
test("getOrganizationIdFromDocumentId throws error when document not found", async () => {
vi.mocked(services.getDocument).mockResolvedValueOnce(null);
await expect(getOrganizationIdFromDocumentId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
});
describe("Project ID retrieval functions", () => {
test("getProjectIdFromEnvironmentId returns project ID directly", async () => {
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
const projectId = await getProjectIdFromEnvironmentId("env1");
expect(projectId).toBe("project1");
expect(services.getEnvironment).toHaveBeenCalledWith("env1");
});
test("getProjectIdFromEnvironmentId throws error when environment not found", async () => {
vi.mocked(services.getEnvironment).mockResolvedValueOnce(null);
await expect(getProjectIdFromEnvironmentId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getProjectIdFromSurveyId returns project ID through environment", async () => {
vi.mocked(services.getSurvey).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
const projectId = await getProjectIdFromSurveyId("survey1");
expect(projectId).toBe("project1");
expect(services.getSurvey).toHaveBeenCalledWith("survey1");
expect(services.getEnvironment).toHaveBeenCalledWith("env1");
});
test("getProjectIdFromSurveyId throws error when survey not found", async () => {
vi.mocked(services.getSurvey).mockResolvedValueOnce(null);
await expect(getProjectIdFromSurveyId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getProjectIdFromContactId returns project ID correctly", async () => {
vi.mocked(services.getContact).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
const projectId = await getProjectIdFromContactId("contact1");
expect(projectId).toBe("project1");
});
test("getProjectIdFromContactId throws error when contact not found", async () => {
vi.mocked(services.getContact).mockResolvedValueOnce(null);
await expect(getProjectIdFromContactId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getProjectIdFromInsightId returns project ID correctly", async () => {
vi.mocked(services.getInsight).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
const projectId = await getProjectIdFromInsightId("insight1");
expect(projectId).toBe("project1");
});
test("getProjectIdFromInsightId throws error when insight not found", async () => {
vi.mocked(services.getInsight).mockResolvedValueOnce(null);
await expect(getProjectIdFromInsightId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getProjectIdFromSegmentId returns project ID correctly", async () => {
vi.mocked(services.getSegment).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
const projectId = await getProjectIdFromSegmentId("segment1");
expect(projectId).toBe("project1");
});
test("getProjectIdFromSegmentId throws error when segment not found", async () => {
vi.mocked(services.getSegment).mockResolvedValueOnce(null);
await expect(getProjectIdFromSegmentId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getProjectIdFromActionClassId returns project ID correctly", async () => {
vi.mocked(services.getActionClass).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
const projectId = await getProjectIdFromActionClassId("action1");
expect(projectId).toBe("project1");
});
test("getProjectIdFromActionClassId throws error when actionClass not found", async () => {
vi.mocked(services.getActionClass).mockResolvedValueOnce(null);
await expect(getProjectIdFromActionClassId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getProjectIdFromTagId returns project ID correctly", async () => {
vi.mocked(services.getTag).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
const projectId = await getProjectIdFromTagId("tag1");
expect(projectId).toBe("project1");
});
test("getProjectIdFromTagId throws error when tag not found", async () => {
vi.mocked(services.getTag).mockResolvedValueOnce(null);
await expect(getProjectIdFromTagId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getProjectIdFromLanguageId returns project ID directly", async () => {
vi.mocked(services.getLanguage).mockResolvedValueOnce({
projectId: "project1",
});
const projectId = await getProjectIdFromLanguageId("lang1");
expect(projectId).toBe("project1");
});
test("getProjectIdFromLanguageId throws error when language not found", async () => {
vi.mocked(services.getLanguage).mockResolvedValueOnce(undefined as unknown as any);
await expect(getProjectIdFromLanguageId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getProjectIdFromResponseId returns project ID correctly", async () => {
vi.mocked(services.getResponse).mockResolvedValueOnce({
surveyId: "survey1",
});
vi.mocked(services.getSurvey).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
const projectId = await getProjectIdFromResponseId("response1");
expect(projectId).toBe("project1");
});
test("getProjectIdFromResponseId throws error when response not found", async () => {
vi.mocked(services.getResponse).mockResolvedValueOnce(null);
await expect(getProjectIdFromResponseId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getProjectIdFromResponseNoteId returns project ID correctly", async () => {
vi.mocked(services.getResponseNote).mockResolvedValueOnce({
responseId: "response1",
});
vi.mocked(services.getResponse).mockResolvedValueOnce({
surveyId: "survey1",
});
vi.mocked(services.getSurvey).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
const projectId = await getProjectIdFromResponseNoteId("note1");
expect(projectId).toBe("project1");
});
test("getProjectIdFromResponseNoteId throws error when responseNote not found", async () => {
vi.mocked(services.getResponseNote).mockResolvedValueOnce(null);
await expect(getProjectIdFromResponseNoteId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getProductIdFromContactId returns project ID correctly", async () => {
vi.mocked(services.getContact).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
const projectId = await getProductIdFromContactId("contact1");
expect(projectId).toBe("project1");
});
test("getProductIdFromContactId throws error when contact not found", async () => {
vi.mocked(services.getContact).mockResolvedValueOnce(null);
await expect(getProductIdFromContactId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getProjectIdFromDocumentId returns project ID correctly", async () => {
vi.mocked(services.getDocument).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
const projectId = await getProjectIdFromDocumentId("doc1");
expect(projectId).toBe("project1");
});
test("getProjectIdFromDocumentId throws error when document not found", async () => {
vi.mocked(services.getDocument).mockResolvedValueOnce(null);
await expect(getProjectIdFromDocumentId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getProjectIdFromIntegrationId returns project ID correctly", async () => {
vi.mocked(services.getIntegration).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
const projectId = await getProjectIdFromIntegrationId("integration1");
expect(projectId).toBe("project1");
});
test("getProjectIdFromIntegrationId throws error when integration not found", async () => {
vi.mocked(services.getIntegration).mockResolvedValueOnce(null);
await expect(getProjectIdFromIntegrationId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getProjectIdFromWebhookId returns project ID correctly", async () => {
vi.mocked(services.getWebhook).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
const projectId = await getProjectIdFromWebhookId("webhook1");
expect(projectId).toBe("project1");
});
test("getProjectIdFromWebhookId throws error when webhook not found", async () => {
vi.mocked(services.getWebhook).mockResolvedValueOnce(null);
await expect(getProjectIdFromWebhookId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
});
describe("Environment ID retrieval functions", () => {
test("getEnvironmentIdFromSurveyId returns environment ID directly", async () => {
vi.mocked(services.getSurvey).mockResolvedValueOnce({
environmentId: "env1",
});
const environmentId = await getEnvironmentIdFromSurveyId("survey1");
expect(environmentId).toBe("env1");
});
test("getEnvironmentIdFromSurveyId throws error when survey not found", async () => {
vi.mocked(services.getSurvey).mockResolvedValueOnce(null);
await expect(getEnvironmentIdFromSurveyId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getEnvironmentIdFromResponseId returns environment ID correctly", async () => {
vi.mocked(services.getResponse).mockResolvedValueOnce({
surveyId: "survey1",
});
vi.mocked(services.getSurvey).mockResolvedValueOnce({
environmentId: "env1",
});
const environmentId = await getEnvironmentIdFromResponseId("response1");
expect(environmentId).toBe("env1");
});
test("getEnvironmentIdFromResponseId throws error when response not found", async () => {
vi.mocked(services.getResponse).mockResolvedValueOnce(null);
await expect(getEnvironmentIdFromResponseId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getEnvironmentIdFromInsightId returns environment ID directly", async () => {
vi.mocked(services.getInsight).mockResolvedValueOnce({
environmentId: "env1",
});
const environmentId = await getEnvironmentIdFromInsightId("insight1");
expect(environmentId).toBe("env1");
});
test("getEnvironmentIdFromInsightId throws error when insight not found", async () => {
vi.mocked(services.getInsight).mockResolvedValueOnce(null);
await expect(getEnvironmentIdFromInsightId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getEnvironmentIdFromSegmentId returns environment ID directly", async () => {
vi.mocked(services.getSegment).mockResolvedValueOnce({
environmentId: "env1",
});
const environmentId = await getEnvironmentIdFromSegmentId("segment1");
expect(environmentId).toBe("env1");
});
test("getEnvironmentIdFromSegmentId throws error when segment not found", async () => {
vi.mocked(services.getSegment).mockResolvedValueOnce(null);
await expect(getEnvironmentIdFromSegmentId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getEnvironmentIdFromTagId returns environment ID directly", async () => {
vi.mocked(services.getTag).mockResolvedValueOnce({
environmentId: "env1",
});
const environmentId = await getEnvironmentIdFromTagId("tag1");
expect(environmentId).toBe("env1");
});
test("getEnvironmentIdFromTagId throws error when tag not found", async () => {
vi.mocked(services.getTag).mockResolvedValueOnce(null);
await expect(getEnvironmentIdFromTagId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
});
describe("isStringMatch", () => {
test("returns true for exact matches", () => {
expect(isStringMatch("test", "test")).toBe(true);
});
test("returns true for case-insensitive matches", () => {
expect(isStringMatch("TEST", "test")).toBe(true);
expect(isStringMatch("test", "TEST")).toBe(true);
});
test("returns true for matches with spaces", () => {
expect(isStringMatch("test case", "testcase")).toBe(true);
expect(isStringMatch("testcase", "test case")).toBe(true);
});
test("returns true for matches with underscores", () => {
expect(isStringMatch("test_case", "testcase")).toBe(true);
expect(isStringMatch("testcase", "test_case")).toBe(true);
});
test("returns true for matches with dashes", () => {
expect(isStringMatch("test-case", "testcase")).toBe(true);
expect(isStringMatch("testcase", "test-case")).toBe(true);
});
test("returns true for partial matches", () => {
expect(isStringMatch("test", "testing")).toBe(true);
});
test("returns false for non-matches", () => {
expect(isStringMatch("test", "other")).toBe(false);
});
});
});

View File

@@ -0,0 +1,87 @@
import { AVAILABLE_LOCALES, DEFAULT_LOCALE } from "@/lib/constants";
import * as nextHeaders from "next/headers";
import { describe, expect, test, vi } from "vitest";
import { findMatchingLocale } from "./locale";
// Mock the Next.js headers function
vi.mock("next/headers", () => ({
headers: vi.fn(),
}));
describe("locale", () => {
test("returns DEFAULT_LOCALE when Accept-Language header is missing", async () => {
// Set up the mock to return null for accept-language header
vi.mocked(nextHeaders.headers).mockReturnValue({
get: vi.fn().mockReturnValue(null),
} as any);
const result = await findMatchingLocale();
expect(result).toBe(DEFAULT_LOCALE);
expect(nextHeaders.headers).toHaveBeenCalled();
});
test("returns exact match when available", async () => {
// Assuming we have 'en-US' in AVAILABLE_LOCALES
const testLocale = AVAILABLE_LOCALES[0];
vi.mocked(nextHeaders.headers).mockReturnValue({
get: vi.fn().mockReturnValue(`${testLocale},fr-FR,de-DE`),
} as any);
const result = await findMatchingLocale();
expect(result).toBe(testLocale);
expect(nextHeaders.headers).toHaveBeenCalled();
});
test("returns normalized match when available", async () => {
// Assuming we have 'en-US' in AVAILABLE_LOCALES but not 'en-GB'
const availableLocale = AVAILABLE_LOCALES.find((locale) => locale.startsWith("en-"));
if (!availableLocale) {
// Skip this test if no English locale is available
return;
}
vi.mocked(nextHeaders.headers).mockReturnValue({
get: vi.fn().mockReturnValue("en-US,fr-FR,de-DE"),
} as any);
const result = await findMatchingLocale();
expect(result).toBe(availableLocale);
expect(nextHeaders.headers).toHaveBeenCalled();
});
test("returns DEFAULT_LOCALE when no match is found", async () => {
// Use a locale that should not exist in AVAILABLE_LOCALES
vi.mocked(nextHeaders.headers).mockReturnValue({
get: vi.fn().mockReturnValue("xx-XX,yy-YY"),
} as any);
const result = await findMatchingLocale();
expect(result).toBe(DEFAULT_LOCALE);
expect(nextHeaders.headers).toHaveBeenCalled();
});
test("handles multiple potential matches correctly", async () => {
// If we have multiple locales for the same language, it should return the first match
const germanLocale = AVAILABLE_LOCALES.find((locale) => locale.toLowerCase().startsWith("de"));
if (!germanLocale) {
// Skip this test if no German locale is available
return;
}
vi.mocked(nextHeaders.headers).mockReturnValue({
get: vi.fn().mockReturnValue("de-DE,en-US,fr-FR"),
} as any);
const result = await findMatchingLocale();
expect(result).toBe(germanLocale);
expect(nextHeaders.headers).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,84 @@
import { describe, expect, test, vi } from "vitest";
import { delay, isFulfilled, isRejected } from "./promises";
describe("promises utilities", () => {
test("delay resolves after specified time", async () => {
const delayTime = 100;
vi.useFakeTimers();
const promise = delay(delayTime);
vi.advanceTimersByTime(delayTime);
await promise;
vi.useRealTimers();
});
test("isFulfilled returns true for fulfilled promises", () => {
const fulfilledResult: PromiseSettledResult<string> = {
status: "fulfilled",
value: "success",
};
expect(isFulfilled(fulfilledResult)).toBe(true);
if (isFulfilled(fulfilledResult)) {
expect(fulfilledResult.value).toBe("success");
}
});
test("isFulfilled returns false for rejected promises", () => {
const rejectedResult: PromiseSettledResult<string> = {
status: "rejected",
reason: "error",
};
expect(isFulfilled(rejectedResult)).toBe(false);
});
test("isRejected returns true for rejected promises", () => {
const rejectedResult: PromiseSettledResult<string> = {
status: "rejected",
reason: "error",
};
expect(isRejected(rejectedResult)).toBe(true);
if (isRejected(rejectedResult)) {
expect(rejectedResult.reason).toBe("error");
}
});
test("isRejected returns false for fulfilled promises", () => {
const fulfilledResult: PromiseSettledResult<string> = {
status: "fulfilled",
value: "success",
};
expect(isRejected(fulfilledResult)).toBe(false);
});
test("delay can be used in actual timing scenarios", async () => {
const mockCallback = vi.fn();
setTimeout(mockCallback, 50);
await delay(100);
expect(mockCallback).toHaveBeenCalled();
});
test("type guard functions work correctly with Promise.allSettled", async () => {
const promises = [Promise.resolve("success"), Promise.reject("failure")];
const results = await Promise.allSettled(promises);
const fulfilled = results.filter(isFulfilled);
const rejected = results.filter(isRejected);
expect(fulfilled.length).toBe(1);
expect(fulfilled[0].value).toBe("success");
expect(rejected.length).toBe(1);
expect(rejected[0].reason).toBe("failure");
});
});

View File

@@ -0,0 +1,516 @@
import { getLocalizedValue } from "@/lib/i18n/utils";
import { structuredClone } from "@/lib/pollyfills/structuredClone";
import { describe, expect, test, vi } from "vitest";
import { TResponseData, TResponseVariables } from "@formbricks/types/responses";
import { TSurvey, TSurveyQuestion, TSurveyRecallItem } from "@formbricks/types/surveys/types";
import {
checkForEmptyFallBackValue,
extractFallbackValue,
extractId,
extractIds,
extractRecallInfo,
fallbacks,
findRecallInfoById,
getFallbackValues,
getRecallItems,
headlineToRecall,
parseRecallInfo,
recallToHeadline,
replaceHeadlineRecall,
replaceRecallInfoWithUnderline,
} from "./recall";
// Mock dependencies
vi.mock("@/lib/i18n/utils", () => ({
getLocalizedValue: vi.fn().mockImplementation((obj, lang) => {
return typeof obj === "string" ? obj : obj[lang] || obj["default"] || "";
}),
}));
vi.mock("@/lib/pollyfills/structuredClone", () => ({
structuredClone: vi.fn((obj) => JSON.parse(JSON.stringify(obj))),
}));
vi.mock("@/lib/utils/datetime", () => ({
isValidDateString: vi.fn((value) => {
try {
return !isNaN(new Date(value as string).getTime());
} catch {
return false;
}
}),
formatDateWithOrdinal: vi.fn((date) => {
return "January 1st, 2023";
}),
}));
describe("recall utility functions", () => {
describe("extractId", () => {
test("extracts ID correctly from a string with recall pattern", () => {
const text = "This is a #recall:question123 example";
const result = extractId(text);
expect(result).toBe("question123");
});
test("returns null when no ID is found", () => {
const text = "This has no recall pattern";
const result = extractId(text);
expect(result).toBeNull();
});
test("returns null for malformed recall pattern", () => {
const text = "This is a #recall: malformed pattern";
const result = extractId(text);
expect(result).toBeNull();
});
});
describe("extractIds", () => {
test("extracts multiple IDs from a string with multiple recall patterns", () => {
const text = "This has #recall:id1 and #recall:id2 and #recall:id3";
const result = extractIds(text);
expect(result).toEqual(["id1", "id2", "id3"]);
});
test("returns empty array when no IDs are found", () => {
const text = "This has no recall patterns";
const result = extractIds(text);
expect(result).toEqual([]);
});
test("handles mixed content correctly", () => {
const text = "Text #recall:id1 more text #recall:id2";
const result = extractIds(text);
expect(result).toEqual(["id1", "id2"]);
});
});
describe("extractFallbackValue", () => {
test("extracts fallback value correctly", () => {
const text = "Text #recall:id1/fallback:defaultValue# more text";
const result = extractFallbackValue(text);
expect(result).toBe("defaultValue");
});
test("returns empty string when no fallback value is found", () => {
const text = "Text with no fallback";
const result = extractFallbackValue(text);
expect(result).toBe("");
});
test("handles empty fallback value", () => {
const text = "Text #recall:id1/fallback:# more text";
const result = extractFallbackValue(text);
expect(result).toBe("");
});
});
describe("extractRecallInfo", () => {
test("extracts complete recall info from text", () => {
const text = "This is #recall:id1/fallback:default# text";
const result = extractRecallInfo(text);
expect(result).toBe("#recall:id1/fallback:default#");
});
test("returns null when no recall info is found", () => {
const text = "This has no recall info";
const result = extractRecallInfo(text);
expect(result).toBeNull();
});
test("extracts recall info for a specific ID when provided", () => {
const text = "This has #recall:id1/fallback:default1# and #recall:id2/fallback:default2#";
const result = extractRecallInfo(text, "id2");
expect(result).toBe("#recall:id2/fallback:default2#");
});
});
describe("findRecallInfoById", () => {
test("finds recall info by ID", () => {
const text = "Text #recall:id1/fallback:value1# and #recall:id2/fallback:value2#";
const result = findRecallInfoById(text, "id2");
expect(result).toBe("#recall:id2/fallback:value2#");
});
test("returns null when ID is not found", () => {
const text = "Text #recall:id1/fallback:value1#";
const result = findRecallInfoById(text, "id2");
expect(result).toBeNull();
});
});
describe("recallToHeadline", () => {
test("converts recall pattern to headline format without slash", () => {
const headline = { en: "How do you like #recall:product/fallback:ournbspproduct#?" };
const survey: TSurvey = {
id: "test-survey",
questions: [{ id: "product", headline: { en: "Product Question" } }] as unknown as TSurveyQuestion[],
hiddenFields: { fieldIds: [] },
variables: [],
} as unknown as TSurvey;
const result = recallToHeadline(headline, survey, false, "en");
expect(result.en).toBe("How do you like @Product Question?");
});
test("converts recall pattern to headline format with slash", () => {
const headline = { en: "Rate #recall:product/fallback:ournbspproduct#" };
const survey: TSurvey = {
id: "test-survey",
questions: [{ id: "product", headline: { en: "Product Question" } }] as unknown as TSurveyQuestion[],
hiddenFields: { fieldIds: [] },
variables: [],
} as unknown as TSurvey;
const result = recallToHeadline(headline, survey, true, "en");
expect(result.en).toBe("Rate /Product Question\\");
});
test("handles hidden fields in recall", () => {
const headline = { en: "Your email is #recall:email/fallback:notnbspprovided#" };
const survey: TSurvey = {
id: "test-survey",
questions: [],
hiddenFields: { fieldIds: ["email"] },
variables: [],
} as unknown as TSurvey;
const result = recallToHeadline(headline, survey, false, "en");
expect(result.en).toBe("Your email is @email");
});
test("handles variables in recall", () => {
const headline = { en: "Your plan is #recall:plan/fallback:unknown#" };
const survey: TSurvey = {
id: "test-survey",
questions: [],
hiddenFields: { fieldIds: [] },
variables: [{ id: "plan", name: "Subscription Plan" }],
} as unknown as TSurvey;
const result = recallToHeadline(headline, survey, false, "en");
expect(result.en).toBe("Your plan is @Subscription Plan");
});
test("returns unchanged headline when no recall pattern is found", () => {
const headline = { en: "Regular headline with no recall" };
const survey = {} as TSurvey;
const result = recallToHeadline(headline, survey, false, "en");
expect(result).toEqual(headline);
});
test("handles nested recall patterns", () => {
const headline = {
en: "This is #recall:outer/fallback:withnbsp#recall:inner/fallback:nested#nbsptext#",
};
const survey: TSurvey = {
id: "test-survey",
questions: [
{ id: "outer", headline: { en: "Outer with @inner" } },
{ id: "inner", headline: { en: "Inner value" } },
] as unknown as TSurveyQuestion[],
hiddenFields: { fieldIds: [] },
variables: [],
} as unknown as TSurvey;
const result = recallToHeadline(headline, survey, false, "en");
expect(result.en).toBe("This is @Outer with @inner");
});
});
describe("replaceRecallInfoWithUnderline", () => {
test("replaces recall info with underline", () => {
const text = "This is a #recall:id1/fallback:default# example";
const result = replaceRecallInfoWithUnderline(text);
expect(result).toBe("This is a ___ example");
});
test("replaces multiple recall infos with underlines", () => {
const text = "This #recall:id1/fallback:v1# has #recall:id2/fallback:v2# multiple recalls";
const result = replaceRecallInfoWithUnderline(text);
expect(result).toBe("This ___ has ___ multiple recalls");
});
test("returns unchanged text when no recall info is present", () => {
const text = "This has no recall info";
const result = replaceRecallInfoWithUnderline(text);
expect(result).toBe(text);
});
});
describe("checkForEmptyFallBackValue", () => {
test("identifies question with empty fallback value", () => {
const questionHeadline = { en: "Question with #recall:id1/fallback:# empty fallback" };
const survey: TSurvey = {
questions: [
{
id: "q1",
headline: questionHeadline,
},
] as unknown as TSurveyQuestion[],
} as unknown as TSurvey;
vi.mocked(getLocalizedValue).mockReturnValueOnce(questionHeadline.en);
const result = checkForEmptyFallBackValue(survey, "en");
expect(result).toBe(survey.questions[0]);
});
test("identifies question with empty fallback in subheader", () => {
const questionSubheader = { en: "Subheader with #recall:id1/fallback:# empty fallback" };
const survey: TSurvey = {
questions: [
{
id: "q1",
headline: { en: "Normal question" },
subheader: questionSubheader,
},
] as unknown as TSurveyQuestion[],
} as unknown as TSurvey;
vi.mocked(getLocalizedValue).mockReturnValueOnce(questionSubheader.en);
const result = checkForEmptyFallBackValue(survey, "en");
expect(result).toBe(survey.questions[0]);
});
test("returns null when no empty fallback values are found", () => {
const questionHeadline = { en: "Question with #recall:id1/fallback:default# valid fallback" };
const survey: TSurvey = {
questions: [
{
id: "q1",
headline: questionHeadline,
},
] as unknown as TSurveyQuestion[],
} as unknown as TSurvey;
vi.mocked(getLocalizedValue).mockReturnValueOnce(questionHeadline.en);
const result = checkForEmptyFallBackValue(survey, "en");
expect(result).toBeNull();
});
});
describe("replaceHeadlineRecall", () => {
test("processes all questions in a survey", () => {
const survey: TSurvey = {
questions: [
{
id: "q1",
headline: { en: "Question with #recall:id1/fallback:default#" },
},
{
id: "q2",
headline: { en: "Another with #recall:id2/fallback:other#" },
},
] as unknown as TSurveyQuestion[],
hiddenFields: { fieldIds: [] },
variables: [],
} as unknown as TSurvey;
vi.mocked(structuredClone).mockImplementation((obj) => JSON.parse(JSON.stringify(obj)));
const result = replaceHeadlineRecall(survey, "en");
// Verify recallToHeadline was called for each question
expect(result).not.toBe(survey); // Should be a clone
expect(result.questions[0].headline).not.toEqual(survey.questions[0].headline);
expect(result.questions[1].headline).not.toEqual(survey.questions[1].headline);
});
});
describe("getRecallItems", () => {
test("extracts recall items from text", () => {
const text = "Text with #recall:id1/fallback:val1# and #recall:id2/fallback:val2#";
const survey: TSurvey = {
questions: [
{ id: "id1", headline: { en: "Question One" } },
{ id: "id2", headline: { en: "Question Two" } },
] as unknown as TSurveyQuestion[],
hiddenFields: { fieldIds: [] },
variables: [],
} as unknown as TSurvey;
const result = getRecallItems(text, survey, "en");
expect(result).toHaveLength(2);
expect(result[0].id).toBe("id1");
expect(result[0].label).toBe("Question One");
expect(result[0].type).toBe("question");
expect(result[1].id).toBe("id2");
expect(result[1].label).toBe("Question Two");
expect(result[1].type).toBe("question");
});
test("handles hidden fields in recall items", () => {
const text = "Text with #recall:hidden1/fallback:val1#";
const survey: TSurvey = {
questions: [],
hiddenFields: { fieldIds: ["hidden1"] },
variables: [],
} as unknown as TSurvey;
const result = getRecallItems(text, survey, "en");
expect(result).toHaveLength(1);
expect(result[0].id).toBe("hidden1");
expect(result[0].type).toBe("hiddenField");
});
test("handles variables in recall items", () => {
const text = "Text with #recall:var1/fallback:val1#";
const survey: TSurvey = {
questions: [],
hiddenFields: { fieldIds: [] },
variables: [{ id: "var1", name: "Variable One" }],
} as unknown as TSurvey;
const result = getRecallItems(text, survey, "en");
expect(result).toHaveLength(1);
expect(result[0].id).toBe("var1");
expect(result[0].label).toBe("Variable One");
expect(result[0].type).toBe("variable");
});
test("returns empty array when no recall items are found", () => {
const text = "Text with no recall items";
const survey: TSurvey = {} as TSurvey;
const result = getRecallItems(text, survey, "en");
expect(result).toEqual([]);
});
});
describe("getFallbackValues", () => {
test("extracts fallback values from text", () => {
const text = "Text #recall:id1/fallback:value1# and #recall:id2/fallback:value2#";
const result = getFallbackValues(text);
expect(result).toEqual({
id1: "value1",
id2: "value2",
});
});
test("returns empty object when no fallback values are found", () => {
const text = "Text with no fallback values";
const result = getFallbackValues(text);
expect(result).toEqual({});
});
});
describe("headlineToRecall", () => {
test("transforms headlines to recall info", () => {
const text = "What do you think of @Product?";
const recallItems: TSurveyRecallItem[] = [{ id: "product", label: "Product", type: "question" }];
const fallbacks: fallbacks = {
product: "our product",
};
const result = headlineToRecall(text, recallItems, fallbacks);
expect(result).toBe("What do you think of #recall:product/fallback:our product#?");
});
test("transforms multiple headlines", () => {
const text = "Rate @Product made by @Company";
const recallItems: TSurveyRecallItem[] = [
{ id: "product", label: "Product", type: "question" },
{ id: "company", label: "Company", type: "question" },
];
const fallbacks: fallbacks = {
product: "our product",
company: "our company",
};
const result = headlineToRecall(text, recallItems, fallbacks);
expect(result).toBe(
"Rate #recall:product/fallback:our product# made by #recall:company/fallback:our company#"
);
});
});
describe("parseRecallInfo", () => {
test("replaces recall info with response data", () => {
const text = "Your answer was #recall:q1/fallback:not-provided#";
const responseData: TResponseData = {
q1: "Yes definitely",
};
const result = parseRecallInfo(text, responseData);
expect(result).toBe("Your answer was Yes definitely");
});
test("uses fallback when response data is missing", () => {
const text = "Your answer was #recall:q1/fallback:notnbspprovided#";
const responseData: TResponseData = {
q2: "Some other answer",
};
const result = parseRecallInfo(text, responseData);
expect(result).toBe("Your answer was not provided");
});
test("formats date values", () => {
const text = "You joined on #recall:joinDate/fallback:an-unknown-date#";
const responseData: TResponseData = {
joinDate: "2023-01-01",
};
const result = parseRecallInfo(text, responseData);
expect(result).toBe("You joined on January 1st, 2023");
});
test("formats array values as comma-separated list", () => {
const text = "Your selections: #recall:preferences/fallback:none#";
const responseData: TResponseData = {
preferences: ["Option A", "Option B", "Option C"],
};
const result = parseRecallInfo(text, responseData);
expect(result).toBe("Your selections: Option A, Option B, Option C");
});
test("uses variables when available", () => {
const text = "Welcome back, #recall:username/fallback:user#";
const variables: TResponseVariables = {
username: "John Doe",
};
const result = parseRecallInfo(text, {}, variables);
expect(result).toBe("Welcome back, John Doe");
});
test("prioritizes variables over response data", () => {
const text = "Your email is #recall:email/fallback:no-email#";
const responseData: TResponseData = {
email: "response@example.com",
};
const variables: TResponseVariables = {
email: "variable@example.com",
};
const result = parseRecallInfo(text, responseData, variables);
expect(result).toBe("Your email is variable@example.com");
});
test("handles withSlash parameter", () => {
const text = "Your name is #recall:name/fallback:anonymous#";
const variables: TResponseVariables = {
name: "John Doe",
};
const result = parseRecallInfo(text, {}, variables, true);
expect(result).toBe("Your name is #/John Doe\\#");
});
test("handles 'nbsp' in fallback values", () => {
const text = "Default spacing: #recall:space/fallback:nonnbspbreaking#";
const result = parseRecallInfo(text);
expect(result).toBe("Default spacing: non breaking");
});
});
});

View File

@@ -124,6 +124,7 @@ export const checkForEmptyFallBackValue = (survey: TSurvey, language: string): T
const recalls = text.match(/#recall:[^ ]+/g);
return recalls && recalls.some((recall) => !extractFallbackValue(recall));
};
for (const question of survey.questions) {
if (
findRecalls(getLocalizedValue(question.headline, language)) ||

View File

@@ -0,0 +1,737 @@
import { validateInputs } from "@/lib/utils/validate";
import { Prisma } from "@prisma/client";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
import {
getActionClass,
getApiKey,
getContact,
getDocument,
getEnvironment,
getInsight,
getIntegration,
getInvite,
getLanguage,
getProject,
getResponse,
getResponseNote,
getSegment,
getSurvey,
getTag,
getTeam,
getWebhook,
isProjectPartOfOrganization,
isTeamPartOfOrganization,
} from "./services";
// Mock all dependencies
vi.mock("@/lib/utils/validate", () => ({
validateInputs: vi.fn(),
}));
vi.mock("@formbricks/database", () => ({
prisma: {
actionClass: {
findUnique: vi.fn(),
},
apiKey: {
findUnique: vi.fn(),
},
environment: {
findUnique: vi.fn(),
},
integration: {
findUnique: vi.fn(),
},
invite: {
findUnique: vi.fn(),
},
language: {
findFirst: vi.fn(),
},
project: {
findUnique: vi.fn(),
},
response: {
findUnique: vi.fn(),
},
responseNote: {
findUnique: vi.fn(),
},
survey: {
findUnique: vi.fn(),
},
tag: {
findUnique: vi.fn(),
},
webhook: {
findUnique: vi.fn(),
},
team: {
findUnique: vi.fn(),
},
insight: {
findUnique: vi.fn(),
},
document: {
findUnique: vi.fn(),
},
contact: {
findUnique: vi.fn(),
},
segment: {
findUnique: vi.fn(),
},
},
}));
// Mock cache
vi.mock("@/lib/cache", () => ({
cache: vi.fn((fn) => fn),
}));
// Mock react cache
vi.mock("react", () => ({
cache: vi.fn((fn) => fn),
}));
// Mock all cache modules
vi.mock("@/lib/actionClass/cache", () => ({
actionClassCache: {
tag: {
byId: vi.fn((id) => `actionClass-${id}`),
},
},
}));
vi.mock("@/lib/cache/api-key", () => ({
apiKeyCache: {
tag: {
byId: vi.fn((id) => `apiKey-${id}`),
},
},
}));
vi.mock("@/lib/environment/cache", () => ({
environmentCache: {
tag: {
byId: vi.fn((id) => `environment-${id}`),
},
},
}));
vi.mock("@/lib/integration/cache", () => ({
integrationCache: {
tag: {
byId: vi.fn((id) => `integration-${id}`),
},
},
}));
vi.mock("@/lib/cache/invite", () => ({
inviteCache: {
tag: {
byId: vi.fn((id) => `invite-${id}`),
},
},
}));
vi.mock("@/lib/project/cache", () => ({
projectCache: {
tag: {
byId: vi.fn((id) => `project-${id}`),
},
},
}));
vi.mock("@/lib/response/cache", () => ({
responseCache: {
tag: {
byId: vi.fn((id) => `response-${id}`),
},
},
}));
vi.mock("@/lib/responseNote/cache", () => ({
responseNoteCache: {
tag: {
byResponseId: vi.fn((id) => `response-${id}-notes`),
byId: vi.fn((id) => `responseNote-${id}`),
},
},
}));
vi.mock("@/lib/survey/cache", () => ({
surveyCache: {
tag: {
byId: vi.fn((id) => `survey-${id}`),
},
},
}));
vi.mock("@/lib/tag/cache", () => ({
tagCache: {
tag: {
byId: vi.fn((id) => `tag-${id}`),
},
},
}));
vi.mock("@/lib/cache/webhook", () => ({
webhookCache: {
tag: {
byId: vi.fn((id) => `webhook-${id}`),
},
},
}));
vi.mock("@/lib/cache/team", () => ({
teamCache: {
tag: {
byId: vi.fn((id) => `team-${id}`),
},
},
}));
vi.mock("@/lib/cache/contact", () => ({
contactCache: {
tag: {
byId: vi.fn((id) => `contact-${id}`),
},
},
}));
vi.mock("@/lib/cache/segment", () => ({
segmentCache: {
tag: {
byId: vi.fn((id) => `segment-${id}`),
},
},
}));
describe("Service Functions", () => {
beforeEach(() => {
vi.resetAllMocks();
});
describe("getActionClass", () => {
const actionClassId = "action123";
test("returns the action class when found", async () => {
const mockActionClass = { environmentId: "env123" };
vi.mocked(prisma.actionClass.findUnique).mockResolvedValue(mockActionClass);
const result = await getActionClass(actionClassId);
expect(validateInputs).toHaveBeenCalled();
expect(prisma.actionClass.findUnique).toHaveBeenCalledWith({
where: { id: actionClassId },
select: { environmentId: true },
});
expect(result).toEqual(mockActionClass);
});
test("throws DatabaseError when database operation fails", async () => {
vi.mocked(prisma.actionClass.findUnique).mockRejectedValue(new Error("Database error"));
await expect(getActionClass(actionClassId)).rejects.toThrow(DatabaseError);
});
});
describe("getApiKey", () => {
const apiKeyId = "apiKey123";
test("returns the api key when found", async () => {
const mockApiKey = { organizationId: "org123" };
vi.mocked(prisma.apiKey.findUnique).mockResolvedValue(mockApiKey);
const result = await getApiKey(apiKeyId);
expect(validateInputs).toHaveBeenCalled();
expect(prisma.apiKey.findUnique).toHaveBeenCalledWith({
where: { id: apiKeyId },
select: { organizationId: true },
});
expect(result).toEqual(mockApiKey);
});
test("throws InvalidInputError if apiKeyId is empty", async () => {
await expect(getApiKey("")).rejects.toThrow(InvalidInputError);
expect(prisma.apiKey.findUnique).not.toHaveBeenCalled();
});
test("throws DatabaseError when database operation fails", async () => {
vi.mocked(prisma.apiKey.findUnique).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError("Error", {
code: "P2002",
clientVersion: "4.7.0",
})
);
await expect(getApiKey(apiKeyId)).rejects.toThrow(DatabaseError);
});
});
describe("getEnvironment", () => {
const environmentId = "env123";
test("returns the environment when found", async () => {
const mockEnvironment = { projectId: "proj123" };
vi.mocked(prisma.environment.findUnique).mockResolvedValue(mockEnvironment);
const result = await getEnvironment(environmentId);
expect(validateInputs).toHaveBeenCalled();
expect(prisma.environment.findUnique).toHaveBeenCalledWith({
where: { id: environmentId },
select: { projectId: true },
});
expect(result).toEqual(mockEnvironment);
});
test("throws DatabaseError when database operation fails", async () => {
vi.mocked(prisma.environment.findUnique).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError("Error", {
code: "P2002",
clientVersion: "4.7.0",
})
);
await expect(getEnvironment(environmentId)).rejects.toThrow(DatabaseError);
});
});
describe("getIntegration", () => {
const integrationId = "int123";
test("returns the integration when found", async () => {
const mockIntegration = { environmentId: "env123" };
vi.mocked(prisma.integration.findUnique).mockResolvedValue(mockIntegration);
const result = await getIntegration(integrationId);
expect(prisma.integration.findUnique).toHaveBeenCalledWith({
where: { id: integrationId },
select: { environmentId: true },
});
expect(result).toEqual(mockIntegration);
});
test("throws DatabaseError when database operation fails", async () => {
vi.mocked(prisma.integration.findUnique).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError("Error", {
code: "P2002",
clientVersion: "4.7.0",
})
);
await expect(getIntegration(integrationId)).rejects.toThrow(DatabaseError);
});
});
describe("getInvite", () => {
const inviteId = "invite123";
test("returns the invite when found", async () => {
const mockInvite = { organizationId: "org123" };
vi.mocked(prisma.invite.findUnique).mockResolvedValue(mockInvite);
const result = await getInvite(inviteId);
expect(validateInputs).toHaveBeenCalled();
expect(prisma.invite.findUnique).toHaveBeenCalledWith({
where: { id: inviteId },
select: { organizationId: true },
});
expect(result).toEqual(mockInvite);
});
test("throws DatabaseError when database operation fails", async () => {
vi.mocked(prisma.invite.findUnique).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError("Error", {
code: "P2002",
clientVersion: "4.7.0",
})
);
await expect(getInvite(inviteId)).rejects.toThrow(DatabaseError);
});
});
describe("getLanguage", () => {
const languageId = "lang123";
test("returns the language when found", async () => {
const mockLanguage = { projectId: "proj123" };
vi.mocked(prisma.language.findFirst).mockResolvedValue(mockLanguage);
const result = await getLanguage(languageId);
expect(validateInputs).toHaveBeenCalled();
expect(prisma.language.findFirst).toHaveBeenCalledWith({
where: { id: languageId },
select: { projectId: true },
});
expect(result).toEqual(mockLanguage);
});
test("throws ResourceNotFoundError when language not found", async () => {
vi.mocked(prisma.language.findFirst).mockResolvedValue(null);
await expect(getLanguage(languageId)).rejects.toThrow(ResourceNotFoundError);
});
test("throws DatabaseError when database operation fails", async () => {
vi.mocked(prisma.language.findFirst).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError("Error", {
code: "P2002",
clientVersion: "4.7.0",
})
);
await expect(getLanguage(languageId)).rejects.toThrow(DatabaseError);
});
});
describe("getProject", () => {
const projectId = "proj123";
test("returns the project when found", async () => {
const mockProject = { organizationId: "org123" };
vi.mocked(prisma.project.findUnique).mockResolvedValue(mockProject);
const result = await getProject(projectId);
expect(prisma.project.findUnique).toHaveBeenCalledWith({
where: { id: projectId },
select: { organizationId: true },
});
expect(result).toEqual(mockProject);
});
test("throws DatabaseError when database operation fails", async () => {
vi.mocked(prisma.project.findUnique).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError("Error", {
code: "P2002",
clientVersion: "4.7.0",
})
);
await expect(getProject(projectId)).rejects.toThrow(DatabaseError);
});
});
describe("getResponse", () => {
const responseId = "resp123";
test("returns the response when found", async () => {
const mockResponse = { surveyId: "survey123" };
vi.mocked(prisma.response.findUnique).mockResolvedValue(mockResponse);
const result = await getResponse(responseId);
expect(validateInputs).toHaveBeenCalled();
expect(prisma.response.findUnique).toHaveBeenCalledWith({
where: { id: responseId },
select: { surveyId: true },
});
expect(result).toEqual(mockResponse);
});
test("throws DatabaseError when database operation fails", async () => {
vi.mocked(prisma.response.findUnique).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError("Error", {
code: "P2002",
clientVersion: "4.7.0",
})
);
await expect(getResponse(responseId)).rejects.toThrow(DatabaseError);
});
});
describe("getResponseNote", () => {
const responseNoteId = "note123";
test("returns the response note when found", async () => {
const mockResponseNote = { responseId: "resp123" };
vi.mocked(prisma.responseNote.findUnique).mockResolvedValue(mockResponseNote);
const result = await getResponseNote(responseNoteId);
expect(prisma.responseNote.findUnique).toHaveBeenCalledWith({
where: { id: responseNoteId },
select: { responseId: true },
});
expect(result).toEqual(mockResponseNote);
});
test("throws DatabaseError when database operation fails", async () => {
vi.mocked(prisma.responseNote.findUnique).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError("Error", {
code: "P2002",
clientVersion: "4.7.0",
})
);
await expect(getResponseNote(responseNoteId)).rejects.toThrow(DatabaseError);
});
});
describe("getSurvey", () => {
const surveyId = "survey123";
test("returns the survey when found", async () => {
const mockSurvey = { environmentId: "env123" };
vi.mocked(prisma.survey.findUnique).mockResolvedValue(mockSurvey);
const result = await getSurvey(surveyId);
expect(validateInputs).toHaveBeenCalled();
expect(prisma.survey.findUnique).toHaveBeenCalledWith({
where: { id: surveyId },
select: { environmentId: true },
});
expect(result).toEqual(mockSurvey);
});
test("throws DatabaseError when database operation fails", async () => {
vi.mocked(prisma.survey.findUnique).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError("Error", {
code: "P2002",
clientVersion: "4.7.0",
})
);
await expect(getSurvey(surveyId)).rejects.toThrow(DatabaseError);
});
});
describe("getTag", () => {
const tagId = "tag123";
test("returns the tag when found", async () => {
const mockTag = { environmentId: "env123" };
vi.mocked(prisma.tag.findUnique).mockResolvedValue(mockTag);
const result = await getTag(tagId);
expect(validateInputs).toHaveBeenCalled();
expect(prisma.tag.findUnique).toHaveBeenCalledWith({
where: { id: tagId },
select: { environmentId: true },
});
expect(result).toEqual(mockTag);
});
});
describe("getWebhook", () => {
const webhookId = "webhook123";
test("returns the webhook when found", async () => {
const mockWebhook = { environmentId: "env123" };
vi.mocked(prisma.webhook.findUnique).mockResolvedValue(mockWebhook);
const result = await getWebhook(webhookId);
expect(validateInputs).toHaveBeenCalled();
expect(prisma.webhook.findUnique).toHaveBeenCalledWith({
where: { id: webhookId },
select: { environmentId: true },
});
expect(result).toEqual(mockWebhook);
});
test("throws DatabaseError when database operation fails", async () => {
vi.mocked(prisma.webhook.findUnique).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError("Error", {
code: "P2002",
clientVersion: "4.7.0",
})
);
await expect(getWebhook(webhookId)).rejects.toThrow(DatabaseError);
});
});
describe("getTeam", () => {
const teamId = "team123";
test("returns the team when found", async () => {
const mockTeam = { organizationId: "org123" };
vi.mocked(prisma.team.findUnique).mockResolvedValue(mockTeam);
const result = await getTeam(teamId);
expect(validateInputs).toHaveBeenCalled();
expect(prisma.team.findUnique).toHaveBeenCalledWith({
where: { id: teamId },
select: { organizationId: true },
});
expect(result).toEqual(mockTeam);
});
test("throws DatabaseError when database operation fails", async () => {
vi.mocked(prisma.team.findUnique).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError("Error", {
code: "P2002",
clientVersion: "4.7.0",
})
);
await expect(getTeam(teamId)).rejects.toThrow(DatabaseError);
});
});
describe("getInsight", () => {
const insightId = "insight123";
test("returns the insight when found", async () => {
const mockInsight = { environmentId: "env123" };
vi.mocked(prisma.insight.findUnique).mockResolvedValue(mockInsight);
const result = await getInsight(insightId);
expect(validateInputs).toHaveBeenCalled();
expect(prisma.insight.findUnique).toHaveBeenCalledWith({
where: { id: insightId },
select: { environmentId: true },
});
expect(result).toEqual(mockInsight);
});
test("throws DatabaseError when database operation fails", async () => {
vi.mocked(prisma.insight.findUnique).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError("Error", {
code: "P2002",
clientVersion: "4.7.0",
})
);
await expect(getInsight(insightId)).rejects.toThrow(DatabaseError);
});
});
describe("getDocument", () => {
const documentId = "doc123";
test("returns the document when found", async () => {
const mockDocument = { environmentId: "env123" };
vi.mocked(prisma.document.findUnique).mockResolvedValue(mockDocument);
const result = await getDocument(documentId);
expect(validateInputs).toHaveBeenCalled();
expect(prisma.document.findUnique).toHaveBeenCalledWith({
where: { id: documentId },
select: { environmentId: true },
});
expect(result).toEqual(mockDocument);
});
test("throws DatabaseError when database operation fails", async () => {
vi.mocked(prisma.document.findUnique).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError("Error", {
code: "P2002",
clientVersion: "4.7.0",
})
);
await expect(getDocument(documentId)).rejects.toThrow(DatabaseError);
});
});
describe("isProjectPartOfOrganization", () => {
const projectId = "proj123";
const organizationId = "org123";
test("returns true when project belongs to organization", async () => {
vi.mocked(prisma.project.findUnique).mockResolvedValue({ organizationId });
const result = await isProjectPartOfOrganization(organizationId, projectId);
expect(result).toBe(true);
});
test("returns false when project belongs to different organization", async () => {
vi.mocked(prisma.project.findUnique).mockResolvedValue({ organizationId: "otherOrg" });
const result = await isProjectPartOfOrganization(organizationId, projectId);
expect(result).toBe(false);
});
test("throws ResourceNotFoundError when project not found", async () => {
vi.mocked(prisma.project.findUnique).mockResolvedValue(null);
await expect(isProjectPartOfOrganization(organizationId, projectId)).rejects.toThrow(
ResourceNotFoundError
);
});
});
describe("isTeamPartOfOrganization", () => {
const teamId = "team123";
const organizationId = "org123";
test("returns true when team belongs to organization", async () => {
vi.mocked(prisma.team.findUnique).mockResolvedValue({ organizationId });
const result = await isTeamPartOfOrganization(organizationId, teamId);
expect(result).toBe(true);
});
test("returns false when team belongs to different organization", async () => {
vi.mocked(prisma.team.findUnique).mockResolvedValue({ organizationId: "otherOrg" });
const result = await isTeamPartOfOrganization(organizationId, teamId);
expect(result).toBe(false);
});
test("throws ResourceNotFoundError when team not found", async () => {
vi.mocked(prisma.team.findUnique).mockResolvedValue(null);
await expect(isTeamPartOfOrganization(organizationId, teamId)).rejects.toThrow(ResourceNotFoundError);
});
});
describe("getContact", () => {
const contactId = "contact123";
test("returns the contact when found", async () => {
const mockContact = { environmentId: "env123" };
vi.mocked(prisma.contact.findUnique).mockResolvedValue(mockContact);
const result = await getContact(contactId);
expect(validateInputs).toHaveBeenCalled();
expect(prisma.contact.findUnique).toHaveBeenCalledWith({
where: { id: contactId },
select: { environmentId: true },
});
expect(result).toEqual(mockContact);
});
test("throws DatabaseError when database operation fails", async () => {
vi.mocked(prisma.contact.findUnique).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError("Error", {
code: "P2002",
clientVersion: "4.7.0",
})
);
await expect(getContact(contactId)).rejects.toThrow(DatabaseError);
});
});
describe("getSegment", () => {
const segmentId = "segment123";
test("returns the segment when found", async () => {
const mockSegment = { environmentId: "env123" };
vi.mocked(prisma.segment.findUnique).mockResolvedValue(mockSegment);
const result = await getSegment(segmentId);
expect(validateInputs).toHaveBeenCalled();
expect(prisma.segment.findUnique).toHaveBeenCalledWith({
where: { id: segmentId },
select: { environmentId: true },
});
expect(result).toEqual(mockSegment);
});
test("throws DatabaseError when database operation fails", async () => {
vi.mocked(prisma.segment.findUnique).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError("Error", {
code: "P2002",
clientVersion: "4.7.0",
})
);
await expect(getSegment(segmentId)).rejects.toThrow(DatabaseError);
});
});
});

View File

@@ -0,0 +1,115 @@
import * as crypto from "@/lib/crypto";
import { env } from "@/lib/env";
import cuid2 from "@paralleldrive/cuid2";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { generateSurveySingleUseId, generateSurveySingleUseIds } from "./single-use-surveys";
vi.mock("@/lib/crypto", () => ({
symmetricEncrypt: vi.fn(),
symmetricDecrypt: vi.fn(),
}));
vi.mock(
"@paralleldrive/cuid2",
async (importOriginal: () => Promise<typeof import("@paralleldrive/cuid2")>) => {
const original = await importOriginal();
return {
...original,
createId: vi.fn(),
isCuid: vi.fn(),
};
}
);
vi.mock("@/lib/env", () => ({
env: {
ENCRYPTION_KEY: "test-encryption-key",
},
}));
describe("Single Use Surveys", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("generateSurveySingleUseId", () => {
test("returns plain cuid when encryption is disabled", () => {
const createIdMock = vi.spyOn(cuid2, "createId");
createIdMock.mockReturnValueOnce("test-cuid");
const result = generateSurveySingleUseId(false);
expect(result).toBe("test-cuid");
expect(createIdMock).toHaveBeenCalledTimes(1);
expect(crypto.symmetricEncrypt).not.toHaveBeenCalled();
});
test("returns encrypted cuid when encryption is enabled", () => {
const createIdMock = vi.spyOn(cuid2, "createId");
createIdMock.mockReturnValueOnce("test-cuid");
vi.mocked(crypto.symmetricEncrypt).mockReturnValueOnce("encrypted-test-cuid");
const result = generateSurveySingleUseId(true);
expect(result).toBe("encrypted-test-cuid");
expect(createIdMock).toHaveBeenCalledTimes(1);
expect(crypto.symmetricEncrypt).toHaveBeenCalledWith("test-cuid", env.ENCRYPTION_KEY);
});
test("throws error when encryption key is missing", () => {
vi.mocked(env).ENCRYPTION_KEY = "";
const createIdMock = vi.spyOn(cuid2, "createId");
createIdMock.mockReturnValueOnce("test-cuid");
expect(() => generateSurveySingleUseId(true)).toThrow("ENCRYPTION_KEY is not set");
// Restore encryption key for subsequent tests
vi.mocked(env).ENCRYPTION_KEY = "test-encryption-key";
});
});
describe("generateSurveySingleUseIds", () => {
beforeEach(() => {
vi.mocked(env).ENCRYPTION_KEY = "test-encryption-key";
});
test("generates multiple single use IDs", () => {
const createIdMock = vi.spyOn(cuid2, "createId");
createIdMock
.mockReturnValueOnce("test-cuid-1")
.mockReturnValueOnce("test-cuid-2")
.mockReturnValueOnce("test-cuid-3");
const result = generateSurveySingleUseIds(3, false);
expect(result).toEqual(["test-cuid-1", "test-cuid-2", "test-cuid-3"]);
expect(createIdMock).toHaveBeenCalledTimes(3);
});
test("generates encrypted IDs when encryption is enabled", () => {
const createIdMock = vi.spyOn(cuid2, "createId");
createIdMock.mockReturnValueOnce("test-cuid-1").mockReturnValueOnce("test-cuid-2");
vi.mocked(crypto.symmetricEncrypt)
.mockReturnValueOnce("encrypted-test-cuid-1")
.mockReturnValueOnce("encrypted-test-cuid-2");
const result = generateSurveySingleUseIds(2, true);
expect(result).toEqual(["encrypted-test-cuid-1", "encrypted-test-cuid-2"]);
expect(createIdMock).toHaveBeenCalledTimes(2);
expect(crypto.symmetricEncrypt).toHaveBeenCalledTimes(2);
});
test("returns empty array when count is zero", () => {
const result = generateSurveySingleUseIds(0, false);
const createIdMock = vi.spyOn(cuid2, "createId");
createIdMock.mockReturnValueOnce("test-cuid");
expect(result).toEqual([]);
expect(createIdMock).not.toHaveBeenCalled();
});
});
});

View File

@@ -1,4 +1,4 @@
import { symmetricDecrypt, symmetricEncrypt } from "@/lib/crypto";
import { symmetricEncrypt } from "@/lib/crypto";
import { env } from "@/lib/env";
import cuid2 from "@paralleldrive/cuid2";
@@ -26,24 +26,3 @@ export const generateSurveySingleUseIds = (count: number, isEncrypted: boolean):
return singleUseIds;
};
// validate the survey single use id
export const validateSurveySingleUseId = (surveySingleUseId: string): string | undefined => {
try {
let decryptedCuid: string | null = null;
if (!env.ENCRYPTION_KEY) {
throw new Error("ENCRYPTION_KEY is not set");
}
decryptedCuid = symmetricDecrypt(surveySingleUseId, env.ENCRYPTION_KEY);
if (cuid2.isCuid(decryptedCuid)) {
return decryptedCuid;
} else {
return undefined;
}
} catch (error) {
return undefined;
}
};

View File

@@ -0,0 +1,133 @@
import { describe, expect, test } from "vitest";
import {
capitalizeFirstLetter,
isCapitalized,
sanitizeString,
startsWithVowel,
truncate,
truncateText,
} from "./strings";
describe("String Utilities", () => {
describe("capitalizeFirstLetter", () => {
test("capitalizes the first letter of a string", () => {
expect(capitalizeFirstLetter("hello")).toBe("Hello");
});
test("returns empty string if input is null", () => {
expect(capitalizeFirstLetter(null)).toBe("");
});
test("returns empty string if input is empty string", () => {
expect(capitalizeFirstLetter("")).toBe("");
});
test("doesn't change already capitalized string", () => {
expect(capitalizeFirstLetter("Hello")).toBe("Hello");
});
test("handles single character string", () => {
expect(capitalizeFirstLetter("a")).toBe("A");
});
});
describe("truncate", () => {
test("returns the string as is if length is less than the specified length", () => {
expect(truncate("hello", 10)).toBe("hello");
});
test("truncates the string and adds ellipsis if length exceeds the specified length", () => {
expect(truncate("hello world", 5)).toBe("hello...");
});
test("returns empty string if input is falsy", () => {
expect(truncate("", 5)).toBe("");
});
test("handles exact length match correctly", () => {
expect(truncate("hello", 5)).toBe("hello");
});
});
describe("sanitizeString", () => {
test("replaces special characters with delimiter", () => {
expect(sanitizeString("hello@world")).toBe("hello_world");
});
test("keeps alphanumeric and allowed characters", () => {
expect(sanitizeString("hello-world.123")).toBe("hello-world.123");
});
test("truncates string to specified length", () => {
const longString = "a".repeat(300);
expect(sanitizeString(longString).length).toBe(255);
});
test("uses custom delimiter when provided", () => {
expect(sanitizeString("hello@world", "-")).toBe("hello-world");
});
test("uses custom length when provided", () => {
expect(sanitizeString("hello world", "_", 5)).toBe("hello");
});
});
describe("isCapitalized", () => {
test("returns true for capitalized strings", () => {
expect(isCapitalized("Hello")).toBe(true);
});
test("returns false for non-capitalized strings", () => {
expect(isCapitalized("hello")).toBe(false);
});
test("handles single uppercase character", () => {
expect(isCapitalized("A")).toBe(true);
});
test("handles single lowercase character", () => {
expect(isCapitalized("a")).toBe(false);
});
});
describe("startsWithVowel", () => {
test("returns true for strings starting with lowercase vowels", () => {
expect(startsWithVowel("apple")).toBe(true);
expect(startsWithVowel("elephant")).toBe(true);
expect(startsWithVowel("igloo")).toBe(true);
expect(startsWithVowel("octopus")).toBe(true);
expect(startsWithVowel("umbrella")).toBe(true);
});
test("returns true for strings starting with uppercase vowels", () => {
expect(startsWithVowel("Apple")).toBe(true);
expect(startsWithVowel("Elephant")).toBe(true);
expect(startsWithVowel("Igloo")).toBe(true);
expect(startsWithVowel("Octopus")).toBe(true);
expect(startsWithVowel("Umbrella")).toBe(true);
});
test("returns false for strings starting with consonants", () => {
expect(startsWithVowel("banana")).toBe(false);
expect(startsWithVowel("Carrot")).toBe(false);
});
test("returns false for empty strings", () => {
expect(startsWithVowel("")).toBe(false);
});
});
describe("truncateText", () => {
test("returns the string as is if length is less than the specified limit", () => {
expect(truncateText("hello", 10)).toBe("hello");
});
test("truncates the string and adds ellipsis if length exceeds the specified limit", () => {
expect(truncateText("hello world", 5)).toBe("hello...");
});
test("handles exact limit match correctly", () => {
expect(truncateText("hello", 5)).toBe("hello");
});
});
});

View File

@@ -0,0 +1,100 @@
import { describe, expect, test } from "vitest";
import { TJsEnvironmentStateProject, TJsEnvironmentStateSurvey } from "@formbricks/types/js";
import { getStyling } from "./styling";
describe("Styling Utilities", () => {
test("returns project styling when project does not allow style overwrite", () => {
const project: TJsEnvironmentStateProject = {
styling: {
allowStyleOverwrite: false,
brandColor: "#000000",
highlightBorderColor: "#000000",
},
} as unknown as TJsEnvironmentStateProject;
const survey: TJsEnvironmentStateSurvey = {
styling: {
overwriteThemeStyling: true,
brandColor: "#ffffff",
highlightBorderColor: "#ffffff",
},
} as unknown as TJsEnvironmentStateSurvey;
expect(getStyling(project, survey)).toBe(project.styling);
});
test("returns project styling when project allows style overwrite but survey does not overwrite", () => {
const project: TJsEnvironmentStateProject = {
styling: {
allowStyleOverwrite: true,
brandColor: "#000000",
highlightBorderColor: "#000000",
},
} as unknown as TJsEnvironmentStateProject;
const survey: TJsEnvironmentStateSurvey = {
styling: {
overwriteThemeStyling: false,
brandColor: "#ffffff",
highlightBorderColor: "#ffffff",
},
} as unknown as TJsEnvironmentStateSurvey;
expect(getStyling(project, survey)).toBe(project.styling);
});
test("returns survey styling when both project and survey allow style overwrite", () => {
const project: TJsEnvironmentStateProject = {
styling: {
allowStyleOverwrite: true,
brandColor: "#000000",
highlightBorderColor: "#000000",
},
} as unknown as TJsEnvironmentStateProject;
const survey: TJsEnvironmentStateSurvey = {
styling: {
overwriteThemeStyling: true,
brandColor: "#ffffff",
highlightBorderColor: "#ffffff",
},
} as unknown as TJsEnvironmentStateSurvey;
expect(getStyling(project, survey)).toBe(survey.styling);
});
test("returns project styling when project allows style overwrite but survey styling is undefined", () => {
const project: TJsEnvironmentStateProject = {
styling: {
allowStyleOverwrite: true,
brandColor: "#000000",
highlightBorderColor: "#000000",
},
} as unknown as TJsEnvironmentStateProject;
const survey: TJsEnvironmentStateSurvey = {
styling: undefined,
} as unknown as TJsEnvironmentStateSurvey;
expect(getStyling(project, survey)).toBe(project.styling);
});
test("returns project styling when project allows style overwrite but survey overwriteThemeStyling is undefined", () => {
const project: TJsEnvironmentStateProject = {
styling: {
allowStyleOverwrite: true,
brandColor: "#000000",
highlightBorderColor: "#000000",
},
} as unknown as TJsEnvironmentStateProject;
const survey: TJsEnvironmentStateSurvey = {
styling: {
brandColor: "#ffffff",
highlightBorderColor: "#ffffff",
},
} as unknown as TJsEnvironmentStateSurvey;
expect(getStyling(project, survey)).toBe(project.styling);
});
});

View File

@@ -0,0 +1,164 @@
import { getLocalizedValue } from "@/lib/i18n/utils";
import { structuredClone } from "@/lib/pollyfills/structuredClone";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { TProject } from "@formbricks/types/project";
import { TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { TTemplate } from "@formbricks/types/templates";
import { replacePresetPlaceholders, replaceQuestionPresetPlaceholders } from "./templates";
// Mock the imported functions
vi.mock("@/lib/i18n/utils", () => ({
getLocalizedValue: vi.fn(),
}));
vi.mock("@/lib/pollyfills/structuredClone", () => ({
structuredClone: vi.fn((obj) => JSON.parse(JSON.stringify(obj))),
}));
describe("Template Utilities", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("replaceQuestionPresetPlaceholders", () => {
test("returns original question when project is not provided", () => {
const question: TSurveyQuestion = {
id: "test-id",
type: TSurveyQuestionTypeEnum.OpenText,
headline: {
default: "Test Question $[projectName]",
},
} as unknown as TSurveyQuestion;
const result = replaceQuestionPresetPlaceholders(question, undefined as unknown as TProject);
expect(result).toEqual(question);
expect(structuredClone).not.toHaveBeenCalled();
});
test("replaces projectName placeholder in subheader", () => {
const question: TSurveyQuestion = {
id: "test-id",
type: TSurveyQuestionTypeEnum.OpenText,
headline: {
default: "Test Question",
},
subheader: {
default: "Subheader for $[projectName]",
},
} as unknown as TSurveyQuestion;
const project: TProject = {
id: "project-id",
name: "Test Project",
organizationId: "org-id",
} as unknown as TProject;
// Mock for headline and subheader with correct return values
vi.mocked(getLocalizedValue).mockReturnValueOnce("Test Question");
vi.mocked(getLocalizedValue).mockReturnValueOnce("Subheader for $[projectName]");
const result = replaceQuestionPresetPlaceholders(question, project);
expect(vi.mocked(getLocalizedValue)).toHaveBeenCalledTimes(2);
expect(result.subheader?.default).toBe("Subheader for Test Project");
});
test("handles missing headline and subheader", () => {
const question: TSurveyQuestion = {
id: "test-id",
type: TSurveyQuestionTypeEnum.OpenText,
} as unknown as TSurveyQuestion;
const project: TProject = {
id: "project-id",
name: "Test Project",
organizationId: "org-id",
} as unknown as TProject;
const result = replaceQuestionPresetPlaceholders(question, project);
expect(structuredClone).toHaveBeenCalledWith(question);
expect(result).toEqual(question);
expect(getLocalizedValue).not.toHaveBeenCalled();
});
});
describe("replacePresetPlaceholders", () => {
test("replaces projectName placeholder in template name and questions", () => {
const template: TTemplate = {
id: "template-1",
name: "Test Template",
description: "Template Description",
preset: {
name: "$[projectName] Feedback",
questions: [
{
id: "q1",
type: TSurveyQuestionTypeEnum.OpenText,
headline: {
default: "How do you like $[projectName]?",
},
},
{
id: "q2",
type: TSurveyQuestionTypeEnum.OpenText,
headline: {
default: "Another question",
},
subheader: {
default: "About $[projectName]",
},
},
],
},
category: "product",
} as unknown as TTemplate;
const project = {
name: "Awesome App",
};
// Mock getLocalizedValue to return the original strings with placeholders
vi.mocked(getLocalizedValue)
.mockReturnValueOnce("How do you like $[projectName]?")
.mockReturnValueOnce("Another question")
.mockReturnValueOnce("About $[projectName]");
const result = replacePresetPlaceholders(template, project);
expect(result.preset.name).toBe("Awesome App Feedback");
expect(structuredClone).toHaveBeenCalledWith(template.preset);
// Verify that replaceQuestionPresetPlaceholders was applied to both questions
expect(vi.mocked(getLocalizedValue)).toHaveBeenCalledTimes(3);
expect(result.preset.questions[0].headline?.default).toBe("How do you like Awesome App?");
expect(result.preset.questions[1].subheader?.default).toBe("About Awesome App");
});
test("maintains other template properties", () => {
const template: TTemplate = {
id: "template-1",
name: "Test Template",
description: "Template Description",
preset: {
name: "$[projectName] Feedback",
questions: [],
},
category: "product",
} as unknown as TTemplate;
const project = {
name: "Awesome App",
};
const result = replacePresetPlaceholders(template, project) as unknown as {
name: string;
description: string;
};
expect(result.name).toBe(template.name);
expect(result.description).toBe(template.description);
});
});
});

View File

@@ -0,0 +1,49 @@
import { cleanup } from "@testing-library/react";
import { afterEach, describe, expect, test } from "vitest";
import { TActionClassPageUrlRule } from "@formbricks/types/action-classes";
import { isValidCallbackUrl, testURLmatch } from "./url";
afterEach(() => {
cleanup();
});
describe("testURLmatch", () => {
const testCases: [string, string, TActionClassPageUrlRule, string][] = [
["https://example.com", "https://example.com", "exactMatch", "yes"],
["https://example.com", "https://example.com/page", "contains", "no"],
["https://example.com/page", "https://example.com", "startsWith", "yes"],
["https://example.com/page", "page", "endsWith", "yes"],
["https://example.com", "https://other.com", "notMatch", "yes"],
["https://example.com", "other", "notContains", "yes"],
];
test.each(testCases)("returns %s for %s with rule %s", (testUrl, pageUrlValue, pageUrlRule, expected) => {
expect(testURLmatch(testUrl, pageUrlValue, pageUrlRule)).toBe(expected);
});
test("throws an error for invalid match type", () => {
expect(() =>
testURLmatch("https://example.com", "https://example.com", "invalidRule" as TActionClassPageUrlRule)
).toThrow("Invalid match type");
});
});
describe("isValidCallbackUrl", () => {
const WEBAPP_URL = "https://webapp.example.com";
test("returns true for valid callback URL", () => {
expect(isValidCallbackUrl("https://webapp.example.com/callback", WEBAPP_URL)).toBe(true);
});
test("returns false for invalid scheme", () => {
expect(isValidCallbackUrl("ftp://webapp.example.com/callback", WEBAPP_URL)).toBe(false);
});
test("returns false for invalid domain", () => {
expect(isValidCallbackUrl("https://malicious.com/callback", WEBAPP_URL)).toBe(false);
});
test("returns false for malformed URL", () => {
expect(isValidCallbackUrl("not-a-valid-url", WEBAPP_URL)).toBe(false);
});
});

View File

@@ -0,0 +1,54 @@
import { afterEach, describe, expect, test, vi } from "vitest";
import { z } from "zod";
import { logger } from "@formbricks/logger";
import { ValidationError } from "@formbricks/types/errors";
import { validateInputs } from "./validate";
vi.mock("@formbricks/logger", () => ({
logger: {
error: vi.fn(),
},
}));
afterEach(() => {
vi.clearAllMocks();
});
describe("validateInputs", () => {
test("validates inputs successfully", () => {
const schema = z.string();
const result = validateInputs(["valid", schema]);
expect(result).toEqual(["valid"]);
});
test("throws ValidationError for invalid inputs", () => {
const schema = z.string();
expect(() => validateInputs([123, schema])).toThrow(ValidationError);
expect(logger.error).toHaveBeenCalledWith(
expect.anything(),
expect.stringContaining("Validation failed")
);
});
test("validates multiple inputs successfully", () => {
const stringSchema = z.string();
const numberSchema = z.number();
const result = validateInputs(["valid", stringSchema], [42, numberSchema]);
expect(result).toEqual(["valid", 42]);
});
test("throws ValidationError for one of multiple invalid inputs", () => {
const stringSchema = z.string();
const numberSchema = z.number();
expect(() => validateInputs(["valid", stringSchema], ["invalid", numberSchema])).toThrow(ValidationError);
expect(logger.error).toHaveBeenCalledWith(
expect.anything(),
expect.stringContaining("Validation failed")
);
});
});

View File

@@ -0,0 +1,131 @@
import { cleanup } from "@testing-library/react";
import { afterEach, describe, expect, test } from "vitest";
import {
checkForLoomUrl,
checkForVimeoUrl,
checkForYoutubeUrl,
convertToEmbedUrl,
extractLoomId,
extractVimeoId,
extractYoutubeId,
} from "./video-upload";
afterEach(() => {
cleanup();
});
describe("checkForYoutubeUrl", () => {
test("returns true for valid YouTube URLs", () => {
expect(checkForYoutubeUrl("https://www.youtube.com/watch?v=dQw4w9WgXcQ")).toBe(true);
expect(checkForYoutubeUrl("https://youtu.be/dQw4w9WgXcQ")).toBe(true);
expect(checkForYoutubeUrl("https://youtube.com/watch?v=dQw4w9WgXcQ")).toBe(true);
expect(checkForYoutubeUrl("https://youtube-nocookie.com/embed/dQw4w9WgXcQ")).toBe(true);
expect(checkForYoutubeUrl("https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ")).toBe(true);
expect(checkForYoutubeUrl("https://www.youtu.be/dQw4w9WgXcQ")).toBe(true);
});
test("returns false for invalid YouTube URLs", () => {
expect(checkForYoutubeUrl("https://www.invalid.com/watch?v=dQw4w9WgXcQ")).toBe(false);
expect(checkForYoutubeUrl("invalid-url")).toBe(false);
expect(checkForYoutubeUrl("http://www.youtube.com/watch?v=dQw4w9WgXcQ")).toBe(false); // Non-HTTPS protocol
});
});
describe("extractYoutubeId", () => {
test("extracts video ID from YouTube URLs", () => {
expect(extractYoutubeId("https://www.youtube.com/watch?v=dQw4w9WgXcQ")).toBe("dQw4w9WgXcQ");
expect(extractYoutubeId("https://youtu.be/dQw4w9WgXcQ")).toBe("dQw4w9WgXcQ");
expect(extractYoutubeId("https://youtube.com/embed/dQw4w9WgXcQ")).toBe("dQw4w9WgXcQ");
expect(extractYoutubeId("https://youtube-nocookie.com/embed/dQw4w9WgXcQ")).toBe("dQw4w9WgXcQ");
});
test("returns null for invalid YouTube URLs", () => {
expect(extractYoutubeId("https://www.invalid.com/watch?v=dQw4w9WgXcQ")).toBeNull();
expect(extractYoutubeId("invalid-url")).toBeNull();
expect(extractYoutubeId("https://youtube.com/notavalidpath")).toBeNull();
});
});
describe("convertToEmbedUrl", () => {
test("converts YouTube URL to embed URL", () => {
expect(convertToEmbedUrl("https://www.youtube.com/watch?v=dQw4w9WgXcQ")).toBe(
"https://www.youtube.com/embed/dQw4w9WgXcQ"
);
expect(convertToEmbedUrl("https://youtu.be/dQw4w9WgXcQ")).toBe(
"https://www.youtube.com/embed/dQw4w9WgXcQ"
);
});
test("converts Vimeo URL to embed URL", () => {
expect(convertToEmbedUrl("https://vimeo.com/123456789")).toBe("https://player.vimeo.com/video/123456789");
expect(convertToEmbedUrl("https://www.vimeo.com/123456789")).toBe(
"https://player.vimeo.com/video/123456789"
);
});
test("converts Loom URL to embed URL", () => {
expect(convertToEmbedUrl("https://www.loom.com/share/abcdef123456")).toBe(
"https://www.loom.com/embed/abcdef123456"
);
expect(convertToEmbedUrl("https://loom.com/share/abcdef123456")).toBe(
"https://www.loom.com/embed/abcdef123456"
);
});
test("returns undefined for unsupported URLs", () => {
expect(convertToEmbedUrl("https://www.invalid.com/watch?v=dQw4w9WgXcQ")).toBeUndefined();
expect(convertToEmbedUrl("invalid-url")).toBeUndefined();
});
});
// Testing private functions by importing them through the module system
describe("checkForVimeoUrl", () => {
test("returns true for valid Vimeo URLs", () => {
expect(checkForVimeoUrl("https://vimeo.com/123456789")).toBe(true);
expect(checkForVimeoUrl("https://www.vimeo.com/123456789")).toBe(true);
});
test("returns false for invalid Vimeo URLs", () => {
expect(checkForVimeoUrl("https://www.invalid.com/123456789")).toBe(false);
expect(checkForVimeoUrl("invalid-url")).toBe(false);
expect(checkForVimeoUrl("http://vimeo.com/123456789")).toBe(false); // Non-HTTPS protocol
});
});
describe("checkForLoomUrl", () => {
test("returns true for valid Loom URLs", () => {
expect(checkForLoomUrl("https://loom.com/share/abcdef123456")).toBe(true);
expect(checkForLoomUrl("https://www.loom.com/share/abcdef123456")).toBe(true);
});
test("returns false for invalid Loom URLs", () => {
expect(checkForLoomUrl("https://www.invalid.com/share/abcdef123456")).toBe(false);
expect(checkForLoomUrl("invalid-url")).toBe(false);
expect(checkForLoomUrl("http://loom.com/share/abcdef123456")).toBe(false); // Non-HTTPS protocol
});
});
describe("extractVimeoId", () => {
test("extracts video ID from Vimeo URLs", () => {
expect(extractVimeoId("https://vimeo.com/123456789")).toBe("123456789");
expect(extractVimeoId("https://www.vimeo.com/123456789")).toBe("123456789");
});
test("returns null for invalid Vimeo URLs", () => {
expect(extractVimeoId("https://www.invalid.com/123456789")).toBeNull();
expect(extractVimeoId("invalid-url")).toBeNull();
});
});
describe("extractLoomId", () => {
test("extracts video ID from Loom URLs", () => {
expect(extractLoomId("https://loom.com/share/abcdef123456")).toBe("abcdef123456");
expect(extractLoomId("https://www.loom.com/share/abcdef123456")).toBe("abcdef123456");
});
test("returns null for invalid Loom URLs", async () => {
expect(extractLoomId("https://www.invalid.com/share/abcdef123456")).toBeNull();
expect(extractLoomId("invalid-url")).toBeNull();
expect(extractLoomId("https://loom.com/invalid/abcdef123456")).toBeNull();
});
});

View File

@@ -15,13 +15,12 @@ export const checkForYoutubeUrl = (url: string): boolean => {
const hostname = youtubeUrl.hostname;
return youtubeDomains.includes(hostname);
} catch (err) {
// invalid URL
} catch {
return false;
}
};
const checkForVimeoUrl = (url: string): boolean => {
export const checkForVimeoUrl = (url: string): boolean => {
try {
const vimeoUrl = new URL(url);
@@ -31,13 +30,12 @@ const checkForVimeoUrl = (url: string): boolean => {
const hostname = vimeoUrl.hostname;
return vimeoDomains.includes(hostname);
} catch (err) {
// invalid URL
} catch {
return false;
}
};
const checkForLoomUrl = (url: string): boolean => {
export const checkForLoomUrl = (url: string): boolean => {
try {
const loomUrl = new URL(url);
@@ -47,8 +45,7 @@ const checkForLoomUrl = (url: string): boolean => {
const hostname = loomUrl.hostname;
return loomDomains.includes(hostname);
} catch (err) {
// invalid URL
} catch {
return false;
}
};
@@ -65,8 +62,8 @@ export const extractYoutubeId = (url: string): string | null => {
];
regExpList.some((regExp) => {
const match = url.match(regExp);
if (match && match[1]) {
const match = regExp.exec(url);
if (match?.[1]) {
id = match[1];
return true;
}
@@ -76,23 +73,25 @@ export const extractYoutubeId = (url: string): string | null => {
return id || null;
};
const extractVimeoId = (url: string): string | null => {
export const extractVimeoId = (url: string): string | null => {
const regExp = /vimeo\.com\/(\d+)/;
const match = url.match(regExp);
const match = regExp.exec(url);
if (match && match[1]) {
if (match?.[1]) {
return match[1];
}
return null;
};
const extractLoomId = (url: string): string | null => {
export const extractLoomId = (url: string): string | null => {
const regExp = /loom\.com\/share\/([a-zA-Z0-9]+)/;
const match = url.match(regExp);
const match = regExp.exec(url);
if (match && match[1]) {
if (match?.[1]) {
return match[1];
}
return null;
};

View File

@@ -193,7 +193,7 @@ export const EmailCustomizationSettings = ({
<div className="mb-10">
<Small>{t("environments.settings.general.logo_in_email_header")}</Small>
<div className="mt-2 mb-6 flex items-center gap-4">
<div className="mb-6 mt-2 flex items-center gap-4">
{logoUrl && (
<div className="flex flex-col gap-2">
<div className="flex w-max items-center justify-center rounded-lg border border-slate-200 px-4 py-2">
@@ -256,7 +256,7 @@ export const EmailCustomizationSettings = ({
</Button>
</div>
</div>
<div className="shadow-card-xl min-h-52 w-[446px] rounded-t-lg border border-slate-100 px-10 pt-10 pb-4">
<div className="shadow-card-xl min-h-52 w-[446px] rounded-t-lg border border-slate-100 px-10 pb-4 pt-10">
<Image
data-testid="email-customization-preview-image"
src={logoUrl || fbLogoUrl}
@@ -284,7 +284,7 @@ export const EmailCustomizationSettings = ({
)}
{hasWhiteLabelPermission && isReadOnly && (
<Alert variant="warning" className="mt-4 mb-6">
<Alert variant="warning" className="mb-6 mt-4">
<AlertDescription>
{t("common.only_owners_managers_and_manage_access_members_can_perform_this_action")}
</AlertDescription>

View File

@@ -8,7 +8,7 @@ import {
getProjectIdFromEnvironmentId,
getProjectIdFromSurveyId,
} from "@/lib/utils/helper";
import { generateSurveySingleUseId } from "@/lib/utils/singleUseSurveys";
import { generateSurveySingleUseId } from "@/lib/utils/single-use-surveys";
import { getProjectIdIfEnvironmentExists } from "@/modules/survey/list/lib/environment";
import { getUserProjects } from "@/modules/survey/list/lib/project";
import {

View File

@@ -1,6 +1,6 @@
"use client";
import { checkForYoutubeUrl, convertToEmbedUrl, extractYoutubeId } from "@/lib/utils/videoUpload";
import { checkForYoutubeUrl, convertToEmbedUrl, extractYoutubeId } from "@/lib/utils/video-upload";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Alert, AlertTitle } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button";

View File

@@ -237,7 +237,7 @@ export const FileInput = ({
/>
{file.uploaded ? (
<div
className="absolute top-2 right-2 flex cursor-pointer items-center justify-center rounded-md bg-slate-100 p-1 hover:bg-slate-200 hover:bg-white/90"
className="absolute right-2 top-2 flex cursor-pointer items-center justify-center rounded-md bg-slate-100 p-1 hover:bg-slate-200 hover:bg-white/90"
onClick={() => handleRemove(idx)}>
<XIcon className="h-5 text-slate-700 hover:text-slate-900" />
</div>
@@ -255,7 +255,7 @@ export const FileInput = ({
</p>
{file.uploaded ? (
<div
className="absolute top-2 right-2 flex cursor-pointer items-center justify-center rounded-md bg-slate-100 p-1 hover:bg-slate-200 hover:bg-white/90"
className="absolute right-2 top-2 flex cursor-pointer items-center justify-center rounded-md bg-slate-100 p-1 hover:bg-slate-200 hover:bg-white/90"
onClick={() => handleRemove(idx)}>
<XIcon className="h-5 text-slate-700 hover:text-slate-900" />
</div>
@@ -295,7 +295,7 @@ export const FileInput = ({
/>
{selectedFiles[0].uploaded ? (
<div
className="absolute top-2 right-2 flex cursor-pointer items-center justify-center rounded-md bg-slate-100 p-1 hover:bg-slate-200 hover:bg-white/90"
className="absolute right-2 top-2 flex cursor-pointer items-center justify-center rounded-md bg-slate-100 p-1 hover:bg-slate-200 hover:bg-white/90"
onClick={() => handleRemove(0)}>
<XIcon className="h-5 text-slate-700 hover:text-slate-900" />
</div>
@@ -311,7 +311,7 @@ export const FileInput = ({
</p>
{selectedFiles[0].uploaded ? (
<div
className="absolute top-2 right-2 flex cursor-pointer items-center justify-center rounded-md bg-slate-100 p-1 hover:bg-slate-200 hover:bg-white/90"
className="absolute right-2 top-2 flex cursor-pointer items-center justify-center rounded-md bg-slate-100 p-1 hover:bg-slate-200 hover:bg-white/90"
onClick={() => handleRemove(0)}>
<XIcon className="h-5 text-slate-700 hover:text-slate-900" />
</div>