import prismock from "../../../../../tests/libs/__mocks__/prisma"; import { describe, expect, it, vi, beforeEach } from "vitest"; import prisma from "@calcom/prisma"; import type { AttributeOption } from "@calcom/prisma/client"; import { AttributeType, MembershipRole } from "@calcom/prisma/enums"; import { assignValueToUserInOrgBulk, buildPrismaQueriesForAttributeOptionToUser } from "./assignValueToUser"; function buildMockAttribute(data: { teamId: number; id: string; type: AttributeType; name: string; slug: string; isLocked: boolean; }) { return { usersCanEditRelation: true, isWeightsEnabled: false, createdAt: new Date(), updatedAt: new Date(), enabled: true, ...data, }; } async function createMockAttribute({ options, ...attributeData }: Parameters[0] & { options: Pick[]; }) { const attribute = await prismock.attribute.create({ data: buildMockAttribute(attributeData) }); const attributeOptions = await prismock.attributeOption.createMany({ data: options.map((option) => ({ ...option, attributeId: attribute.id, })), }); return { attribute, attributeOptions, }; } async function createMockUserWithMembership({ orgId }: { orgId: number }) { const user = await prismock.user.create({ data: { name: "Test User", email: "test@test.com", }, }); const membership = await prismock.membership.create({ data: { role: MembershipRole.MEMBER, disableImpersonation: false, accepted: true, teamId: orgId, userId: user.id, }, }); return { user: user, membership: membership, }; } async function createMockDSyncData({ directoryId, tenant, organizationId, }: { directoryId: string; tenant: string; organizationId: number; }) { return prismock.dSyncData.create({ data: { directoryId, tenant, organizationId, }, }); } async function createMockAssignment({ membershipId, attributeOptionId, dsyncId, }: { membershipId: number; attributeOptionId: string; orgId: number; dsyncId: string | null; }) { let dsync; if (dsyncId) { dsync = await prismock.dSyncData.findUnique({ where: { id: dsyncId as unknown as number, }, }); } const attributeToUser = await prismock.attributeToUser.create({ data: { memberId: membershipId, attributeOptionId, createdByDSyncId: dsync ? (dsync.id as unknown as string) : null, }, }); return { attributeToUser, dsync, }; } describe("addValueForMember", () => { const orgId = 1001; beforeEach(() => { vi.clearAllMocks(); }); describe("Misc", () => { let dsyncDoingAssignment_id: string; beforeEach(async () => { const dsync = await createMockDSyncData({ directoryId: "1", tenant: "testorg-000", organizationId: orgId, }); dsyncDoingAssignment_id = dsync.id as unknown as string; }); it("should correctly identify attribute and attribute options by name and set them", async () => { await createMockAttribute({ teamId: orgId, id: "1", name: "department", slug: "department-slug", type: AttributeType.SINGLE_SELECT, isLocked: true, options: [ { id: "101", value: "engineering", slug: "engineering-slug" }, { id: "102", value: "sales", slug: "sales-slug" }, ], }); const { user, membership } = await createMockUserWithMembership({ orgId }); await assignValueToUserInOrgBulk({ orgId, userId: user.id, attributeLabelToValueMap: { Department: "Sales", }, updater: { userId: 999, }, }); const createdAttributeToUsers = await prisma.attributeToUser.findMany({ where: { memberId: membership.id, }, }); expect(createdAttributeToUsers).toHaveLength(1); expect(createdAttributeToUsers[0].attributeOptionId).toEqual("102"); // Do another update. This should reset the options assigned to the user and add just this one await assignValueToUserInOrgBulk({ orgId, userId: user.id, attributeLabelToValueMap: { Department: "engineering", }, updater: { userId: 999, }, }); const createdAttributeToUsers2 = await prisma.attributeToUser.findMany({ where: { memberId: membership.id, }, }); expect(createdAttributeToUsers2).toHaveLength(1); expect(createdAttributeToUsers2[0].attributeOptionId).toEqual("101"); }); it("should allow setting an array of strings as a value for a multi-select attribute", async () => { await createMockAttribute({ teamId: orgId, id: "1", name: "department", slug: "department-slug", type: AttributeType.MULTI_SELECT, isLocked: true, options: [ { id: "101", value: "engineering", slug: "engineering-slug" }, { id: "102", value: "sales", slug: "sales-slug" }, { id: "103", value: "marketing", slug: "marketing-slug" }, ], }); const { user, membership } = await createMockUserWithMembership({ orgId }); // In Cal.com user created pool await createMockAssignment({ orgId, membershipId: membership.id, // Sales attributeOptionId: "102", dsyncId: null, }); await assignValueToUserInOrgBulk({ orgId, userId: user.id, attributeLabelToValueMap: { Department: ["marketing", "engineering", "unknown"], }, // Setting value using the same dsyncId that was used to create the option in SCIM Pool updater: { dsyncId: dsyncDoingAssignment_id, }, }); const attributeToUsers = await prisma.attributeToUser.findMany({ where: { memberId: membership.id, }, }); expect(attributeToUsers).toHaveLength(2); expect(attributeToUsers[0].attributeOptionId).toEqual("103"); expect(attributeToUsers[1].attributeOptionId).toEqual("101"); }); }); describe("Locked Attribute(Source of Truth - Dsync)", () => { const isLocked = true; describe("Update from SCIM", async () => { let dsyncDoingAssignment_id: string; beforeEach(async () => { const dsync = await createMockDSyncData({ directoryId: "1", tenant: "testorg-000", organizationId: orgId, }); dsyncDoingAssignment_id = dsync.id as unknown as string; }); it("should reset all the options(Multi-Select) in all the SCIM Pools and Cal.com User Pool", async () => { await createMockAttribute({ teamId: orgId, id: "1", name: "department", slug: "department-slug", type: AttributeType.MULTI_SELECT, isLocked, options: [ { id: "101", value: "engineering", slug: "engineering-slug" }, { id: "102", value: "sales", slug: "sales-slug" }, { id: "103", value: "marketing", slug: "marketing-slug" }, ], }); const { user, membership } = await createMockUserWithMembership({ orgId }); // In Cal.com user created pool await createMockAssignment({ orgId, membershipId: membership.id, // Sales attributeOptionId: "102", dsyncId: null, }); // In SCIM dsync-1 Pool await createMockAssignment({ orgId, membershipId: membership.id, // Engineering attributeOptionId: "101", dsyncId: dsyncDoingAssignment_id, }); const dsync2 = await createMockDSyncData({ directoryId: "2", tenant: "testorg-000", organizationId: orgId, }); // In SCIM dsync-2 Pool await createMockAssignment({ orgId, membershipId: membership.id, // Engineering attributeOptionId: "101", dsyncId: dsync2!.id as unknown as string, }); await assignValueToUserInOrgBulk({ orgId, userId: user.id, attributeLabelToValueMap: { Department: "marketing", }, // Setting value using the same dsyncId that was used to create the option in SCIM Pool updater: { dsyncId: dsyncDoingAssignment_id, }, }); const attributeToUsers = await prisma.attributeToUser.findMany({ where: { memberId: membership.id, }, }); expect(attributeToUsers).toHaveLength(1); // Cal.com User Pool - Intact expect(attributeToUsers[0].attributeOptionId).toEqual("103"); }); it("should allow setting an array of strings as a value for a multi-select attribute", async () => { await createMockAttribute({ teamId: orgId, id: "1", name: "department", slug: "department-slug", type: AttributeType.MULTI_SELECT, isLocked, options: [ { id: "101", value: "engineering", slug: "engineering-slug" }, { id: "102", value: "sales", slug: "sales-slug" }, { id: "103", value: "marketing", slug: "marketing-slug" }, ], }); const { user, membership } = await createMockUserWithMembership({ orgId }); // In Cal.com user created pool await createMockAssignment({ orgId, membershipId: membership.id, // Sales attributeOptionId: "102", dsyncId: null, }); // In SCIM dsync-1 Pool await createMockAssignment({ orgId, membershipId: membership.id, // Engineering attributeOptionId: "101", dsyncId: dsyncDoingAssignment_id, }); const dsync2 = await createMockDSyncData({ directoryId: "2", tenant: "testorg-000", organizationId: orgId, }); // In SCIM dsync-2 Pool await createMockAssignment({ orgId, membershipId: membership.id, // Engineering attributeOptionId: "101", dsyncId: dsync2!.id as unknown as string, }); await assignValueToUserInOrgBulk({ orgId, userId: user.id, attributeLabelToValueMap: { Department: ["marketing", "engineering"], }, // Setting value using the same dsyncId that was used to create the option in SCIM Pool updater: { dsyncId: dsyncDoingAssignment_id, }, }); const attributeToUsers = await prisma.attributeToUser.findMany({ where: { memberId: membership.id, }, }); expect(attributeToUsers).toHaveLength(2); expect(attributeToUsers[0].attributeOptionId).toEqual("103"); expect(attributeToUsers[1].attributeOptionId).toEqual("101"); }); it("should not recreate an assignment that already exists", async () => { await createMockAttribute({ teamId: orgId, id: "1", name: "department", slug: "department-slug", type: AttributeType.SINGLE_SELECT, isLocked, options: [ { id: "101", value: "engineering", slug: "engineering-slug" }, { id: "102", value: "sales", slug: "sales-slug" }, ], }); const { user, membership } = await createMockUserWithMembership({ orgId }); // In Cal.com user created pool await createMockAssignment({ orgId, membershipId: membership.id, // Sales attributeOptionId: "102", dsyncId: null, }); const beforeAssignments = await prisma.attributeToUser.findMany({ where: { memberId: membership.id, }, }); await assignValueToUserInOrgBulk({ orgId, userId: user.id, attributeLabelToValueMap: { Department: "Sales", }, updater: { dsyncId: dsyncDoingAssignment_id, }, }); const afterAssignments = await prisma.attributeToUser.findMany({ where: { memberId: membership.id, }, }); expect(afterAssignments).toHaveLength(1); expect(afterAssignments[0].attributeOptionId).toEqual("102"); expect(beforeAssignments[0].id).toEqual(afterAssignments[0].id); expect(beforeAssignments[0].createdByDSyncId).toEqual(afterAssignments[0].createdByDSyncId); expect(afterAssignments[0].updatedByDSyncId).toEqual(dsyncDoingAssignment_id); }); it("should be able to set a value for TEXT(non-enum) attribute that already has an option set by Cal.com user", async () => { await createMockAttribute({ teamId: orgId, id: "1", name: "department", slug: "department-slug", type: AttributeType.TEXT, isLocked, options: [{ id: "102", value: "sales", slug: "sales-slug" }], }); const { user, membership } = await createMockUserWithMembership({ orgId }); // "Sales" Option set by cal.com user await createMockAssignment({ orgId, membershipId: membership.id, // Sales attributeOptionId: "102", dsyncId: null, }); await assignValueToUserInOrgBulk({ orgId, userId: user.id, attributeLabelToValueMap: { Department: "not-already-an-option", }, updater: { dsyncId: dsyncDoingAssignment_id, }, }); const attributeToUsers = await prisma.attributeToUser.findMany({ where: { memberId: membership.id, }, select: { attributeOption: { select: { id: true, value: true, }, }, }, }); expect(attributeToUsers).toHaveLength(1); expect(attributeToUsers[0].attributeOption.value).toEqual("not-already-an-option"); }); it.skip("should only delete the assignments for the user that is being pushed", async () => { // TODO }); }); }); describe("Unlocked Attribute", () => { const isLocked = false; describe("Update from SCIM", () => { let dsyncDoingAssignment_id: string; beforeEach(async () => { const dsync = await createMockDSyncData({ directoryId: "1", tenant: "testorg-000", organizationId: orgId, }); dsyncDoingAssignment_id = dsync.id as unknown as string; }); it("should reset the options(Multi-Select) in the SCIM Pool only and keep Cal.com User Pool options intact when assignValueToUserInOrgBulk is from SCIM", async () => { await createMockAttribute({ teamId: orgId, id: "1", name: "department", slug: "department-slug", type: AttributeType.MULTI_SELECT, isLocked, options: [ { id: "scim-101", value: "engineering", slug: "engineering-slug" }, { id: "calcom-102", value: "sales", slug: "sales-slug" }, { id: "103", value: "marketing", slug: "marketing-slug" }, ], }); const { user, membership } = await createMockUserWithMembership({ orgId }); // In Cal.com user created pool await createMockAssignment({ orgId, membershipId: membership.id, // Sales attributeOptionId: "calcom-102", dsyncId: null, }); // In SCIM Pool await createMockAssignment({ orgId, membershipId: membership.id, // Engineering attributeOptionId: "scim-101", dsyncId: dsyncDoingAssignment_id, }); await assignValueToUserInOrgBulk({ orgId, userId: user.id, attributeLabelToValueMap: { Department: "marketing", }, updater: { dsyncId: dsyncDoingAssignment_id, }, }); const attributeToUsers = await prisma.attributeToUser.findMany({ where: { memberId: membership.id, }, }); expect(attributeToUsers).toHaveLength(2); // Cal.com User Pool - Intact expect(attributeToUsers[0].attributeOptionId).toEqual("calcom-102"); // SCIM Pool - Previous value removed expect(attributeToUsers[1].attributeOptionId).toEqual("103"); }); it("should not be able to set a value for single-select attribute that already has an option set by Cal.com user", async () => { await createMockAttribute({ teamId: orgId, id: "1", name: "department", slug: "department-slug", type: AttributeType.SINGLE_SELECT, isLocked, options: [ { id: "101", value: "engineering", slug: "engineering-slug" }, { id: "102", value: "sales", slug: "sales-slug" }, { id: "103", value: "marketing", slug: "marketing-slug" }, ], }); const { user, membership } = await createMockUserWithMembership({ orgId }); // "Sales" Option set by cal.com user await createMockAssignment({ orgId, membershipId: membership.id, // Sales attributeOptionId: "102", dsyncId: null, }); const dsync = await createMockDSyncData({ directoryId: "1", tenant: "testorg-000", organizationId: orgId, }); await assignValueToUserInOrgBulk({ orgId, userId: user.id, attributeLabelToValueMap: { Department: "engineering", }, updater: { dsyncId: dsync.id as unknown as string, }, }); const attributeToUsers = await prisma.attributeToUser.findMany({ where: { memberId: membership.id, }, select: { attributeOption: { select: { id: true, value: true, }, }, }, }); expect(attributeToUsers).toHaveLength(1); expect(attributeToUsers[0].attributeOption.value).toEqual("sales"); expect(attributeToUsers[0].attributeOption.id).toEqual("102"); }); it("should not be able to set a value for TEXT(non-enum) attribute that already has an option set by Cal.com user", async () => { await createMockAttribute({ teamId: orgId, id: "1", name: "department", slug: "department-slug", type: AttributeType.TEXT, isLocked, options: [{ id: "102", value: "sales", slug: "sales-slug" }], }); const { user, membership } = await createMockUserWithMembership({ orgId }); // "Sales" Option set by cal.com user await createMockAssignment({ orgId, membershipId: membership.id, // Sales attributeOptionId: "102", dsyncId: null, }); const dsync = await createMockDSyncData({ directoryId: "1", tenant: "testorg-000", organizationId: orgId, }); await assignValueToUserInOrgBulk({ orgId, userId: user.id, attributeLabelToValueMap: { Department: "not-already-an-option", }, updater: { dsyncId: dsync.id as unknown as string, }, }); const attributeToUsers = await prisma.attributeToUser.findMany({ where: { memberId: membership.id, }, select: { attributeOption: { select: { id: true, value: true, }, }, }, }); expect(attributeToUsers).toHaveLength(1); expect(attributeToUsers[0].attributeOption.value).toEqual("sales"); expect(attributeToUsers[0].attributeOption.id).toEqual("102"); }); it("should not recreate an assignment that already exists", async () => { await createMockAttribute({ teamId: orgId, id: "1", name: "department", slug: "department-slug", type: AttributeType.SINGLE_SELECT, isLocked, options: [ { id: "101", value: "engineering", slug: "engineering-slug" }, { id: "102", value: "sales", slug: "sales-slug" }, ], }); const { user, membership } = await createMockUserWithMembership({ orgId }); // In Cal.com user created pool await createMockAssignment({ orgId, membershipId: membership.id, // Sales attributeOptionId: "102", dsyncId: null, }); const beforeAssignments = await prisma.attributeToUser.findMany({ where: { memberId: membership.id, }, }); await assignValueToUserInOrgBulk({ orgId, userId: user.id, attributeLabelToValueMap: { Department: "Sales", }, updater: { dsyncId: dsyncDoingAssignment_id, }, }); const afterAssignments = await prisma.attributeToUser.findMany({ where: { memberId: membership.id, }, }); expect(afterAssignments).toHaveLength(1); expect(afterAssignments[0].attributeOptionId).toEqual("102"); expect(beforeAssignments[0].id).toEqual(afterAssignments[0].id); }); it.skip("should only delete the assignments for the user that is being pushed", async () => { // TODO }); }); describe("Update from Cal.com", () => { const userDoingAssignment_id = 999; it("should reset the options(Multi-Select) in the Cal.com User Pool only and keep SCIM Pool options intact when assignValueToUserInOrgBulk is from Cal.com User Pool", async () => { await createMockAttribute({ teamId: orgId, id: "1", name: "department", slug: "department-slug", type: AttributeType.MULTI_SELECT, isLocked: false, options: [ { id: "101", value: "engineering", slug: "engineering-slug" }, { id: "102", value: "sales", slug: "sales-slug" }, { id: "103", value: "marketing", slug: "marketing-slug" }, ], }); const { user, membership } = await createMockUserWithMembership({ orgId }); // In Cal.com user created pool await createMockAssignment({ orgId, membershipId: membership.id, // Sales attributeOptionId: "102", dsyncId: null, }); const dsync = await createMockDSyncData({ directoryId: "1", tenant: "testorg-000", organizationId: orgId, }); // In SCIM Pool await createMockAssignment({ orgId, membershipId: membership.id, // Engineering attributeOptionId: "101", //@ts-expect-error - prismock can't handle string ids it seems dsyncId: dsync.id, }); await assignValueToUserInOrgBulk({ orgId, userId: user.id, attributeLabelToValueMap: { Department: "marketing", }, updater: { userId: userDoingAssignment_id, }, }); const attributeToUsers = await prisma.attributeToUser.findMany({ where: { memberId: membership.id, }, }); expect(attributeToUsers).toHaveLength(2); // Cal.com User Pool - Previous value removed and new value added expect(attributeToUsers[1].attributeOptionId).toEqual("103"); }); it("should not be able to assign values beyond the options for SINGLE_SELECT(enum based) attribute", async () => { await createMockAttribute({ teamId: orgId, id: "1", name: "department", slug: "department-slug", type: AttributeType.SINGLE_SELECT, isLocked: false, options: [ { id: "101", value: "engineering", slug: "engineering-slug" }, { id: "102", value: "sales", slug: "sales-slug" }, { id: "103", value: "marketing", slug: "marketing-slug" }, ], }); const { user, membership } = await createMockUserWithMembership({ orgId }); await assignValueToUserInOrgBulk({ orgId, userId: user.id, attributeLabelToValueMap: { Department: "not-already-an-option", }, updater: { userId: userDoingAssignment_id, }, }); const attributeToUsers = await prisma.attributeToUser.findMany({ where: { memberId: membership.id, }, select: { attributeOption: { select: { id: true, value: true, }, }, }, }); expect(attributeToUsers).toHaveLength(0); }); it("should be able to assign values beyond the options for TEXT(non-enum) attribute", async () => { await createMockAttribute({ teamId: orgId, id: "1", name: "department", slug: "department-slug", type: AttributeType.TEXT, isLocked: false, options: [ { id: "101", value: "engineering", slug: "engineering-slug" }, { id: "102", value: "sales", slug: "sales-slug" }, { id: "103", value: "marketing", slug: "marketing-slug" }, ], }); const { user, membership } = await createMockUserWithMembership({ orgId }); await assignValueToUserInOrgBulk({ orgId, userId: user.id, attributeLabelToValueMap: { Department: "not-already-an-option", }, updater: { userId: userDoingAssignment_id, }, }); const attributeToUsers = await prisma.attributeToUser.findMany({ where: { memberId: membership.id, }, select: { attributeOption: { select: { id: true, value: true, }, }, }, }); expect(attributeToUsers).toHaveLength(1); expect(attributeToUsers[0].attributeOption.value).toEqual("not-already-an-option"); }); it.skip("should only delete the assignments for the user that is being pushed", async () => { // TODO }); }); }); }); describe("buildPrismaQueriesForAttributeOptionToUser", () => { describe("when building queries for attribute option to user", () => { const mockCreator = { userId: 123 }; const mockMemberId = 456; const mockOrgId = 789; it("should not return any queries when there are no assignments", () => { const result = buildPrismaQueriesForAttributeOptionToUser({ orgId: mockOrgId, memberId: mockMemberId, existingAttributeOptionAssignments: [], attributeOptionsToAssign: [], updater: mockCreator, }); expect(result.attributeToUserCreateManyInput).toHaveLength(0); expect(result.attributeToUserUpdateManyInput).toBeNull(); expect(result.attributeToUserDeleteQueries.locked).toBeNull(); expect(result.attributeToUserDeleteQueries.unlocked).toEqual(null); }); it("should create new assignments for new options", () => { const attributeOptionsToAssign = [ { attribute: { id: "attr1", isLocked: false }, optionsToAssign: [{ id: "opt1", label: "Option 1" }], }, ]; const result = buildPrismaQueriesForAttributeOptionToUser({ orgId: mockOrgId, memberId: mockMemberId, existingAttributeOptionAssignments: [], attributeOptionsToAssign, updater: mockCreator, }); expect(result.attributeToUserCreateManyInput).toHaveLength(1); expect(result.attributeToUserCreateManyInput[0]).toEqual({ memberId: mockMemberId, attributeOptionId: "opt1", createdById: mockCreator.userId, createdByDSyncId: null, }); expect(result.attributeToUserUpdateManyInput).toBeNull(); expect(result.attributeToUserDeleteQueries.locked).toBeNull(); expect(result.attributeToUserDeleteQueries.unlocked).toBeNull(); }); it("should handle locked attributes correctly", () => { const attributeOptionsToAssign = [ { attribute: { id: "attr1", isLocked: true }, optionsToAssign: [{ id: "opt1", label: "Option 1" }], }, ]; const result = buildPrismaQueriesForAttributeOptionToUser({ orgId: mockOrgId, memberId: mockMemberId, existingAttributeOptionAssignments: [], attributeOptionsToAssign, updater: mockCreator, }); expect(result.attributeToUserDeleteQueries.locked).toEqual({ memberId: mockMemberId, attributeOption: { attribute: { id: { in: ["attr1"], }, }, }, }); }); it("should update existing assignments with same values", () => { const existingAssignments = [ { id: "assign1", attributeOption: { label: "APAC", attribute: { id: "territory" }, }, }, ] as any; const attributeOptionsToAssign = [ { attribute: { id: "territory", isLocked: false }, optionsToAssign: [{ id: "1", label: "ApAc" }], }, ]; const result = buildPrismaQueriesForAttributeOptionToUser({ orgId: mockOrgId, memberId: mockMemberId, existingAttributeOptionAssignments: existingAssignments, attributeOptionsToAssign, updater: mockCreator, }); expect(result.attributeToUserUpdateManyInput).toEqual({ where: { memberId: mockMemberId, id: { in: ["assign1"] }, }, data: { updatedById: mockCreator.userId, updatedByDSyncId: null, }, }); }); }); });