chore: 576 test coverage: apps/web/modules/survey/list/lib (#5706)

This commit is contained in:
Jakob Schott
2025-05-07 23:32:52 +02:00
committed by GitHub
parent 4dcf9b093b
commit 03c9a6aaae
3 changed files with 1086 additions and 0 deletions

View File

@@ -0,0 +1,201 @@
// Retain only vitest import here
// Import modules after mocks
import { cache as libCacheImport } from "@/lib/cache";
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 { logger } from "@formbricks/logger";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { doesEnvironmentExist, getEnvironment, getProjectIdIfEnvironmentExists } from "./environment";
// Mock dependencies
vi.mock("@/lib/cache", () => ({
cache: vi.fn((workFn: () => Promise<any>, _cacheKey?: string, _options?: any) =>
vi.fn(async () => await workFn())
),
}));
vi.mock("@/lib/environment/cache", () => ({
environmentCache: {
tag: {
byId: vi.fn((id) => `environment-${id}`),
},
},
}));
vi.mock("@/lib/utils/validate");
vi.mock("@formbricks/database", () => ({
prisma: {
environment: {
findUnique: vi.fn(),
},
},
}));
vi.mock("@formbricks/logger", () => ({
logger: {
error: vi.fn(),
},
}));
vi.mock("react", async () => {
const actualReact = await vi.importActual("react");
return {
...actualReact,
cache: vi.fn((fnToMemoize: (...args: any[]) => any) => fnToMemoize),
};
});
const mockEnvironmentId = "clxko31qs000008jya8v4ah0a";
const mockProjectId = "clxko31qt000108jyd64v5688";
describe("doesEnvironmentExist", () => {
beforeEach(() => {
vi.resetAllMocks();
// No need to call mockImplementation for libCacheImport or reactCacheImport here anymore
});
test("should return environmentId if environment exists", async () => {
vi.mocked(prisma.environment.findUnique).mockResolvedValue({ id: mockEnvironmentId });
const result = await doesEnvironmentExist(mockEnvironmentId);
expect(result).toBe(mockEnvironmentId);
expect(prisma.environment.findUnique).toHaveBeenCalledWith({
where: { id: mockEnvironmentId },
select: { id: true },
});
// Check if mocks were called as expected by the new setup
expect(libCacheImport).toHaveBeenCalledTimes(1);
// Check that the function returned by libCacheImport was called
const libCacheReturnedFn = vi.mocked(libCacheImport).mock.results[0].value;
expect(libCacheReturnedFn).toHaveBeenCalledTimes(1);
});
test("should throw ResourceNotFoundError if environment does not exist", async () => {
vi.mocked(prisma.environment.findUnique).mockResolvedValue(null);
await expect(doesEnvironmentExist(mockEnvironmentId)).rejects.toThrow(ResourceNotFoundError);
expect(prisma.environment.findUnique).toHaveBeenCalledWith({
where: { id: mockEnvironmentId },
select: { id: true },
});
expect(libCacheImport).toHaveBeenCalledTimes(1);
// Check that the function returned by libCacheImport was called
const libCacheReturnedFn = vi.mocked(libCacheImport).mock.results[0].value;
expect(libCacheReturnedFn).toHaveBeenCalledTimes(1);
});
});
describe("getProjectIdIfEnvironmentExists", () => {
beforeEach(() => {
vi.resetAllMocks();
});
test("should return projectId if environment exists", async () => {
vi.mocked(prisma.environment.findUnique).mockResolvedValue({ projectId: mockProjectId }); // Ensure correct mock value
const result = await getProjectIdIfEnvironmentExists(mockEnvironmentId);
expect(result).toBe(mockProjectId);
expect(prisma.environment.findUnique).toHaveBeenCalledWith({
where: { id: mockEnvironmentId },
select: { projectId: true },
});
expect(libCacheImport).toHaveBeenCalledTimes(1);
// Check that the function returned by libCacheImport was called
const libCacheReturnedFn = vi.mocked(libCacheImport).mock.results[0].value;
expect(libCacheReturnedFn).toHaveBeenCalledTimes(1);
});
test("should throw ResourceNotFoundError if environment does not exist", async () => {
vi.mocked(prisma.environment.findUnique).mockResolvedValue(null);
await expect(getProjectIdIfEnvironmentExists(mockEnvironmentId)).rejects.toThrow(ResourceNotFoundError);
expect(prisma.environment.findUnique).toHaveBeenCalledWith({
where: { id: mockEnvironmentId },
select: { projectId: true },
});
expect(libCacheImport).toHaveBeenCalledTimes(1);
// Check that the function returned by libCacheImport was called
const libCacheReturnedFn = vi.mocked(libCacheImport).mock.results[0].value;
expect(libCacheReturnedFn).toHaveBeenCalledTimes(1);
});
});
describe("getEnvironment", () => {
beforeEach(() => {
vi.resetAllMocks();
});
test("should return environment if it exists", async () => {
const mockEnvData = { id: mockEnvironmentId, type: "production" as const };
vi.mocked(prisma.environment.findUnique).mockResolvedValue(mockEnvData);
const result = await getEnvironment(mockEnvironmentId);
expect(result).toEqual(mockEnvData);
expect(validateInputs).toHaveBeenCalledWith([mockEnvironmentId, expect.any(Object)]);
expect(prisma.environment.findUnique).toHaveBeenCalledWith({
where: { id: mockEnvironmentId },
select: { id: true, type: true },
});
expect(libCacheImport).toHaveBeenCalledTimes(1);
// Check that the function returned by libCacheImport was called
const libCacheReturnedFn = vi.mocked(libCacheImport).mock.results[0].value;
expect(libCacheReturnedFn).toHaveBeenCalledTimes(1);
});
test("should return null if environment does not exist (as per select, though findUnique would return null directly)", async () => {
vi.mocked(prisma.environment.findUnique).mockResolvedValue(null);
const result = await getEnvironment(mockEnvironmentId);
expect(result).toBeNull();
expect(validateInputs).toHaveBeenCalledWith([mockEnvironmentId, expect.any(Object)]);
expect(prisma.environment.findUnique).toHaveBeenCalledWith({
where: { id: mockEnvironmentId },
select: { id: true, type: true },
});
// Additional checks for cache mocks
expect(libCacheImport).toHaveBeenCalledTimes(1);
// Check that the function returned by libCacheImport was called
const libCacheReturnedFn = vi.mocked(libCacheImport).mock.results[0].value;
expect(libCacheReturnedFn).toHaveBeenCalledTimes(1);
});
test("should throw DatabaseError if PrismaClientKnownRequestError occurs", async () => {
const prismaError = new Prisma.PrismaClientKnownRequestError("Test Prisma Error", {
code: "P2001",
clientVersion: "2.0.0", // Ensure clientVersion is a string
});
vi.mocked(prisma.environment.findUnique).mockRejectedValue(prismaError);
await expect(getEnvironment(mockEnvironmentId)).rejects.toThrow(DatabaseError);
expect(validateInputs).toHaveBeenCalledWith([mockEnvironmentId, expect.any(Object)]);
expect(logger.error).toHaveBeenCalledWith(prismaError, "Error fetching environment");
expect(libCacheImport).toHaveBeenCalledTimes(1);
// Check that the function returned by libCacheImport was called
const libCacheReturnedFn = vi.mocked(libCacheImport).mock.results[0].value;
expect(libCacheReturnedFn).toHaveBeenCalledTimes(1);
});
test("should re-throw error if a generic error occurs", async () => {
const genericError = new Error("Test Generic Error");
vi.mocked(prisma.environment.findUnique).mockRejectedValue(genericError);
await expect(getEnvironment(mockEnvironmentId)).rejects.toThrow(genericError);
expect(validateInputs).toHaveBeenCalledWith([mockEnvironmentId, expect.any(Object)]);
expect(logger.error).not.toHaveBeenCalled();
expect(libCacheImport).toHaveBeenCalledTimes(1);
// Check that the function returned by libCacheImport was called
const libCacheReturnedFn = vi.mocked(libCacheImport).mock.results[0].value;
expect(libCacheReturnedFn).toHaveBeenCalledTimes(1);
});
});
// Remove the global afterEach if it was only for vi.useRealTimers() and no fake timers are used.
// vi.resetAllMocks() in beforeEach is generally the preferred way to ensure test isolation for mocks.
// The specific afterEach(() => { vi.clearAllMocks(); }) inside each describe block can also be removed.
// For consistency, I'll remove the afterEach blocks from the describe suites.

View File

@@ -0,0 +1,817 @@
import { actionClassCache } from "@/lib/actionClass/cache";
import { cache } from "@/lib/cache";
import { segmentCache } from "@/lib/cache/segment";
import { projectCache } from "@/lib/project/cache";
import { responseCache } from "@/lib/response/cache";
import { surveyCache } from "@/lib/survey/cache";
import { checkForInvalidImagesInQuestions } from "@/lib/survey/utils";
import { validateInputs } from "@/lib/utils/validate";
import { buildOrderByClause, buildWhereClause } from "@/modules/survey/lib/utils";
import { doesEnvironmentExist } from "@/modules/survey/list/lib/environment";
import { getProjectWithLanguagesByEnvironmentId } from "@/modules/survey/list/lib/project";
import { createId } from "@paralleldrive/cuid2";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { logger } from "@formbricks/logger";
import { TActionClassType } from "@formbricks/types/action-classes";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TProjectWithLanguages, TSurvey } from "../types/surveys";
// Import the module to be tested
import {
copySurveyToOtherEnvironment,
deleteSurvey,
getSurvey,
getSurveyCount,
getSurveys,
getSurveysSortedByRelevance,
surveySelect,
} from "./survey";
// Mocked modules
vi.mock("@/lib/cache", () => ({
cache: vi.fn((fn, _options) => fn), // Return the function itself, not its execution result
}));
vi.mock("react", async (importOriginal) => {
const actual = await importOriginal<typeof import("react")>();
return {
...actual,
cache: vi.fn((fn) => fn), // Return the function itself, as reactCache is a HOF
};
});
vi.mock("@/lib/actionClass/cache", () => ({
actionClassCache: {
revalidate: vi.fn(),
},
}));
vi.mock("@/lib/cache/segment", () => ({
segmentCache: {
revalidate: vi.fn(),
},
}));
vi.mock("@/lib/project/cache", () => ({
projectCache: {
revalidate: vi.fn(),
},
}));
vi.mock("@/lib/response/cache", () => ({
responseCache: {
revalidate: vi.fn(),
tag: {
byEnvironmentId: vi.fn((id) => `response-env-${id}`),
bySurveyId: vi.fn((id) => `response-survey-${id}`),
},
},
}));
vi.mock("@/lib/survey/cache", () => ({
surveyCache: {
revalidate: vi.fn(),
tag: {
byEnvironmentId: vi.fn((id) => `survey-env-${id}`),
byId: vi.fn((id) => `survey-${id}`),
byActionClassId: vi.fn((id) => `survey-actionclass-${id}`),
byResultShareKey: vi.fn((key) => `survey-resultsharekey-${key}`),
},
},
}));
vi.mock("@/lib/survey/utils", () => ({
checkForInvalidImagesInQuestions: vi.fn(),
}));
vi.mock("@/lib/utils/validate", () => ({
validateInputs: vi.fn(),
}));
vi.mock("@/modules/survey/lib/utils", () => ({
buildOrderByClause: vi.fn((sortBy) => (sortBy ? [{ [sortBy]: "desc" }] : [])),
buildWhereClause: vi.fn((filterCriteria) => (filterCriteria ? { name: filterCriteria.name } : {})),
}));
vi.mock("@/modules/survey/list/lib/environment", () => ({
doesEnvironmentExist: vi.fn(),
}));
vi.mock("@/modules/survey/list/lib/project", () => ({
getProjectWithLanguagesByEnvironmentId: vi.fn(),
}));
vi.mock("@paralleldrive/cuid2", () => ({
createId: vi.fn(() => "new_cuid2_id"),
}));
vi.mock("@formbricks/database", () => ({
prisma: {
survey: {
findMany: vi.fn(),
findUnique: vi.fn(),
count: vi.fn(),
delete: vi.fn(),
create: vi.fn(),
},
segment: {
delete: vi.fn(),
findFirst: vi.fn(),
},
language: {
// Added for language connectOrCreate in copySurvey
findUnique: vi.fn(),
create: vi.fn(),
},
},
}));
vi.mock("@formbricks/logger", () => ({
logger: {
error: vi.fn(),
},
}));
// Helper to reset mocks
const resetMocks = () => {
vi.mocked(cache).mockClear();
vi.mocked(reactCache).mockClear();
vi.mocked(actionClassCache.revalidate).mockClear();
vi.mocked(segmentCache.revalidate).mockClear();
vi.mocked(projectCache.revalidate).mockClear();
vi.mocked(responseCache.revalidate).mockClear();
vi.mocked(surveyCache.revalidate).mockClear();
vi.mocked(checkForInvalidImagesInQuestions).mockClear();
vi.mocked(validateInputs).mockClear();
vi.mocked(buildOrderByClause).mockClear();
vi.mocked(buildWhereClause).mockClear();
vi.mocked(doesEnvironmentExist).mockClear();
vi.mocked(getProjectWithLanguagesByEnvironmentId).mockClear();
vi.mocked(createId).mockClear();
vi.mocked(prisma.survey.findMany).mockReset();
vi.mocked(prisma.survey.findUnique).mockReset();
vi.mocked(prisma.survey.count).mockReset();
vi.mocked(prisma.survey.delete).mockReset();
vi.mocked(prisma.survey.create).mockReset();
vi.mocked(prisma.segment.delete).mockReset();
vi.mocked(prisma.segment.findFirst).mockReset();
vi.mocked(logger.error).mockClear();
};
const makePrismaKnownError = () =>
new Prisma.PrismaClientKnownRequestError("Test Prisma Error", {
code: "P2001",
clientVersion: "test",
meta: {},
});
// Sample data
const environmentId = "env_1";
const surveyId = "survey_1";
const userId = "user_1";
const mockSurveyPrisma = {
id: surveyId,
createdAt: new Date(),
updatedAt: new Date(),
name: "Test Survey",
type: "web" as any,
creator: { name: "Test User" },
status: "draft" as any,
singleUse: null,
environmentId,
_count: { responses: 10 },
};
describe("getSurveyCount", () => {
beforeEach(() => {
resetMocks();
});
test("should return survey count successfully", async () => {
vi.mocked(prisma.survey.count).mockResolvedValue(5);
const count = await getSurveyCount(environmentId);
expect(count).toBe(5);
expect(prisma.survey.count).toHaveBeenCalledWith({
where: { environmentId },
});
expect(validateInputs).toHaveBeenCalledWith([environmentId, expect.any(Object)]);
});
test("should throw DatabaseError on Prisma error", async () => {
const prismaError = makePrismaKnownError();
vi.mocked(prisma.survey.count).mockRejectedValue(prismaError);
await expect(getSurveyCount(environmentId)).rejects.toThrow(DatabaseError);
expect(logger.error).toHaveBeenCalledWith(prismaError, "Error getting survey count");
});
test("should rethrow unknown error", async () => {
const unknownError = new Error("Unknown error");
vi.mocked(prisma.survey.count).mockRejectedValue(unknownError);
await expect(getSurveyCount(environmentId)).rejects.toThrow(unknownError);
});
});
describe("getSurvey", () => {
beforeEach(() => {
resetMocks();
});
test("should return a survey if found", async () => {
const prismaSurvey = { ...mockSurveyPrisma, _count: { responses: 5 } };
vi.mocked(prisma.survey.findUnique).mockResolvedValue(prismaSurvey);
const survey = await getSurvey(surveyId);
expect(survey).toEqual({ ...prismaSurvey, responseCount: 5 });
expect(prisma.survey.findUnique).toHaveBeenCalledWith({
where: { id: surveyId },
select: surveySelect,
});
expect(surveyCache.tag.byId).toHaveBeenCalledWith(surveyId);
expect(responseCache.tag.bySurveyId).toHaveBeenCalledWith(surveyId);
});
test("should return null if survey not found", async () => {
vi.mocked(prisma.survey.findUnique).mockResolvedValue(null);
const survey = await getSurvey(surveyId);
expect(survey).toBeNull();
});
test("should throw DatabaseError on Prisma error", async () => {
const prismaError = makePrismaKnownError();
vi.mocked(prisma.survey.findUnique).mockRejectedValue(prismaError);
await expect(getSurvey(surveyId)).rejects.toThrow(DatabaseError);
expect(logger.error).toHaveBeenCalledWith(prismaError, "Error getting survey");
});
test("should rethrow unknown error", async () => {
const unknownError = new Error("Unknown error");
vi.mocked(prisma.survey.findUnique).mockRejectedValue(unknownError);
await expect(getSurvey(surveyId)).rejects.toThrow(unknownError);
});
});
describe("getSurveys", () => {
beforeEach(() => {
resetMocks();
});
const mockPrismaSurveys = [
{ ...mockSurveyPrisma, id: "s1", name: "Survey 1", _count: { responses: 1 } },
{ ...mockSurveyPrisma, id: "s2", name: "Survey 2", _count: { responses: 2 } },
];
const expectedSurveys: TSurvey[] = mockPrismaSurveys.map((s) => ({
...s,
responseCount: s._count.responses,
}));
test("should return surveys with default parameters", async () => {
vi.mocked(prisma.survey.findMany).mockResolvedValue(mockPrismaSurveys);
const surveys = await getSurveys(environmentId);
expect(surveys).toEqual(expectedSurveys);
expect(prisma.survey.findMany).toHaveBeenCalledWith({
where: { environmentId, ...buildWhereClause() },
select: surveySelect,
orderBy: buildOrderByClause(),
take: undefined,
skip: undefined,
});
expect(surveyCache.tag.byEnvironmentId).toHaveBeenCalledWith(environmentId);
expect(responseCache.tag.byEnvironmentId).toHaveBeenCalledWith(environmentId);
});
test("should return surveys with limit and offset", async () => {
vi.mocked(prisma.survey.findMany).mockResolvedValue([mockPrismaSurveys[0]]);
const surveys = await getSurveys(environmentId, 1, 1);
expect(surveys).toEqual([expectedSurveys[0]]);
expect(prisma.survey.findMany).toHaveBeenCalledWith({
where: { environmentId, ...buildWhereClause() },
select: surveySelect,
orderBy: buildOrderByClause(),
take: 1,
skip: 1,
});
});
test("should return surveys with filterCriteria", async () => {
const filterCriteria: any = { name: "Test", sortBy: "createdAt" };
vi.mocked(buildWhereClause).mockReturnValue({ AND: [{ name: { contains: "Test" } }] }); // Mock correct return type
vi.mocked(buildOrderByClause).mockReturnValue([{ createdAt: "desc" }]); // Mock specific return
vi.mocked(prisma.survey.findMany).mockResolvedValue(mockPrismaSurveys);
const surveys = await getSurveys(environmentId, undefined, undefined, filterCriteria);
expect(surveys).toEqual(expectedSurveys);
expect(buildWhereClause).toHaveBeenCalledWith(filterCriteria);
expect(buildOrderByClause).toHaveBeenCalledWith("createdAt");
expect(prisma.survey.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: { environmentId, AND: [{ name: { contains: "Test" } }] }, // Check with correct structure
orderBy: [{ createdAt: "desc" }], // Check the mocked order by
})
);
});
test("should throw DatabaseError on Prisma error", async () => {
const prismaError = makePrismaKnownError();
vi.mocked(prisma.survey.findMany).mockRejectedValue(prismaError);
await expect(getSurveys(environmentId)).rejects.toThrow(DatabaseError);
expect(logger.error).toHaveBeenCalledWith(prismaError, "Error getting surveys");
});
test("should rethrow unknown error", async () => {
const unknownError = new Error("Unknown error");
vi.mocked(prisma.survey.findMany).mockRejectedValue(unknownError);
await expect(getSurveys(environmentId)).rejects.toThrow(unknownError);
});
});
describe("getSurveysSortedByRelevance", () => {
beforeEach(() => {
resetMocks();
});
const mockInProgressPrisma = {
...mockSurveyPrisma,
id: "s_inprog",
status: "inProgress" as any,
_count: { responses: 3 },
};
const mockOtherPrisma = {
...mockSurveyPrisma,
id: "s_other",
status: "completed" as any,
_count: { responses: 5 },
};
const expectedInProgressSurvey: TSurvey = { ...mockInProgressPrisma, responseCount: 3 };
const expectedOtherSurvey: TSurvey = { ...mockOtherPrisma, responseCount: 5 };
test("should fetch inProgress surveys first, then others if limit not met", async () => {
vi.mocked(prisma.survey.count).mockResolvedValue(1); // 1 inProgress survey
vi.mocked(prisma.survey.findMany)
.mockResolvedValueOnce([mockInProgressPrisma]) // In-progress surveys
.mockResolvedValueOnce([mockOtherPrisma]); // Additional surveys
const surveys = await getSurveysSortedByRelevance(environmentId, 2, 0);
expect(surveys).toEqual([expectedInProgressSurvey, expectedOtherSurvey]);
expect(prisma.survey.count).toHaveBeenCalledWith({
where: { environmentId, status: "inProgress", ...buildWhereClause() },
});
expect(prisma.survey.findMany).toHaveBeenNthCalledWith(1, {
where: { environmentId, status: "inProgress", ...buildWhereClause() },
select: surveySelect,
orderBy: buildOrderByClause("updatedAt"),
take: 2,
skip: 0,
});
expect(prisma.survey.findMany).toHaveBeenNthCalledWith(2, {
where: { environmentId, status: { not: "inProgress" }, ...buildWhereClause() },
select: surveySelect,
orderBy: buildOrderByClause("updatedAt"),
take: 1,
skip: 0,
});
});
test("should only fetch inProgress surveys if limit is met", async () => {
vi.mocked(prisma.survey.count).mockResolvedValue(1);
vi.mocked(prisma.survey.findMany).mockResolvedValueOnce([mockInProgressPrisma]);
const surveys = await getSurveysSortedByRelevance(environmentId, 1, 0);
expect(surveys).toEqual([expectedInProgressSurvey]);
expect(prisma.survey.findMany).toHaveBeenCalledTimes(1);
});
test("should throw DatabaseError on Prisma error", async () => {
const prismaError = makePrismaKnownError();
vi.mocked(prisma.survey.count).mockRejectedValue(prismaError);
await expect(getSurveysSortedByRelevance(environmentId)).rejects.toThrow(DatabaseError);
expect(logger.error).toHaveBeenCalledWith(prismaError, "Error getting surveys sorted by relevance");
resetMocks(); // Reset for the next part of the test
vi.mocked(prisma.survey.count).mockResolvedValue(0); // Make count succeed
vi.mocked(prisma.survey.findMany).mockRejectedValue(prismaError); // Error on findMany
await expect(getSurveysSortedByRelevance(environmentId)).rejects.toThrow(DatabaseError);
});
test("should rethrow unknown error", async () => {
const unknownError = new Error("Unknown error");
vi.mocked(prisma.survey.count).mockRejectedValue(unknownError);
await expect(getSurveysSortedByRelevance(environmentId)).rejects.toThrow(unknownError);
});
});
describe("deleteSurvey", () => {
beforeEach(() => {
resetMocks();
});
const mockDeletedSurveyData = {
id: surveyId,
environmentId,
segment: null,
type: "web" as any,
resultShareKey: "sharekey1",
triggers: [{ actionClass: { id: "action_1" } }],
};
test("should delete a survey and revalidate caches (no private segment)", async () => {
vi.mocked(prisma.survey.delete).mockResolvedValue(mockDeletedSurveyData as any);
const result = await deleteSurvey(surveyId);
expect(result).toBe(true);
expect(prisma.survey.delete).toHaveBeenCalledWith({
where: { id: surveyId },
select: expect.objectContaining({ id: true, environmentId: true, segment: expect.anything() }),
});
expect(responseCache.revalidate).toHaveBeenCalledWith({ surveyId, environmentId });
expect(surveyCache.revalidate).toHaveBeenCalledWith({
id: surveyId,
environmentId,
resultShareKey: "sharekey1",
});
expect(surveyCache.revalidate).toHaveBeenCalledWith({ actionClassId: "action_1" });
expect(prisma.segment.delete).not.toHaveBeenCalled();
});
test("should revalidate segment cache for non-private segment if segment exists", async () => {
const surveyWithPublicSegment = {
...mockDeletedSurveyData,
segment: { id: "segment_public_1", isPrivate: false },
};
vi.mocked(prisma.survey.delete).mockResolvedValue(surveyWithPublicSegment as any);
await deleteSurvey(surveyId);
expect(prisma.segment.delete).not.toHaveBeenCalled();
expect(segmentCache.revalidate).toHaveBeenCalledWith({ id: "segment_public_1", environmentId });
});
test("should throw DatabaseError on Prisma error", async () => {
const prismaError = makePrismaKnownError();
vi.mocked(prisma.survey.delete).mockRejectedValue(prismaError);
await expect(deleteSurvey(surveyId)).rejects.toThrow(DatabaseError);
expect(logger.error).toHaveBeenCalledWith(prismaError, "Error deleting survey");
});
test("should rethrow unknown error", async () => {
const unknownError = new Error("Unknown error");
vi.mocked(prisma.survey.delete).mockRejectedValue(unknownError);
await expect(deleteSurvey(surveyId)).rejects.toThrow(unknownError);
});
});
const mockExistingSurveyDetails = {
name: "Original Survey",
type: "web" as any,
languages: [{ default: true, enabled: true, language: { code: "en", alias: "English" } }],
welcomeCard: { enabled: true, headline: { default: "Welcome!" } },
questions: [{ id: "q1", type: "openText", headline: { default: "Question 1" } }],
endings: [{ type: "default", headline: { default: "Thanks!" } }],
variables: [{ id: "var1", name: "Var One" }],
hiddenFields: { enabled: true, fieldIds: ["hf1"] },
surveyClosedMessage: { enabled: false },
singleUse: { enabled: false },
projectOverwrites: null,
styling: { theme: {} },
segment: null,
followUps: [{ name: "Follow Up 1", trigger: {}, action: {} }],
triggers: [
{
actionClass: {
id: "ac1",
name: "Code Action",
environmentId,
description: "",
type: "code" as TActionClassType,
key: "code_action_key",
noCodeConfig: null,
},
},
{
actionClass: {
id: "ac2",
name: "No-Code Action",
environmentId,
description: "",
type: "noCode" as TActionClassType,
key: null,
noCodeConfig: { type: "url" },
},
},
],
};
describe("copySurveyToOtherEnvironment", () => {
const targetEnvironmentId = "env_target";
const sourceProjectId = "proj_source";
const targetProjectId = "proj_target";
const mockSourceProject: TProjectWithLanguages = {
id: sourceProjectId,
languages: [{ code: "en", alias: "English" }],
};
const mockTargetProject: TProjectWithLanguages = {
id: targetProjectId,
languages: [{ code: "en", alias: "English" }],
};
const mockNewSurveyResult = {
id: "new_cuid2_id",
environmentId: targetEnvironmentId,
segment: null,
triggers: [
{ actionClass: { id: "new_ac1", name: "Code Action", environmentId: targetEnvironmentId } },
{ actionClass: { id: "new_ac2", name: "No-Code Action", environmentId: targetEnvironmentId } },
],
languages: [{ language: { code: "en" } }],
resultShareKey: null,
};
beforeEach(() => {
resetMocks();
vi.mocked(createId).mockReturnValue("new_cuid2_id");
vi.mocked(prisma.survey.findUnique).mockResolvedValue(mockExistingSurveyDetails as any);
vi.mocked(doesEnvironmentExist).mockResolvedValue(environmentId);
vi.mocked(getProjectWithLanguagesByEnvironmentId)
.mockResolvedValueOnce(mockSourceProject)
.mockResolvedValueOnce(mockTargetProject);
vi.mocked(prisma.survey.create).mockResolvedValue(mockNewSurveyResult as any);
vi.mocked(prisma.segment.findFirst).mockResolvedValue(null);
});
test("should copy survey to a different environment successfully", async () => {
const newSurvey = await copySurveyToOtherEnvironment(
environmentId,
surveyId,
targetEnvironmentId,
userId
);
expect(newSurvey).toBeDefined();
expect(prisma.survey.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
id: "new_cuid2_id",
name: `${mockExistingSurveyDetails.name} (copy)`,
environment: { connect: { id: targetEnvironmentId } },
creator: { connect: { id: userId } },
status: "draft",
triggers: {
create: [
expect.objectContaining({
actionClass: {
connectOrCreate: {
where: {
key_environmentId: { key: "code_action_key", environmentId: targetEnvironmentId },
},
create: expect.objectContaining({ name: "Code Action", key: "code_action_key" }),
},
},
}),
expect.objectContaining({
actionClass: {
connectOrCreate: {
where: {
name_environmentId: { name: "No-Code Action", environmentId: targetEnvironmentId },
},
create: expect.objectContaining({
name: "No-Code Action",
noCodeConfig: { type: "url" },
}),
},
},
}),
],
},
}),
})
);
expect(checkForInvalidImagesInQuestions).toHaveBeenCalledWith(mockExistingSurveyDetails.questions);
expect(actionClassCache.revalidate).toHaveBeenCalledTimes(2);
expect(surveyCache.revalidate).toHaveBeenCalledWith(expect.objectContaining({ id: "new_cuid2_id" }));
expect(surveyCache.revalidate).toHaveBeenCalledWith({ actionClassId: "ac1" });
expect(surveyCache.revalidate).toHaveBeenCalledWith({ actionClassId: "ac2" });
});
test("should copy survey to the same environment successfully", async () => {
vi.mocked(getProjectWithLanguagesByEnvironmentId).mockReset();
vi.mocked(getProjectWithLanguagesByEnvironmentId).mockResolvedValue(mockSourceProject);
await copySurveyToOtherEnvironment(environmentId, surveyId, environmentId, userId);
expect(getProjectWithLanguagesByEnvironmentId).toHaveBeenCalledTimes(1);
expect(prisma.survey.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
environment: { connect: { id: environmentId } },
triggers: {
create: [
{ actionClass: { connect: { id: "ac1" } } },
{ actionClass: { connect: { id: "ac2" } } },
],
},
}),
})
);
});
test("should handle private segment: create new private segment in target", async () => {
const surveyWithPrivateSegment = {
...mockExistingSurveyDetails,
segment: { id: "seg_private", isPrivate: true, filters: [{ type: "user", value: "test" }] },
};
vi.mocked(prisma.survey.findUnique).mockResolvedValue(surveyWithPrivateSegment as any);
const mockNewSurveyWithSegment = { ...mockNewSurveyResult, segment: { id: "new_seg_private" } };
vi.mocked(prisma.survey.create).mockResolvedValue(mockNewSurveyWithSegment as any);
await copySurveyToOtherEnvironment(environmentId, surveyId, targetEnvironmentId, userId);
expect(prisma.survey.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
segment: {
create: {
title: "new_cuid2_id",
isPrivate: true,
filters: surveyWithPrivateSegment.segment.filters,
environment: { connect: { id: targetEnvironmentId } },
},
},
}),
})
);
expect(segmentCache.revalidate).toHaveBeenCalledWith({
id: "new_seg_private",
environmentId: targetEnvironmentId,
});
});
test("should handle public segment: connect if same env, create new if different env (no existing in target)", async () => {
const surveyWithPublicSegment = {
...mockExistingSurveyDetails,
segment: { id: "seg_public", title: "Public Segment", isPrivate: false, filters: [] },
};
vi.mocked(prisma.survey.findUnique).mockResolvedValue(surveyWithPublicSegment as any);
vi.mocked(getProjectWithLanguagesByEnvironmentId)
.mockReset() // for same env part
.mockResolvedValueOnce(mockSourceProject);
// Case 1: Same environment
await copySurveyToOtherEnvironment(environmentId, surveyId, environmentId, userId); // target is same
expect(prisma.survey.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
segment: { connect: { id: "seg_public" } },
}),
})
);
// Reset for different env part
resetMocks();
vi.mocked(createId).mockReturnValue("new_cuid2_id");
vi.mocked(prisma.survey.findUnique).mockResolvedValue(surveyWithPublicSegment as any);
vi.mocked(doesEnvironmentExist).mockResolvedValue(environmentId);
vi.mocked(getProjectWithLanguagesByEnvironmentId)
.mockResolvedValueOnce(mockSourceProject)
.mockResolvedValueOnce(mockTargetProject);
vi.mocked(prisma.survey.create).mockResolvedValue(mockNewSurveyResult as any);
vi.mocked(prisma.segment.findFirst).mockResolvedValue(null); // No existing public segment with same title in target
// Case 2: Different environment, segment with same title does not exist in target
await copySurveyToOtherEnvironment(environmentId, surveyId, targetEnvironmentId, userId);
expect(prisma.survey.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
segment: {
create: {
title: "Public Segment",
isPrivate: false,
filters: [],
environment: { connect: { id: targetEnvironmentId } },
},
},
}),
})
);
});
test("should handle public segment: create new with appended timestamp if different env and segment with same title exists in target", async () => {
const surveyWithPublicSegment = {
...mockExistingSurveyDetails,
segment: { id: "seg_public", title: "Public Segment", isPrivate: false, filters: [] },
};
resetMocks();
vi.mocked(createId).mockReturnValue("new_cuid2_id");
vi.mocked(prisma.survey.findUnique).mockResolvedValue(surveyWithPublicSegment as any);
vi.mocked(doesEnvironmentExist).mockResolvedValue(environmentId);
vi.mocked(getProjectWithLanguagesByEnvironmentId)
.mockResolvedValueOnce(mockSourceProject)
.mockResolvedValueOnce(mockTargetProject);
vi.mocked(prisma.survey.create).mockResolvedValue(mockNewSurveyResult as any);
vi.mocked(prisma.segment.findFirst).mockResolvedValue({ id: "existing_target_seg" } as any); // Segment with same title EXISTS
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(1234567890);
await copySurveyToOtherEnvironment(environmentId, surveyId, targetEnvironmentId, userId);
expect(prisma.survey.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
segment: {
create: {
title: `Public Segment-1234567890`,
isPrivate: false,
filters: [],
environment: { connect: { id: targetEnvironmentId } },
},
},
}),
})
);
dateNowSpy.mockRestore();
});
test("should throw ResourceNotFoundError if source environment not found", async () => {
vi.mocked(doesEnvironmentExist).mockResolvedValueOnce(null);
await expect(
copySurveyToOtherEnvironment(environmentId, surveyId, targetEnvironmentId, userId)
).rejects.toThrow(new ResourceNotFoundError("Environment", environmentId));
});
test("should throw ResourceNotFoundError if source project not found", async () => {
vi.mocked(getProjectWithLanguagesByEnvironmentId).mockReset().mockResolvedValueOnce(null);
await expect(
copySurveyToOtherEnvironment(environmentId, surveyId, targetEnvironmentId, userId)
).rejects.toThrow(new ResourceNotFoundError("Project", environmentId));
});
test("should throw ResourceNotFoundError if existing survey not found", async () => {
vi.mocked(prisma.survey.findUnique).mockResolvedValue(null);
await expect(
copySurveyToOtherEnvironment(environmentId, surveyId, targetEnvironmentId, userId)
).rejects.toThrow(new ResourceNotFoundError("Survey", surveyId));
});
test("should throw ResourceNotFoundError if target environment not found (different env copy)", async () => {
vi.mocked(doesEnvironmentExist).mockResolvedValueOnce(environmentId).mockResolvedValueOnce(null);
await expect(
copySurveyToOtherEnvironment(environmentId, surveyId, targetEnvironmentId, userId)
).rejects.toThrow(new ResourceNotFoundError("Environment", targetEnvironmentId));
});
test("should throw DatabaseError on Prisma create error", async () => {
const prismaError = makePrismaKnownError();
vi.mocked(prisma.survey.create).mockRejectedValue(prismaError);
await expect(
copySurveyToOtherEnvironment(environmentId, surveyId, targetEnvironmentId, userId)
).rejects.toThrow(DatabaseError);
expect(logger.error).toHaveBeenCalledWith(prismaError, "Error copying survey to other environment");
});
test("should rethrow unknown error during copy", async () => {
const unknownError = new Error("Some unknown error during copy");
vi.mocked(prisma.survey.create).mockRejectedValue(unknownError);
await expect(
copySurveyToOtherEnvironment(environmentId, surveyId, targetEnvironmentId, userId)
).rejects.toThrow(unknownError);
});
test("should handle survey with no languages", async () => {
const surveyWithoutLanguages = { ...mockExistingSurveyDetails, languages: [] };
vi.mocked(prisma.survey.findUnique).mockResolvedValue(surveyWithoutLanguages as any);
await copySurveyToOtherEnvironment(environmentId, surveyId, targetEnvironmentId, userId);
expect(prisma.survey.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
languages: undefined,
}),
})
);
expect(projectCache.revalidate).not.toHaveBeenCalled();
});
test("should handle survey with no triggers", async () => {
const surveyWithoutTriggers = { ...mockExistingSurveyDetails, triggers: [] };
vi.mocked(prisma.survey.findUnique).mockResolvedValue(surveyWithoutTriggers as any);
await copySurveyToOtherEnvironment(environmentId, surveyId, targetEnvironmentId, userId);
expect(prisma.survey.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
triggers: { create: [] },
}),
})
);
expect(surveyCache.revalidate).not.toHaveBeenCalledWith(
expect.objectContaining({ actionClassId: expect.any(String) })
);
});
});

