Skip to content

Commit

Permalink
fix: Error in team members migration during org onboarding (#15349)
Browse files Browse the repository at this point in the history
* fix: Error in team members migration during org onboarding

* Add invitationMemberHandler tests

* Add unit tests

* Improve tests and refactor

* Improve tests and refactor

* Fix type issue

* Fix createNewUsersConnectToOrgIfExists args
  • Loading branch information
hariombalhara authored Jun 27, 2024
1 parent 750676f commit 6670bbc
Show file tree
Hide file tree
Showing 15 changed files with 1,195 additions and 436 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,15 @@ export class OAuthClientUsersService {
const email = this.getOAuthUserEmail(oAuthClientId, body.email);
user = (
await createNewUsersConnectToOrgIfExists({
usernamesOrEmails: [email],
input: {
teamId: organizationId,
role: "MEMBER",
usernameOrEmail: [email],
isOrg: true,
language: "en",
},
invitations: [{
usernameOrEmail: email,
role: "MEMBER"
}],
teamId: organizationId,
isOrg: true,
parentId: null,
autoAcceptEmailDomain: "never-auto-accept-email-domain-for-managed-users",
connectionInfoMap: {
orgConnectInfoByUsernameOrEmail: {
[email]: {
orgId: organizationId,
autoAccept: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/emails/src/templates/TeamInviteEmail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ export const TeamInviteEmail = (
</Trans>
) : (
<Trans i18nKey="email_team_invite|content|invited_to_subteam">
{invitedBy} has added you to the team <strong>{teamName}</strong> in their organization{" "}
{invitedBy} has invited you to the team <strong>{teamName}</strong> in their organization{" "}
<strong>{parentTeamName}</strong>.
</Trans>
)}{" "}
Expand Down
3 changes: 1 addition & 2 deletions packages/features/ee/dsync/lib/handleGroupEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { createAProfileForAnExistingUser } from "@calcom/lib/createAProfileForAn
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma from "@calcom/prisma";
import { IdentityProvider, MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import {
getTeamOrThrow,
sendSignupToOrganizationEmail,
Expand Down Expand Up @@ -115,7 +114,7 @@ const handleGroupEvents = async (event: DirectorySyncEvent, organizationId: numb
newUserEmails.map((email) => {
return sendSignupToOrganizationEmail({
usernameOrEmail: email,
team: { ...group.team, metadata: teamMetadataSchema.parse(group.team.metadata) },
team: group.team,
translation,
inviterName: org.name,
teamId: group.teamId,
Expand Down
21 changes: 21 additions & 0 deletions packages/features/ee/teams/lib/__mocks__/payments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { beforeEach, vi, expect } from "vitest";
import { mockReset, mockDeep } from "vitest-mock-extended";

import type * as payments from "@calcom/features/ee/teams/lib/payments";

vi.mock("@calcom/features/ee/teams/lib/payments", () => paymentsMock);

beforeEach(() => {
mockReset(paymentsMock);
});

const paymentsMock = mockDeep<typeof payments>();

export const paymentsScenarios = {};
export const paymentsExpects = {
expectQuantitySubscriptionToBeUpdatedForTeam: (teamId: number) => {
expect(paymentsMock.updateQuantitySubscriptionFromStripe).toHaveBeenCalledWith(teamId);
},
};

export default paymentsMock;
28 changes: 28 additions & 0 deletions packages/lib/__mocks__/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { vi, beforeEach } from "vitest";

import type * as constants from "@calcom/lib/constants";

const mockedConstants = {
IS_PRODUCTION: false,
IS_TEAM_BILLING_ENABLED: false,
} as typeof constants;

vi.mock("@calcom/lib/constants", () => {
return mockedConstants;
});

beforeEach(() => {
Object.entries(mockedConstants).forEach(([key]) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
delete mockedConstants[key];
});
});

export const constantsScenarios = {
enableTeamBilling: () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
mockedConstants.IS_TEAM_BILLING_ENABLED = true;
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -219,22 +219,19 @@ async function moveTeam({
},
});

await Promise.all(
// TODO: Support different role for different members in usernameOrEmail list and then remove this map
team.members.map(async (membership) => {
// Invite team members to the new org. They are already members of the team.
await inviteMemberHandler({
ctx,
input: {
teamId: org.id,
language: "en",
role: membership.role,
usernameOrEmail: membership.user.email,
isOrg: true,
},
});
})
);
// Invite team members to the new org. They are already members of the team.
await inviteMemberHandler({
ctx,
input: {
teamId: org.id,
language: "en",
usernameOrEmail: team.members.map((m) => ({
email: m.user.email,
role: m.role,
})),
isOrg: true,
},
});

await addTeamRedirect({
oldTeamSlug: team.slug,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { beforeEach, vi, expect } from "vitest";
import { mockReset, mockDeep } from "vitest-mock-extended";

import type * as inviteMemberUtils from "../utils";

vi.mock("../utils", async () => {
return inviteMemberUtilsMock;
});

beforeEach(() => {
mockReset(inviteMemberUtilsMock);
});
const inviteMemberUtilsMock = mockDeep<typeof inviteMemberUtils>();

export const inviteMemberutilsScenarios = {
checkPermissions: {
fakePassed: () =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
inviteMemberUtilsMock.checkPermissions.mockResolvedValue(undefined),
},
getTeamOrThrow: {
fakeReturnTeam: (team: { id: number } & Record<string, any>, forInput: { teamId: number }) => {
const fakedVal = {
organizationSettings: null,
parent: null,
parentId: null,
...team,
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
inviteMemberUtilsMock.getTeamOrThrow.mockImplementation((teamId) => {
if (forInput.teamId === teamId) {
return fakedVal;
}
throw new Error("Mock Error: Unhandled input");
});
return fakedVal;
},
},
getOrgState: {
/**
* `getOrgState` completely generates the return value from input without using any outside variable like DB, etc.
* So, it makes sense to let it use the actual implementation instead of mocking the output based on input
*/
useActual: async function () {
const actualImport = await vi.importActual<typeof inviteMemberUtils>("../utils");

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
return inviteMemberUtilsMock.getOrgState.mockImplementation(actualImport.getOrgState);
},
},
getUniqueInvitationsOrThrowIfEmpty: {
useActual: async function () {
const actualImport = await vi.importActual<typeof inviteMemberUtils>("../utils");

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
return inviteMemberUtilsMock.getUniqueInvitationsOrThrowIfEmpty.mockImplementation(
actualImport.getUniqueInvitationsOrThrowIfEmpty
);
},
},
findUsersWithInviteStatus: {
useAdvancedMock: function (
returnVal: Awaited<ReturnType<typeof inviteMemberUtilsMock.findUsersWithInviteStatus>>,
forInput: {
team: any;
invitations: {
usernameOrEmail: string;
}[];
}
) {
inviteMemberUtilsMock.findUsersWithInviteStatus.mockImplementation(({ invitations, team }) => {
const allInvitationsExist = invitations.every((invitation) =>
forInput.invitations.find((i) => i.usernameOrEmail === invitation.usernameOrEmail)
);
if (forInput.team.id == team.id && allInvitationsExist) return returnVal;
});
return returnVal;
},
},
getOrgConnectionInfo: {
useActual: async function () {
const actualImport = await vi.importActual<typeof inviteMemberUtils>("../utils");

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
return inviteMemberUtilsMock.getOrgConnectionInfo.mockImplementation(actualImport.getOrgConnectionInfo);
},
},
};

export const expects = {
expectSignupEmailsToBeSent: ({
emails,
team,
inviterName,
isOrg,
teamId,
}: {
emails: string[];
team;
inviterName: string;
teamId: number;
isOrg: boolean;
}) => {
emails.forEach((email, index) => {
expect(inviteMemberUtilsMock.sendSignupToOrganizationEmail.mock.calls[index][0]).toEqual(
expect.objectContaining({
usernameOrEmail: email,
team: team,
inviterName: inviterName,
teamId: teamId,
isOrg: isOrg,
})
);
});
},
};
export default inviteMemberUtilsMock;
Loading

0 comments on commit 6670bbc

Please sign in to comment.