2
0
Files
cal/calcom/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.test.ts
2024-08-09 00:39:27 +02:00

402 lines
12 KiB
TypeScript

import { scenarios as checkRateLimitAndThrowErrorScenarios } from "../../../../../../../tests/libs/__mocks__/checkRateLimitAndThrowError";
import { mock as getTranslationMock } from "../../../../../../../tests/libs/__mocks__/getTranslation";
import {
inviteMemberutilsScenarios as inviteMemberUtilsScenarios,
default as inviteMemberUtilsMock,
} from "./__mocks__/inviteMemberUtils";
import { default as paymentsMock } from "@calcom/features/ee/teams/lib/__mocks__/payments";
import { constantsScenarios } from "@calcom/lib/__mocks__/constants";
import { describe, it, expect, beforeEach, vi } from "vitest";
import type { Profile } from "@calcom/prisma/client";
import { IdentityProvider, MembershipRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
import type { TrpcSessionUser } from "../../../../trpc";
import inviteMemberHandler from "./inviteMember.handler";
import { INVITE_STATUS } from "./utils";
vi.mock("@trpc/server", () => {
return {
TRPCError: class TRPCError {
code: string;
message: unknown;
constructor({ code, message }: { code: string; message: unknown }) {
this.code = code;
this.message = message;
}
},
};
});
function fakeNoUsersFoundMatchingInvitations(args: {
team: any;
invitations: {
role: MembershipRole;
usernameOrEmail: string;
}[];
}) {
inviteMemberUtilsScenarios.findUsersWithInviteStatus.useAdvancedMock([], args);
}
function getPersonalProfile({ username }: { username: string }) {
return {
id: null,
upId: "abc",
organization: null,
organizationId: null,
username,
startTime: 0,
endTime: 0,
avatarUrl: "",
name: "",
bufferTime: 0,
};
}
function getLoggedInUser() {
return {
id: 123,
name: "John Doe",
organization: {
id: 456,
isOrgAdmin: true,
metadata: null,
requestedSlug: null,
},
profile: getPersonalProfile({ username: "john_doe" }),
} as NonNullable<TrpcSessionUser>;
}
function buildExistingUser(user: { id: number; email: string; username: string }) {
return {
password: {
hash: "hash",
userId: 1,
},
identityProvider: IdentityProvider.CAL,
profiles: [] as Profile[],
completedOnboarding: false,
...user,
};
}
describe("inviteMemberHandler", () => {
beforeEach(async () => {
await inviteMemberUtilsScenarios.getOrgState.useActual();
await inviteMemberUtilsScenarios.getUniqueInvitationsOrThrowIfEmpty.useActual();
await inviteMemberUtilsScenarios.getOrgConnectionInfo.useActual();
checkRateLimitAndThrowErrorScenarios.fakeRateLimitPassed();
getTranslationMock.fakeIdentityFn();
inviteMemberUtilsScenarios.checkPermissions.fakePassed();
constantsScenarios.enableTeamBilling();
});
describe("Regular Team", () => {
describe("with 2 emails in input and when there are no users matching the emails", () => {
it("should call appropriate utilities to send email, add users and update in stripe. It should return `numUsersInvited`=2", async () => {
const usersToBeInvited = [
{
id: 1,
email: "user1@example.com",
},
{
id: 2,
email: "user2@example.com",
},
];
const loggedInUser = getLoggedInUser();
const input = {
teamId: 1,
role: MembershipRole.MEMBER,
isOrg: false,
language: "en",
usernameOrEmail: usersToBeInvited.map((u) => u.email),
};
const team = {
id: input.teamId,
name: "Team 1",
parent: null,
};
const retValueOfGetTeamOrThrowError = inviteMemberUtilsScenarios.getTeamOrThrow.fakeReturnTeam(team, {
teamId: input.teamId,
});
const allExpectedInvitations = [
{
role: input.role,
usernameOrEmail: usersToBeInvited[0].email,
},
{
role: input.role,
usernameOrEmail: usersToBeInvited[1].email,
},
];
fakeNoUsersFoundMatchingInvitations({
team,
invitations: allExpectedInvitations,
});
const ctx = {
user: loggedInUser,
};
// Call the inviteMemberHandler function
const result = await inviteMemberHandler({ ctx, input });
const expectedConnectionInfoMap = {
[usersToBeInvited[0].email]: {
orgId: undefined,
autoAccept: false,
},
[usersToBeInvited[1].email]: {
orgId: undefined,
autoAccept: false,
},
};
const inviter = {
name: loggedInUser.name,
};
expect(inviteMemberUtilsMock.handleNewUsersInvites).toHaveBeenCalledWith({
invitationsForNewUsers: allExpectedInvitations,
team: retValueOfGetTeamOrThrowError,
orgConnectInfoByUsernameOrEmail: expectedConnectionInfoMap,
teamId: input.teamId,
language: input.language,
inviter,
autoAcceptEmailDomain: null,
});
expect(paymentsMock.updateQuantitySubscriptionFromStripe).toHaveBeenCalledWith(input.teamId);
expect(inviteMemberUtilsMock.handleExistingUsersInvites).not.toHaveBeenCalled();
expect(inviteMemberUtilsMock.getUniqueInvitationsOrThrowIfEmpty).toHaveBeenCalledWith([
{
role: input.role,
usernameOrEmail: usersToBeInvited[0].email,
},
{
role: input.role,
usernameOrEmail: usersToBeInvited[1].email,
},
]);
// Assert the result
expect(result).toEqual({
usernameOrEmail: input.usernameOrEmail,
numUsersInvited: 2,
});
});
});
describe("with 2 emails in input and when there is one user matching the email", () => {
it("should call appropriate utilities to add users and update in stripe. It should return `numUsersInvited=2`", async () => {
const usersToBeInvited = [
buildExistingUser({
id: 1,
email: "user1@example.com",
username: "user1",
}),
{
id: null,
email: "user2@example.com",
},
] as const;
const loggedInUser = getLoggedInUser();
const input = {
teamId: 1,
role: MembershipRole.MEMBER,
language: "en",
usernameOrEmail: usersToBeInvited.map((u) => u.email),
};
const team = {
id: input.teamId,
name: "Team 1",
parent: null,
isOrganization: false,
};
const retValueOfGetTeamOrThrowError = inviteMemberUtilsScenarios.getTeamOrThrow.fakeReturnTeam(team, {
teamId: input.teamId,
});
const allExpectedInvitations = [
{
role: input.role,
usernameOrEmail: usersToBeInvited[0].email,
},
{
role: input.role,
usernameOrEmail: usersToBeInvited[1].email,
},
];
const retValueOfFindUsersWithInviteStatus =
inviteMemberUtilsScenarios.findUsersWithInviteStatus.useAdvancedMock(
[
{
...usersToBeInvited[0],
canBeInvited: INVITE_STATUS.CAN_BE_INVITED,
newRole: input.role,
},
],
{
invitations: allExpectedInvitations,
team,
}
);
const ctx = {
user: loggedInUser,
};
const result = await inviteMemberHandler({ ctx, input });
const expectedConnectionInfoMap = {
[usersToBeInvited[0].email]: {
orgId: undefined,
autoAccept: false,
},
[usersToBeInvited[1].email]: {
orgId: undefined,
autoAccept: false,
},
};
const inviter = {
name: loggedInUser.name,
};
expect(inviteMemberUtilsMock.handleNewUsersInvites).toHaveBeenCalledWith({
invitationsForNewUsers: allExpectedInvitations.slice(1),
team: retValueOfGetTeamOrThrowError,
orgConnectInfoByUsernameOrEmail: expectedConnectionInfoMap,
teamId: input.teamId,
language: input.language,
inviter,
autoAcceptEmailDomain: null,
isOrg: false,
});
expect(paymentsMock.updateQuantitySubscriptionFromStripe).toHaveBeenCalledWith(input.teamId);
expect(inviteMemberUtilsMock.handleExistingUsersInvites).toHaveBeenCalledWith({
invitableExistingUsers: retValueOfFindUsersWithInviteStatus,
teamId: input.teamId,
language: input.language,
inviter,
orgConnectInfoByUsernameOrEmail: expectedConnectionInfoMap,
orgSlug: null,
team: retValueOfGetTeamOrThrowError,
isOrg: false,
});
// Assert the result
expect(result).toEqual({
usernameOrEmail: input.usernameOrEmail,
numUsersInvited: 2,
});
});
});
it("With one email in input and that email is already a member of the team, it should throw error", async () => {
const userToBeInvited = buildExistingUser({
id: 1,
email: "user1@example.com",
username: "user1",
});
const loggedInUser = getLoggedInUser();
const input = {
teamId: 1,
role: MembershipRole.MEMBER,
isOrg: false,
language: "en",
usernameOrEmail: userToBeInvited.email,
};
const team = {
id: input.teamId,
name: "Team 1",
parent: null,
};
inviteMemberUtilsScenarios.getTeamOrThrow.fakeReturnTeam(team, {
teamId: input.teamId,
});
inviteMemberUtilsScenarios.findUsersWithInviteStatus.useAdvancedMock(
[
{
...userToBeInvited,
canBeInvited: INVITE_STATUS.USER_ALREADY_INVITED_OR_MEMBER,
newRole: input.role,
},
],
{
invitations: [
{
newRole: input.role,
usernameOrEmail: userToBeInvited.email,
},
],
team,
}
);
const ctx = {
user: loggedInUser,
};
try {
await inviteMemberHandler({ ctx, input });
throw new Error("Expected an error to be thrown");
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
expect(e).toBeInstanceOf(TRPCError);
expect(e.code).toEqual("BAD_REQUEST");
expect(e.message).toBe(INVITE_STATUS.USER_ALREADY_INVITED_OR_MEMBER);
}
});
});
it("When rate limit exceeded, it should throw error", async () => {
const userToBeInvited = buildExistingUser({
id: 1,
email: "user1@example.com",
username: "user1",
});
const errorMessageForRateLimit = checkRateLimitAndThrowErrorScenarios.fakeRateLimitFailed();
const loggedInUser = getLoggedInUser();
const input = {
teamId: 1,
role: MembershipRole.MEMBER,
isOrg: false,
language: "en",
usernameOrEmail: userToBeInvited.email,
};
const ctx = {
user: loggedInUser,
};
try {
await inviteMemberHandler({ ctx, input });
throw new Error("Expected an error to be thrown");
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
expect(e).toBeInstanceOf(Error);
expect(e.message).toEqual(errorMessageForRateLimit);
}
});
});