mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
chore: tests for lib/utils and lib/survey (#5676)
This commit is contained in:
@@ -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";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mockSurveyLanguages } from "@/lib/survey/tests/__mock__/survey.mock";
|
||||
import { mockSurveyLanguages } from "@/lib/survey/__mock__/survey.mock";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyCTAQuestion,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
mockContactAttributeKey,
|
||||
mockOrganizationOutput,
|
||||
mockSurveyOutput,
|
||||
} from "../../survey/tests/__mock__/survey.mock";
|
||||
} from "../../survey/__mock__/survey.mock";
|
||||
import {
|
||||
deleteResponse,
|
||||
getResponse,
|
||||
|
||||
@@ -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,
|
||||
113
apps/web/lib/survey/auth.test.ts
Normal file
113
apps/web/lib/survey/auth.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
122
apps/web/lib/survey/cache.test.ts
Normal file
122
apps/web/lib/survey/cache.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
1037
apps/web/lib/survey/service.test.ts
Normal file
1037
apps/web/lib/survey/service.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
254
apps/web/lib/survey/utils.test.ts
Normal file
254
apps/web/lib/survey/utils.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
386
apps/web/lib/utils/action-client-middleware.test.ts
Normal file
386
apps/web/lib/utils/action-client-middleware.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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(".")] = {
|
||||
|
||||
70
apps/web/lib/utils/colors.test.ts
Normal file
70
apps/web/lib/utils/colors.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
64
apps/web/lib/utils/contact.test.ts
Normal file
64
apps/web/lib/utils/contact.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
27
apps/web/lib/utils/datetime.test.ts
Normal file
27
apps/web/lib/utils/datetime.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
50
apps/web/lib/utils/email.test.ts
Normal file
50
apps/web/lib/utils/email.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
63
apps/web/lib/utils/file-conversion.test.ts
Normal file
63
apps/web/lib/utils/file-conversion.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
36
apps/web/lib/utils/headers.test.ts
Normal file
36
apps/web/lib/utils/headers.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
795
apps/web/lib/utils/helper.test.ts
Normal file
795
apps/web/lib/utils/helper.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
87
apps/web/lib/utils/locale.test.ts
Normal file
87
apps/web/lib/utils/locale.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
84
apps/web/lib/utils/promises.test.ts
Normal file
84
apps/web/lib/utils/promises.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
516
apps/web/lib/utils/recall.test.ts
Normal file
516
apps/web/lib/utils/recall.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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)) ||
|
||||
|
||||
737
apps/web/lib/utils/services.test.ts
Normal file
737
apps/web/lib/utils/services.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
115
apps/web/lib/utils/single-use-surveys.test.ts
Normal file
115
apps/web/lib/utils/single-use-surveys.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
133
apps/web/lib/utils/strings.test.ts
Normal file
133
apps/web/lib/utils/strings.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
100
apps/web/lib/utils/styling.test.ts
Normal file
100
apps/web/lib/utils/styling.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
164
apps/web/lib/utils/templates.test.ts
Normal file
164
apps/web/lib/utils/templates.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
49
apps/web/lib/utils/url.test.ts
Normal file
49
apps/web/lib/utils/url.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
54
apps/web/lib/utils/validate.test.ts
Normal file
54
apps/web/lib/utils/validate.test.ts
Normal 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")
|
||||
);
|
||||
});
|
||||
});
|
||||
131
apps/web/lib/utils/video-upload.test.ts
Normal file
131
apps/web/lib/utils/video-upload.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user