Skip to content

Commit e5b54ad

Browse files
fix: Error in team members migration during org onboarding (#15349)
* 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
1 parent 170559e commit e5b54ad

File tree

15 files changed

+1195
-436
lines changed

15 files changed

+1195
-436
lines changed

apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts

+7-9
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,15 @@ export class OAuthClientUsersService {
3636
const email = this.getOAuthUserEmail(oAuthClientId, body.email);
3737
user = (
3838
await createNewUsersConnectToOrgIfExists({
39-
usernamesOrEmails: [email],
40-
input: {
41-
teamId: organizationId,
42-
role: "MEMBER",
43-
usernameOrEmail: [email],
44-
isOrg: true,
45-
language: "en",
46-
},
39+
invitations: [{
40+
usernameOrEmail: email,
41+
role: "MEMBER"
42+
}],
43+
teamId: organizationId,
44+
isOrg: true,
4745
parentId: null,
4846
autoAcceptEmailDomain: "never-auto-accept-email-domain-for-managed-users",
49-
connectionInfoMap: {
47+
orgConnectInfoByUsernameOrEmail: {
5048
[email]: {
5149
orgId: organizationId,
5250
autoAccept: true,

packages/emails/src/templates/TeamInviteEmail.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ export const TeamInviteEmail = (
234234
</Trans>
235235
) : (
236236
<Trans i18nKey="email_team_invite|content|invited_to_subteam">
237-
{invitedBy} has added you to the team <strong>{teamName}</strong> in their organization{" "}
237+
{invitedBy} has invited you to the team <strong>{teamName}</strong> in their organization{" "}
238238
<strong>{parentTeamName}</strong>.
239239
</Trans>
240240
)}{" "}

packages/features/ee/dsync/lib/handleGroupEvents.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { createAProfileForAnExistingUser } from "@calcom/lib/createAProfileForAn
55
import { getTranslation } from "@calcom/lib/server/i18n";
66
import prisma from "@calcom/prisma";
77
import { IdentityProvider, MembershipRole } from "@calcom/prisma/enums";
8-
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
98
import {
109
getTeamOrThrow,
1110
sendSignupToOrganizationEmail,
@@ -115,7 +114,7 @@ const handleGroupEvents = async (event: DirectorySyncEvent, organizationId: numb
115114
newUserEmails.map((email) => {
116115
return sendSignupToOrganizationEmail({
117116
usernameOrEmail: email,
118-
team: { ...group.team, metadata: teamMetadataSchema.parse(group.team.metadata) },
117+
team: group.team,
119118
translation,
120119
inviterName: org.name,
121120
teamId: group.teamId,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { beforeEach, vi, expect } from "vitest";
2+
import { mockReset, mockDeep } from "vitest-mock-extended";
3+
4+
import type * as payments from "@calcom/features/ee/teams/lib/payments";
5+
6+
vi.mock("@calcom/features/ee/teams/lib/payments", () => paymentsMock);
7+
8+
beforeEach(() => {
9+
mockReset(paymentsMock);
10+
});
11+
12+
const paymentsMock = mockDeep<typeof payments>();
13+
14+
export const paymentsScenarios = {};
15+
export const paymentsExpects = {
16+
expectQuantitySubscriptionToBeUpdatedForTeam: (teamId: number) => {
17+
expect(paymentsMock.updateQuantitySubscriptionFromStripe).toHaveBeenCalledWith(teamId);
18+
},
19+
};
20+
21+
export default paymentsMock;

packages/lib/__mocks__/constants.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { vi, beforeEach } from "vitest";
2+
3+
import type * as constants from "@calcom/lib/constants";
4+
5+
const mockedConstants = {
6+
IS_PRODUCTION: false,
7+
IS_TEAM_BILLING_ENABLED: false,
8+
} as typeof constants;
9+
10+
vi.mock("@calcom/lib/constants", () => {
11+
return mockedConstants;
12+
});
13+
14+
beforeEach(() => {
15+
Object.entries(mockedConstants).forEach(([key]) => {
16+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
17+
// @ts-ignore
18+
delete mockedConstants[key];
19+
});
20+
});
21+
22+
export const constantsScenarios = {
23+
enableTeamBilling: () => {
24+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
25+
// @ts-ignore
26+
mockedConstants.IS_TEAM_BILLING_ENABLED = true;
27+
},
28+
};

packages/trpc/server/routers/viewer/organizations/createTeams.handler.ts

+13-16
Original file line numberDiff line numberDiff line change
@@ -219,22 +219,19 @@ async function moveTeam({
219219
},
220220
});
221221

222-
await Promise.all(
223-
// TODO: Support different role for different members in usernameOrEmail list and then remove this map
224-
team.members.map(async (membership) => {
225-
// Invite team members to the new org. They are already members of the team.
226-
await inviteMemberHandler({
227-
ctx,
228-
input: {
229-
teamId: org.id,
230-
language: "en",
231-
role: membership.role,
232-
usernameOrEmail: membership.user.email,
233-
isOrg: true,
234-
},
235-
});
236-
})
237-
);
222+
// Invite team members to the new org. They are already members of the team.
223+
await inviteMemberHandler({
224+
ctx,
225+
input: {
226+
teamId: org.id,
227+
language: "en",
228+
usernameOrEmail: team.members.map((m) => ({
229+
email: m.user.email,
230+
role: m.role,
231+
})),
232+
isOrg: true,
233+
},
234+
});
238235

239236
await addTeamRedirect({
240237
oldTeamSlug: team.slug,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { beforeEach, vi, expect } from "vitest";
2+
import { mockReset, mockDeep } from "vitest-mock-extended";
3+
4+
import type * as inviteMemberUtils from "../utils";
5+
6+
vi.mock("../utils", async () => {
7+
return inviteMemberUtilsMock;
8+
});
9+
10+
beforeEach(() => {
11+
mockReset(inviteMemberUtilsMock);
12+
});
13+
const inviteMemberUtilsMock = mockDeep<typeof inviteMemberUtils>();
14+
15+
export const inviteMemberutilsScenarios = {
16+
checkPermissions: {
17+
fakePassed: () =>
18+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
19+
//@ts-ignore
20+
inviteMemberUtilsMock.checkPermissions.mockResolvedValue(undefined),
21+
},
22+
getTeamOrThrow: {
23+
fakeReturnTeam: (team: { id: number } & Record<string, any>, forInput: { teamId: number }) => {
24+
const fakedVal = {
25+
organizationSettings: null,
26+
parent: null,
27+
parentId: null,
28+
...team,
29+
};
30+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
31+
//@ts-ignore
32+
inviteMemberUtilsMock.getTeamOrThrow.mockImplementation((teamId) => {
33+
if (forInput.teamId === teamId) {
34+
return fakedVal;
35+
}
36+
throw new Error("Mock Error: Unhandled input");
37+
});
38+
return fakedVal;
39+
},
40+
},
41+
getOrgState: {
42+
/**
43+
* `getOrgState` completely generates the return value from input without using any outside variable like DB, etc.
44+
* So, it makes sense to let it use the actual implementation instead of mocking the output based on input
45+
*/
46+
useActual: async function () {
47+
const actualImport = await vi.importActual<typeof inviteMemberUtils>("../utils");
48+
49+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
50+
//@ts-ignore
51+
return inviteMemberUtilsMock.getOrgState.mockImplementation(actualImport.getOrgState);
52+
},
53+
},
54+
getUniqueInvitationsOrThrowIfEmpty: {
55+
useActual: async function () {
56+
const actualImport = await vi.importActual<typeof inviteMemberUtils>("../utils");
57+
58+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
59+
//@ts-ignore
60+
return inviteMemberUtilsMock.getUniqueInvitationsOrThrowIfEmpty.mockImplementation(
61+
actualImport.getUniqueInvitationsOrThrowIfEmpty
62+
);
63+
},
64+
},
65+
findUsersWithInviteStatus: {
66+
useAdvancedMock: function (
67+
returnVal: Awaited<ReturnType<typeof inviteMemberUtilsMock.findUsersWithInviteStatus>>,
68+
forInput: {
69+
team: any;
70+
invitations: {
71+
usernameOrEmail: string;
72+
}[];
73+
}
74+
) {
75+
inviteMemberUtilsMock.findUsersWithInviteStatus.mockImplementation(({ invitations, team }) => {
76+
const allInvitationsExist = invitations.every((invitation) =>
77+
forInput.invitations.find((i) => i.usernameOrEmail === invitation.usernameOrEmail)
78+
);
79+
if (forInput.team.id == team.id && allInvitationsExist) return returnVal;
80+
});
81+
return returnVal;
82+
},
83+
},
84+
getOrgConnectionInfo: {
85+
useActual: async function () {
86+
const actualImport = await vi.importActual<typeof inviteMemberUtils>("../utils");
87+
88+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
89+
//@ts-ignore
90+
return inviteMemberUtilsMock.getOrgConnectionInfo.mockImplementation(actualImport.getOrgConnectionInfo);
91+
},
92+
},
93+
};
94+
95+
export const expects = {
96+
expectSignupEmailsToBeSent: ({
97+
emails,
98+
team,
99+
inviterName,
100+
isOrg,
101+
teamId,
102+
}: {
103+
emails: string[];
104+
team;
105+
inviterName: string;
106+
teamId: number;
107+
isOrg: boolean;
108+
}) => {
109+
emails.forEach((email, index) => {
110+
expect(inviteMemberUtilsMock.sendSignupToOrganizationEmail.mock.calls[index][0]).toEqual(
111+
expect.objectContaining({
112+
usernameOrEmail: email,
113+
team: team,
114+
inviterName: inviterName,
115+
teamId: teamId,
116+
isOrg: isOrg,
117+
})
118+
);
119+
});
120+
},
121+
};
122+
export default inviteMemberUtilsMock;

0 commit comments

Comments
 (0)