diff --git a/apps/web/modules/ee/contacts/lib/attributes.test.ts b/apps/web/modules/ee/contacts/lib/attributes.test.ts index f340e18d8f..2675da913f 100644 --- a/apps/web/modules/ee/contacts/lib/attributes.test.ts +++ b/apps/web/modules/ee/contacts/lib/attributes.test.ts @@ -20,7 +20,7 @@ vi.mock("@/modules/ee/contacts/lib/contact-attributes", () => ({ vi.mock("@formbricks/database", () => ({ prisma: { $transaction: vi.fn(), - contactAttribute: { upsert: vi.fn() }, + contactAttribute: { upsert: vi.fn(), findMany: vi.fn(), deleteMany: vi.fn() }, contactAttributeKey: { create: vi.fn() }, }, })); @@ -52,17 +52,37 @@ const attributeKeys: TContactAttributeKey[] = [ type: "default", environmentId, }, + { + id: "key-3", + key: "customAttr", + createdAt: new Date(), + updatedAt: new Date(), + isUnique: false, + name: "Custom Attribute", + description: null, + type: "custom", + environmentId, + }, ]; describe("updateAttributes", () => { beforeEach(() => { vi.clearAllMocks(); + // Set default mock return values - these will be overridden in individual tests + vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue([]); + vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 }); + vi.mocked(prisma.$transaction).mockResolvedValue(undefined); }); test("updates existing attributes", async () => { vi.mocked(getContactAttributeKeys).mockResolvedValue(attributeKeys); vi.mocked(hasEmailAttribute).mockResolvedValue(false); + vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue([ + { attributeKey: { key: "name" } }, + { attributeKey: { key: "email" } }, + ] as any); vi.mocked(prisma.$transaction).mockResolvedValue(undefined); + vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 }); const attributes = { name: "John", email: "john@example.com" }; const result = await updateAttributes(contactId, userId, environmentId, attributes); expect(prisma.$transaction).toHaveBeenCalled(); @@ -73,7 +93,12 @@ describe("updateAttributes", () => { test("skips updating email if it already exists", async () => { vi.mocked(getContactAttributeKeys).mockResolvedValue(attributeKeys); vi.mocked(hasEmailAttribute).mockResolvedValue(true); + vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue([ + { attributeKey: { key: "name" } }, + { attributeKey: { key: "email" } }, + ] as any); vi.mocked(prisma.$transaction).mockResolvedValue(undefined); + vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 }); const attributes = { name: "John", email: "john@example.com" }; const result = await updateAttributes(contactId, userId, environmentId, attributes); expect(prisma.$transaction).toHaveBeenCalled(); @@ -85,7 +110,9 @@ describe("updateAttributes", () => { test("creates new attributes if under limit", async () => { vi.mocked(getContactAttributeKeys).mockResolvedValue([attributeKeys[0]]); vi.mocked(hasEmailAttribute).mockResolvedValue(false); + vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue([{ attributeKey: { key: "name" } }] as any); vi.mocked(prisma.$transaction).mockResolvedValue(undefined); + vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 }); const attributes = { name: "John", newAttr: "val" }; const result = await updateAttributes(contactId, userId, environmentId, attributes); expect(prisma.$transaction).toHaveBeenCalled(); @@ -97,7 +124,12 @@ describe("updateAttributes", () => { test("does not create new attributes if over the limit", async () => { vi.mocked(getContactAttributeKeys).mockResolvedValue(attributeKeys); vi.mocked(hasEmailAttribute).mockResolvedValue(false); + vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue([ + { attributeKey: { key: "name" } }, + { attributeKey: { key: "email" } }, + ] as any); vi.mocked(prisma.$transaction).mockResolvedValue(undefined); + vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 }); const attributes = { name: "John", newAttr: "val" }; const result = await updateAttributes(contactId, userId, environmentId, attributes); expect(result.success).toBe(true); @@ -107,10 +139,109 @@ describe("updateAttributes", () => { test("returns success with no attributes to update or create", async () => { vi.mocked(getContactAttributeKeys).mockResolvedValue([]); vi.mocked(hasEmailAttribute).mockResolvedValue(false); + vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue([]); vi.mocked(prisma.$transaction).mockResolvedValue(undefined); + vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 }); const attributes = {}; const result = await updateAttributes(contactId, userId, environmentId, attributes); expect(result.success).toBe(true); expect(result.messages).toEqual([]); }); + + test("deletes non-default attributes that are removed from payload", async () => { + // Reset mocks explicitly for this test + vi.mocked(prisma.contactAttribute.deleteMany).mockClear(); + + vi.mocked(getContactAttributeKeys).mockResolvedValue(attributeKeys); + vi.mocked(hasEmailAttribute).mockResolvedValue(false); + vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue([ + { attributeKey: { key: "name" } }, + { attributeKey: { key: "email" } }, + { attributeKey: { key: "customAttr" } }, + ] as any); + vi.mocked(prisma.$transaction).mockResolvedValue(undefined); + vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 1 }); + const attributes = { name: "John", email: "john@example.com" }; + const result = await updateAttributes(contactId, userId, environmentId, attributes); + // Only customAttr (key-3) should be deleted, not name (key-1) or email (key-2) + expect(prisma.contactAttribute.deleteMany).toHaveBeenCalledWith({ + where: { + contactId, + attributeKeyId: { + in: ["key-3"], + }, + }, + }); + expect(result.success).toBe(true); + expect(result.messages).toEqual([]); + }); + + test("does not delete default attributes even if removed from payload", async () => { + // Reset mocks explicitly for this test + vi.mocked(prisma.contactAttribute.deleteMany).mockClear(); + + // Need to include userId and firstName in attributeKeys for this test + // Note: DEFAULT_ATTRIBUTES includes: email, userId, firstName, lastName (not "name") + const attributeKeysWithDefaults: TContactAttributeKey[] = [ + { + id: "key-2", + key: "email", + createdAt: new Date(), + updatedAt: new Date(), + isUnique: false, + name: "Email", + description: null, + type: "default", + environmentId, + }, + { + id: "key-4", + key: "userId", + createdAt: new Date(), + updatedAt: new Date(), + isUnique: false, + name: "User ID", + description: null, + type: "default", + environmentId, + }, + { + id: "key-5", + key: "firstName", + createdAt: new Date(), + updatedAt: new Date(), + isUnique: false, + name: "First Name", + description: null, + type: "default", + environmentId, + }, + { + id: "key-3", + key: "customAttr", + createdAt: new Date(), + updatedAt: new Date(), + isUnique: false, + name: "Custom Attribute", + description: null, + type: "custom", + environmentId, + }, + ]; + vi.mocked(getContactAttributeKeys).mockResolvedValue(attributeKeysWithDefaults); + vi.mocked(hasEmailAttribute).mockResolvedValue(false); + vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue([ + { attributeKey: { key: "email" } }, + { attributeKey: { key: "userId" } }, + { attributeKey: { key: "firstName" } }, + ] as any); + vi.mocked(prisma.$transaction).mockResolvedValue(undefined); + vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 }); + const attributes = { customAttr: "value" }; + const result = await updateAttributes(contactId, userId, environmentId, attributes); + // Should not delete default attributes (email, userId, firstName) - deleteMany should not be called + // since all current attributes are default attributes + expect(prisma.contactAttribute.deleteMany).not.toHaveBeenCalled(); + expect(result.success).toBe(true); + }); }); diff --git a/apps/web/modules/ee/contacts/lib/update-contact-attributes.test.ts b/apps/web/modules/ee/contacts/lib/update-contact-attributes.test.ts index 6e5a787cee..748fc6d0c7 100644 --- a/apps/web/modules/ee/contacts/lib/update-contact-attributes.test.ts +++ b/apps/web/modules/ee/contacts/lib/update-contact-attributes.test.ts @@ -21,6 +21,7 @@ describe("updateContactAttributes", () => { const environmentId = "env123"; const userId = "user123"; const attributes = { + userId, firstName: "John", lastName: "Doe", email: "john@example.com", @@ -37,9 +38,39 @@ describe("updateContactAttributes", () => { }; const mockCurrentKeys = [ - { id: "key1", key: "firstName", name: "First Name", environmentId }, - { id: "key2", key: "lastName", name: "Last Name", environmentId }, - { id: "key3", key: "email", name: "Email", environmentId }, + { + id: "key1", + key: "firstName", + name: "First Name", + environmentId, + createdAt: new Date(), + updatedAt: new Date(), + type: "default" as const, + isUnique: false, + description: null, + }, + { + id: "key2", + key: "lastName", + name: "Last Name", + environmentId, + createdAt: new Date(), + updatedAt: new Date(), + type: "default" as const, + isUnique: false, + description: null, + }, + { + id: "key3", + key: "email", + name: "Email", + environmentId, + createdAt: new Date(), + updatedAt: new Date(), + type: "default" as const, + isUnique: false, + description: null, + }, ]; const mockUpdatedAttributes = { @@ -84,10 +115,42 @@ describe("updateContactAttributes", () => { }, }; - const mockCurrentKeys = [{ id: "key1", key: "firstName", name: "First Name", environmentId }]; + const mockCurrentKeys = [ + { + id: "key1", + key: "firstName", + name: "First Name", + environmentId, + createdAt: new Date(), + updatedAt: new Date(), + type: "default" as const, + isUnique: false, + description: null, + }, + ]; const mockUpdatedKeys = [ - { id: "key1", key: "firstName", name: "First Name", environmentId }, - { id: "key2", key: "newCustomField", name: "newCustomField", environmentId }, + { + id: "key1", + key: "firstName", + name: "First Name", + environmentId, + createdAt: new Date(), + updatedAt: new Date(), + type: "default" as const, + isUnique: false, + description: null, + }, + { + id: "key2", + key: "newCustomField", + name: "newCustomField", + environmentId, + createdAt: new Date(), + updatedAt: new Date(), + type: "custom" as const, + isUnique: false, + description: null, + }, ]; const mockUpdatedAttributes = { @@ -107,7 +170,17 @@ describe("updateContactAttributes", () => { expect(result.updatedAttributes).toEqual(mockUpdatedAttributes); expect(result.updatedAttributeKeys).toEqual([ - { id: "key2", key: "newCustomField", name: "newCustomField", environmentId }, + { + id: "key2", + key: "newCustomField", + name: "newCustomField", + environmentId, + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + type: "custom", + isUnique: false, + description: null, + }, ]); }); @@ -126,7 +199,19 @@ describe("updateContactAttributes", () => { }, }; - const mockCurrentKeys = [{ id: "key1", key: "firstName", name: "First Name", environmentId }]; + const mockCurrentKeys = [ + { + id: "key1", + key: "firstName", + name: "First Name", + environmentId, + createdAt: new Date(), + updatedAt: new Date(), + type: "default" as const, + isUnique: false, + description: null, + }, + ]; const mockUpdatedAttributes = { firstName: "John", }; @@ -163,7 +248,19 @@ describe("updateContactAttributes", () => { }, }; - const mockCurrentKeys = [{ id: "key1", key: "email", name: "Email", environmentId }]; + const mockCurrentKeys = [ + { + id: "key1", + key: "email", + name: "Email", + environmentId, + createdAt: new Date(), + updatedAt: new Date(), + type: "default" as const, + isUnique: false, + description: null, + }, + ]; const mockUpdatedAttributes = { email: "existing@example.com", }; @@ -190,6 +287,8 @@ describe("updateContactAttributes", () => { vi.mocked(getContact).mockResolvedValue(null); - await expect(updateContactAttributes(contactId, attributes)).rejects.toThrow("Contact not found"); + await expect(updateContactAttributes(contactId, attributes)).rejects.toThrow( + "contact with ID contact123 not found" + ); }); });