* chore: Upgrade prisma to 6.7.0 * Build fixes * type fixes Signed-off-by: Omar López <zomars@me.com> * Update schema.prisma * Patching * Revert "Update schema.prisma" This reverts commit 47d8618bf89ef4d007b30084df766f17281e21a1. * Revert "Patching" This reverts commit a1d2e3040e71690a44d4324db95d73b4d68c6adb. * Revert schema changes Signed-off-by: Omar López <zomars@me.com> * WIP Signed-off-by: Omar López <zomars@me.com> * Update getPublicEvent.ts * Update imports Signed-off-by: Omar López <zomars@me.com> * Update gitignore Signed-off-by: Omar López <zomars@me.com> * update remaining imports Signed-off-by: Omar López <zomars@me.com> * Delete .cursor/config.json * Discard changes to packages/features/eventtypes/lib/getPublicEvent.ts * Update _get.ts * Update user.ts * Update .gitignore * update * Update WorkflowStepContainer.tsx * Update next-auth-custom-adapter.ts * Update getPublicEvent.ts * Update workflow.ts * Update next-auth-custom-adapter.ts * Update next-auth-options.ts * Update bookingScenario.ts * fix missing imports * upgrades prismock Signed-off-by: Omar López <zomars@me.com> * patches prismock Signed-off-by: Omar López <zomars@me.com> * Update reschedule.test.ts * Update prisma.ts * patch prismock Signed-off-by: Omar López <zomars@me.com> * fix enums imports Signed-off-by: Omar López <zomars@me.com> * Revert "Update prisma.ts" This reverts commit 64edcf8db54171ff4456c209d563b5d431d99619. * Revert "patch prismock" This reverts commit e95819113dc9d88e7130947aa120cd42710977c8. * fix patch * Fix test that overrun the boundary, it shouldn't test too much * Move prisma import to changeSMSLockState * Bring back broken test without illegal imports * Merge with main and fix filter hosts by same round robin host * Fixed buildDryRunBooking fn tests * Fix and move ooo create or update handler test * Fix packages/features/eventtypes/lib/isCurrentlyAvailable.test.ts * Fix packages/trpc/server/routers/viewer/organizations/listMembers.handler.test.ts * Mock @calcom/prisma * Fix: verify-email.test.ts * fix: Moved WebhookService test and fixed default import mock * Fix: Added missing prisma mock, handleNewBooking uses that of course * We're not testing createContext here * fix: Prisma mock fix for listMembers.test.ts * More fixes to broken testcases * Forgot to remove borked test * Prevent the need to mock a lot of dependencies by moving out buildBaseWhereCondition to its own file * Temporarily skip getCalendarEvents, needs a rewrite * Fix: turns out you can access protected in testcases * fix further mocks * Added packages/features/insights/server/buildBaseWhereCondition.ts, types * Always great to have a mock and then not use it * And one less again. * fix: confirm.handler.test, didn't mock prisma * fix: Address minor nit by @eunjae & fix ImpersonationProvider test * Updated isPrismaAvailableCheck that doesn't crash on import * fix: Get Prisma directly from the client, it usually involves the Validator and does not need 'local' inclusion * Add zod-prisma-types without the generator enabled (commented out) * Uncomment and see what happens * Change method of import as imports did not work in Input Schemas * Remove custom 'zod' booking model, it does not belong with Prisma * Fix all other global Model imports * Rewrite most schema includes AND remove barrel file * Add bookingCreateBodySchema to features/bookings * Flurry of type fixes for compatibility with new zod gen * Refactor out the custom prisma type createEventTypeInput * Work around nullable eventTypeLocations * HandlePayment type fix * More fixes, final fix remaining is CompleteEventType * Should fix a bunch more booking related type errors * Missed one * Some props missing from BookingCreateBodySchema * Fix location type in handleChildrenEventTypes * Little bit hacky imo but it works * Final type error \o/ * Forgot to include Prisma * Do not include zod-utils in booker/types * Oops, was already including Booker/types * Fix membership type, also disallow updating createdAt/updatedAt, make part of patch/post * Fix api v1 type errors * Fix EventTypeDescription typings * Remove getParserWithGeneric, use userBodySchema with UserSchema * use centralized timeZoneSchema * Implement feedback by @zomars * Couple of WIP pushes * Fix tests * Type fixes in `handleChildrenEventTypes` test * Try and parse metadata before use * Change zod-prisma-types configuration for optimal performance * Fix prisma validator error in `prisma/selects/credential` * Disable seperate relations model, hits a bug * Import absolute - this makes rollup work in @platform/libraries * Attempt at removing resolutions override * Refactor using `Prisma.validator` to `satisfies` * Build atoms using @calcom/prisma/client * Build atoms using @calcom/prisma/client * fixes * Update eventTypeSelect.ts * Adjust `eventTypeMetaDataSchemaWithUntypedApps` from `unknown` to `record(any)` * `EventTypeDescription` rely on `descriptionAsSafeHTML` instead of `description` * Add `seatsPerTimeSlot` to event type public select * Fix typing in `users-public-view` getServerSide props * Add missing `schedulingType` to prop * chore: bump platform libraries * Function return type is illegal, not sure how this passed eslint (#21567) * Merged with main * Update updateTokenObject.ts * Update handleResponse.ts * Update index.ts * Update handleChildrenEventTypes.ts * Update booking-idempotency-key.ts * Update WebhookService.test.ts * Update events.test.ts * Update queued-response.test.ts * Update events.test.ts * Update getRoutedUrl.test.ts * fix: type checks Signed-off-by: Omar López <zomars@me.com> * fixes Signed-off-by: Omar López <zomars@me.com> * chore: bump platform libraries * Update yarn.lock * more fixes Signed-off-by: Omar López <zomars@me.com> * fixes Signed-off-by: Omar López <zomars@me.com> * biuld fixes * chore: bump platform libraries * Update conferencing.repository.ts * Update conferencing.repository.ts * Update getCalendarsEvents.test.ts * Update vite.config.js * chore: bump platform libraries * Update users.ts * Discard changes to docs/api-reference/v2/openapi.json * Update vite.config.ts * updated platform libraries * Update get.handler.test.ts * Update get.handler.test.ts * Update schema.prisma * Discard changes to docs/api-reference/v2/openapi.json * Update next-auth-custom-adapter.ts * Update team.ts * Flurry of type fixes * Fix majority of insight related type errors * Type fixes for unlink of account * Make user nullable again * Fixed a bunch of unit tests and one type error * Attempted mock fix * Attempted fix for Attribute type * Ensure default import becomes prisma, but not direct usage * Import default as prisma in prisma.module * Add attributeOption to attribute type * Fix calcom/prisma mock * Refactor Prisma client imports to @calcom/prisma/client Updated all imports from '@prisma/client' to '@calcom/prisma/client' across tests and repository files for consistency and to use the correct Prisma client package. This change improves maintainability and ensures the correct client is referenced throughout the codebase. * Undo removal of max-warnings=0 to get main to merge * Remove unit tests for e2e fixtures, provide new prisma mock * Mock @calcom/prisma in event manager * Mock @calcom/prisma in event manager * Add correct format even with --no-verify * Mock prisma in CalendarManager * Add mock for permission-check.service * Better injection in PrismaApiKeyRepository imports * More mock fixes :) * Fix listMembers.handler.test * Fix User import * Appropriately adjust all types to be imported as types, there were a lot of types imported as normal deps * Why was this a thing? * Strictly speaking; Not using prismock anymore * Ditched patch file for prismock * Fix output.service.ts platform type imports, need concrete for plainToClass * Better typing and tests for unlinkConnectedAccount.handler * Small type fix * Disable calendar cache tests as they are dependent on prismock * chore: bump platform lib * getRoutedUrl test remove of unused import * Extract select to external const on getEventTypesFromDB * Direct select of userSelect from selects/user * fix type error from merging 23653 * Fixed integration tests by removing hardcoded values that were possible due to mocking, but as its now directly hitting the db no longer * fix: vite config atoms prisma client type location * revert: example app prisma client * revert: example app prisma client * bump platform libs * fix: use class instead of type for DI of PlatformBookingsService * update platform libs * remove unused variable * chore: generate prisma client for api v2 * fix: api v2 e2e * fix: atoms e2e * fix: atoms e2e * fix: atoms e2e * fix: api v2 e2e * fix: tsconfig apiv2 enums * publish libraries * Simplify check for existence teamId --------- Signed-off-by: Omar López <zomars@me.com> Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com> Co-authored-by: supalarry <laurisskraucis@gmail.com> Co-authored-by: cal.com <morgan@cal.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Benny Joo <sldisek783@gmail.com>
1259 lines
37 KiB
TypeScript
1259 lines
37 KiB
TypeScript
import type { BaseWidget } from "react-awesome-query-builder";
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
import { RouteActionType } from "@calcom/app-store/routing-forms/zod";
|
|
import { RaqbLogicResult } from "@calcom/lib/raqb/evaluateRaqbLogic";
|
|
import {
|
|
findTeamMembersMatchingAttributeLogic,
|
|
TroubleshooterCase,
|
|
} from "@calcom/lib/raqb/findTeamMembersMatchingAttributeLogic";
|
|
import * as getAttributesModule from "@calcom/lib/service/attribute/server/getAttributes";
|
|
import type { AttributeType } from "@calcom/prisma/enums";
|
|
import { RoutingFormFieldType } from "@calcom/routing-forms/lib/FieldTypes";
|
|
import type { AttributesQueryValue, FormFieldsQueryValue } from "@calcom/routing-forms/types/types";
|
|
|
|
vi.mock("@calcom/lib/service/attribute/server/getAttributes", () => {
|
|
return {
|
|
getAttributesAssignmentData: vi.fn(),
|
|
};
|
|
});
|
|
|
|
vi.mock("../../components/react-awesome-query-builder/widgets", () => ({
|
|
default: {},
|
|
}));
|
|
|
|
const orgId = 1001;
|
|
function mockAttributesScenario({
|
|
attributes,
|
|
teamMembersWithAttributeOptionValuePerAttribute,
|
|
}: {
|
|
attributes: {
|
|
id: string;
|
|
name: string;
|
|
type: AttributeType;
|
|
slug: string;
|
|
options: {
|
|
id: string;
|
|
value: string;
|
|
slug: string;
|
|
}[];
|
|
}[];
|
|
teamMembersWithAttributeOptionValuePerAttribute: {
|
|
userId: number;
|
|
attributes: Record<string, string | string[]>;
|
|
}[];
|
|
}) {
|
|
const commonOptionsProps = {
|
|
isGroup: false,
|
|
contains: [],
|
|
};
|
|
vi.mocked(getAttributesModule.getAttributesAssignmentData).mockResolvedValue({
|
|
attributesOfTheOrg: attributes.map((attribute) => ({
|
|
...attribute,
|
|
options: attribute.options.map((option) => ({
|
|
...option,
|
|
attributeId: attribute.id,
|
|
...commonOptionsProps,
|
|
})),
|
|
})),
|
|
attributesAssignedToTeamMembersWithOptions: teamMembersWithAttributeOptionValuePerAttribute.map(
|
|
(member) => {
|
|
return {
|
|
...member,
|
|
attributes: Object.fromEntries(
|
|
Object.entries(member.attributes).map(([attributeId, value]) => {
|
|
return [
|
|
attributeId,
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
|
{
|
|
attributeOption:
|
|
value instanceof Array
|
|
? value.map((value) => ({ value, isGroup: false, contains: [] }))
|
|
: { value, isGroup: false, contains: [] },
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
type: attributes.find((attribute) => attribute.id === attributeId)!.type,
|
|
},
|
|
];
|
|
})
|
|
),
|
|
};
|
|
}
|
|
),
|
|
});
|
|
}
|
|
|
|
function mockHugeAttributesOfTypeSingleSelect({
|
|
numAttributes,
|
|
numOptionsPerAttribute,
|
|
numTeamMembers,
|
|
numAttributesUsedPerTeamMember,
|
|
}: {
|
|
numAttributes: number;
|
|
numOptionsPerAttribute: number;
|
|
numTeamMembers: number;
|
|
numAttributesUsedPerTeamMember: number;
|
|
}) {
|
|
if (numAttributesUsedPerTeamMember > numAttributes) {
|
|
throw new Error("numAttributesUsedPerTeamMember cannot be greater than numAttributes");
|
|
}
|
|
const attributes = Array.from({ length: numAttributes }, (_, i) => ({
|
|
id: `attr${i + 1}`,
|
|
name: `Attribute ${i + 1}`,
|
|
type: "SINGLE_SELECT" as const,
|
|
slug: `attribute-${i + 1}`,
|
|
options: Array.from({ length: numOptionsPerAttribute }, (_, i) => ({
|
|
id: `opt${i + 1}`,
|
|
value: `Option ${i + 1}`,
|
|
slug: `option-${i + 1}`,
|
|
})),
|
|
}));
|
|
|
|
const assignedAttributeOptionIdForEachMember = 1;
|
|
|
|
const teamMembersWithAttributeOptionValuePerAttribute = Array.from({ length: numTeamMembers }, (_, i) => ({
|
|
userId: i + 1,
|
|
attributes: Object.fromEntries(
|
|
Array.from({ length: numAttributesUsedPerTeamMember }, (_, j) => [
|
|
attributes[j].id,
|
|
attributes[j].options[assignedAttributeOptionIdForEachMember].value,
|
|
])
|
|
),
|
|
}));
|
|
|
|
mockAttributesScenario({
|
|
attributes,
|
|
teamMembersWithAttributeOptionValuePerAttribute,
|
|
});
|
|
|
|
return {
|
|
attributes,
|
|
teamMembersWithAttributeOptionValuePerAttribute,
|
|
};
|
|
}
|
|
|
|
function buildQueryValue({
|
|
rules,
|
|
}: {
|
|
rules: {
|
|
raqbFieldId: string;
|
|
value: string | number | string[] | [string[]];
|
|
operator: string;
|
|
valueSrc: NonNullable<BaseWidget["valueSrc"]>[];
|
|
valueType: string[];
|
|
}[];
|
|
}) {
|
|
const queryValue = {
|
|
id: "query-id-1",
|
|
type: "group",
|
|
children1: rules.reduce((acc, rule, index) => {
|
|
acc[`rule-${index + 1}`] = {
|
|
type: "rule",
|
|
properties: {
|
|
field: rule.raqbFieldId,
|
|
value: rule.value,
|
|
operator: rule.operator,
|
|
valueSrc: rule.valueSrc,
|
|
valueType: rule.valueType,
|
|
},
|
|
};
|
|
return acc;
|
|
}, {} as any),
|
|
};
|
|
|
|
return queryValue;
|
|
}
|
|
|
|
function buildSelectTypeFieldQueryValue({
|
|
rules,
|
|
}: {
|
|
rules: {
|
|
raqbFieldId: string;
|
|
value: string | number | string[] | [string[]];
|
|
operator: string;
|
|
valueType?: string[];
|
|
}[];
|
|
}) {
|
|
return buildQueryValue({
|
|
rules: rules.map((rule) => ({
|
|
raqbFieldId: rule.raqbFieldId,
|
|
value: rule.value,
|
|
operator: rule.operator,
|
|
valueSrc: ["value"],
|
|
valueType: rule.valueType ?? ["select"],
|
|
})),
|
|
}) as AttributesQueryValue;
|
|
}
|
|
|
|
function buildRoute({
|
|
id,
|
|
action,
|
|
queryValue,
|
|
attributesQueryValue,
|
|
}: {
|
|
id: string;
|
|
action: {
|
|
type: RouteActionType;
|
|
value: string;
|
|
};
|
|
queryValue: FormFieldsQueryValue;
|
|
attributesQueryValue?: AttributesQueryValue;
|
|
}) {
|
|
return {
|
|
id,
|
|
action,
|
|
queryValue,
|
|
attributesQueryValue,
|
|
};
|
|
}
|
|
|
|
function buildDefaultCustomPageRoute({
|
|
id,
|
|
attributesQueryValue,
|
|
}: {
|
|
id: string;
|
|
attributesQueryValue?: AttributesQueryValue;
|
|
}) {
|
|
return buildRoute({
|
|
id,
|
|
action: { type: RouteActionType.CustomPageMessage, value: "test" },
|
|
queryValue: { type: "group" } as unknown as FormFieldsQueryValue,
|
|
attributesQueryValue,
|
|
});
|
|
}
|
|
function buildScenarioWhereMainAttributeLogicFails() {
|
|
const Option1OfAttribute1 = { id: "opt1", value: "Option 1", slug: "option-1" };
|
|
const Option2OfAttribute1 = { id: "opt2", value: "Option 2", slug: "option-2" };
|
|
const Attribute1 = {
|
|
id: "attr1",
|
|
name: "Attribute 1",
|
|
type: "SINGLE_SELECT" as const,
|
|
slug: "attribute-1",
|
|
options: [Option1OfAttribute1, Option2OfAttribute1],
|
|
};
|
|
|
|
mockAttributesScenario({
|
|
attributes: [Attribute1],
|
|
teamMembersWithAttributeOptionValuePerAttribute: [
|
|
{ userId: 1, attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
|
|
],
|
|
});
|
|
|
|
const failingAttributesQueryValue = buildSelectTypeFieldQueryValue({
|
|
rules: [
|
|
{
|
|
raqbFieldId: Attribute1.id,
|
|
value: [Option2OfAttribute1.id],
|
|
operator: "select_equals",
|
|
},
|
|
],
|
|
}) as AttributesQueryValue;
|
|
|
|
const matchingAttributesQueryValue = buildSelectTypeFieldQueryValue({
|
|
rules: [
|
|
{
|
|
raqbFieldId: Attribute1.id,
|
|
value: [Option1OfAttribute1.id],
|
|
operator: "select_equals",
|
|
},
|
|
],
|
|
}) as AttributesQueryValue;
|
|
|
|
return { failingAttributesQueryValue, matchingAttributesQueryValue };
|
|
}
|
|
|
|
describe("findTeamMembersMatchingAttributeLogic", () => {
|
|
beforeEach(() => {
|
|
vi.resetAllMocks();
|
|
});
|
|
|
|
it("should return null if the route does not have an attributesQueryValue set", async () => {
|
|
mockAttributesScenario({
|
|
attributes: [],
|
|
teamMembersWithAttributeOptionValuePerAttribute: [],
|
|
});
|
|
|
|
const { teamMembersMatchingAttributeLogic, troubleshooter } = await findTeamMembersMatchingAttributeLogic(
|
|
{
|
|
dynamicFieldValueOperands: {
|
|
fields: [],
|
|
response: {},
|
|
},
|
|
attributesQueryValue: { type: "group" } as unknown as AttributesQueryValue,
|
|
teamId: 1,
|
|
orgId,
|
|
},
|
|
{
|
|
enableTroubleshooter: true,
|
|
}
|
|
);
|
|
|
|
expect(teamMembersMatchingAttributeLogic).toBeNull();
|
|
expect(troubleshooter).toEqual(
|
|
expect.objectContaining({
|
|
type: TroubleshooterCase.MATCHES_ALL_MEMBERS_BECAUSE_OF_EMPTY_QUERY_VALUE,
|
|
})
|
|
);
|
|
});
|
|
|
|
describe("with a SINGLE_SELECT attribute", () => {
|
|
it("when a static option is selected, troubleshooter should be null by default", async () => {
|
|
const Option1OfAttribute1 = { id: "opt1", value: "Option 1", slug: "option-1" };
|
|
const Attribute1 = {
|
|
id: "attr1",
|
|
name: "Attribute 1",
|
|
type: "SINGLE_SELECT" as const,
|
|
slug: "attribute-1",
|
|
options: [Option1OfAttribute1],
|
|
};
|
|
|
|
mockAttributesScenario({
|
|
attributes: [Attribute1],
|
|
teamMembersWithAttributeOptionValuePerAttribute: [
|
|
{ userId: 1, attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
|
|
],
|
|
});
|
|
|
|
const attributesQueryValue = buildSelectTypeFieldQueryValue({
|
|
rules: [
|
|
{
|
|
raqbFieldId: Attribute1.id,
|
|
value: [Option1OfAttribute1.id],
|
|
operator: "select_equals",
|
|
},
|
|
],
|
|
}) as AttributesQueryValue;
|
|
|
|
const { teamMembersMatchingAttributeLogic: result, troubleshooter } =
|
|
await findTeamMembersMatchingAttributeLogic({
|
|
dynamicFieldValueOperands: {
|
|
fields: [],
|
|
response: {},
|
|
},
|
|
attributesQueryValue,
|
|
teamId: 1,
|
|
orgId,
|
|
});
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
userId: 1,
|
|
result: RaqbLogicResult.MATCH,
|
|
},
|
|
]);
|
|
|
|
expect(troubleshooter).toBeUndefined();
|
|
});
|
|
|
|
describe("with `Value of field`", () => {
|
|
it("when 'Value of Field' option is selected and ", async () => {
|
|
const Option1OfAttribute1HumanReadableValue = "Option 1";
|
|
const Option1OfField1HumanReadableValue = Option1OfAttribute1HumanReadableValue;
|
|
const Field1Id = "field-1";
|
|
|
|
const Option1OfAttribute1 = {
|
|
id: "attr-1-opt-1",
|
|
value: Option1OfAttribute1HumanReadableValue,
|
|
slug: "option-1",
|
|
};
|
|
|
|
const Option1OfField1 = {
|
|
id: "field-1-opt-1",
|
|
label: Option1OfField1HumanReadableValue,
|
|
};
|
|
|
|
const Attribute1 = {
|
|
id: "attr1",
|
|
name: "Attribute 1",
|
|
type: "SINGLE_SELECT" as const,
|
|
slug: "attribute-1",
|
|
options: [Option1OfAttribute1],
|
|
};
|
|
|
|
mockAttributesScenario({
|
|
attributes: [Attribute1],
|
|
teamMembersWithAttributeOptionValuePerAttribute: [
|
|
{ userId: 1, attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
|
|
],
|
|
});
|
|
|
|
const attributesQueryValue = buildSelectTypeFieldQueryValue({
|
|
rules: [
|
|
{
|
|
raqbFieldId: Attribute1.id,
|
|
value: [`{field:${Field1Id}}`],
|
|
operator: "select_equals",
|
|
},
|
|
],
|
|
}) as AttributesQueryValue;
|
|
|
|
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
|
|
dynamicFieldValueOperands: {
|
|
fields: [
|
|
{
|
|
id: Field1Id,
|
|
type: RoutingFormFieldType.SINGLE_SELECT,
|
|
label: "Field 1",
|
|
options: [Option1OfField1],
|
|
},
|
|
],
|
|
response: {
|
|
[Field1Id]: {
|
|
value: Option1OfAttribute1HumanReadableValue,
|
|
label: Option1OfAttribute1HumanReadableValue,
|
|
},
|
|
},
|
|
},
|
|
attributesQueryValue: attributesQueryValue,
|
|
teamId: 1,
|
|
orgId,
|
|
});
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
userId: 1,
|
|
result: RaqbLogicResult.MATCH,
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
it("when 'Any in'(select_any_in) option is selected", async () => {
|
|
const Option1OfAttribute1HumanReadableValue = "Option 1";
|
|
|
|
const Option1OfAttribute1 = {
|
|
id: "attr-1-opt-1",
|
|
value: Option1OfAttribute1HumanReadableValue,
|
|
slug: "option-1",
|
|
};
|
|
|
|
const Option2OfAttribute1 = {
|
|
id: "attr-1-opt-2",
|
|
value: "Option 2",
|
|
slug: "option-2",
|
|
};
|
|
|
|
const Option3OfAttribute1 = {
|
|
id: "attr-1-opt-3",
|
|
value: "Option 3",
|
|
slug: "option-3",
|
|
};
|
|
|
|
const Attribute1 = {
|
|
id: "attr1",
|
|
name: "Attribute 1",
|
|
type: "SINGLE_SELECT" as const,
|
|
slug: "attribute-1",
|
|
options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1],
|
|
};
|
|
|
|
mockAttributesScenario({
|
|
attributes: [Attribute1],
|
|
teamMembersWithAttributeOptionValuePerAttribute: [
|
|
{ userId: 1, attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
|
|
],
|
|
});
|
|
|
|
const attributesQueryValue = buildSelectTypeFieldQueryValue({
|
|
rules: [
|
|
{
|
|
raqbFieldId: Attribute1.id,
|
|
value: [[Option1OfAttribute1.id, Option2OfAttribute1.id]],
|
|
operator: "select_any_in",
|
|
valueType: ["multiselect"],
|
|
},
|
|
],
|
|
}) as AttributesQueryValue;
|
|
|
|
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
|
|
dynamicFieldValueOperands: {
|
|
fields: [],
|
|
response: {},
|
|
},
|
|
attributesQueryValue,
|
|
teamId: 1,
|
|
orgId,
|
|
});
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
userId: 1,
|
|
result: RaqbLogicResult.MATCH,
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("with a MULTI_SELECT attribute", () => {
|
|
it("when 'Any in'(multiselect_some_in) option is selected and just one option is used in attribute for the user", async () => {
|
|
const Option1OfAttribute1HumanReadableValue = "Option 1";
|
|
|
|
const Option1OfAttribute1 = {
|
|
id: "attr-1-opt-1",
|
|
value: Option1OfAttribute1HumanReadableValue,
|
|
slug: "option-1",
|
|
};
|
|
|
|
const Option2OfAttribute1 = {
|
|
id: "attr-1-opt-2",
|
|
value: "Option 2",
|
|
slug: "option-2",
|
|
};
|
|
|
|
const Option3OfAttribute1 = {
|
|
id: "attr-1-opt-3",
|
|
value: "Option 3",
|
|
slug: "option-3",
|
|
};
|
|
|
|
const Attribute1 = {
|
|
id: "attr1",
|
|
name: "Attribute 1",
|
|
type: "MULTI_SELECT" as const,
|
|
slug: "attribute-1",
|
|
options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1],
|
|
};
|
|
|
|
mockAttributesScenario({
|
|
attributes: [Attribute1],
|
|
teamMembersWithAttributeOptionValuePerAttribute: [
|
|
// user 1 has only one option selected for the attribute
|
|
{ userId: 1, attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
|
|
],
|
|
});
|
|
|
|
const attributesQueryValue = buildSelectTypeFieldQueryValue({
|
|
rules: [
|
|
{
|
|
raqbFieldId: Attribute1.id,
|
|
value: [[Option1OfAttribute1.id, Option2OfAttribute1.id]],
|
|
operator: "multiselect_some_in",
|
|
valueType: ["multiselect"],
|
|
},
|
|
],
|
|
}) as AttributesQueryValue;
|
|
|
|
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
|
|
dynamicFieldValueOperands: {
|
|
fields: [],
|
|
response: {},
|
|
},
|
|
attributesQueryValue,
|
|
teamId: 1,
|
|
orgId,
|
|
});
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
userId: 1,
|
|
result: RaqbLogicResult.MATCH,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("when 'Any in'(multiselect_some_in) option is selected and more than one option is used in attribute for the user", async () => {
|
|
const Option1OfAttribute1HumanReadableValue = "Option 1";
|
|
|
|
const Option1OfAttribute1 = {
|
|
id: "attr-1-opt-1",
|
|
value: Option1OfAttribute1HumanReadableValue,
|
|
slug: "option-1",
|
|
};
|
|
|
|
const Option2OfAttribute1 = {
|
|
id: "attr-1-opt-2",
|
|
value: "Option 2",
|
|
slug: "option-2",
|
|
};
|
|
|
|
const Option3OfAttribute1 = {
|
|
id: "attr-1-opt-3",
|
|
value: "Option 3",
|
|
slug: "option-3",
|
|
};
|
|
|
|
const Attribute1 = {
|
|
id: "attr1",
|
|
name: "Attribute 1",
|
|
type: "MULTI_SELECT" as const,
|
|
slug: "attribute-1",
|
|
options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1],
|
|
};
|
|
|
|
mockAttributesScenario({
|
|
attributes: [Attribute1],
|
|
teamMembersWithAttributeOptionValuePerAttribute: [
|
|
{
|
|
userId: 1,
|
|
// user 1 has two options selected for the attribute
|
|
attributes: { [Attribute1.id]: [Option2OfAttribute1.value, Option1OfAttribute1.value] },
|
|
},
|
|
],
|
|
});
|
|
|
|
const attributesQueryValue = buildSelectTypeFieldQueryValue({
|
|
rules: [
|
|
{
|
|
raqbFieldId: Attribute1.id,
|
|
value: [[Option1OfAttribute1.id, Option2OfAttribute1.id]],
|
|
operator: "multiselect_some_in",
|
|
valueType: ["multiselect"],
|
|
},
|
|
],
|
|
}) as AttributesQueryValue;
|
|
|
|
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
|
|
dynamicFieldValueOperands: {
|
|
fields: [],
|
|
response: {},
|
|
},
|
|
attributesQueryValue,
|
|
teamId: 1,
|
|
orgId,
|
|
});
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
userId: 1,
|
|
result: RaqbLogicResult.MATCH,
|
|
},
|
|
]);
|
|
});
|
|
|
|
describe("with `Value of field`", () => {
|
|
it("when 'Any in'(multiselect_some_in) option is selected and more than one option is used in attribute and more than one option is selected in the field", async () => {
|
|
const Field1Id = "location-field";
|
|
const DelhiOption = {
|
|
id: "delhi-opt",
|
|
label: "Delhi",
|
|
};
|
|
const HaryanaOption = {
|
|
id: "haryana-opt",
|
|
label: "Haryana",
|
|
};
|
|
const MumbaiOption = {
|
|
id: "mumbai-opt",
|
|
label: "Mumbai",
|
|
};
|
|
|
|
const HeadquartersAttribute = {
|
|
id: "headquarters-attr",
|
|
name: "Headquarters",
|
|
type: "MULTI_SELECT" as const,
|
|
slug: "headquarters",
|
|
options: [
|
|
{ id: "delhi-hq", value: "Delhi", slug: "delhi" },
|
|
{ id: "haryana-hq", value: "Haryana", slug: "haryana" },
|
|
{ id: "mumbai-hq", value: "Mumbai", slug: "mumbai" },
|
|
{ id: "chennai-hq", value: "Chennai", slug: "chennai" },
|
|
],
|
|
};
|
|
|
|
mockAttributesScenario({
|
|
attributes: [HeadquartersAttribute],
|
|
teamMembersWithAttributeOptionValuePerAttribute: [
|
|
// User 1: headquarters in Delhi and Haryana
|
|
{ userId: 1, attributes: { [HeadquartersAttribute.id]: ["Delhi", "Haryana"] } },
|
|
// User 2: headquarters only in Mumbai
|
|
{ userId: 2, attributes: { [HeadquartersAttribute.id]: ["Mumbai"] } },
|
|
// User 3: headquarters in Chennai
|
|
{ userId: 3, attributes: { [HeadquartersAttribute.id]: ["Chennai"] } },
|
|
],
|
|
});
|
|
|
|
const attributesQueryValue = buildSelectTypeFieldQueryValue({
|
|
rules: [
|
|
{
|
|
raqbFieldId: HeadquartersAttribute.id,
|
|
value: [[`{field:${Field1Id}}`]],
|
|
operator: "multiselect_some_in",
|
|
valueType: ["multiselect"],
|
|
},
|
|
],
|
|
}) as AttributesQueryValue;
|
|
|
|
// Test case 1: Booker selects Delhi and Mumbai
|
|
const { teamMembersMatchingAttributeLogic: result1 } = await findTeamMembersMatchingAttributeLogic({
|
|
dynamicFieldValueOperands: {
|
|
fields: [
|
|
{
|
|
id: Field1Id,
|
|
type: RoutingFormFieldType.MULTI_SELECT,
|
|
label: "Location",
|
|
options: [DelhiOption, HaryanaOption, MumbaiOption],
|
|
},
|
|
],
|
|
response: {
|
|
[Field1Id]: { label: "Location", value: [DelhiOption.id, MumbaiOption.id] }, // Booker selected Delhi and Mumbai
|
|
},
|
|
},
|
|
attributesQueryValue,
|
|
teamId: 1,
|
|
orgId,
|
|
});
|
|
|
|
// Should match users 1 and 2 (Delhi matches user 1, Mumbai matches user 2)
|
|
expect(result1).toEqual(
|
|
expect.arrayContaining([
|
|
{ userId: 1, result: RaqbLogicResult.MATCH },
|
|
{ userId: 2, result: RaqbLogicResult.MATCH },
|
|
])
|
|
);
|
|
expect(result1).not.toContainEqual({ userId: 3, result: RaqbLogicResult.MATCH });
|
|
|
|
// Test case 2: Booker selects only Chennai
|
|
const { teamMembersMatchingAttributeLogic: result2 } = await findTeamMembersMatchingAttributeLogic({
|
|
dynamicFieldValueOperands: {
|
|
fields: [
|
|
{
|
|
id: Field1Id,
|
|
type: RoutingFormFieldType.MULTI_SELECT,
|
|
label: "Location",
|
|
options: [DelhiOption, HaryanaOption, MumbaiOption, { id: "chennai-opt", label: "Chennai" }],
|
|
},
|
|
],
|
|
response: {
|
|
[Field1Id]: { label: "Location", value: ["chennai-opt"] }, // Booker selected only Chennai
|
|
},
|
|
},
|
|
attributesQueryValue,
|
|
teamId: 1,
|
|
orgId,
|
|
});
|
|
|
|
// Should match only user 3
|
|
expect(result2).toEqual([{ userId: 3, result: RaqbLogicResult.MATCH }]);
|
|
});
|
|
});
|
|
|
|
it("should handle field template mixed with regular values in multiselect array", async () => {
|
|
const LocationFieldId = "location-field";
|
|
const FixedValueId = "fixed-location-id";
|
|
|
|
const LocationAttribute = {
|
|
id: "location-attr",
|
|
name: "Service Locations",
|
|
type: "MULTI_SELECT" as const,
|
|
slug: "service-locations",
|
|
options: [
|
|
{ id: "delhi-loc", value: "Delhi", slug: "delhi" },
|
|
{ id: "mumbai-loc", value: "Mumbai", slug: "mumbai" },
|
|
{ id: FixedValueId, value: "Chennai", slug: "chennai" },
|
|
],
|
|
};
|
|
|
|
mockAttributesScenario({
|
|
attributes: [LocationAttribute],
|
|
teamMembersWithAttributeOptionValuePerAttribute: [
|
|
// User 1: services Delhi and Chennai
|
|
{ userId: 1, attributes: { [LocationAttribute.id]: ["Delhi", "Chennai"] } },
|
|
// User 2: services only Mumbai
|
|
{ userId: 2, attributes: { [LocationAttribute.id]: ["Mumbai"] } },
|
|
],
|
|
});
|
|
|
|
// This simulates a complex rule: Location must include field value OR Chennai (fixed)
|
|
const attributesQueryValue = buildSelectTypeFieldQueryValue({
|
|
rules: [
|
|
{
|
|
raqbFieldId: LocationAttribute.id,
|
|
value: [[`{field:${LocationFieldId}}`, "Chennai"]], // Mixed: field template + fixed value
|
|
operator: "multiselect_some_in",
|
|
valueType: ["multiselect"],
|
|
},
|
|
],
|
|
}) as AttributesQueryValue;
|
|
|
|
// When booker selects Mumbai
|
|
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
|
|
dynamicFieldValueOperands: {
|
|
fields: [
|
|
{
|
|
id: LocationFieldId,
|
|
type: RoutingFormFieldType.MULTI_SELECT,
|
|
label: "Preferred Location",
|
|
options: [
|
|
{ id: "delhi-opt", label: "Delhi" },
|
|
{ id: "mumbai-opt", label: "Mumbai" },
|
|
],
|
|
},
|
|
],
|
|
response: {
|
|
[LocationFieldId]: { value: ["Mumbai"], label: "Mumbai" }, // Booker selected Mumbai
|
|
},
|
|
},
|
|
attributesQueryValue,
|
|
teamId: 1,
|
|
orgId,
|
|
});
|
|
|
|
// Should match both users:
|
|
// User 1: matches because they service Chennai (the fixed value)
|
|
// User 2: matches because they service Mumbai (the field value)
|
|
expect(result).toEqual(
|
|
expect.arrayContaining([
|
|
{ userId: 1, result: RaqbLogicResult.MATCH },
|
|
{ userId: 2, result: RaqbLogicResult.MATCH },
|
|
])
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Fallback", () => {
|
|
it("should return null when main attribute logic fails and no fallback is defined", async () => {
|
|
const { failingAttributesQueryValue } = buildScenarioWhereMainAttributeLogicFails();
|
|
const {
|
|
teamMembersMatchingAttributeLogic: result,
|
|
checkedFallback,
|
|
troubleshooter,
|
|
} = await findTeamMembersMatchingAttributeLogic({
|
|
dynamicFieldValueOperands: {
|
|
fields: [],
|
|
response: {},
|
|
},
|
|
attributesQueryValue: failingAttributesQueryValue,
|
|
fallbackAttributesQueryValue: null,
|
|
teamId: 1,
|
|
orgId,
|
|
});
|
|
|
|
expect(result).toEqual(null);
|
|
// We checked the fallback, that is why we know it is not there
|
|
expect(checkedFallback).toEqual(true);
|
|
expect(troubleshooter).toBeUndefined();
|
|
});
|
|
|
|
it("should return matching members when main attribute logic fails and but fallback matches", async () => {
|
|
const { failingAttributesQueryValue, matchingAttributesQueryValue } =
|
|
buildScenarioWhereMainAttributeLogicFails();
|
|
const {
|
|
teamMembersMatchingAttributeLogic: result,
|
|
checkedFallback,
|
|
troubleshooter,
|
|
} = await findTeamMembersMatchingAttributeLogic({
|
|
dynamicFieldValueOperands: {
|
|
fields: [],
|
|
response: {},
|
|
},
|
|
attributesQueryValue: failingAttributesQueryValue,
|
|
fallbackAttributesQueryValue: matchingAttributesQueryValue,
|
|
teamId: 1,
|
|
orgId,
|
|
});
|
|
|
|
expect(checkedFallback).toEqual(true);
|
|
expect(result).toEqual([
|
|
{
|
|
userId: 1,
|
|
result: RaqbLogicResult.MATCH,
|
|
},
|
|
]);
|
|
|
|
expect(troubleshooter).toBeUndefined();
|
|
});
|
|
|
|
it("should return 0 matching members when main attribute logic and fallback attribute logic fail", async () => {
|
|
const { failingAttributesQueryValue, matchingAttributesQueryValue } =
|
|
buildScenarioWhereMainAttributeLogicFails();
|
|
const {
|
|
teamMembersMatchingAttributeLogic: result,
|
|
checkedFallback,
|
|
troubleshooter,
|
|
} = await findTeamMembersMatchingAttributeLogic({
|
|
dynamicFieldValueOperands: {
|
|
fields: [],
|
|
response: {},
|
|
},
|
|
attributesQueryValue: failingAttributesQueryValue,
|
|
fallbackAttributesQueryValue: failingAttributesQueryValue,
|
|
teamId: 1,
|
|
orgId,
|
|
});
|
|
|
|
expect(checkedFallback).toEqual(true);
|
|
expect(troubleshooter).toBeUndefined();
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("Error handling", () => {
|
|
it("should throw an error if the attribute type is not supported", async () => {
|
|
const Option1OfAttribute1 = { id: "opt1", value: "Option 1", slug: "option-1" };
|
|
const Attribute1 = {
|
|
id: "attr1",
|
|
name: "Attribute 1",
|
|
type: "UNSUPPORTED_ATTRIBUTE_TYPE" as unknown as AttributeType,
|
|
slug: "attribute-1",
|
|
options: [Option1OfAttribute1],
|
|
};
|
|
mockAttributesScenario({
|
|
attributes: [Attribute1],
|
|
teamMembersWithAttributeOptionValuePerAttribute: [
|
|
{
|
|
userId: 1,
|
|
attributes: { [Attribute1.id]: Option1OfAttribute1.value },
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(
|
|
findTeamMembersMatchingAttributeLogic({
|
|
dynamicFieldValueOperands: {
|
|
fields: [],
|
|
response: {},
|
|
},
|
|
attributesQueryValue: buildSelectTypeFieldQueryValue({
|
|
rules: [
|
|
{
|
|
raqbFieldId: Attribute1.id,
|
|
value: [Option1OfAttribute1.id],
|
|
operator: "select_equals",
|
|
},
|
|
],
|
|
}),
|
|
teamId: 1,
|
|
orgId,
|
|
})
|
|
).rejects.toThrow("Unsupported attribute type");
|
|
});
|
|
|
|
it("should return warnings in preview and live mode", async () => {
|
|
const Option1OfAttribute1HumanReadableValue = "Option 1";
|
|
|
|
const Option1OfAttribute1 = {
|
|
id: "attr-1-opt-1",
|
|
value: Option1OfAttribute1HumanReadableValue,
|
|
slug: "option-1",
|
|
};
|
|
|
|
const Option2OfAttribute1 = {
|
|
id: "attr-1-opt-2",
|
|
value: "Option 2",
|
|
slug: "option-2",
|
|
};
|
|
|
|
const Option3OfAttribute1 = {
|
|
id: "attr-1-opt-3",
|
|
value: "Option 3",
|
|
slug: "option-3",
|
|
};
|
|
|
|
const Attribute1 = {
|
|
id: "attr1",
|
|
name: "Attribute 1",
|
|
type: "SINGLE_SELECT" as const,
|
|
slug: "attribute-1",
|
|
options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1],
|
|
};
|
|
|
|
mockAttributesScenario({
|
|
attributes: [Attribute1],
|
|
teamMembersWithAttributeOptionValuePerAttribute: [
|
|
{ userId: 1, attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
|
|
],
|
|
});
|
|
|
|
const attributesQueryValue = buildSelectTypeFieldQueryValue({
|
|
rules: [
|
|
{
|
|
raqbFieldId: Attribute1.id,
|
|
value: [["NON_EXISTING_OPTION_1", "NON_EXISTING_OPTION_2"]],
|
|
operator: "select_any_in",
|
|
valueType: ["multiselect"],
|
|
},
|
|
],
|
|
}) as AttributesQueryValue;
|
|
|
|
async function runInMode({ mode }: { mode: "preview" | "live" }) {
|
|
const result = await findTeamMembersMatchingAttributeLogic({
|
|
dynamicFieldValueOperands: {
|
|
fields: [],
|
|
response: {},
|
|
},
|
|
attributesQueryValue,
|
|
teamId: 1,
|
|
orgId,
|
|
isPreview: mode === "preview" ? true : false,
|
|
fallbackAttributesQueryValue: null,
|
|
});
|
|
return result;
|
|
}
|
|
|
|
await (async function liveMode() {
|
|
const result = await runInMode({ mode: "live" });
|
|
// it will fallback to the fallback attribute logic which isn't defined and thus will return null
|
|
expect(result.teamMembersMatchingAttributeLogic).toEqual(null);
|
|
expect(result.mainAttributeLogicBuildingWarnings).toEqual([
|
|
"Value NON_EXISTING_OPTION_1 is not in list of values",
|
|
]);
|
|
})();
|
|
|
|
await (async function previewMode() {
|
|
const result = await runInMode({ mode: "preview" });
|
|
// it will fallback to the fallback attribute logic which isn't defined and thus will return null
|
|
expect(result.teamMembersMatchingAttributeLogic).toEqual(null);
|
|
expect(result.mainAttributeLogicBuildingWarnings).toEqual([
|
|
"Value NON_EXISTING_OPTION_1 is not in list of values",
|
|
]);
|
|
})();
|
|
});
|
|
|
|
it("should not throw error if children1 is empty", async () => {
|
|
const Option1OfAttribute1HumanReadableValue = "Option 1";
|
|
|
|
const Option1OfAttribute1 = {
|
|
id: "attr-1-opt-1",
|
|
value: Option1OfAttribute1HumanReadableValue,
|
|
slug: "option-1",
|
|
};
|
|
|
|
const Option2OfAttribute1 = {
|
|
id: "attr-1-opt-2",
|
|
value: "Option 2",
|
|
slug: "option-2",
|
|
};
|
|
|
|
const Option3OfAttribute1 = {
|
|
id: "attr-1-opt-3",
|
|
value: "Option 3",
|
|
slug: "option-3",
|
|
};
|
|
|
|
const Attribute1 = {
|
|
id: "attr1",
|
|
name: "Attribute 1",
|
|
type: "SINGLE_SELECT" as const,
|
|
slug: "attribute-1",
|
|
options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1],
|
|
};
|
|
|
|
mockAttributesScenario({
|
|
attributes: [Attribute1],
|
|
teamMembersWithAttributeOptionValuePerAttribute: [
|
|
{ userId: 1, attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
|
|
],
|
|
});
|
|
|
|
const attributesQueryValue = buildQueryValue({
|
|
rules: [],
|
|
}) as AttributesQueryValue;
|
|
|
|
expect(attributesQueryValue.children1).toEqual({});
|
|
|
|
async function runInMode({ mode }: { mode: "preview" | "live" }) {
|
|
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
|
|
dynamicFieldValueOperands: {
|
|
fields: [],
|
|
response: {},
|
|
},
|
|
attributesQueryValue,
|
|
teamId: 1,
|
|
orgId,
|
|
isPreview: mode === "preview" ? true : false,
|
|
});
|
|
return result;
|
|
}
|
|
|
|
await (async function liveMode() {
|
|
const result = await runInMode({ mode: "live" });
|
|
expect(result).toEqual(null);
|
|
})();
|
|
|
|
await (async function previewMode() {
|
|
const result = await runInMode({ mode: "preview" });
|
|
expect(result).toEqual(null);
|
|
})();
|
|
});
|
|
});
|
|
|
|
describe("Performance testing", () => {
|
|
describe("20 attributes, 4000 team members", async () => {
|
|
// In tests, the performance is actually really bad than real world. So, skipping this test for now
|
|
it("should return matching team members with a SINGLE_SELECT attribute when 'all in' option is selected", async () => {
|
|
const { attributes } = mockHugeAttributesOfTypeSingleSelect({
|
|
numAttributes: 20,
|
|
numOptionsPerAttribute: 30,
|
|
numTeamMembers: 4000,
|
|
numAttributesUsedPerTeamMember: 10,
|
|
});
|
|
const attributesQueryValue = buildSelectTypeFieldQueryValue({
|
|
rules: [
|
|
{
|
|
raqbFieldId: attributes[0].id,
|
|
value: [attributes[0].options[1].id],
|
|
operator: "select_equals",
|
|
},
|
|
],
|
|
}) as AttributesQueryValue;
|
|
|
|
const { teamMembersMatchingAttributeLogic: result, timeTaken } =
|
|
await findTeamMembersMatchingAttributeLogic(
|
|
{
|
|
dynamicFieldValueOperands: {
|
|
fields: [],
|
|
response: {},
|
|
},
|
|
attributesQueryValue,
|
|
teamId: 1,
|
|
orgId,
|
|
},
|
|
{
|
|
concurrency: 1,
|
|
enablePerf: true,
|
|
}
|
|
);
|
|
|
|
expect(result).toEqual(
|
|
expect.arrayContaining([
|
|
{
|
|
userId: 1,
|
|
result: RaqbLogicResult.MATCH,
|
|
},
|
|
{
|
|
userId: 2,
|
|
result: RaqbLogicResult.MATCH,
|
|
},
|
|
{
|
|
userId: 3,
|
|
result: RaqbLogicResult.MATCH,
|
|
},
|
|
{
|
|
userId: 2000,
|
|
result: RaqbLogicResult.MATCH,
|
|
},
|
|
// Last Item
|
|
{
|
|
userId: 4000,
|
|
result: RaqbLogicResult.MATCH,
|
|
},
|
|
])
|
|
);
|
|
|
|
if (!timeTaken) {
|
|
throw new Error("Looks like performance testing is not enabled");
|
|
}
|
|
const totalTimeTaken = Object.values(timeTaken).reduce((sum, time) => (sum ?? 0) + (time ?? 0), 0);
|
|
console.log("Total time taken", totalTimeTaken, {
|
|
timeTaken,
|
|
});
|
|
expect(totalTimeTaken).toBeLessThan(1000);
|
|
// All of them should match
|
|
expect(result?.length).toBe(4000);
|
|
}, 10000);
|
|
});
|
|
});
|
|
|
|
describe("Troubleshooter", () => {
|
|
it("troubleshooter data should be there when enableTroubleshooter is true", async () => {
|
|
const Option1OfAttribute1HumanReadableValue = "Option 1";
|
|
|
|
const Option1OfAttribute1 = {
|
|
id: "attr-1-opt-1",
|
|
value: Option1OfAttribute1HumanReadableValue,
|
|
slug: "option-1",
|
|
};
|
|
|
|
const Option2OfAttribute1 = {
|
|
id: "attr-1-opt-2",
|
|
value: "Option 2",
|
|
slug: "option-2",
|
|
};
|
|
|
|
const Option3OfAttribute1 = {
|
|
id: "attr-1-opt-3",
|
|
value: "Option 3",
|
|
slug: "option-3",
|
|
};
|
|
|
|
const Attribute1 = {
|
|
id: "attr1",
|
|
name: "Attribute 1",
|
|
type: "SINGLE_SELECT" as const,
|
|
slug: "attribute-1",
|
|
options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1],
|
|
};
|
|
|
|
mockAttributesScenario({
|
|
attributes: [Attribute1],
|
|
teamMembersWithAttributeOptionValuePerAttribute: [
|
|
{ userId: 1, attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
|
|
],
|
|
});
|
|
|
|
const attributesQueryValue = buildSelectTypeFieldQueryValue({
|
|
rules: [
|
|
{
|
|
raqbFieldId: Attribute1.id,
|
|
value: [[Option1OfAttribute1.id, Option2OfAttribute1.id]],
|
|
operator: "select_any_in",
|
|
valueType: ["multiselect"],
|
|
},
|
|
],
|
|
}) as AttributesQueryValue;
|
|
|
|
const { teamMembersMatchingAttributeLogic: result, troubleshooter } =
|
|
await findTeamMembersMatchingAttributeLogic(
|
|
{
|
|
dynamicFieldValueOperands: {
|
|
fields: [],
|
|
response: {},
|
|
},
|
|
attributesQueryValue,
|
|
teamId: 1,
|
|
orgId,
|
|
},
|
|
{
|
|
enableTroubleshooter: true,
|
|
}
|
|
);
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
userId: 1,
|
|
result: RaqbLogicResult.MATCH,
|
|
},
|
|
]);
|
|
expect(troubleshooter).not.toBeUndefined();
|
|
});
|
|
});
|
|
|
|
it("should handle non-existent option IDs gracefully", async () => {
|
|
const LocationAttribute = {
|
|
id: "location-attr",
|
|
name: "Location",
|
|
type: "SINGLE_SELECT" as const,
|
|
slug: "location",
|
|
options: [{ id: "ny-opt", value: "New York", slug: "new-york" }],
|
|
};
|
|
|
|
mockAttributesScenario({
|
|
attributes: [LocationAttribute],
|
|
teamMembersWithAttributeOptionValuePerAttribute: [
|
|
{ userId: 1, attributes: { [LocationAttribute.id]: "New York" } },
|
|
],
|
|
});
|
|
|
|
const attributesQueryValue = buildSelectTypeFieldQueryValue({
|
|
rules: [
|
|
{
|
|
raqbFieldId: LocationAttribute.id,
|
|
value: ["non-existent-id"], // Non-existent option ID
|
|
operator: "select_equals",
|
|
},
|
|
],
|
|
}) as AttributesQueryValue;
|
|
|
|
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogic({
|
|
dynamicFieldValueOperands: {
|
|
fields: [],
|
|
response: {},
|
|
},
|
|
attributesQueryValue,
|
|
teamId: 1,
|
|
orgId,
|
|
});
|
|
|
|
// Should not match anyone as the option ID doesn't exist
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|