mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-24 01:03:41 -06:00
chore: modules/survey/lib and modules/ee/contacts/api/v1 (#5711)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
142
apps/web/modules/survey/lib/action-class.test.ts
Normal file
142
apps/web/modules/survey/lib/action-class.test.ts
Normal 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.
|
||||
});
|
||||
});
|
||||
28
apps/web/modules/survey/lib/client-utils.test.ts
Normal file
28
apps/web/modules/survey/lib/client-utils.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
153
apps/web/modules/survey/lib/organization.test.ts
Normal file
153
apps/web/modules/survey/lib/organization.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
148
apps/web/modules/survey/lib/project.test.ts
Normal file
148
apps/web/modules/survey/lib/project.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
83
apps/web/modules/survey/lib/response.test.ts
Normal file
83
apps/web/modules/survey/lib/response.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
124
apps/web/modules/survey/lib/survey.test.ts
Normal file
124
apps/web/modules/survey/lib/survey.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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}`],
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
249
apps/web/modules/survey/lib/utils.test.ts
Normal file
249
apps/web/modules/survey/lib/utils.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user