View File

@@ -0,0 +1,68 @@
import { describe, expect, test } from "vitest";
import type { TSurveyFilters } from "@formbricks/types/surveys/types";
import { getFormattedFilters } from "./utils";
describe("getFormattedFilters", () => {
test("returns empty object when no filters provided", () => {
const result = getFormattedFilters({} as TSurveyFilters, "user1");
expect(result).toEqual({});
});
test("includes name filter", () => {
const result = getFormattedFilters({ name: "surveyName" } as TSurveyFilters, "user1");
expect(result).toEqual({ name: "surveyName" });
});
test("includes status filter when array is non-empty", () => {
const result = getFormattedFilters({ status: ["active", "inactive"] } as any, "user1");
expect(result).toEqual({ status: ["active", "inactive"] });
});
test("ignores status filter when empty array", () => {
const result = getFormattedFilters({ status: [] } as any, "user1");
expect(result).toEqual({});
});
test("includes type filter when array is non-empty", () => {
const result = getFormattedFilters({ type: ["typeA"] } as any, "user1");
expect(result).toEqual({ type: ["typeA"] });
});
test("ignores type filter when empty array", () => {
const result = getFormattedFilters({ type: [] } as any, "user1");
expect(result).toEqual({});
});
test("includes createdBy filter when array is non-empty", () => {
const result = getFormattedFilters({ createdBy: ["ownerA", "ownerB"] } as any, "user1");
expect(result).toEqual({ createdBy: { userId: "user1", value: ["ownerA", "ownerB"] } });
});
test("ignores createdBy filter when empty array", () => {
const result = getFormattedFilters({ createdBy: [] } as any, "user1");
expect(result).toEqual({});
});
test("includes sortBy filter", () => {
const result = getFormattedFilters({ sortBy: "date" } as any, "user1");
expect(result).toEqual({ sortBy: "date" });
});
test("combines multiple filters", () => {
const input: TSurveyFilters = {
name: "nameVal",
status: ["draft"],
type: ["link", "app"],
createdBy: ["you"],
sortBy: "name",
};
const result = getFormattedFilters(input, "userX");
expect(result).toEqual({
name: "nameVal",
status: ["draft"],
type: ["link", "app"],
createdBy: { userId: "userX", value: ["you"] },
sortBy: "name",
});
});
});