mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-22 11:29:22 -05:00
1027 lines
32 KiB
TypeScript
1027 lines
32 KiB
TypeScript
import { prisma } from "@/lib/__mocks__/database";
|
|
import { ActionClass, Prisma, Survey } from "@prisma/client";
|
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
import { testInputValidation } from "vitestSetup";
|
|
import { PrismaErrorType } from "@formbricks/database/types/error";
|
|
import { TSurveyFollowUp } from "@formbricks/database/types/survey-follow-up";
|
|
import { TActionClass } from "@formbricks/types/action-classes";
|
|
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
|
import { TSegment } from "@formbricks/types/segment";
|
|
import { TSurvey, TSurveyCreateInput, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
|
import { getActionClasses } from "@/lib/actionClass/service";
|
|
import {
|
|
getOrganizationByEnvironmentId,
|
|
subscribeOrganizationMembersToSurveyResponses,
|
|
} from "@/lib/organization/service";
|
|
import { evaluateLogic } from "@/lib/surveyLogic/utils";
|
|
import {
|
|
mockActionClass,
|
|
mockId,
|
|
mockOrganizationOutput,
|
|
mockSurveyOutput,
|
|
mockSurveyWithLogic,
|
|
mockTransformedSurveyOutput,
|
|
updateSurveyInput,
|
|
} from "./__mock__/survey.mock";
|
|
import {
|
|
createSurvey,
|
|
getSurvey,
|
|
getSurveyCount,
|
|
getSurveys,
|
|
getSurveysByActionClassId,
|
|
getSurveysBySegmentId,
|
|
handleTriggerUpdates,
|
|
loadNewSegmentInSurvey,
|
|
updateSurvey,
|
|
updateSurveyInternal,
|
|
} from "./service";
|
|
|
|
// Mock organization service
|
|
vi.mock("@/lib/organization/service", () => ({
|
|
getOrganizationByEnvironmentId: vi.fn().mockResolvedValue({
|
|
id: "org123",
|
|
}),
|
|
subscribeOrganizationMembersToSurveyResponses: vi.fn(),
|
|
}));
|
|
|
|
// Mock actionClass service
|
|
vi.mock("@/lib/actionClass/service", () => ({
|
|
getActionClasses: vi.fn(),
|
|
}));
|
|
|
|
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.blocks[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.blocks[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.blocks[0].logic![1].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.blocks[0].logic![1].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.blocks[0].logic![2].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.blocks[0].logic![3].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.blocks[0].logic![3].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.blocks[0].logic![4].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.blocks[0].logic![4].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.blocks[0].logic![5].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(() => {
|
|
vi.mocked(getActionClasses).mockResolvedValueOnce([mockActionClass] as TActionClass[]);
|
|
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce(mockOrganizationOutput);
|
|
});
|
|
|
|
describe("Happy Path", () => {
|
|
test("Updates a survey successfully", async () => {
|
|
prisma.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput);
|
|
prisma.survey.update.mockResolvedValueOnce(mockSurveyOutput);
|
|
const updatedSurvey = await updateSurvey(updateSurveyInput);
|
|
expect(updatedSurvey).toEqual(mockTransformedSurveyOutput);
|
|
});
|
|
|
|
// Note: Language handling tests (for languages.length > 0 fix) are covered in
|
|
// apps/web/modules/survey/editor/lib/survey.test.ts where we have better control
|
|
// over the test mocks. The key fix ensures languages.length > 0 (not > 1) is used.
|
|
});
|
|
|
|
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.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 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);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Tests for handleTriggerUpdates", () => {
|
|
const mockEnvironmentId = "env-123";
|
|
const mockActionClassId1 = "action-123";
|
|
const mockActionClassId2 = "action-456";
|
|
|
|
const mockActionClasses: ActionClass[] = [
|
|
{
|
|
id: mockActionClassId1,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
environmentId: mockEnvironmentId,
|
|
name: "Test Action 1",
|
|
description: "Test action description 1",
|
|
type: "code",
|
|
key: "test-action-1",
|
|
noCodeConfig: null,
|
|
},
|
|
{
|
|
id: mockActionClassId2,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
environmentId: mockEnvironmentId,
|
|
name: "Test Action 2",
|
|
description: "Test action description 2",
|
|
type: "code",
|
|
key: "test-action-2",
|
|
noCodeConfig: null,
|
|
},
|
|
];
|
|
|
|
test("adds new triggers correctly", () => {
|
|
const updatedTriggers = [
|
|
{
|
|
actionClass: {
|
|
id: mockActionClassId1,
|
|
name: "Test Action 1",
|
|
environmentId: mockEnvironmentId,
|
|
type: "code",
|
|
key: "test-action-1",
|
|
},
|
|
},
|
|
] as TSurvey["triggers"];
|
|
const currentTriggers: TSurvey["triggers"] = [];
|
|
|
|
const result = handleTriggerUpdates(updatedTriggers, currentTriggers, mockActionClasses);
|
|
|
|
expect(result).toHaveProperty("create");
|
|
expect(result.create).toEqual([{ actionClassId: mockActionClassId1 }]);
|
|
});
|
|
|
|
test("removes deleted triggers correctly", () => {
|
|
const updatedTriggers: TSurvey["triggers"] = [];
|
|
const currentTriggers = [
|
|
{
|
|
actionClass: {
|
|
id: mockActionClassId1,
|
|
name: "Test Action 1",
|
|
environmentId: mockEnvironmentId,
|
|
type: "code",
|
|
key: "test-action-1",
|
|
},
|
|
},
|
|
] as TSurvey["triggers"];
|
|
|
|
const result = handleTriggerUpdates(updatedTriggers, currentTriggers, mockActionClasses);
|
|
|
|
expect(result).toHaveProperty("deleteMany");
|
|
expect(result.deleteMany).toEqual({ actionClassId: { in: [mockActionClassId1] } });
|
|
});
|
|
|
|
test("handles both adding and removing triggers", () => {
|
|
const updatedTriggers = [
|
|
{
|
|
actionClass: {
|
|
id: mockActionClassId2,
|
|
name: "Test Action 2",
|
|
environmentId: mockEnvironmentId,
|
|
type: "code",
|
|
key: "test-action-2",
|
|
},
|
|
},
|
|
] as TSurvey["triggers"];
|
|
|
|
const currentTriggers = [
|
|
{
|
|
actionClass: {
|
|
id: mockActionClassId1,
|
|
name: "Test Action 1",
|
|
environmentId: mockEnvironmentId,
|
|
type: "code",
|
|
key: "test-action-1",
|
|
},
|
|
},
|
|
] as TSurvey["triggers"];
|
|
|
|
const result = handleTriggerUpdates(updatedTriggers, currentTriggers, mockActionClasses);
|
|
|
|
expect(result).toHaveProperty("create");
|
|
expect(result).toHaveProperty("deleteMany");
|
|
expect(result.create).toEqual([{ actionClassId: mockActionClassId2 }]);
|
|
expect(result.deleteMany).toEqual({ actionClassId: { in: [mockActionClassId1] } });
|
|
});
|
|
|
|
test("returns empty object when no triggers provided", () => {
|
|
// @ts-expect-error -- This is a test case to check the empty input
|
|
const result = handleTriggerUpdates(undefined, [], mockActionClasses);
|
|
expect(result).toEqual({});
|
|
});
|
|
|
|
test("throws InvalidInputError for invalid trigger IDs", () => {
|
|
const updatedTriggers = [
|
|
{
|
|
actionClass: {
|
|
id: "invalid-action-id",
|
|
name: "Invalid Action",
|
|
environmentId: mockEnvironmentId,
|
|
type: "code",
|
|
key: "invalid-action",
|
|
},
|
|
},
|
|
] as TSurvey["triggers"];
|
|
|
|
const currentTriggers: TSurvey["triggers"] = [];
|
|
|
|
expect(() => handleTriggerUpdates(updatedTriggers, currentTriggers, mockActionClasses)).toThrow(
|
|
InvalidInputError
|
|
);
|
|
});
|
|
|
|
test("throws InvalidInputError for duplicate trigger IDs", () => {
|
|
const updatedTriggers = [
|
|
{
|
|
actionClass: {
|
|
id: mockActionClassId1,
|
|
name: "Test Action 1",
|
|
environmentId: mockEnvironmentId,
|
|
type: "code",
|
|
key: "test-action-1",
|
|
},
|
|
},
|
|
{
|
|
actionClass: {
|
|
id: mockActionClassId1, // Duplicated ID
|
|
name: "Test Action 1",
|
|
environmentId: mockEnvironmentId,
|
|
type: "code",
|
|
key: "test-action-1",
|
|
},
|
|
},
|
|
] as TSurvey["triggers"];
|
|
const currentTriggers: TSurvey["triggers"] = [];
|
|
|
|
expect(() => handleTriggerUpdates(updatedTriggers, currentTriggers, mockActionClasses)).toThrow(
|
|
InvalidInputError
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Tests for createSurvey", () => {
|
|
const mockEnvironmentId = "env123";
|
|
const mockUserId = "user123";
|
|
|
|
const mockCreateSurveyInput = {
|
|
name: "Test Survey",
|
|
type: "app" as const,
|
|
createdBy: mockUserId,
|
|
status: "inProgress" as const,
|
|
welcomeCard: {
|
|
enabled: true,
|
|
headline: { default: "Welcome" },
|
|
html: { default: "<p>Welcome to our survey</p>" },
|
|
},
|
|
questions: [
|
|
{
|
|
id: "q1",
|
|
type: TSurveyQuestionTypeEnum.OpenText,
|
|
inputType: "text",
|
|
headline: { default: "What is your favorite color?" },
|
|
required: true,
|
|
charLimit: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
{
|
|
id: "q2",
|
|
type: TSurveyQuestionTypeEnum.OpenText,
|
|
inputType: "text",
|
|
headline: { default: "What is your favorite food?" },
|
|
required: true,
|
|
charLimit: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
{
|
|
id: "q3",
|
|
type: TSurveyQuestionTypeEnum.OpenText,
|
|
inputType: "text",
|
|
headline: { default: "What is your favorite movie?" },
|
|
required: true,
|
|
charLimit: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
{
|
|
id: "q4",
|
|
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
|
headline: { default: "Select a number:" },
|
|
choices: [
|
|
{ id: "mvedaklp0gxxycprpyhhwen7", label: { default: "lol" } },
|
|
{ id: "i7ws8uqyj66q5x086vbqtm8n", label: { default: "lmao" } },
|
|
{ id: "cy8hbbr9e2q6ywbfjbzwdsqn", label: { default: "XD" } },
|
|
{ id: "sojc5wwxc5gxrnuib30w7t6s", label: { default: "hehe" } },
|
|
],
|
|
required: true,
|
|
},
|
|
{
|
|
id: "q5",
|
|
type: TSurveyQuestionTypeEnum.OpenText,
|
|
inputType: "number",
|
|
headline: { default: "Select your age group:" },
|
|
required: true,
|
|
charLimit: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
{
|
|
id: "q6",
|
|
type: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
|
|
headline: { default: "Select your age group:" },
|
|
required: true,
|
|
choices: [
|
|
{ id: "mvedaklp0gxxycprpyhhwen7", label: { default: "lol" } },
|
|
{ id: "i7ws8uqyj66q5x086vbqtm8n", label: { default: "lmao" } },
|
|
{ id: "cy8hbbr9e2q6ywbfjbzwdsqn", label: { default: "XD" } },
|
|
{ id: "sojc5wwxc5gxrnuib30w7t6s", label: { default: "hehe" } },
|
|
],
|
|
},
|
|
],
|
|
variables: [],
|
|
hiddenFields: { enabled: false, fieldIds: [] },
|
|
endings: [],
|
|
displayOption: "respondMultiple" as const,
|
|
languages: [],
|
|
} as TSurveyCreateInput;
|
|
|
|
const mockActionClasses = [
|
|
{
|
|
id: "action-123",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
environmentId: mockEnvironmentId,
|
|
name: "Test Action",
|
|
description: "Test action description",
|
|
type: "code",
|
|
key: "test-action",
|
|
noCodeConfig: null,
|
|
},
|
|
];
|
|
|
|
beforeEach(() => {
|
|
vi.mocked(getActionClasses).mockResolvedValue(mockActionClasses as TActionClass[]);
|
|
});
|
|
|
|
describe("Happy Path", () => {
|
|
test("creates a survey successfully", async () => {
|
|
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce(mockOrganizationOutput);
|
|
prisma.survey.create.mockResolvedValueOnce({
|
|
...mockSurveyOutput,
|
|
});
|
|
|
|
const result = await createSurvey(mockEnvironmentId, mockCreateSurveyInput);
|
|
|
|
expect(prisma.survey.create).toHaveBeenCalled();
|
|
expect(result.name).toEqual(mockSurveyOutput.name);
|
|
expect(subscribeOrganizationMembersToSurveyResponses).toHaveBeenCalled();
|
|
});
|
|
|
|
test("creates a private segment for app surveys", async () => {
|
|
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce(mockOrganizationOutput);
|
|
prisma.survey.create.mockResolvedValueOnce({
|
|
...mockSurveyOutput,
|
|
type: "app",
|
|
});
|
|
|
|
prisma.segment.create.mockResolvedValueOnce({
|
|
id: "segment-123",
|
|
environmentId: mockEnvironmentId,
|
|
title: mockSurveyOutput.id,
|
|
isPrivate: true,
|
|
filters: [],
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
} as unknown as TSegment);
|
|
|
|
await createSurvey(mockEnvironmentId, {
|
|
...mockCreateSurveyInput,
|
|
type: "app",
|
|
});
|
|
|
|
expect(prisma.segment.create).toHaveBeenCalled();
|
|
expect(prisma.survey.update).toHaveBeenCalled();
|
|
});
|
|
|
|
test("creates survey with follow-ups", async () => {
|
|
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce(mockOrganizationOutput);
|
|
const followUp = {
|
|
id: "followup1",
|
|
name: "Follow up 1",
|
|
trigger: { type: "response", properties: null },
|
|
action: {
|
|
type: "send-email",
|
|
properties: {
|
|
to: "abc@example.com",
|
|
attachResponseData: true,
|
|
body: "Hello",
|
|
from: "hello@exmaple.com",
|
|
replyTo: ["hello@example.com"],
|
|
subject: "Follow up",
|
|
},
|
|
},
|
|
surveyId: mockSurveyOutput.id,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
} as TSurveyFollowUp;
|
|
|
|
const surveyWithFollowUps = {
|
|
...mockCreateSurveyInput,
|
|
followUps: [followUp],
|
|
};
|
|
|
|
prisma.survey.create.mockResolvedValueOnce({
|
|
...mockSurveyOutput,
|
|
});
|
|
|
|
await createSurvey(mockEnvironmentId, surveyWithFollowUps);
|
|
|
|
expect(prisma.survey.create).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
data: expect.objectContaining({
|
|
followUps: {
|
|
create: [
|
|
expect.objectContaining({
|
|
name: "Follow up 1",
|
|
}),
|
|
],
|
|
},
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Sad Path", () => {
|
|
testInputValidation(createSurvey, "123#", mockCreateSurveyInput);
|
|
|
|
test("throws ResourceNotFoundError if organization not found", async () => {
|
|
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce(null);
|
|
await expect(createSurvey(mockEnvironmentId, mockCreateSurveyInput)).rejects.toThrow(
|
|
ResourceNotFoundError
|
|
);
|
|
});
|
|
|
|
test("throws DatabaseError if there is a Prisma error", async () => {
|
|
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce(mockOrganizationOutput);
|
|
const mockError = new Prisma.PrismaClientKnownRequestError("Database error", {
|
|
code: PrismaErrorType.UniqueConstraintViolation,
|
|
clientVersion: "1.0.0",
|
|
});
|
|
prisma.survey.create.mockRejectedValueOnce(mockError);
|
|
|
|
await expect(createSurvey(mockEnvironmentId, mockCreateSurveyInput)).rejects.toThrow(DatabaseError);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Tests for loadNewSegmentInSurvey", () => {
|
|
const mockSurveyId = mockId;
|
|
const mockNewSegmentId = "segment456";
|
|
const mockCurrentSegmentId = "segment-123";
|
|
const mockEnvironmentId = "env-123";
|
|
|
|
describe("Happy Path", () => {
|
|
test("loads new segment successfully", async () => {
|
|
// Set up mocks for existing survey
|
|
prisma.survey.findUnique.mockResolvedValueOnce({
|
|
...mockSurveyOutput,
|
|
});
|
|
// Mock segment exists
|
|
prisma.segment.findUnique.mockResolvedValueOnce({
|
|
id: mockNewSegmentId,
|
|
environmentId: mockEnvironmentId,
|
|
filters: [],
|
|
title: "Test Segment",
|
|
isPrivate: false,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
description: "Test Segment Description",
|
|
});
|
|
// Mock survey update
|
|
prisma.survey.update.mockResolvedValueOnce({
|
|
...mockSurveyOutput,
|
|
segmentId: mockNewSegmentId,
|
|
});
|
|
const result = await loadNewSegmentInSurvey(mockSurveyId, mockNewSegmentId);
|
|
expect(prisma.survey.update).toHaveBeenCalledWith({
|
|
where: { id: mockSurveyId },
|
|
data: {
|
|
segment: {
|
|
connect: {
|
|
id: mockNewSegmentId,
|
|
},
|
|
},
|
|
},
|
|
select: expect.anything(),
|
|
});
|
|
expect(result).toEqual(
|
|
expect.objectContaining({
|
|
segmentId: mockNewSegmentId,
|
|
})
|
|
);
|
|
});
|
|
|
|
test("deletes private segment when changing to a new segment", async () => {
|
|
const mockSegment = {
|
|
id: mockCurrentSegmentId,
|
|
environmentId: mockEnvironmentId,
|
|
title: mockId, // Private segments have title = surveyId
|
|
isPrivate: true,
|
|
filters: [],
|
|
surveys: [mockSurveyId],
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
description: "Test Segment Description",
|
|
};
|
|
|
|
// Set up mocks for existing survey with private segment
|
|
prisma.survey.findUnique.mockResolvedValueOnce({
|
|
...mockSurveyOutput,
|
|
segment: mockSegment,
|
|
} as Survey);
|
|
|
|
// Mock segment exists
|
|
prisma.segment.findUnique.mockResolvedValueOnce({
|
|
...mockSegment,
|
|
id: mockNewSegmentId,
|
|
environmentId: mockEnvironmentId,
|
|
});
|
|
|
|
// Mock survey update
|
|
prisma.survey.update.mockResolvedValueOnce({
|
|
...mockSurveyOutput,
|
|
segment: {
|
|
id: mockNewSegmentId,
|
|
environmentId: mockEnvironmentId,
|
|
title: "Test Segment",
|
|
isPrivate: false,
|
|
filters: [],
|
|
surveys: [{ id: mockSurveyId }],
|
|
},
|
|
} as Survey);
|
|
|
|
// Mock segment delete
|
|
prisma.segment.delete.mockResolvedValueOnce({
|
|
id: mockCurrentSegmentId,
|
|
environmentId: mockEnvironmentId,
|
|
surveys: [{ id: mockSurveyId }],
|
|
} as unknown as TSegment);
|
|
|
|
await loadNewSegmentInSurvey(mockSurveyId, mockNewSegmentId);
|
|
|
|
// Verify the private segment was deleted
|
|
expect(prisma.segment.delete).toHaveBeenCalledWith({
|
|
where: { id: mockCurrentSegmentId },
|
|
select: expect.anything(),
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Sad Path", () => {
|
|
testInputValidation(loadNewSegmentInSurvey, "123#", "123#");
|
|
|
|
test("throws ResourceNotFoundError when survey not found", async () => {
|
|
prisma.survey.findUnique.mockResolvedValueOnce(null);
|
|
|
|
await expect(loadNewSegmentInSurvey(mockSurveyId, mockNewSegmentId)).rejects.toThrow(
|
|
ResourceNotFoundError
|
|
);
|
|
});
|
|
|
|
test("throws ResourceNotFoundError when segment not found", async () => {
|
|
// Set up mock for existing survey
|
|
prisma.survey.findUnique.mockResolvedValueOnce({
|
|
...mockSurveyOutput,
|
|
});
|
|
|
|
// Segment not found
|
|
prisma.segment.findUnique.mockResolvedValueOnce(null);
|
|
|
|
await expect(loadNewSegmentInSurvey(mockSurveyId, mockNewSegmentId)).rejects.toThrow(
|
|
ResourceNotFoundError
|
|
);
|
|
});
|
|
|
|
test("throws DatabaseError on Prisma error", async () => {
|
|
// Set up mock for existing survey
|
|
prisma.survey.findUnique.mockResolvedValueOnce({
|
|
...mockSurveyOutput,
|
|
});
|
|
|
|
// // Mock segment exists
|
|
prisma.segment.findUnique.mockResolvedValueOnce({
|
|
id: mockNewSegmentId,
|
|
environmentId: mockEnvironmentId,
|
|
filters: [],
|
|
title: "Test Segment",
|
|
isPrivate: false,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
description: "Test Segment Description",
|
|
});
|
|
|
|
// Mock Prisma error on update
|
|
const mockError = new Prisma.PrismaClientKnownRequestError("Database error", {
|
|
code: PrismaErrorType.UniqueConstraintViolation,
|
|
clientVersion: "1.0.0",
|
|
});
|
|
|
|
prisma.survey.update.mockRejectedValueOnce(mockError);
|
|
|
|
await expect(loadNewSegmentInSurvey(mockSurveyId, mockNewSegmentId)).rejects.toThrow(DatabaseError);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Tests for getSurveysBySegmentId", () => {
|
|
const mockSegmentId = "segment-123";
|
|
|
|
describe("Happy Path", () => {
|
|
test("returns surveys associated with a segment", async () => {
|
|
prisma.survey.findMany.mockResolvedValueOnce([mockSurveyOutput]);
|
|
|
|
const result = await getSurveysBySegmentId(mockSegmentId);
|
|
|
|
expect(prisma.survey.findMany).toHaveBeenCalledWith({
|
|
where: { segmentId: mockSegmentId },
|
|
select: expect.anything(),
|
|
});
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toEqual(
|
|
expect.objectContaining({
|
|
id: mockSurveyOutput.id,
|
|
})
|
|
);
|
|
});
|
|
|
|
test("returns empty array when no surveys found", async () => {
|
|
prisma.survey.findMany.mockResolvedValueOnce([]);
|
|
|
|
const result = await getSurveysBySegmentId(mockSegmentId);
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("Sad Path", () => {
|
|
test("throws DatabaseError on Prisma error", async () => {
|
|
const mockError = new Prisma.PrismaClientKnownRequestError("Database error", {
|
|
code: PrismaErrorType.UniqueConstraintViolation,
|
|
clientVersion: "1.0.0",
|
|
});
|
|
prisma.survey.findMany.mockRejectedValueOnce(mockError);
|
|
|
|
await expect(getSurveysBySegmentId(mockSegmentId)).rejects.toThrow(DatabaseError);
|
|
});
|
|
|
|
test("throws error on unexpected error", async () => {
|
|
prisma.survey.findMany.mockRejectedValueOnce(new Error("Unexpected error"));
|
|
|
|
await expect(getSurveysBySegmentId(mockSegmentId)).rejects.toThrow(Error);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("updateSurveyDraftAction", () => {
|
|
beforeEach(() => {
|
|
vi.mocked(getActionClasses).mockResolvedValue([mockActionClass] as TActionClass[]);
|
|
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganizationOutput);
|
|
});
|
|
|
|
describe("Happy Path", () => {
|
|
test("should save draft with missing translations", async () => {
|
|
prisma.survey.findUnique.mockResolvedValue(mockSurveyOutput);
|
|
prisma.survey.update.mockResolvedValue(mockSurveyOutput);
|
|
|
|
// Create a survey with incomplete i18n/fields
|
|
const incompleteSurvey = {
|
|
...updateSurveyInput,
|
|
questions: [
|
|
{
|
|
id: "q1",
|
|
type: TSurveyQuestionTypeEnum.OpenText,
|
|
// Missing headline or other required fields
|
|
},
|
|
],
|
|
} as unknown as TSurvey;
|
|
|
|
// Expect success (skipValidation = true)
|
|
const result = await updateSurveyInternal(incompleteSurvey, true);
|
|
expect(result).toBeDefined();
|
|
expect(prisma.survey.update).toHaveBeenCalled();
|
|
});
|
|
|
|
test("should allow draft with invalid images if gating is applied", async () => {
|
|
prisma.survey.findUnique.mockResolvedValue(mockSurveyOutput);
|
|
prisma.survey.update.mockResolvedValue(mockSurveyOutput);
|
|
|
|
const surveyWithInvalidImage = {
|
|
...updateSurveyInput,
|
|
questions: [
|
|
{
|
|
id: "q1",
|
|
type: TSurveyQuestionTypeEnum.OpenText,
|
|
headline: { default: "Question" },
|
|
imageUrl: "http://invalid-image-url.com/image.txt", // Invalid image extension
|
|
},
|
|
],
|
|
} as unknown as TSurvey;
|
|
|
|
// Expect success (skipValidation = true)
|
|
await updateSurveyInternal(surveyWithInvalidImage, true);
|
|
expect(prisma.survey.update).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("Sad Path", () => {
|
|
test("should reject publishing survey with incomplete translations", async () => {
|
|
// Create a draft with missing translations
|
|
const incompleteSurvey = {
|
|
...updateSurveyInput,
|
|
questions: [
|
|
{
|
|
id: "q1",
|
|
type: TSurveyQuestionTypeEnum.OpenText,
|
|
// Missing headline
|
|
},
|
|
],
|
|
} as unknown as TSurvey;
|
|
|
|
// Expect validation error (skipValidation = false)
|
|
await expect(updateSurveyInternal(incompleteSurvey, false)).rejects.toThrow();
|
|
});
|
|
});
|
|
});
|