chore: modules/survey/lib and modules/ee/contacts/api/v1 (#5711)

This commit is contained in:
Anshuman Pandey
2025-05-08 16:22:55 +05:30
committed by GitHub
parent 67d7fe016d
commit ce8b019e93
30 changed files with 3155 additions and 42 deletions

View File

@@ -0,0 +1,169 @@
import { describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { getContactByUserIdWithAttributes } from "./contact";
vi.mock("@formbricks/database", () => ({
prisma: {
contact: {
findFirst: vi.fn(),
},
},
}));
const mockEnvironmentId = "testEnvironmentId";
const mockUserId = "testUserId";
const mockContactId = "testContactId";
describe("getContactByUserIdWithAttributes", () => {
test("should return contact with filtered attributes when found", async () => {
const mockUpdatedAttributes = { email: "new@example.com", plan: "premium" };
const mockDbContact = {
id: mockContactId,
attributes: [
{ attributeKey: { key: "email" }, value: "new@example.com" },
{ attributeKey: { key: "plan" }, value: "premium" },
],
};
vi.mocked(prisma.contact.findFirst).mockResolvedValue(mockDbContact as any);
const result = await getContactByUserIdWithAttributes(
mockEnvironmentId,
mockUserId,
mockUpdatedAttributes
);
expect(prisma.contact.findFirst).toHaveBeenCalledWith({
where: {
environmentId: mockEnvironmentId,
attributes: {
some: { attributeKey: { key: "userId", environmentId: mockEnvironmentId }, value: mockUserId },
},
},
select: {
id: true,
attributes: {
where: {
attributeKey: {
key: {
in: Object.keys(mockUpdatedAttributes),
},
},
},
select: { attributeKey: { select: { key: true } }, value: true },
},
},
});
expect(result).toEqual(mockDbContact);
});
test("should return null if contact not found", async () => {
const mockUpdatedAttributes = { email: "new@example.com" };
vi.mocked(prisma.contact.findFirst).mockResolvedValue(null);
const result = await getContactByUserIdWithAttributes(
mockEnvironmentId,
mockUserId,
mockUpdatedAttributes
);
expect(prisma.contact.findFirst).toHaveBeenCalledWith({
where: {
environmentId: mockEnvironmentId,
attributes: {
some: { attributeKey: { key: "userId", environmentId: mockEnvironmentId }, value: mockUserId },
},
},
select: {
id: true,
attributes: {
where: {
attributeKey: {
key: {
in: Object.keys(mockUpdatedAttributes),
},
},
},
select: { attributeKey: { select: { key: true } }, value: true },
},
},
});
expect(result).toBeNull();
});
test("should handle empty updatedAttributes", async () => {
const mockUpdatedAttributes = {};
const mockDbContact = {
id: mockContactId,
attributes: [], // No attributes should be fetched if updatedAttributes is empty
};
vi.mocked(prisma.contact.findFirst).mockResolvedValue(mockDbContact as any);
const result = await getContactByUserIdWithAttributes(
mockEnvironmentId,
mockUserId,
mockUpdatedAttributes
);
expect(prisma.contact.findFirst).toHaveBeenCalledWith({
where: {
environmentId: mockEnvironmentId,
attributes: {
some: { attributeKey: { key: "userId", environmentId: mockEnvironmentId }, value: mockUserId },
},
},
select: {
id: true,
attributes: {
where: {
attributeKey: {
key: {
in: [], // Object.keys({}) results in an empty array
},
},
},
select: { attributeKey: { select: { key: true } }, value: true },
},
},
});
expect(result).toEqual(mockDbContact);
});
test("should return contact with only requested attributes even if DB stores more", async () => {
const mockUpdatedAttributes = { email: "new@example.com" }; // only request email
// The prisma call will filter attributes based on `Object.keys(mockUpdatedAttributes)`
const mockPrismaResponse = {
id: mockContactId,
attributes: [{ attributeKey: { key: "email" }, value: "new@example.com" }],
};
vi.mocked(prisma.contact.findFirst).mockResolvedValue(mockPrismaResponse as any);
const result = await getContactByUserIdWithAttributes(
mockEnvironmentId,
mockUserId,
mockUpdatedAttributes
);
expect(prisma.contact.findFirst).toHaveBeenCalledWith({
where: {
environmentId: mockEnvironmentId,
attributes: {
some: { attributeKey: { key: "userId", environmentId: mockEnvironmentId }, value: mockUserId },
},
},
select: {
id: true,
attributes: {
where: {
attributeKey: {
key: {
in: ["email"],
},
},
},
select: { attributeKey: { select: { key: true } }, value: true },
},
},
});
expect(result).toEqual(mockPrismaResponse);
});
});

View File

@@ -0,0 +1,67 @@
import { describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { ValidationError } from "@formbricks/types/errors";
import { getContactAttributes } from "./attributes";
vi.mock("@formbricks/database", () => ({
prisma: {
contactAttribute: {
findMany: vi.fn(),
},
},
}));
vi.mock("@/lib/cache/contact-attribute", () => ({
contactAttributeCache: {
tag: {
byContactId: vi.fn((contactId) => `contact-${contactId}-contactAttributes`),
},
},
}));
const mockContactId = "xn8b8ol97q2pcp8dnlpsfs1m";
describe("getContactAttributes", () => {
test("should return transformed attributes when found", async () => {
const mockContactAttributes = [
{ attributeKey: { key: "email" }, value: "test@example.com" },
{ attributeKey: { key: "name" }, value: "Test User" },
];
const expectedTransformedAttributes = {
email: "test@example.com",
name: "Test User",
};
vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue(mockContactAttributes);
const result = await getContactAttributes(mockContactId);
expect(result).toEqual(expectedTransformedAttributes);
expect(prisma.contactAttribute.findMany).toHaveBeenCalledWith({
where: {
contactId: mockContactId,
},
select: { attributeKey: { select: { key: true } }, value: true },
});
});
test("should return an empty object when no attributes are found", async () => {
vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue([]);
const result = await getContactAttributes(mockContactId);
expect(result).toEqual({});
expect(prisma.contactAttribute.findMany).toHaveBeenCalledWith({
where: {
contactId: mockContactId,
},
select: { attributeKey: { select: { key: true } }, value: true },
});
});
test("should throw a ValidationError when contactId is invalid", async () => {
const invalidContactId = "hello-world";
await expect(getContactAttributes(invalidContactId)).rejects.toThrowError(ValidationError);
});
});

View File

@@ -0,0 +1,91 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { getContactByUserId } from "./contact";
// Mock prisma
vi.mock("@formbricks/database", () => ({
prisma: {
contact: {
findFirst: vi.fn(),
},
},
}));
const mockEnvironmentId = "clxmg5n79000008l9df7b8nh8";
const mockUserId = "dpqs2axc6v3b5cjcgtnqhwov";
const mockContactId = "clxmg5n79000108l9df7b8xyz";
const mockReturnedContact = {
id: mockContactId,
environmentId: mockEnvironmentId,
createdAt: new Date("2024-01-01T10:00:00.000Z"),
updatedAt: new Date("2024-01-01T11:00:00.000Z"),
};
describe("getContactByUserId", () => {
beforeEach(() => {
vi.resetAllMocks();
});
test("should return contact if found", async () => {
vi.mocked(prisma.contact.findFirst).mockResolvedValue(mockReturnedContact as any);
const result = await getContactByUserId(mockEnvironmentId, mockUserId);
expect(result).toEqual(mockReturnedContact);
expect(prisma.contact.findFirst).toHaveBeenCalledWith({
where: {
attributes: {
some: {
attributeKey: {
key: "userId",
environmentId: mockEnvironmentId,
},
value: mockUserId,
},
},
},
});
});
test("should return null if contact not found", async () => {
vi.mocked(prisma.contact.findFirst).mockResolvedValue(null);
const result = await getContactByUserId(mockEnvironmentId, mockUserId);
expect(result).toBeNull();
expect(prisma.contact.findFirst).toHaveBeenCalledWith({
where: {
attributes: {
some: {
attributeKey: {
key: "userId",
environmentId: mockEnvironmentId,
},
value: mockUserId,
},
},
},
});
});
test("should call prisma.contact.findFirst with correct parameters", async () => {
vi.mocked(prisma.contact.findFirst).mockResolvedValue(mockReturnedContact as any);
await getContactByUserId(mockEnvironmentId, mockUserId);
expect(prisma.contact.findFirst).toHaveBeenCalledTimes(1);
expect(prisma.contact.findFirst).toHaveBeenCalledWith({
where: {
attributes: {
some: {
attributeKey: {
key: "userId",
environmentId: mockEnvironmentId,
},
value: mockUserId,
},
},
},
});
});
});

View File

@@ -0,0 +1,200 @@
import { getEnvironment } from "@/lib/environment/service";
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { TEnvironment } from "@formbricks/types/environment";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { TOrganization } from "@formbricks/types/organizations";
import { getContactByUserId } from "./contact";
import { getPersonState } from "./person-state";
import { getPersonSegmentIds } from "./segments";
vi.mock("@/lib/environment/service", () => ({
getEnvironment: vi.fn(),
}));
vi.mock("@/lib/organization/service", () => ({
getOrganizationByEnvironmentId: vi.fn(),
}));
vi.mock("@/modules/ee/contacts/api/v1/client/[environmentId]/identify/contacts/[userId]/lib/contact", () => ({
getContactByUserId: vi.fn(),
}));
vi.mock("@formbricks/database", () => ({
prisma: {
contact: {
create: vi.fn(),
},
response: {
findMany: vi.fn(),
},
display: {
findMany: vi.fn(),
},
},
}));
vi.mock("./segments", () => ({
getPersonSegmentIds: vi.fn(),
}));
const mockEnvironmentId = "jubz514cwdmjvnbadsfd7ez3";
const mockUserId = "huli1kfpw1r6vn00vjxetdob";
const mockContactId = "e71zwzi6zgrdzutbb0q8spui";
const mockProjectId = "d6o07l7ieizdioafgelrioao";
const mockOrganizationId = "xa4oltlfkmqq3r4e3m3ocss1";
const mockDevice = "desktop";
const mockEnvironment: TEnvironment = {
id: mockEnvironmentId,
createdAt: new Date(),
updatedAt: new Date(),
type: "development",
projectId: mockProjectId,
appSetupCompleted: false,
};
const mockOrganization: TOrganization = {
id: mockOrganizationId,
createdAt: new Date(),
updatedAt: new Date(),
name: "Test Organization",
billing: {
stripeCustomerId: null,
plan: "free",
period: "monthly",
limits: { projects: 1, monthly: { responses: 100, miu: 100 } },
periodStart: new Date(),
},
isAIEnabled: false,
};
const mockResolvedContactFromGetContactByUserId = {
id: mockContactId,
createdAt: new Date(),
updatedAt: new Date(),
environmentId: mockEnvironmentId,
userId: mockUserId,
};
const mockResolvedContactFromPrismaCreate = {
id: mockContactId,
createdAt: new Date(),
updatedAt: new Date(),
environmentId: mockEnvironmentId,
userId: mockUserId,
};
describe("getPersonState", () => {
beforeEach(() => {
vi.resetAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
test("should throw ResourceNotFoundError if environment is not found", async () => {
vi.mocked(getEnvironment).mockResolvedValue(null);
await expect(
getPersonState({ environmentId: mockEnvironmentId, userId: mockUserId, device: mockDevice })
).rejects.toThrow(new ResourceNotFoundError("environment", mockEnvironmentId));
});
test("should throw ResourceNotFoundError if organization is not found", async () => {
vi.mocked(getEnvironment).mockResolvedValue(mockEnvironment as TEnvironment);
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(null);
await expect(
getPersonState({ environmentId: mockEnvironmentId, userId: mockUserId, device: mockDevice })
).rejects.toThrow(new ResourceNotFoundError("organization", mockEnvironmentId));
});
test("should return person state if contact exists", async () => {
vi.mocked(getEnvironment).mockResolvedValue(mockEnvironment as TEnvironment);
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization as TOrganization);
vi.mocked(getContactByUserId).mockResolvedValue(mockResolvedContactFromGetContactByUserId);
vi.mocked(prisma.response.findMany).mockResolvedValue([]);
vi.mocked(prisma.display.findMany).mockResolvedValue([]);
vi.mocked(getPersonSegmentIds).mockResolvedValue([]);
const result = await getPersonState({
environmentId: mockEnvironmentId,
userId: mockUserId,
device: mockDevice,
});
expect(result.state.contactId).toBe(mockContactId);
expect(result.state.userId).toBe(mockUserId);
expect(result.state.segments).toEqual([]);
expect(result.state.displays).toEqual([]);
expect(result.state.responses).toEqual([]);
expect(result.state.lastDisplayAt).toBeNull();
expect(result.revalidateProps).toBeUndefined();
expect(prisma.contact.create).not.toHaveBeenCalled();
});
test("should create contact and return person state if contact does not exist", async () => {
vi.mocked(getEnvironment).mockResolvedValue(mockEnvironment as TEnvironment);
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization as TOrganization);
vi.mocked(getContactByUserId).mockResolvedValue(null);
vi.mocked(prisma.contact.create).mockResolvedValue(mockResolvedContactFromPrismaCreate as any);
vi.mocked(prisma.response.findMany).mockResolvedValue([]);
vi.mocked(prisma.display.findMany).mockResolvedValue([]);
vi.mocked(getPersonSegmentIds).mockResolvedValue(["segment1"]);
const result = await getPersonState({
environmentId: mockEnvironmentId,
userId: mockUserId,
device: mockDevice,
});
expect(prisma.contact.create).toHaveBeenCalledWith({
data: {
environment: { connect: { id: mockEnvironmentId } },
attributes: {
create: [
{
attributeKey: {
connect: { key_environmentId: { key: "userId", environmentId: mockEnvironmentId } },
},
value: mockUserId,
},
],
},
},
});
expect(result.state.contactId).toBe(mockContactId);
expect(result.state.userId).toBe(mockUserId);
expect(result.state.segments).toEqual(["segment1"]);
expect(result.revalidateProps).toEqual({ contactId: mockContactId, revalidate: true });
});
test("should correctly map displays and responses", async () => {
const displayDate = new Date();
const mockDisplays = [
{ surveyId: "survey1", createdAt: displayDate },
{ surveyId: "survey2", createdAt: new Date(displayDate.getTime() - 1000) },
];
const mockResponses = [{ surveyId: "survey1" }, { surveyId: "survey3" }];
vi.mocked(getEnvironment).mockResolvedValue(mockEnvironment as TEnvironment);
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization as TOrganization);
vi.mocked(getContactByUserId).mockResolvedValue(mockResolvedContactFromGetContactByUserId);
vi.mocked(prisma.response.findMany).mockResolvedValue(mockResponses as any);
vi.mocked(prisma.display.findMany).mockResolvedValue(mockDisplays as any);
vi.mocked(getPersonSegmentIds).mockResolvedValue([]);
const result = await getPersonState({
environmentId: mockEnvironmentId,
userId: mockUserId,
device: mockDevice,
});
expect(result.state.displays).toEqual(
mockDisplays.map((d) => ({ surveyId: d.surveyId, createdAt: d.createdAt }))
);
expect(result.state.responses).toEqual(mockResponses.map((r) => r.surveyId));
expect(result.state.lastDisplayAt).toEqual(displayDate);
});
});

View File

@@ -86,7 +86,7 @@ export const getPersonState = async ({
},
});
const contactDisplayes = await prisma.display.findMany({
const contactDisplays = await prisma.display.findMany({
where: {
contactId: contact.id,
},
@@ -98,21 +98,22 @@ export const getPersonState = async ({
const segments = await getPersonSegmentIds(environmentId, contact.id, userId, device);
const sortedContactDisplaysDate = contactDisplays?.toSorted(
(a, b) => b.createdAt.getTime() - a.createdAt.getTime()
)[0]?.createdAt;
// If the person exists, return the persons's state
const userState: TJsPersonState["data"] = {
contactId: contact.id,
userId,
segments,
displays:
contactDisplayes?.map((display) => ({
contactDisplays?.map((display) => ({
surveyId: display.surveyId,
createdAt: display.createdAt,
})) ?? [],
responses: contactResponses?.map((response) => response.surveyId) ?? [],
lastDisplayAt:
contactDisplayes.length > 0
? contactDisplayes.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0].createdAt
: null,
lastDisplayAt: contactDisplays?.length > 0 ? sortedContactDisplaysDate : null,
};
return {

View File

@@ -0,0 +1,190 @@
import { contactAttributeCache } from "@/lib/cache/contact-attribute";
import { segmentCache } from "@/lib/cache/segment";
import { getContactAttributes } from "@/modules/ee/contacts/api/v1/client/[environmentId]/identify/contacts/[userId]/lib/attributes";
import { evaluateSegment } from "@/modules/ee/contacts/segments/lib/segments";
import { Prisma } from "@prisma/client";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { PrismaErrorType } from "@formbricks/database/types/error";
import { DatabaseError } from "@formbricks/types/errors";
import { TBaseFilter } from "@formbricks/types/segment";
import { getPersonSegmentIds, getSegments } from "./segments";
vi.mock("@/lib/cache/contact-attribute", () => ({
contactAttributeCache: {
tag: {
byContactId: vi.fn((contactId) => `contactAttributeCache-contactId-${contactId}`),
},
},
}));
vi.mock("@/lib/cache/segment", () => ({
segmentCache: {
tag: {
byEnvironmentId: vi.fn((environmentId) => `segmentCache-environmentId-${environmentId}`),
},
},
}));
vi.mock(
"@/modules/ee/contacts/api/v1/client/[environmentId]/identify/contacts/[userId]/lib/attributes",
() => ({
getContactAttributes: vi.fn(),
})
);
vi.mock("@/modules/ee/contacts/segments/lib/segments", () => ({
evaluateSegment: vi.fn(),
}));
vi.mock("@formbricks/database", () => ({
prisma: {
segment: {
findMany: vi.fn(),
},
},
}));
const mockEnvironmentId = "bbn7e47f6etoai6usxezxd4a";
const mockContactId = "cworhmq5yqvnb0tsfw9yka4b";
const mockContactUserId = "xrgbcxn5y9so92igacthutfw";
const mockDeviceType = "desktop";
const mockSegmentsData = [
{ id: "segment1", filters: [{}] as TBaseFilter[] },
{ id: "segment2", filters: [{}] as TBaseFilter[] },
];
const mockContactAttributesData = {
attribute1: "value1",
attribute2: "value2",
};
describe("segments lib", () => {
beforeEach(() => {
vi.resetAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
describe("getSegments", () => {
test("should return segments successfully", async () => {
vi.mocked(prisma.segment.findMany).mockResolvedValue(mockSegmentsData);
const result = await getSegments(mockEnvironmentId);
expect(prisma.segment.findMany).toHaveBeenCalledWith({
where: { environmentId: mockEnvironmentId },
select: { id: true, filters: true },
});
expect(result).toEqual(mockSegmentsData);
expect(segmentCache.tag.byEnvironmentId).toHaveBeenCalledWith(mockEnvironmentId);
});
test("should throw DatabaseError on Prisma known request error", async () => {
const mockErrorMessage = "Prisma error";
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
code: PrismaErrorType.UniqueConstraintViolation,
clientVersion: "0.0.1",
});
vi.mocked(prisma.segment.findMany).mockRejectedValueOnce(errToThrow);
await expect(getSegments(mockEnvironmentId)).rejects.toThrow(DatabaseError);
});
test("should throw original error on other errors", async () => {
const genericError = new Error("Test Generic Error");
vi.mocked(prisma.segment.findMany).mockRejectedValueOnce(genericError);
await expect(getSegments(mockEnvironmentId)).rejects.toThrow("Test Generic Error");
});
});
describe("getPersonSegmentIds", () => {
beforeEach(() => {
vi.mocked(getContactAttributes).mockResolvedValue(mockContactAttributesData);
vi.mocked(prisma.segment.findMany).mockResolvedValue(mockSegmentsData); // Mock for getSegments call
});
test("should return person segment IDs successfully", async () => {
vi.mocked(evaluateSegment).mockResolvedValue(true); // All segments evaluate to true
const result = await getPersonSegmentIds(
mockEnvironmentId,
mockContactId,
mockContactUserId,
mockDeviceType
);
expect(getContactAttributes).toHaveBeenCalledWith(mockContactId);
expect(evaluateSegment).toHaveBeenCalledTimes(mockSegmentsData.length);
mockSegmentsData.forEach((segment) => {
expect(evaluateSegment).toHaveBeenCalledWith(
{
attributes: mockContactAttributesData,
deviceType: mockDeviceType,
environmentId: mockEnvironmentId,
contactId: mockContactId,
userId: mockContactUserId,
},
segment.filters
);
});
expect(result).toEqual(mockSegmentsData.map((s) => s.id));
expect(segmentCache.tag.byEnvironmentId).toHaveBeenCalledWith(mockEnvironmentId);
expect(contactAttributeCache.tag.byContactId).toHaveBeenCalledWith(mockContactId);
});
test("should return empty array if no segments exist", async () => {
// @ts-expect-error -- this is a valid test case to check for null
vi.mocked(prisma.segment.findMany).mockResolvedValue(null); // No segments
const result = await getPersonSegmentIds(
mockEnvironmentId,
mockContactId,
mockContactUserId,
mockDeviceType
);
expect(result).toEqual([]);
expect(getContactAttributes).not.toHaveBeenCalled();
expect(evaluateSegment).not.toHaveBeenCalled();
});
test("should return empty array if segments is null", async () => {
vi.mocked(prisma.segment.findMany).mockResolvedValue(null as any); // segments is null
const result = await getPersonSegmentIds(
mockEnvironmentId,
mockContactId,
mockContactUserId,
mockDeviceType
);
expect(result).toEqual([]);
expect(getContactAttributes).not.toHaveBeenCalled();
expect(evaluateSegment).not.toHaveBeenCalled();
});
test("should return only matching segment IDs", async () => {
vi.mocked(evaluateSegment)
.mockResolvedValueOnce(true) // First segment matches
.mockResolvedValueOnce(false); // Second segment does not match
const result = await getPersonSegmentIds(
mockEnvironmentId,
mockContactId,
mockContactUserId,
mockDeviceType
);
expect(result).toEqual([mockSegmentsData[0].id]);
expect(evaluateSegment).toHaveBeenCalledTimes(mockSegmentsData.length);
});
});
});

View File

@@ -11,14 +11,16 @@ import { ZId, ZString } from "@formbricks/types/common";
import { DatabaseError } from "@formbricks/types/errors";
import { TBaseFilter } from "@formbricks/types/segment";
const getSegments = reactCache((environmentId: string) =>
export const getSegments = reactCache((environmentId: string) =>
cache(
async () => {
try {
return prisma.segment.findMany({
const segments = await prisma.segment.findMany({
where: { environmentId },
select: { id: true, filters: true },
});
return segments;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);

View File

@@ -6,7 +6,7 @@ import { NextRequest, userAgent } from "next/server";
import { logger } from "@formbricks/logger";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { ZJsUserIdentifyInput } from "@formbricks/types/js";
import { getPersonState } from "./lib/personState";
import { getPersonState } from "./lib/person-state";
export const OPTIONS = async (): Promise<Response> => {
return responses.successResponse({}, true);

View File

@@ -0,0 +1,78 @@
import { describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { getContactByUserIdWithAttributes } from "./contact";
vi.mock("@formbricks/database", () => ({
prisma: {
contact: {
findFirst: vi.fn(),
},
},
}));
const environmentId = "testEnvironmentId";
const userId = "testUserId";
const mockContactDbData = {
id: "contactId123",
attributes: [
{ attributeKey: { key: "userId" }, value: userId },
{ attributeKey: { key: "email" }, value: "test@example.com" },
],
};
describe("getContactByUserIdWithAttributes", () => {
test("should return contact with attributes when found", async () => {
vi.mocked(prisma.contact.findFirst).mockResolvedValue(mockContactDbData);
const contact = await getContactByUserIdWithAttributes(environmentId, userId);
expect(prisma.contact.findFirst).toHaveBeenCalledWith({
where: {
environmentId,
attributes: { some: { attributeKey: { key: "userId", environmentId }, value: userId } },
},
select: {
id: true,
attributes: {
select: { attributeKey: { select: { key: true } }, value: true },
},
},
});
expect(contact).toEqual({
id: "contactId123",
attributes: [
{
attributeKey: { key: "userId" },
value: userId,
},
{
attributeKey: { key: "email" },
value: "test@example.com",
},
],
});
});
test("should return null when contact not found", async () => {
vi.mocked(prisma.contact.findFirst).mockResolvedValue(null);
const contact = await getContactByUserIdWithAttributes(environmentId, userId);
expect(prisma.contact.findFirst).toHaveBeenCalledWith({
where: {
environmentId,
attributes: { some: { attributeKey: { key: "userId", environmentId }, value: userId } },
},
select: {
id: true,
attributes: {
select: { attributeKey: { select: { key: true } }, value: true },
},
},
});
expect(contact).toBeNull();
});
});

View File

@@ -0,0 +1,199 @@
import { contactAttributeCache } from "@/lib/cache/contact-attribute";
import { segmentCache } from "@/lib/cache/segment";
import { validateInputs } from "@/lib/utils/validate";
import { evaluateSegment } from "@/modules/ee/contacts/segments/lib/segments";
import { Prisma } from "@prisma/client";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { DatabaseError } from "@formbricks/types/errors";
import { TBaseFilter } from "@formbricks/types/segment";
import { getPersonSegmentIds, getSegments } from "./segments";
vi.mock("@/lib/cache/contact-attribute", () => ({
contactAttributeCache: {
tag: {
byContactId: vi.fn((contactId) => `contactAttributeCache-contactId-${contactId}`),
},
},
}));
vi.mock("@/lib/cache/segment", () => ({
segmentCache: {
tag: {
byEnvironmentId: vi.fn((environmentId) => `segmentCache-environmentId-${environmentId}`),
},
},
}));
vi.mock("@/lib/utils/validate", () => ({
validateInputs: vi.fn(),
}));
vi.mock("@/modules/ee/contacts/segments/lib/segments", () => ({
evaluateSegment: vi.fn(),
}));
vi.mock("@formbricks/database", () => ({
prisma: {
segment: {
findMany: vi.fn(),
},
},
}));
const mockEnvironmentId = "test-environment-id";
const mockContactId = "test-contact-id";
const mockContactUserId = "test-contact-user-id";
const mockAttributes = { email: "test@example.com" };
const mockDeviceType = "desktop";
const mockSegmentsData = [
{ id: "segment1", filters: [{}] as TBaseFilter[] },
{ id: "segment2", filters: [{}] as TBaseFilter[] },
];
describe("segments lib", () => {
beforeEach(() => {
vi.resetAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
describe("getSegments", () => {
test("should return segments successfully", async () => {
vi.mocked(prisma.segment.findMany).mockResolvedValue(mockSegmentsData);
const result = await getSegments(mockEnvironmentId);
expect(prisma.segment.findMany).toHaveBeenCalledWith({
where: { environmentId: mockEnvironmentId },
select: { id: true, filters: true },
});
expect(result).toEqual(mockSegmentsData);
expect(segmentCache.tag.byEnvironmentId).toHaveBeenCalledWith(mockEnvironmentId);
});
test("should throw DatabaseError on Prisma known request error", async () => {
const prismaError = new Prisma.PrismaClientKnownRequestError("Test Prisma Error", {
code: "P2001",
clientVersion: "2.0.0",
});
vi.mocked(prisma.segment.findMany).mockRejectedValue(prismaError);
await expect(getSegments(mockEnvironmentId)).rejects.toThrow(DatabaseError);
});
test("should throw generic error if not Prisma error", async () => {
const genericError = new Error("Test Generic Error");
vi.mocked(prisma.segment.findMany).mockRejectedValue(genericError);
await expect(getSegments(mockEnvironmentId)).rejects.toThrow("Test Generic Error");
});
});
describe("getPersonSegmentIds", () => {
beforeEach(() => {
vi.mocked(prisma.segment.findMany).mockResolvedValue(mockSegmentsData); // Mock for getSegments call
});
test("should return person segment IDs successfully", async () => {
vi.mocked(evaluateSegment).mockResolvedValue(true); // All segments evaluate to true
const result = await getPersonSegmentIds(
mockEnvironmentId,
mockContactId,
mockContactUserId,
mockAttributes,
mockDeviceType
);
expect(validateInputs).toHaveBeenCalled();
expect(prisma.segment.findMany).toHaveBeenCalledWith({
where: { environmentId: mockEnvironmentId },
select: { id: true, filters: true },
});
expect(evaluateSegment).toHaveBeenCalledTimes(mockSegmentsData.length);
mockSegmentsData.forEach((segment) => {
expect(evaluateSegment).toHaveBeenCalledWith(
{
attributes: mockAttributes,
deviceType: mockDeviceType,
environmentId: mockEnvironmentId,
contactId: mockContactId,
userId: mockContactUserId,
},
segment.filters
);
});
expect(result).toEqual(mockSegmentsData.map((s) => s.id));
expect(segmentCache.tag.byEnvironmentId).toHaveBeenCalledWith(mockEnvironmentId);
expect(contactAttributeCache.tag.byContactId).toHaveBeenCalledWith(mockContactId);
});
test("should return empty array if no segments exist", async () => {
vi.mocked(prisma.segment.findMany).mockResolvedValue([]); // No segments
const result = await getPersonSegmentIds(
mockEnvironmentId,
mockContactId,
mockContactUserId,
mockAttributes,
mockDeviceType
);
expect(result).toEqual([]);
expect(evaluateSegment).not.toHaveBeenCalled();
});
test("should return empty array if segments exist but none match", async () => {
vi.mocked(evaluateSegment).mockResolvedValue(false); // All segments evaluate to false
const result = await getPersonSegmentIds(
mockEnvironmentId,
mockContactId,
mockContactUserId,
mockAttributes,
mockDeviceType
);
expect(result).toEqual([]);
expect(evaluateSegment).toHaveBeenCalledTimes(mockSegmentsData.length);
});
test("should call validateInputs with correct parameters", async () => {
await getPersonSegmentIds(
mockEnvironmentId,
mockContactId,
mockContactUserId,
mockAttributes,
mockDeviceType
);
expect(validateInputs).toHaveBeenCalledWith(
[mockEnvironmentId, expect.anything()],
[mockContactId, expect.anything()],
[mockContactUserId, expect.anything()]
);
});
test("should return only matching segment IDs", async () => {
vi.mocked(evaluateSegment)
.mockResolvedValueOnce(true) // First segment matches
.mockResolvedValueOnce(false); // Second segment does not match
const result = await getPersonSegmentIds(
mockEnvironmentId,
mockContactId,
mockContactUserId,
mockAttributes,
mockDeviceType
);
expect(result).toEqual([mockSegmentsData[0].id]);
expect(evaluateSegment).toHaveBeenCalledTimes(mockSegmentsData.length);
});
});
});

View File

@@ -10,14 +10,16 @@ import { ZId, ZString } from "@formbricks/types/common";
import { DatabaseError } from "@formbricks/types/errors";
import { TBaseFilter } from "@formbricks/types/segment";
const getSegments = reactCache((environmentId: string) =>
export const getSegments = reactCache((environmentId: string) =>
cache(
async () => {
try {
return prisma.segment.findMany({
const segments = await prisma.segment.findMany({
where: { environmentId },
select: { id: true, filters: true },
});
return segments;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);

View File

@@ -0,0 +1,243 @@
import { contactCache } from "@/lib/cache/contact";
import { getEnvironment } from "@/lib/environment/service";
import { updateAttributes } from "@/modules/ee/contacts/lib/attributes";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { TEnvironment } from "@formbricks/types/environment";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { getContactByUserIdWithAttributes } from "./contact";
import { updateUser } from "./update-user";
import { getUserState } from "./user-state";
vi.mock("@/lib/cache/contact", () => ({
contactCache: {
revalidate: vi.fn(),
},
}));
vi.mock("@/lib/environment/service", () => ({
getEnvironment: vi.fn(),
}));
vi.mock("@/modules/ee/contacts/lib/attributes", () => ({
updateAttributes: vi.fn(),
}));
vi.mock("@formbricks/database", () => ({
prisma: {
contact: {
create: vi.fn(),
},
},
}));
vi.mock("./contact", () => ({
getContactByUserIdWithAttributes: vi.fn(),
}));
vi.mock("./user-state", () => ({
getUserState: vi.fn(),
}));
const mockEnvironmentId = "test-environment-id";
const mockUserId = "test-user-id";
const mockContactId = "test-contact-id";
const mockProjectId = "v7cxgsb4pzupdkr9xs14ldmb";
const mockEnvironment: TEnvironment = {
id: mockEnvironmentId,
createdAt: new Date(),
updatedAt: new Date(),
type: "production",
appSetupCompleted: false,
projectId: mockProjectId,
};
const mockContactAttributes = [
{ attributeKey: { key: "userId" }, value: mockUserId },
{ attributeKey: { key: "email" }, value: "test@example.com" },
];
const mockContact = {
id: mockContactId,
environmentId: mockEnvironmentId,
attributes: mockContactAttributes,
createdAt: new Date(),
updatedAt: new Date(),
name: null,
email: null,
};
const mockUserState = {
surveys: [],
noCodeActionClasses: [],
attributeClasses: [],
contactId: mockContactId,
userId: mockUserId,
displays: [],
responses: [],
segments: [],
lastDisplayAt: null,
};
describe("updateUser", () => {
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(getEnvironment).mockResolvedValue(mockEnvironment);
vi.mocked(getUserState).mockResolvedValue(mockUserState);
vi.mocked(updateAttributes).mockResolvedValue({ success: true, messages: [] });
});
afterEach(() => {
vi.clearAllMocks();
});
test("should throw ResourceNotFoundError if environment is not found", async () => {
vi.mocked(getEnvironment).mockResolvedValue(null);
await expect(updateUser(mockEnvironmentId, mockUserId, "desktop")).rejects.toThrow(
new ResourceNotFoundError("environment", mockEnvironmentId)
);
});
test("should create a new contact if not found", async () => {
vi.mocked(getContactByUserIdWithAttributes).mockResolvedValue(null);
vi.mocked(prisma.contact.create).mockResolvedValue({
id: mockContactId,
attributes: [{ attributeKey: { key: "userId" }, value: mockUserId }],
} as any); // Type assertion for mock
const result = await updateUser(mockEnvironmentId, mockUserId, "desktop");
expect(prisma.contact.create).toHaveBeenCalledWith({
data: {
environment: { connect: { id: mockEnvironmentId } },
attributes: {
create: [
{
attributeKey: {
connect: { key_environmentId: { key: "userId", environmentId: mockEnvironmentId } },
},
value: mockUserId,
},
],
},
},
select: {
id: true,
attributes: {
select: { attributeKey: { select: { key: true } }, value: true },
},
},
});
expect(contactCache.revalidate).toHaveBeenCalledWith({
environmentId: mockEnvironmentId,
userId: mockUserId,
id: mockContactId,
});
expect(result.state.data).toEqual(expect.objectContaining(mockUserState));
expect(result.messages).toEqual([]);
});
test("should update existing contact attributes", async () => {
vi.mocked(getContactByUserIdWithAttributes).mockResolvedValue(mockContact);
const newAttributes = { email: "new@example.com", language: "en" };
const result = await updateUser(mockEnvironmentId, mockUserId, "desktop", newAttributes);
expect(updateAttributes).toHaveBeenCalledWith(
mockContactId,
mockUserId,
mockEnvironmentId,
newAttributes
);
expect(result.state.data?.language).toBe("en");
expect(result.messages).toEqual([]);
});
test("should not update attributes if they are the same", async () => {
vi.mocked(getContactByUserIdWithAttributes).mockResolvedValue(mockContact);
const existingAttributes = { email: "test@example.com" }; // Same as in mockContact
await updateUser(mockEnvironmentId, mockUserId, "desktop", existingAttributes);
expect(updateAttributes).not.toHaveBeenCalled();
});
test("should return messages from updateAttributes if any", async () => {
vi.mocked(getContactByUserIdWithAttributes).mockResolvedValue(mockContact);
const newAttributes = { company: "Formbricks" };
const updateMessages = ["Attribute 'company' created."];
vi.mocked(updateAttributes).mockResolvedValue({ success: true, messages: updateMessages });
const result = await updateUser(mockEnvironmentId, mockUserId, "desktop", newAttributes);
expect(updateAttributes).toHaveBeenCalledWith(
mockContactId,
mockUserId,
mockEnvironmentId,
newAttributes
);
expect(result.messages).toEqual(updateMessages);
});
test("should use device type 'phone'", async () => {
vi.mocked(getContactByUserIdWithAttributes).mockResolvedValue(mockContact);
await updateUser(mockEnvironmentId, mockUserId, "phone");
expect(getUserState).toHaveBeenCalledWith(
expect.objectContaining({
device: "phone",
})
);
});
test("should use device type 'desktop'", async () => {
vi.mocked(getContactByUserIdWithAttributes).mockResolvedValue(mockContact);
await updateUser(mockEnvironmentId, mockUserId, "desktop");
expect(getUserState).toHaveBeenCalledWith(
expect.objectContaining({
device: "desktop",
})
);
});
test("should set language from attributes if provided and update is successful", async () => {
vi.mocked(getContactByUserIdWithAttributes).mockResolvedValue(mockContact);
const newAttributes = { language: "de" };
vi.mocked(updateAttributes).mockResolvedValue({ success: true, messages: [] });
const result = await updateUser(mockEnvironmentId, mockUserId, "desktop", newAttributes);
expect(result.state.data?.language).toBe("de");
});
test("should not set language from attributes if update is not successful", async () => {
const initialContactWithLanguage = {
...mockContact,
attributes: [...mockContact.attributes, { attributeKey: { key: "language" }, value: "fr" }],
};
vi.mocked(getContactByUserIdWithAttributes).mockResolvedValue(initialContactWithLanguage);
const newAttributes = { language: "de" };
vi.mocked(updateAttributes).mockResolvedValue({ success: false, messages: ["Update failed"] });
const result = await updateUser(mockEnvironmentId, mockUserId, "desktop", newAttributes);
// Language should remain 'fr' from the initial contact attributes, not 'de'
expect(result.state.data?.language).toBe("fr");
});
test("should handle empty attributes object", async () => {
vi.mocked(getContactByUserIdWithAttributes).mockResolvedValue(mockContact);
const result = await updateUser(mockEnvironmentId, mockUserId, "desktop", {});
expect(updateAttributes).not.toHaveBeenCalled();
expect(result.state.data).toEqual(expect.objectContaining(mockUserState));
expect(result.messages).toEqual([]);
});
test("should handle undefined attributes", async () => {
vi.mocked(getContactByUserIdWithAttributes).mockResolvedValue(mockContact);
const result = await updateUser(mockEnvironmentId, mockUserId, "desktop", undefined);
expect(updateAttributes).not.toHaveBeenCalled();
expect(result.state.data).toEqual(expect.objectContaining(mockUserState));
expect(result.messages).toEqual([]);
});
});

View File

@@ -0,0 +1,132 @@
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { TJsPersonState } from "@formbricks/types/js";
import { getPersonSegmentIds } from "./segments";
import { getUserState } from "./user-state";
vi.mock("@formbricks/database", () => ({
prisma: {
response: {
findMany: vi.fn(),
},
display: {
findMany: vi.fn(),
},
},
}));
vi.mock("./segments", () => ({
getPersonSegmentIds: vi.fn(),
}));
const mockEnvironmentId = "test-environment-id";
const mockUserId = "test-user-id";
const mockContactId = "test-contact-id";
const mockDevice = "desktop";
const mockAttributes = { email: "test@example.com" };
describe("getUserState", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
test("should return user state with empty responses and displays", async () => {
vi.mocked(prisma.response.findMany).mockResolvedValue([]);
vi.mocked(prisma.display.findMany).mockResolvedValue([]);
vi.mocked(getPersonSegmentIds).mockResolvedValue(["segment1"]);
const result = await getUserState({
environmentId: mockEnvironmentId,
userId: mockUserId,
contactId: mockContactId,
device: mockDevice,
attributes: mockAttributes,
});
expect(prisma.response.findMany).toHaveBeenCalledWith({
where: { contactId: mockContactId },
select: { surveyId: true },
});
expect(prisma.display.findMany).toHaveBeenCalledWith({
where: { contactId: mockContactId },
select: { surveyId: true, createdAt: true },
});
expect(getPersonSegmentIds).toHaveBeenCalledWith(
mockEnvironmentId,
mockContactId,
mockUserId,
mockAttributes,
mockDevice
);
expect(result).toEqual<TJsPersonState["data"]>({
contactId: mockContactId,
userId: mockUserId,
segments: ["segment1"],
displays: [],
responses: [],
lastDisplayAt: null,
});
});
test("should return user state with responses and displays, and sort displays by createdAt", async () => {
const mockDate1 = new Date("2023-01-01T00:00:00.000Z");
const mockDate2 = new Date("2023-01-02T00:00:00.000Z");
const mockResponses = [{ surveyId: "survey1" }, { surveyId: "survey2" }];
const mockDisplays = [
{ surveyId: "survey3", createdAt: mockDate1 },
{ surveyId: "survey4", createdAt: mockDate2 }, // most recent
];
vi.mocked(prisma.response.findMany).mockResolvedValue(mockResponses);
vi.mocked(prisma.display.findMany).mockResolvedValue(mockDisplays);
vi.mocked(getPersonSegmentIds).mockResolvedValue(["segment2", "segment3"]);
const result = await getUserState({
environmentId: mockEnvironmentId,
userId: mockUserId,
contactId: mockContactId,
device: mockDevice,
attributes: mockAttributes,
});
expect(result).toEqual<TJsPersonState["data"]>({
contactId: mockContactId,
userId: mockUserId,
segments: ["segment2", "segment3"],
displays: [
{ surveyId: "survey3", createdAt: mockDate1 },
{ surveyId: "survey4", createdAt: mockDate2 },
],
responses: ["survey1", "survey2"],
lastDisplayAt: mockDate2,
});
});
test("should handle null responses and displays from prisma (though unlikely)", async () => {
// This case tests the nullish coalescing, though prisma.findMany usually returns []
vi.mocked(prisma.response.findMany).mockResolvedValue(null as any);
vi.mocked(prisma.display.findMany).mockResolvedValue(null as any);
vi.mocked(getPersonSegmentIds).mockResolvedValue([]);
const result = await getUserState({
environmentId: mockEnvironmentId,
userId: mockUserId,
contactId: mockContactId,
device: mockDevice,
attributes: mockAttributes,
});
expect(result).toEqual<TJsPersonState["data"]>({
contactId: mockContactId,
userId: mockUserId,
segments: [],
displays: [],
responses: [],
lastDisplayAt: null,
});
});
});

View File

@@ -56,6 +56,10 @@ export const getUserState = async ({
const segments = await getPersonSegmentIds(environmentId, contactId, userId, attributes, device);
const sortedContactDisplaysDate = contactDisplays?.toSorted(
(a, b) => b.createdAt.getTime() - a.createdAt.getTime()
)[0]?.createdAt;
// If the person exists, return the persons's state
const userState: TJsPersonState["data"] = {
contactId,
@@ -67,10 +71,7 @@ export const getUserState = async ({
createdAt: display.createdAt,
})) ?? [],
responses: contactResponses?.map((response) => response.surveyId) ?? [],
lastDisplayAt:
contactDisplays.length > 0
? contactDisplays.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0].createdAt
: null,
lastDisplayAt: contactDisplays?.length > 0 ? sortedContactDisplaysDate : null,
};
return userState;

View File

@@ -0,0 +1,297 @@
import { contactAttributeKeyCache } from "@/lib/cache/contact-attribute-key";
import { ContactAttributeKey, Prisma } from "@prisma/client";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { TContactAttributeKey, TContactAttributeKeyType } from "@formbricks/types/contact-attribute-key";
import { DatabaseError, OperationNotAllowedError } from "@formbricks/types/errors";
import { TContactAttributeKeyUpdateInput } from "../types/contact-attribute-keys";
import {
createContactAttributeKey,
deleteContactAttributeKey,
getContactAttributeKey,
updateContactAttributeKey,
} from "./contact-attribute-key";
// Mock dependencies
vi.mock("@formbricks/database", () => ({
prisma: {
contactAttributeKey: {
findUnique: vi.fn(),
create: vi.fn(),
delete: vi.fn(),
update: vi.fn(),
count: vi.fn(),
},
},
}));
vi.mock("@/lib/cache/contact-attribute-key", () => ({
contactAttributeKeyCache: {
tag: {
byId: vi.fn((id) => `contactAttributeKey-${id}`),
byEnvironmentId: vi.fn((environmentId) => `environments-${environmentId}-contactAttributeKeys`),
byEnvironmentIdAndKey: vi.fn(
(environmentId, key) => `contactAttributeKey-environment-${environmentId}-key-${key}`
),
},
revalidate: vi.fn(),
},
}));
vi.mock("@/lib/constants", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/constants")>();
return {
...actual,
MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT: 10, // Default mock value for tests
};
});
// Constants used in tests
const mockContactAttributeKeyId = "drw0gc3oa67q113w68wdif0x";
const mockEnvironmentId = "fndlzrzlqw8c6zu9jfwxf34k";
const mockKey = "testKey";
const mockName = "Test Key";
const mockContactAttributeKey: TContactAttributeKey = {
id: mockContactAttributeKeyId,
createdAt: new Date(),
updatedAt: new Date(),
name: mockName,
key: mockKey,
environmentId: mockEnvironmentId,
type: "custom" as TContactAttributeKeyType,
description: "A test key",
isUnique: false,
};
// Define a compatible type for test data, as TContactAttributeKeyUpdateInput might be complex
interface TMockContactAttributeKeyUpdateInput {
description?: string | null;
}
describe("getContactAttributeKey", () => {
beforeEach(() => {
vi.clearAllMocks();
});
test("should return contact attribute key if found", async () => {
vi.mocked(prisma.contactAttributeKey.findUnique).mockResolvedValue(mockContactAttributeKey);
const result = await getContactAttributeKey(mockContactAttributeKeyId);
expect(result).toEqual(mockContactAttributeKey);
expect(prisma.contactAttributeKey.findUnique).toHaveBeenCalledWith({
where: { id: mockContactAttributeKeyId },
});
expect(contactAttributeKeyCache.tag.byId).toHaveBeenCalledWith(mockContactAttributeKeyId);
});
test("should return null if contact attribute key not found", async () => {
vi.mocked(prisma.contactAttributeKey.findUnique).mockResolvedValue(null);
const result = await getContactAttributeKey(mockContactAttributeKeyId);
expect(result).toBeNull();
expect(prisma.contactAttributeKey.findUnique).toHaveBeenCalledWith({
where: { id: mockContactAttributeKeyId },
});
});
test("should throw DatabaseError if Prisma call fails", async () => {
const errorMessage = "Prisma findUnique error";
vi.mocked(prisma.contactAttributeKey.findUnique).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError(errorMessage, { code: "P1000", clientVersion: "test" })
);
await expect(getContactAttributeKey(mockContactAttributeKeyId)).rejects.toThrow(DatabaseError);
await expect(getContactAttributeKey(mockContactAttributeKeyId)).rejects.toThrow(errorMessage);
});
test("should throw generic error if non-Prisma error occurs", async () => {
const errorMessage = "Some other error";
vi.mocked(prisma.contactAttributeKey.findUnique).mockRejectedValue(new Error(errorMessage));
await expect(getContactAttributeKey(mockContactAttributeKeyId)).rejects.toThrow(Error);
await expect(getContactAttributeKey(mockContactAttributeKeyId)).rejects.toThrow(errorMessage);
});
});
describe("createContactAttributeKey", () => {
const type: TContactAttributeKeyType = "custom";
beforeEach(() => {
vi.clearAllMocks();
});
test("should create and return a new contact attribute key", async () => {
const createdAttributeKey = { ...mockContactAttributeKey, id: "new_cak_id", key: mockKey, type };
vi.mocked(prisma.contactAttributeKey.count).mockResolvedValue(5); // Below limit
vi.mocked(prisma.contactAttributeKey.create).mockResolvedValue(createdAttributeKey);
const result = await createContactAttributeKey(mockEnvironmentId, mockKey, type);
expect(result).toEqual(createdAttributeKey);
expect(prisma.contactAttributeKey.count).toHaveBeenCalledWith({
where: { environmentId: mockEnvironmentId },
});
expect(prisma.contactAttributeKey.create).toHaveBeenCalledWith({
data: {
key: mockKey,
name: mockKey, // As per implementation
type,
environment: { connect: { id: mockEnvironmentId } },
},
});
expect(contactAttributeKeyCache.revalidate).toHaveBeenCalledWith({
id: createdAttributeKey.id,
environmentId: createdAttributeKey.environmentId,
key: createdAttributeKey.key,
});
});
test("should throw OperationNotAllowedError if max attribute classes reached", async () => {
// MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT is mocked to 10
vi.mocked(prisma.contactAttributeKey.count).mockResolvedValue(10);
await expect(createContactAttributeKey(mockEnvironmentId, mockKey, type)).rejects.toThrow(
OperationNotAllowedError
);
expect(prisma.contactAttributeKey.count).toHaveBeenCalledWith({
where: { environmentId: mockEnvironmentId },
});
expect(prisma.contactAttributeKey.create).not.toHaveBeenCalled();
});
test("should throw Prisma error if prisma.contactAttributeKey.count fails", async () => {
const errorMessage = "Prisma count error";
const prismaError = new Prisma.PrismaClientKnownRequestError(errorMessage, {
code: "P1000",
clientVersion: "test",
});
vi.mocked(prisma.contactAttributeKey.count).mockRejectedValue(prismaError);
await expect(createContactAttributeKey(mockEnvironmentId, mockKey, type)).rejects.toThrow(prismaError);
});
test("should throw DatabaseError if Prisma create fails", async () => {
vi.mocked(prisma.contactAttributeKey.count).mockResolvedValue(5); // Below limit
const errorMessage = "Prisma create error";
vi.mocked(prisma.contactAttributeKey.create).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError(errorMessage, { code: "P2000", clientVersion: "test" })
);
await expect(createContactAttributeKey(mockEnvironmentId, mockKey, type)).rejects.toThrow(DatabaseError);
await expect(createContactAttributeKey(mockEnvironmentId, mockKey, type)).rejects.toThrow(errorMessage);
});
test("should throw generic error if non-Prisma error occurs during create", async () => {
vi.mocked(prisma.contactAttributeKey.count).mockResolvedValue(5);
const errorMessage = "Some other error during create";
vi.mocked(prisma.contactAttributeKey.create).mockRejectedValue(new Error(errorMessage));
await expect(createContactAttributeKey(mockEnvironmentId, mockKey, type)).rejects.toThrow(Error);
await expect(createContactAttributeKey(mockEnvironmentId, mockKey, type)).rejects.toThrow(errorMessage);
});
});
describe("deleteContactAttributeKey", () => {
beforeEach(() => {
vi.clearAllMocks();
});
test("should delete contact attribute key and revalidate cache", async () => {
const deletedAttributeKey = { ...mockContactAttributeKey };
vi.mocked(prisma.contactAttributeKey.delete).mockResolvedValue(deletedAttributeKey);
const result = await deleteContactAttributeKey(mockContactAttributeKeyId);
expect(result).toEqual(deletedAttributeKey);
expect(prisma.contactAttributeKey.delete).toHaveBeenCalledWith({
where: { id: mockContactAttributeKeyId },
});
expect(contactAttributeKeyCache.revalidate).toHaveBeenCalledWith({
id: deletedAttributeKey.id,
environmentId: deletedAttributeKey.environmentId,
key: deletedAttributeKey.key,
});
});
test("should throw DatabaseError if Prisma delete fails", async () => {
const errorMessage = "Prisma delete error";
vi.mocked(prisma.contactAttributeKey.delete).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError(errorMessage, { code: "P2025", clientVersion: "test" })
);
await expect(deleteContactAttributeKey(mockContactAttributeKeyId)).rejects.toThrow(DatabaseError);
await expect(deleteContactAttributeKey(mockContactAttributeKeyId)).rejects.toThrow(errorMessage);
});
test("should throw generic error if non-Prisma error occurs during delete", async () => {
const errorMessage = "Some other error during delete";
vi.mocked(prisma.contactAttributeKey.delete).mockRejectedValue(new Error(errorMessage));
await expect(deleteContactAttributeKey(mockContactAttributeKeyId)).rejects.toThrow(Error);
await expect(deleteContactAttributeKey(mockContactAttributeKeyId)).rejects.toThrow(errorMessage);
});
});
describe("updateContactAttributeKey", () => {
const updateData: TMockContactAttributeKeyUpdateInput = {
description: "Updated description",
};
// Cast to TContactAttributeKeyUpdateInput for the function call, if strict typing is needed beyond the mock.
const typedUpdateData = updateData as TContactAttributeKeyUpdateInput;
const updatedAttributeKey = {
...mockContactAttributeKey,
description: updateData.description,
updatedAt: new Date(), // Update timestamp
} as ContactAttributeKey;
beforeEach(() => {
vi.clearAllMocks();
});
test("should update contact attribute key and revalidate cache", async () => {
vi.mocked(prisma.contactAttributeKey.update).mockResolvedValue(updatedAttributeKey);
const result = await updateContactAttributeKey(mockContactAttributeKeyId, typedUpdateData);
expect(result).toEqual(updatedAttributeKey);
expect(prisma.contactAttributeKey.update).toHaveBeenCalledWith({
where: { id: mockContactAttributeKeyId },
data: { description: updateData.description },
});
expect(contactAttributeKeyCache.revalidate).toHaveBeenCalledWith({
id: updatedAttributeKey.id,
environmentId: updatedAttributeKey.environmentId,
key: updatedAttributeKey.key,
});
});
test("should throw DatabaseError if Prisma update fails", async () => {
const errorMessage = "Prisma update error";
vi.mocked(prisma.contactAttributeKey.update).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError(errorMessage, { code: "P2025", clientVersion: "test" })
);
await expect(updateContactAttributeKey(mockContactAttributeKeyId, typedUpdateData)).rejects.toThrow(
DatabaseError
);
await expect(updateContactAttributeKey(mockContactAttributeKeyId, typedUpdateData)).rejects.toThrow(
errorMessage
);
});
test("should throw generic error if non-Prisma error occurs during update", async () => {
const errorMessage = "Some other error during update";
vi.mocked(prisma.contactAttributeKey.update).mockRejectedValue(new Error(errorMessage));
await expect(updateContactAttributeKey(mockContactAttributeKeyId, typedUpdateData)).rejects.toThrow(
Error
);
await expect(updateContactAttributeKey(mockContactAttributeKeyId, typedUpdateData)).rejects.toThrow(
errorMessage
);
});
});

View File

@@ -42,12 +42,13 @@ export const getContactAttributeKey = reactCache(
}
)()
);
export const createContactAttributeKey = async (
environmentId: string,
key: string,
type: TContactAttributeKeyType
): Promise<TContactAttributeKey | null> => {
validateInputs([environmentId, ZId], [name, ZString], [type, ZContactAttributeKeyType]);
validateInputs([environmentId, ZId], [key, ZString], [type, ZContactAttributeKeyType]);
const contactAttributeKeysCount = await prisma.contactAttributeKey.count({
where: {

View File

@@ -0,0 +1,152 @@
import { contactAttributeKeyCache } from "@/lib/cache/contact-attribute-key";
import { MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT } from "@/lib/constants";
import { Prisma } from "@prisma/client";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { PrismaErrorType } from "@formbricks/database/types/error";
import { TContactAttributeKeyType } from "@formbricks/types/contact-attribute-key";
import { DatabaseError, OperationNotAllowedError } from "@formbricks/types/errors";
import { createContactAttributeKey, getContactAttributeKeys } from "./contact-attribute-keys";
vi.mock("@formbricks/database", () => ({
prisma: {
contactAttributeKey: {
findMany: vi.fn(),
create: vi.fn(),
count: vi.fn(),
},
},
}));
vi.mock("@/lib/cache/contact-attribute-key", () => ({
contactAttributeKeyCache: {
tag: {
byEnvironmentId: vi.fn((id) => `contactAttributeKey-environment-${id}`),
},
revalidate: vi.fn(),
},
}));
vi.mock("@/lib/utils/validate");
describe("getContactAttributeKeys", () => {
beforeEach(() => {
vi.clearAllMocks();
});
test("should return contact attribute keys when found", async () => {
const mockEnvironmentIds = ["env1", "env2"];
const mockAttributeKeys = [
{ id: "key1", environmentId: "env1", name: "Key One", key: "keyOne", type: "custom" },
{ id: "key2", environmentId: "env2", name: "Key Two", key: "keyTwo", type: "custom" },
];
vi.mocked(prisma.contactAttributeKey.findMany).mockResolvedValue(mockAttributeKeys);
const result = await getContactAttributeKeys(mockEnvironmentIds);
expect(prisma.contactAttributeKey.findMany).toHaveBeenCalledWith({
where: { environmentId: { in: mockEnvironmentIds } },
});
expect(result).toEqual(mockAttributeKeys);
expect(contactAttributeKeyCache.tag.byEnvironmentId).toHaveBeenCalledTimes(mockEnvironmentIds.length);
});
test("should throw DatabaseError if Prisma call fails", async () => {
const mockEnvironmentIds = ["env1"];
const errorMessage = "Prisma error";
vi.mocked(prisma.contactAttributeKey.findMany).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError(errorMessage, { code: "P1000", clientVersion: "test" })
);
await expect(getContactAttributeKeys(mockEnvironmentIds)).rejects.toThrow(DatabaseError);
});
test("should throw generic error if non-Prisma error occurs", async () => {
const mockEnvironmentIds = ["env1"];
const errorMessage = "Some other error";
const errToThrow = new Prisma.PrismaClientKnownRequestError(errorMessage, {
clientVersion: "0.0.1",
code: PrismaErrorType.UniqueConstraintViolation,
});
vi.mocked(prisma.contactAttributeKey.findMany).mockRejectedValue(errToThrow);
await expect(getContactAttributeKeys(mockEnvironmentIds)).rejects.toThrow(errorMessage);
});
});
describe("createContactAttributeKey", () => {
const environmentId = "testEnvId";
const key = "testKey";
const type: TContactAttributeKeyType = "custom";
const mockCreatedAttributeKey = {
id: "newKeyId",
environmentId,
name: key,
key,
type,
createdAt: new Date(),
updatedAt: new Date(),
isUnique: false,
description: null,
};
beforeEach(() => {
vi.clearAllMocks();
});
test("should create and return a new contact attribute key", async () => {
vi.mocked(prisma.contactAttributeKey.count).mockResolvedValue(0);
vi.mocked(prisma.contactAttributeKey.create).mockResolvedValue({
...mockCreatedAttributeKey,
description: null, // ensure description is explicitly null if that's the case
});
const result = await createContactAttributeKey(environmentId, key, type);
expect(prisma.contactAttributeKey.count).toHaveBeenCalledWith({ where: { environmentId } });
expect(prisma.contactAttributeKey.create).toHaveBeenCalledWith({
data: {
key,
name: key,
type,
environment: { connect: { id: environmentId } },
},
});
expect(contactAttributeKeyCache.revalidate).toHaveBeenCalledWith({
id: mockCreatedAttributeKey.id,
environmentId: mockCreatedAttributeKey.environmentId,
key: mockCreatedAttributeKey.key,
});
expect(result).toEqual(mockCreatedAttributeKey);
});
test("should throw OperationNotAllowedError if max attribute classes reached", async () => {
vi.mocked(prisma.contactAttributeKey.count).mockResolvedValue(MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT);
await expect(createContactAttributeKey(environmentId, key, type)).rejects.toThrow(
OperationNotAllowedError
);
expect(prisma.contactAttributeKey.count).toHaveBeenCalledWith({ where: { environmentId } });
expect(prisma.contactAttributeKey.create).not.toHaveBeenCalled();
});
test("should throw DatabaseError if Prisma create fails", async () => {
vi.mocked(prisma.contactAttributeKey.count).mockResolvedValue(0);
const errorMessage = "Prisma create error";
vi.mocked(prisma.contactAttributeKey.create).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError(errorMessage, { code: "P2000", clientVersion: "test" })
);
await expect(createContactAttributeKey(environmentId, key, type)).rejects.toThrow(DatabaseError);
await expect(createContactAttributeKey(environmentId, key, type)).rejects.toThrow(errorMessage);
});
test("should throw generic error if non-Prisma error occurs during create", async () => {
vi.mocked(prisma.contactAttributeKey.count).mockResolvedValue(0);
const errorMessage = "Some other create error";
vi.mocked(prisma.contactAttributeKey.create).mockRejectedValue(new Error(errorMessage));
await expect(createContactAttributeKey(environmentId, key, type)).rejects.toThrow(Error);
await expect(createContactAttributeKey(environmentId, key, type)).rejects.toThrow(errorMessage);
});
});

View File

@@ -42,7 +42,7 @@ export const createContactAttributeKey = async (
key: string,
type: TContactAttributeKeyType
): Promise<TContactAttributeKey | null> => {
validateInputs([environmentId, ZId], [name, ZString], [type, ZContactAttributeKeyType]);
validateInputs([environmentId, ZId], [key, ZString], [type, ZContactAttributeKeyType]);
const contactAttributeKeysCount = await prisma.contactAttributeKey.count({
where: {

View File

@@ -0,0 +1,119 @@
import { Prisma } from "@prisma/client";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { DatabaseError } from "@formbricks/types/errors";
import { getContactAttributes } from "./contact-attributes";
vi.mock("@formbricks/database", () => ({
prisma: {
contactAttribute: {
findMany: vi.fn(),
},
},
}));
vi.mock("@/lib/cache/contact-attribute", () => ({
contactAttributeCache: {
tag: {
byEnvironmentId: vi.fn((environmentId) => `contactAttributes-${environmentId}`),
},
},
}));
const mockEnvironmentId1 = "testEnvId1";
const mockEnvironmentId2 = "testEnvId2";
const mockEnvironmentIds = [mockEnvironmentId1, mockEnvironmentId2];
const mockContactAttributes = [
{
id: "attr1",
value: "value1",
attributeKeyId: "key1",
contactId: "contact1",
createdAt: new Date(),
updatedAt: new Date(),
attributeKey: {
id: "key1",
key: "attrKey1",
name: "Attribute Key 1",
description: "Description 1",
environmentId: mockEnvironmentId1,
isUnique: false,
type: "custom",
createdAt: new Date(),
updatedAt: new Date(),
},
},
{
id: "attr2",
value: "value2",
attributeKeyId: "key2",
contactId: "contact2",
createdAt: new Date(),
updatedAt: new Date(),
attributeKey: {
id: "key2",
key: "attrKey2",
name: "Attribute Key 2",
description: "Description 2",
environmentId: mockEnvironmentId2,
isUnique: false,
type: "custom",
createdAt: new Date(),
updatedAt: new Date(),
},
},
];
describe("getContactAttributes", () => {
beforeEach(() => {
vi.clearAllMocks();
});
test("should return contact attributes when found", async () => {
vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue(mockContactAttributes as any);
const result = await getContactAttributes(mockEnvironmentIds);
expect(prisma.contactAttribute.findMany).toHaveBeenCalledWith({
where: {
attributeKey: {
environmentId: { in: mockEnvironmentIds },
},
},
});
expect(result).toEqual(mockContactAttributes);
});
test("should throw DatabaseError when PrismaClientKnownRequestError occurs", async () => {
const prismaError = new Prisma.PrismaClientKnownRequestError("Test Prisma Error", {
code: "P2001",
clientVersion: "test",
});
vi.mocked(prisma.contactAttribute.findMany).mockRejectedValue(prismaError);
await expect(getContactAttributes(mockEnvironmentIds)).rejects.toThrow(DatabaseError);
});
test("should throw generic error when an unknown error occurs", async () => {
const genericError = new Error("Test Generic Error");
vi.mocked(prisma.contactAttribute.findMany).mockRejectedValue(genericError);
await expect(getContactAttributes(mockEnvironmentIds)).rejects.toThrow(genericError);
});
test("should return empty array when no contact attributes are found", async () => {
vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue([]);
const result = await getContactAttributes(mockEnvironmentIds);
expect(result).toEqual([]);
expect(prisma.contactAttribute.findMany).toHaveBeenCalledWith({
where: {
attributeKey: {
environmentId: { in: mockEnvironmentIds },
},
},
});
});
});

View File

@@ -0,0 +1,152 @@
import { contactCache } from "@/lib/cache/contact";
import { Contact, Prisma } from "@prisma/client";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { DatabaseError } from "@formbricks/types/errors";
import { deleteContact, getContact } from "./contact";
vi.mock("@formbricks/database", () => ({
prisma: {
contact: {
findUnique: vi.fn(),
delete: vi.fn(),
},
},
}));
vi.mock("@/lib/cache/contact", () => ({
contactCache: {
revalidate: vi.fn(),
tag: {
byId: vi.fn((id) => `contact-${id}`),
},
},
}));
const mockContactId = "eegeo7qmz9sn5z85fi76lg8o";
const mockEnvironmentId = "sv7jqr9qjmayp1hc6xm7rfud";
const mockContact = {
id: mockContactId,
environmentId: mockEnvironmentId,
createdAt: new Date(),
updatedAt: new Date(),
attributes: [],
};
describe("contact lib", () => {
beforeEach(() => {
vi.resetAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
describe("getContact", () => {
test("should return contact if found", async () => {
vi.mocked(prisma.contact.findUnique).mockResolvedValue(mockContact);
const result = await getContact(mockContactId);
expect(result).toEqual(mockContact);
expect(prisma.contact.findUnique).toHaveBeenCalledWith({ where: { id: mockContactId } });
expect(contactCache.tag.byId).toHaveBeenCalledWith(mockContactId);
});
test("should return null if contact not found", async () => {
vi.mocked(prisma.contact.findUnique).mockResolvedValue(null);
const result = await getContact(mockContactId);
expect(result).toBeNull();
expect(prisma.contact.findUnique).toHaveBeenCalledWith({ where: { id: mockContactId } });
});
test("should throw DatabaseError if prisma throws PrismaClientKnownRequestError", async () => {
const prismaError = new Prisma.PrismaClientKnownRequestError("Test Prisma Error", {
code: "P2001",
clientVersion: "2.0.0",
});
vi.mocked(prisma.contact.findUnique).mockRejectedValue(prismaError);
await expect(getContact(mockContactId)).rejects.toThrow(DatabaseError);
});
test("should throw error for other errors", async () => {
const genericError = new Error("Test Generic Error");
vi.mocked(prisma.contact.findUnique).mockRejectedValue(genericError);
await expect(getContact(mockContactId)).rejects.toThrow(genericError);
});
});
describe("deleteContact", () => {
const mockDeletedContact = {
id: mockContactId,
environmentId: mockEnvironmentId,
attributes: [{ attributeKey: { key: "email" }, value: "test@example.com" }],
} as unknown as Contact;
const mockDeletedContactWithUserId = {
id: mockContactId,
environmentId: mockEnvironmentId,
attributes: [
{ attributeKey: { key: "email" }, value: "test@example.com" },
{ attributeKey: { key: "userId" }, value: "user123" },
],
} as unknown as Contact;
test("should delete contact and revalidate cache", async () => {
vi.mocked(prisma.contact.delete).mockResolvedValue(mockDeletedContact);
await deleteContact(mockContactId);
expect(prisma.contact.delete).toHaveBeenCalledWith({
where: { id: mockContactId },
select: {
id: true,
environmentId: true,
attributes: { select: { attributeKey: { select: { key: true } }, value: true } },
},
});
expect(contactCache.revalidate).toHaveBeenCalledWith({
id: mockContactId,
userId: undefined,
environmentId: mockEnvironmentId,
});
});
test("should delete contact and revalidate cache with userId", async () => {
vi.mocked(prisma.contact.delete).mockResolvedValue(mockDeletedContactWithUserId);
await deleteContact(mockContactId);
expect(prisma.contact.delete).toHaveBeenCalledWith({
where: { id: mockContactId },
select: {
id: true,
environmentId: true,
attributes: { select: { attributeKey: { select: { key: true } }, value: true } },
},
});
expect(contactCache.revalidate).toHaveBeenCalledWith({
id: mockContactId,
userId: "user123",
environmentId: mockEnvironmentId,
});
});
test("should throw DatabaseError if prisma throws PrismaClientKnownRequestError", async () => {
const prismaError = new Prisma.PrismaClientKnownRequestError("Test Prisma Error", {
code: "P2001",
clientVersion: "2.0.0",
});
vi.mocked(prisma.contact.delete).mockRejectedValue(prismaError);
await expect(deleteContact(mockContactId)).rejects.toThrow(DatabaseError);
});
test("should throw error for other errors", async () => {
const genericError = new Error("Test Generic Error");
vi.mocked(prisma.contact.delete).mockRejectedValue(genericError);
await expect(deleteContact(mockContactId)).rejects.toThrow(genericError);
});
});
});

View File

@@ -0,0 +1,99 @@
import { contactCache } from "@/lib/cache/contact";
import { Prisma } from "@prisma/client";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { DatabaseError } from "@formbricks/types/errors";
import { getContacts } from "./contacts";
vi.mock("@/lib/cache/contact", () => ({
contactCache: {
tag: {
byEnvironmentId: vi.fn((id) => `contact-environment-${id}`),
},
},
}));
vi.mock("@formbricks/database", () => ({
prisma: {
contact: {
findMany: vi.fn(),
},
},
}));
const mockEnvironmentId1 = "ay70qluzic16hu8fu6xrqebq";
const mockEnvironmentId2 = "raeeymwqrn9iqwe5rp13vwem";
const mockEnvironmentIds = [mockEnvironmentId1, mockEnvironmentId2];
const mockContacts = [
{
id: "contactId1",
environmentId: mockEnvironmentId1,
name: "Contact 1",
email: "contact1@example.com",
attributes: {},
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: "contactId2",
environmentId: mockEnvironmentId2,
name: "Contact 2",
email: "contact2@example.com",
attributes: {},
createdAt: new Date(),
updatedAt: new Date(),
},
];
describe("getContacts", () => {
beforeEach(() => {
vi.resetAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
test("should return contacts for given environmentIds", async () => {
vi.mocked(prisma.contact.findMany).mockResolvedValue(mockContacts);
const result = await getContacts(mockEnvironmentIds);
expect(prisma.contact.findMany).toHaveBeenCalledWith({
where: { environmentId: { in: mockEnvironmentIds } },
});
expect(result).toEqual(mockContacts);
expect(contactCache.tag.byEnvironmentId).toHaveBeenCalledWith(mockEnvironmentId1);
expect(contactCache.tag.byEnvironmentId).toHaveBeenCalledWith(mockEnvironmentId2);
});
test("should throw DatabaseError on PrismaClientKnownRequestError", async () => {
const prismaError = new Prisma.PrismaClientKnownRequestError("Test Prisma Error", {
code: "P2002",
clientVersion: "2.0.0",
});
vi.mocked(prisma.contact.findMany).mockRejectedValue(prismaError);
await expect(getContacts(mockEnvironmentIds)).rejects.toThrow(DatabaseError);
expect(prisma.contact.findMany).toHaveBeenCalledWith({
where: { environmentId: { in: mockEnvironmentIds } },
});
});
test("should throw original error for other errors", async () => {
const genericError = new Error("Test Generic Error");
vi.mocked(prisma.contact.findMany).mockRejectedValue(genericError);
await expect(getContacts(mockEnvironmentIds)).rejects.toThrow(genericError);
expect(prisma.contact.findMany).toHaveBeenCalledWith({
where: { environmentId: { in: mockEnvironmentIds } },
});
});
test("should use cache with correct tags", async () => {
vi.mocked(prisma.contact.findMany).mockResolvedValue(mockContacts);
await getContacts(mockEnvironmentIds);
});
});

View File

@@ -0,0 +1,142 @@
import { actionClassCache } from "@/lib/actionClass/cache";
import { cache } from "@/lib/cache";
import { validateInputs } from "@/lib/utils/validate";
import { type ActionClass } from "@prisma/client";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { DatabaseError, ValidationError } from "@formbricks/types/errors";
import { getActionClasses } from "./action-class";
// Mock dependencies
vi.mock("@/lib/actionClass/cache", () => ({
actionClassCache: {
tag: {
byEnvironmentId: vi.fn((environmentId: string) => `actionClass-environment-${environmentId}`),
},
},
}));
vi.mock("@/lib/cache", () => ({
cache: vi.fn((fn) => fn), // Mock cache to just return the function
}));
vi.mock("@/lib/utils/validate");
// Mock prisma
vi.mock("@formbricks/database", () => ({
prisma: {
actionClass: {
findMany: vi.fn(),
},
},
}));
// Mock react's cache
vi.mock("react", async () => {
const actual = await vi.importActual("react");
return {
...actual,
cache: vi.fn((fn) => fn), // Mock react's cache to just return the function
};
});
const environmentId = "test-environment-id";
const mockActionClasses: ActionClass[] = [
{
id: "action1",
createdAt: new Date(),
updatedAt: new Date(),
name: "Action 1",
description: "Description 1",
type: "code",
noCodeConfig: null,
environmentId: environmentId,
key: "key1",
},
{
id: "action2",
createdAt: new Date(),
updatedAt: new Date(),
name: "Action 2",
description: "Description 2",
type: "noCode",
noCodeConfig: {
type: "click",
elementSelector: { cssSelector: ".btn" },
urlFilters: [],
},
environmentId: environmentId,
key: null,
},
];
describe("getActionClasses", () => {
beforeEach(() => {
vi.resetAllMocks();
// Redefine the mock for cache before each test to ensure it's clean
vi.mocked(cache).mockImplementation((fn) => fn);
});
afterEach(() => {
vi.clearAllMocks();
});
test("should return action classes successfully", async () => {
vi.mocked(prisma.actionClass.findMany).mockResolvedValue(mockActionClasses);
const result = await getActionClasses(environmentId);
expect(result).toEqual(mockActionClasses);
expect(validateInputs).toHaveBeenCalledWith([environmentId, expect.any(Object)]);
expect(prisma.actionClass.findMany).toHaveBeenCalledWith({
where: {
environmentId: environmentId,
},
orderBy: {
createdAt: "asc",
},
});
expect(cache).toHaveBeenCalledTimes(1);
expect(actionClassCache.tag.byEnvironmentId).toHaveBeenCalledWith(environmentId);
});
test("should throw DatabaseError when prisma.actionClass.findMany fails", async () => {
const errorMessage = "Prisma error";
vi.mocked(prisma.actionClass.findMany).mockRejectedValue(new Error(errorMessage));
await expect(getActionClasses(environmentId)).rejects.toThrow(DatabaseError);
await expect(getActionClasses(environmentId)).rejects.toThrow(
`Database error when fetching actions for environment ${environmentId}`
);
expect(validateInputs).toHaveBeenCalledWith([environmentId, expect.any(Object)]);
expect(prisma.actionClass.findMany).toHaveBeenCalledTimes(2); // Called twice due to rejection
expect(cache).toHaveBeenCalledTimes(2);
});
test("should throw ValidationError when validateInputs fails", async () => {
const validationErrorMessage = "Validation failed";
vi.mocked(validateInputs).mockImplementation(() => {
throw new ValidationError(validationErrorMessage);
});
await expect(getActionClasses(environmentId)).rejects.toThrow(ValidationError);
await expect(getActionClasses(environmentId)).rejects.toThrow(validationErrorMessage);
expect(validateInputs).toHaveBeenCalledWith([environmentId, expect.any(Object)]);
expect(prisma.actionClass.findMany).not.toHaveBeenCalled();
expect(cache).toHaveBeenCalledTimes(2); // cache wrapper is still called
});
test("should use reactCache and our custom cache", async () => {
vi.mocked(prisma.actionClass.findMany).mockResolvedValue(mockActionClasses);
// We need to import the actual react cache to test it with vi.spyOn if we weren't mocking it.
// However, since we are mocking it to be a pass-through, we just check if our main cache is called.
await getActionClasses(environmentId);
expect(cache).toHaveBeenCalledTimes(1);
// Check if the function passed to react.cache (which is our main cache function due to mocking) was called
// This is implicitly tested by cache being called.
});
});

View File

@@ -0,0 +1,28 @@
import { describe, expect, test } from "vitest";
import { copySurveyLink } from "./client-utils";
describe("copySurveyLink", () => {
const surveyUrl = "https://app.formbricks.com/s/someSurveyId";
test("should return the surveyUrl with suId when singleUseId is provided", () => {
const singleUseId = "someSingleUseId";
const result = copySurveyLink(surveyUrl, singleUseId);
expect(result).toBe(`${surveyUrl}?suId=${singleUseId}`);
});
test("should return just the surveyUrl when singleUseId is not provided", () => {
const result = copySurveyLink(surveyUrl);
expect(result).toBe(surveyUrl);
});
test("should return just the surveyUrl when singleUseId is an empty string", () => {
const singleUseId = "";
const result = copySurveyLink(surveyUrl, singleUseId);
expect(result).toBe(surveyUrl);
});
test("should return just the surveyUrl when singleUseId is undefined", () => {
const result = copySurveyLink(surveyUrl, undefined);
expect(result).toBe(surveyUrl);
});
});

View File

@@ -0,0 +1,153 @@
import { Organization, Prisma } from "@prisma/client";
import { Mocked, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { PrismaErrorType } from "@formbricks/database/types/error";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { getOrganizationAIKeys, getOrganizationIdFromEnvironmentId } from "./organization";
// Mock prisma
vi.mock("@formbricks/database", () => ({
prisma: {
organization: {
findFirst: vi.fn(),
findUnique: vi.fn(),
},
},
}));
// Mock organizationCache tags
vi.mock("@/lib/organization/cache", () => ({
organizationCache: {
tag: {
byEnvironmentId: vi.fn((id) => `org-env-${id}`),
byId: vi.fn((id) => `org-${id}`),
},
},
}));
// Mock reactCache
vi.mock("react", () => ({
cache: vi.fn((fn) => fn), // reactCache(fn) returns fn, which is then invoked
}));
const mockPrismaOrganization = prisma.organization as Mocked<typeof prisma.organization>;
describe("getOrganizationIdFromEnvironmentId", () => {
beforeEach(() => {
vi.resetAllMocks();
});
afterEach(() => {
vi.restoreAllMocks(); // Restore spies and mocks
});
test("should return organization ID if found", async () => {
const mockEnvId = "env_test123";
const mockOrgId = "org_test456";
mockPrismaOrganization.findFirst.mockResolvedValueOnce({ id: mockOrgId } as Organization);
const result = await getOrganizationIdFromEnvironmentId(mockEnvId);
expect(result).toBe(mockOrgId);
expect(mockPrismaOrganization.findFirst).toHaveBeenCalledWith({
where: {
projects: {
some: {
environments: {
some: { id: mockEnvId },
},
},
},
},
select: {
id: true,
},
});
});
test("should throw ResourceNotFoundError if organization not found", async () => {
const mockEnvId = "env_test123_notfound";
mockPrismaOrganization.findFirst.mockResolvedValueOnce(null);
await expect(getOrganizationIdFromEnvironmentId(mockEnvId)).rejects.toThrow(ResourceNotFoundError);
await expect(getOrganizationIdFromEnvironmentId(mockEnvId)).rejects.toThrow("Organization not found");
});
test("should propagate prisma error", async () => {
const mockEnvId = "env_test123_dberror";
const errorMessage = "Database connection lost";
mockPrismaOrganization.findFirst.mockRejectedValueOnce(new Error(errorMessage));
await expect(getOrganizationIdFromEnvironmentId(mockEnvId)).rejects.toThrow(Error);
});
});
describe("getOrganizationAIKeys", () => {
beforeEach(() => {
vi.resetAllMocks();
});
afterEach(() => {
vi.restoreAllMocks(); // Restore spies and mocks
});
const mockOrgId = "org_test789";
const mockOrganizationData: Pick<Organization, "isAIEnabled" | "billing"> = {
isAIEnabled: true,
billing: {
plan: "free",
stripeCustomerId: null,
period: "monthly",
periodStart: new Date(),
limits: {
monthly: { responses: null, miu: null },
projects: null,
},
}, // Prisma.JsonValue compatible
};
test("should return organization AI keys if found", async () => {
mockPrismaOrganization.findUnique.mockResolvedValueOnce(
mockOrganizationData as Organization // Cast to full Organization for mock purposes
);
const result = await getOrganizationAIKeys(mockOrgId);
expect(result).toEqual(mockOrganizationData);
expect(mockPrismaOrganization.findUnique).toHaveBeenCalledWith({
where: {
id: mockOrgId,
},
select: {
isAIEnabled: true,
billing: true,
},
});
});
test("should return null if organization not found", async () => {
mockPrismaOrganization.findUnique.mockResolvedValueOnce(null);
const result = await getOrganizationAIKeys(mockOrgId);
expect(result).toBeNull();
});
test("should throw DatabaseError on PrismaClientKnownRequestError", async () => {
const mockErrorMessage = "Unique constraint failed on table";
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
code: PrismaErrorType.UniqueConstraintViolation,
clientVersion: "0.0.1",
});
mockPrismaOrganization.findUnique.mockRejectedValueOnce(errToThrow);
await expect(getOrganizationAIKeys(mockOrgId)).rejects.toThrow(DatabaseError);
});
test("should re-throw other errors from prisma", async () => {
const errorMessage = "Some other unexpected DB error";
const genericError = new Error(errorMessage);
mockPrismaOrganization.findUnique.mockRejectedValueOnce(genericError);
await expect(getOrganizationAIKeys(mockOrgId)).rejects.toThrow(genericError);
});
});

View File

@@ -0,0 +1,148 @@
import { Prisma, Project } from "@prisma/client";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { PrismaErrorType } from "@formbricks/database/types/error";
import { logger } from "@formbricks/logger";
import { DatabaseError } from "@formbricks/types/errors";
import { getProjectWithTeamIdsByEnvironmentId } from "./project";
vi.mock("@formbricks/database", () => ({
prisma: {
project: {
findFirst: vi.fn(),
},
},
}));
vi.mock("@formbricks/logger", () => ({
logger: {
error: vi.fn(),
},
}));
// Mock reactCache as it's a React-specific import and not needed for these tests
vi.mock("react", () => ({
cache: vi.fn((fn) => fn),
}));
const environmentId = "test-environment-id";
const mockProjectPrisma = {
id: "clq6167un000008l56jd8s3f9",
name: "Test Project",
createdAt: new Date(),
updatedAt: new Date(),
createdById: null,
projectTeams: [{ teamId: "team1" }, { teamId: "team2" }],
environments: [],
surveys: [],
webhooks: [],
apiKey: null,
styling: {
allowStyleOverwrite: true,
},
variables: {},
languages: [],
recontactDays: 0,
inAppSurveyBranding: false,
linkSurveyBranding: false,
placement: "bottomRight",
clickOutsideClose: false,
darkOverlay: false,
segment: null,
surveyClosedMessage: null,
singleUseId: null,
verifyEmail: null,
productOverwrites: null,
brandColor: null,
highlightBorderColor: null,
responseCount: null,
organizationId: "clq6167un000008l56jd8s3f9",
config: { channel: "app", industry: "eCommerce" },
logo: null,
} as Project;
const mockProjectWithTeam: Project & { teamIds: string[] } = {
...mockProjectPrisma,
teamIds: ["team1", "team2"],
};
describe("getProjectWithTeamIdsByEnvironmentId", () => {
beforeEach(() => {
vi.resetAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
test("should return project with team IDs when project is found", async () => {
vi.mocked(prisma.project.findFirst).mockResolvedValue(mockProjectPrisma);
const project = await getProjectWithTeamIdsByEnvironmentId(environmentId);
expect(prisma.project.findFirst).toHaveBeenCalledWith({
where: {
environments: {
some: {
id: environmentId,
},
},
},
include: {
projectTeams: {
select: {
teamId: true,
},
},
},
});
expect(project).toEqual(mockProjectWithTeam);
});
test("should return null when project is not found", async () => {
vi.mocked(prisma.project.findFirst).mockResolvedValue(null);
const project = await getProjectWithTeamIdsByEnvironmentId(environmentId);
expect(prisma.project.findFirst).toHaveBeenCalledWith({
where: {
environments: {
some: {
id: environmentId,
},
},
},
include: {
projectTeams: {
select: {
teamId: true,
},
},
},
});
expect(project).toBeNull();
});
test("should throw DatabaseError when prisma query fails", async () => {
const mockErrorMessage = "Prisma error";
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
code: PrismaErrorType.UniqueConstraintViolation,
clientVersion: "0.0.1",
});
vi.mocked(prisma.project.findFirst).mockRejectedValue(errToThrow);
await expect(getProjectWithTeamIdsByEnvironmentId(environmentId)).rejects.toThrow(DatabaseError);
expect(logger.error).toHaveBeenCalled();
});
test("should rethrow error if not PrismaClientKnownRequestError", async () => {
const errorMessage = "Some other error";
const error = new Error(errorMessage);
vi.mocked(prisma.project.findFirst).mockRejectedValue(error);
await expect(getProjectWithTeamIdsByEnvironmentId(environmentId)).rejects.toThrow(error);
expect(logger.error).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,83 @@
import { cache } from "@/lib/cache";
import { responseCache } from "@/lib/response/cache";
import { Prisma } from "@prisma/client";
import { afterEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { DatabaseError } from "@formbricks/types/errors";
import { getResponseCountBySurveyId } from "./response";
// Mock dependencies
vi.mock("@/lib/cache", () => ({
cache: vi.fn((fn) => fn),
}));
vi.mock("@/lib/response/cache", () => ({
responseCache: {
tag: {
bySurveyId: vi.fn((surveyId) => `survey-${surveyId}-responses`),
},
},
}));
vi.mock("react", async () => {
const actual = await vi.importActual("react");
return {
...actual,
cache: vi.fn((fn) => fn), // Mock react's cache to just return the function
};
});
vi.mock("@formbricks/database", () => ({
prisma: {
response: {
count: vi.fn(),
},
},
}));
const surveyId = "test-survey-id";
describe("getResponseCountBySurveyId", () => {
afterEach(() => {
vi.clearAllMocks();
});
test("should return the response count for a survey", async () => {
const mockCount = 5;
vi.mocked(prisma.response.count).mockResolvedValue(mockCount);
const result = await getResponseCountBySurveyId(surveyId);
expect(result).toBe(mockCount);
expect(prisma.response.count).toHaveBeenCalledWith({
where: { surveyId },
});
expect(cache).toHaveBeenCalledTimes(1);
expect(responseCache.tag.bySurveyId).toHaveBeenCalledWith(surveyId);
});
test("should throw DatabaseError if PrismaClientKnownRequestError occurs", async () => {
const prismaError = new Prisma.PrismaClientKnownRequestError("Test Prisma Error", {
code: "P2002",
clientVersion: "2.0.0",
});
vi.mocked(prisma.response.count).mockRejectedValue(prismaError);
await expect(getResponseCountBySurveyId(surveyId)).rejects.toThrow(DatabaseError);
expect(prisma.response.count).toHaveBeenCalledWith({
where: { surveyId },
});
expect(cache).toHaveBeenCalledTimes(1);
});
test("should throw generic error if an unknown error occurs", async () => {
const genericError = new Error("Test Generic Error");
vi.mocked(prisma.response.count).mockRejectedValue(genericError);
await expect(getResponseCountBySurveyId(surveyId)).rejects.toThrow(genericError);
expect(prisma.response.count).toHaveBeenCalledWith({
where: { surveyId },
});
expect(cache).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,124 @@
import { Organization, Prisma } from "@prisma/client";
import { afterEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { PrismaErrorType } from "@formbricks/database/types/error";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TSurvey } from "@formbricks/types/surveys/types";
import { getOrganizationBilling, getSurvey } from "./survey";
// Mock prisma
vi.mock("@formbricks/database", () => ({
prisma: {
organization: {
findFirst: vi.fn(),
},
survey: {
findUnique: vi.fn(),
},
},
}));
// Mock surveyCache
vi.mock("@/lib/survey/cache", () => ({
surveyCache: {
tag: {
byId: vi.fn((id) => `survey-${id}`),
},
},
}));
// Mock organizationCache
vi.mock("@/lib/organization/cache", () => ({
organizationCache: {
tag: {
byId: vi.fn((id) => `organization-${id}`),
},
},
}));
// Mock transformPrismaSurvey
vi.mock("@/modules/survey/lib/utils", () => ({
transformPrismaSurvey: vi.fn((survey) => survey),
}));
describe("Survey Library Tests", () => {
afterEach(() => {
vi.clearAllMocks();
});
describe("getOrganizationBilling", () => {
test("should return organization billing when found", async () => {
const mockBilling = {
stripeCustomerId: "cus_123",
features: { linkSurvey: { status: "active" } },
subscriptionStatus: "active",
nextRenewalDate: new Date(),
} as unknown as Organization["billing"];
vi.mocked(prisma.organization.findFirst).mockResolvedValueOnce({ billing: mockBilling } as any);
const billing = await getOrganizationBilling("org_123");
expect(billing).toEqual(mockBilling);
expect(prisma.organization.findFirst).toHaveBeenCalledWith({
where: { id: "org_123" },
select: { billing: true },
});
});
test("should throw ResourceNotFoundError when organization not found", async () => {
vi.mocked(prisma.organization.findFirst).mockResolvedValueOnce(null);
await expect(getOrganizationBilling("org_nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("should throw DatabaseError on Prisma client known request error", async () => {
const mockErrorMessage = "Prisma error";
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
code: PrismaErrorType.UniqueConstraintViolation,
clientVersion: "0.0.1",
});
vi.mocked(prisma.organization.findFirst).mockRejectedValue(errToThrow);
await expect(getOrganizationBilling("org_dberror")).rejects.toThrow(DatabaseError);
});
test("should throw other errors", async () => {
const genericError = new Error("Generic error");
vi.mocked(prisma.organization.findFirst).mockRejectedValueOnce(genericError);
await expect(getOrganizationBilling("org_error")).rejects.toThrow(genericError);
});
});
describe("getSurvey", () => {
test("should return survey when found", async () => {
const mockSurvey = { id: "survey_123", name: "Test Survey" } as unknown as TSurvey;
vi.mocked(prisma.survey.findUnique).mockResolvedValueOnce(mockSurvey as any); // Type assertion needed due to complex select
const survey = await getSurvey("survey_123");
expect(survey).toEqual(mockSurvey);
expect(prisma.survey.findUnique).toHaveBeenCalledWith({
where: { id: "survey_123" },
select: expect.any(Object), // selectSurvey is a large object, checking for existence
});
});
test("should throw ResourceNotFoundError when survey not found", async () => {
vi.mocked(prisma.survey.findUnique).mockResolvedValueOnce(null);
await expect(getSurvey("survey_nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("should throw DatabaseError on Prisma client known request error", async () => {
const mockErrorMessage = "Prisma error";
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
code: PrismaErrorType.UniqueConstraintViolation,
clientVersion: "0.0.1",
});
vi.mocked(prisma.survey.findUnique).mockRejectedValue(errToThrow);
await expect(getSurvey("survey_dberror")).rejects.toThrow(DatabaseError);
});
test("should throw other errors", async () => {
const genericError = new Error("Generic error");
vi.mocked(prisma.survey.findUnique).mockRejectedValueOnce(genericError);
await expect(getSurvey("survey_error")).rejects.toThrow(genericError);
});
});
});

View File

@@ -126,16 +126,24 @@ export const getSurvey = reactCache(
async (surveyId: string): Promise<TSurvey> =>
cache(
async () => {
const survey = await prisma.survey.findUnique({
where: { id: surveyId },
select: selectSurvey,
});
try {
const survey = await prisma.survey.findUnique({
where: { id: surveyId },
select: selectSurvey,
});
if (!survey) {
throw new ResourceNotFoundError("Survey", surveyId);
if (!survey) {
throw new ResourceNotFoundError("Survey", surveyId);
}
return transformPrismaSurvey<TSurvey>(survey);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
return transformPrismaSurvey<TSurvey>(survey);
},
[`survey-editor-getSurvey-${surveyId}`],
{

View File

@@ -1,17 +0,0 @@
import { describe, expect, test } from "vitest";
import { copySurveyLink } from "../client-utils";
describe("copySurveyLink", () => {
test("appends singleUseId when provided", () => {
const surveyUrl = "http://example.com/survey";
const singleUseId = "12345";
const result = copySurveyLink(surveyUrl, singleUseId);
expect(result).toBe("http://example.com/survey?suId=12345");
});
test("returns original surveyUrl when singleUseId is not provided", () => {
const surveyUrl = "http://example.com/survey";
const result = copySurveyLink(surveyUrl);
expect(result).toBe(surveyUrl);
});
});

View File

@@ -0,0 +1,249 @@
import { describe, expect, test } from "vitest";
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
import { TSegment } from "@formbricks/types/segment";
import { TSurvey, TSurveyFilterCriteria, TSurveyStatus, TSurveyType } from "@formbricks/types/surveys/types";
import { anySurveyHasFilters, buildOrderByClause, buildWhereClause, transformPrismaSurvey } from "./utils";
describe("Survey Utils", () => {
describe("transformPrismaSurvey", () => {
test("should transform a Prisma survey object with a segment", () => {
const surveyPrisma = {
id: "survey1",
name: "Test Survey",
displayPercentage: "50.5",
segment: {
id: "segment1",
title: "Test Segment",
filters: [],
surveys: [{ id: "survey1" }, { id: "survey2" }],
},
// other survey properties
};
const expectedSegment = {
id: "segment1",
title: "Test Segment",
filters: [],
surveys: ["survey1", "survey2"],
} as unknown as TSegment;
const expectedTransformedSurvey: TSurvey = {
id: "survey1",
name: "Test Survey",
displayPercentage: 50.5,
segment: expectedSegment,
// other survey properties
} as TSurvey; // Cast to TSurvey to satisfy type checker for other missing props
const result = transformPrismaSurvey<TSurvey>(surveyPrisma);
expect(result.displayPercentage).toBe(expectedTransformedSurvey.displayPercentage);
expect(result.segment).toEqual(expectedTransformedSurvey.segment);
// Check other properties if necessary, ensuring they are passed through
expect(result.id).toBe(expectedTransformedSurvey.id);
expect(result.name).toBe(expectedTransformedSurvey.name);
});
test("should transform a Prisma survey object without a segment", () => {
const surveyPrisma = {
id: "survey2",
name: "Another Survey",
displayPercentage: "75",
segment: null,
// other survey properties
};
const expectedTransformedSurvey: TSurvey = {
id: "survey2",
name: "Another Survey",
displayPercentage: 75,
segment: null,
// other survey properties
} as TSurvey;
const result = transformPrismaSurvey<TSurvey>(surveyPrisma);
expect(result.displayPercentage).toBe(expectedTransformedSurvey.displayPercentage);
expect(result.segment).toBeNull();
expect(result.id).toBe(expectedTransformedSurvey.id);
expect(result.name).toBe(expectedTransformedSurvey.name);
});
test("should handle null displayPercentage", () => {
const surveyPrisma = {
id: "survey3",
name: "Survey with null percentage",
displayPercentage: null,
segment: null,
};
const result = transformPrismaSurvey<TSurvey>(surveyPrisma);
expect(result.displayPercentage).toBeNull();
});
test("should handle undefined displayPercentage", () => {
const surveyPrisma = {
id: "survey4",
name: "Survey with undefined percentage",
displayPercentage: undefined,
segment: null,
};
const result = transformPrismaSurvey<TSurvey>(surveyPrisma);
expect(result.displayPercentage).toBeNull();
});
test("should transform for TJsEnvironmentStateSurvey type", () => {
const surveyPrisma = {
id: "surveyJs",
name: "JS Survey",
displayPercentage: "10.0",
segment: null,
// other specific TJsEnvironmentStateSurvey properties if any
};
const result = transformPrismaSurvey<TJsEnvironmentStateSurvey>(surveyPrisma);
expect(result.displayPercentage).toBe(10.0);
expect(result.segment).toBeNull();
expect(result.id).toBe("surveyJs");
});
});
describe("buildWhereClause", () => {
test("should return an empty AND array if no filterCriteria is provided", () => {
const result = buildWhereClause();
expect(result).toEqual({ AND: [] });
});
test("should build where clause for name", () => {
const filterCriteria: TSurveyFilterCriteria = { name: "Test Survey" };
const result = buildWhereClause(filterCriteria);
expect(result.AND).toContainEqual({ name: { contains: "Test Survey", mode: "insensitive" } });
});
test("should build where clause for status", () => {
const filterCriteria: TSurveyFilterCriteria = { status: ["draft", "paused"] };
const result = buildWhereClause(filterCriteria);
expect(result.AND).toContainEqual({ status: { in: ["draft", "paused"] } });
});
test("should build where clause for type", () => {
const filterCriteria: TSurveyFilterCriteria = { type: ["link", "app"] };
const result = buildWhereClause(filterCriteria);
expect(result.AND).toContainEqual({ type: { in: ["link", "app"] } });
});
test("should build where clause for createdBy 'you'", () => {
const filterCriteria: TSurveyFilterCriteria = {
createdBy: { value: ["you"], userId: "user123" },
};
const result = buildWhereClause(filterCriteria);
expect(result.AND).toContainEqual({ createdBy: "user123" });
});
test("should build where clause for createdBy 'others'", () => {
const filterCriteria: TSurveyFilterCriteria = {
createdBy: { value: ["others"], userId: "user123" },
};
const result = buildWhereClause(filterCriteria);
expect(result.AND).toContainEqual({
OR: [
{
createdBy: {
not: "user123",
},
},
{
createdBy: null,
},
],
});
});
test("should build where clause for multiple criteria", () => {
const filterCriteria: TSurveyFilterCriteria = {
name: "Feedback Survey",
status: ["inProgress" as TSurveyStatus],
type: ["app" as TSurveyType],
createdBy: { value: ["you"], userId: "user456" },
};
const result = buildWhereClause(filterCriteria);
expect(result.AND).toEqual([
{ name: { contains: "Feedback Survey", mode: "insensitive" } },
{ status: { in: ["inProgress" as TSurveyStatus] } },
{ type: { in: ["app" as TSurveyType] } },
{ createdBy: "user456" },
]);
});
test("should not add createdBy clause if value is empty or not 'you' or 'others'", () => {
let filterCriteria: TSurveyFilterCriteria = { createdBy: { value: [], userId: "user123" } };
let result = buildWhereClause(filterCriteria);
expect(result.AND).not.toContainEqual(expect.objectContaining({ createdBy: expect.anything() }));
filterCriteria = { createdBy: { value: ["others"], userId: "user123" } };
result = buildWhereClause(filterCriteria);
expect(result.AND).not.toContainEqual(expect.objectContaining({ createdBy: expect.anything() }));
});
});
describe("buildOrderByClause", () => {
test("should return undefined if no sortBy is provided", () => {
const result = buildOrderByClause();
expect(result).toBeUndefined();
});
test("should return orderBy clause for name", () => {
const result = buildOrderByClause("name");
expect(result).toEqual([{ name: "asc" }]);
});
test("should return orderBy clause for createdAt", () => {
const result = buildOrderByClause("createdAt");
expect(result).toEqual([{ createdAt: "desc" }]);
});
test("should return orderBy clause for updatedAt", () => {
const result = buildOrderByClause("updatedAt");
expect(result).toEqual([{ updatedAt: "desc" }]);
});
test("should default to updatedAt for unknown sortBy value", () => {
const result = buildOrderByClause("invalidSortBy" as any);
expect(result).toEqual([{ updatedAt: "desc" }]);
});
});
describe("anySurveyHasFilters", () => {
test("should return true if any survey has segment filters", () => {
const surveys: TSurvey[] = [
{ id: "1", name: "Survey 1", segment: { id: "seg1", filters: [{ id: "f1" }] } } as TSurvey,
{ id: "2", name: "Survey 2", segment: null } as TSurvey,
];
expect(anySurveyHasFilters(surveys)).toBe(true);
});
test("should return false if no survey has segment filters", () => {
const surveys: TSurvey[] = [
{ id: "1", name: "Survey 1", segment: { id: "seg1", filters: [] } } as unknown as TSurvey,
{ id: "2", name: "Survey 2", segment: null } as TSurvey,
];
expect(anySurveyHasFilters(surveys)).toBe(false);
});
test("should return false if surveys array is empty", () => {
const surveys: TSurvey[] = [];
expect(anySurveyHasFilters(surveys)).toBe(false);
});
test("should return false if segment is null or filters are undefined", () => {
const surveys: TSurvey[] = [
{ id: "1", name: "Survey 1", segment: null } as TSurvey,
{ id: "2", name: "Survey 2", segment: { id: "seg2" } } as TSurvey, // filters undefined
];
expect(anySurveyHasFilters(surveys)).toBe(false);
});
test("should handle surveys that are not TSurvey but TJsEnvironmentStateSurvey (no segment)", () => {
const surveys = [
{ id: "js1", name: "JS Survey 1" }, // TJsEnvironmentStateSurvey like, no segment property
] as any[]; // Using any[] to simulate mixed types or types without segment
expect(anySurveyHasFilters(surveys)).toBe(false);
});
});
});