Skip to content

Commit d4a0590

Browse files
fix: restore listWithTeam query to include team events with userId set and add comprehensive tests (calcom#27752)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent eba0635 commit d4a0590

2 files changed

Lines changed: 244 additions & 62 deletions

File tree

Lines changed: 229 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
1-
import { describe, it, expect, afterAll, beforeAll } from "vitest";
2-
31
import prisma from "@calcom/prisma";
4-
import type { User, Team, EventType } from "@calcom/prisma/client";
5-
import { MembershipRole } from "@calcom/prisma/enums";
6-
2+
import type { EventType, Team, User } from "@calcom/prisma/client";
3+
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
4+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
75
import { listWithTeamHandler } from "./listWithTeam.handler";
86

97
let user1: User;
108
let user2: User;
9+
let user3: User;
1110
let team1: Team;
1211
let team2: Team;
12+
let team3: Team;
1313
let eventType1: EventType;
1414
let eventType2: EventType;
1515
let eventType3: EventType;
1616
let eventType4: EventType;
17+
let managedParent: EventType;
18+
let managedChild: EventType;
19+
let teamEventNoUserId: EventType;
1720

1821
describe("listWithTeamHandler", () => {
1922
beforeAll(async () => {
20-
// Create users, teams and event types
2123
const timestamp = Date.now();
2224
user1 = await prisma.user.create({
2325
data: {
@@ -33,6 +35,13 @@ describe("listWithTeamHandler", () => {
3335
name: "Test User 2",
3436
},
3537
});
38+
user3 = await prisma.user.create({
39+
data: {
40+
username: `testuser-lwt-3-${timestamp}`,
41+
email: `testuser-lwt-3-${timestamp}@example.com`,
42+
name: "Test User 3",
43+
},
44+
});
3645

3746
team1 = await prisma.team.create({
3847
data: {
@@ -55,9 +64,23 @@ describe("listWithTeamHandler", () => {
5564
},
5665
});
5766

67+
team3 = await prisma.team.create({
68+
data: {
69+
name: "Team 3 lwt",
70+
slug: `team-3-lwt-${timestamp}`,
71+
members: {
72+
create: {
73+
userId: user1.id,
74+
role: MembershipRole.ADMIN,
75+
accepted: true,
76+
},
77+
},
78+
},
79+
});
80+
5881
eventType1 = await prisma.eventType.create({
5982
data: {
60-
title: "User 1 Event",
83+
title: "User 1 Individual Event",
6184
slug: `user1-event-lwt-${timestamp}`,
6285
length: 30,
6386
userId: user1.id,
@@ -66,7 +89,7 @@ describe("listWithTeamHandler", () => {
6689

6790
eventType2 = await prisma.eventType.create({
6891
data: {
69-
title: "Team 1 Event",
92+
title: "Team 1 Event (user1 owner + member)",
7093
slug: `team1-event-lwt-${timestamp}`,
7194
length: 30,
7295
teamId: team1.id,
@@ -76,7 +99,7 @@ describe("listWithTeamHandler", () => {
7699

77100
eventType3 = await prisma.eventType.create({
78101
data: {
79-
title: "User 2 Event",
102+
title: "User 2 Individual Event",
80103
slug: `user2-event-lwt-${timestamp}`,
81104
length: 30,
82105
userId: user2.id,
@@ -85,75 +108,233 @@ describe("listWithTeamHandler", () => {
85108

86109
eventType4 = await prisma.eventType.create({
87110
data: {
88-
title: "Team 2 Event",
111+
title: "Team 2 Event (user2 owner, NOT member)",
89112
slug: `team2-event-lwt-${timestamp}`,
90113
length: 30,
91114
teamId: team2.id,
92115
userId: user2.id,
93116
},
94117
});
118+
119+
managedParent = await prisma.eventType.create({
120+
data: {
121+
title: "Managed Parent Event",
122+
slug: `managed-parent-lwt-${timestamp}`,
123+
length: 45,
124+
teamId: team3.id,
125+
schedulingType: SchedulingType.MANAGED,
126+
},
127+
});
128+
129+
managedChild = await prisma.eventType.create({
130+
data: {
131+
title: "Managed Child Event",
132+
slug: `managed-child-lwt-${timestamp}`,
133+
length: 45,
134+
userId: user1.id,
135+
parentId: managedParent.id,
136+
},
137+
});
138+
139+
teamEventNoUserId = await prisma.eventType.create({
140+
data: {
141+
title: "Team 3 Event (no userId)",
142+
slug: `team3-nouserid-lwt-${timestamp}`,
143+
length: 60,
144+
teamId: team3.id,
145+
},
146+
});
95147
});
96148

97149
afterAll(async () => {
98150
try {
99-
if (eventType1?.id || eventType2?.id || eventType3?.id || eventType4?.id) {
151+
const eventTypeIds = [
152+
eventType1?.id,
153+
eventType2?.id,
154+
eventType3?.id,
155+
eventType4?.id,
156+
managedChild?.id,
157+
managedParent?.id,
158+
teamEventNoUserId?.id,
159+
].filter(Boolean);
160+
if (eventTypeIds.length > 0) {
100161
await prisma.eventType.deleteMany({
101-
where: {
102-
id: {
103-
in: [eventType1?.id, eventType2?.id, eventType3?.id, eventType4?.id].filter(Boolean),
104-
},
105-
},
162+
where: { id: { in: eventTypeIds } },
106163
});
107164
}
108-
if (team1?.id || team2?.id) {
165+
const teamIds = [team1?.id, team2?.id, team3?.id].filter(Boolean);
166+
if (teamIds.length > 0) {
109167
await prisma.team.deleteMany({
110-
where: {
111-
id: {
112-
in: [team1?.id, team2?.id].filter(Boolean),
113-
},
114-
},
168+
where: { id: { in: teamIds } },
115169
});
116170
}
117-
if (user1?.id || user2?.id) {
171+
const userIds = [user1?.id, user2?.id, user3?.id].filter(Boolean);
172+
if (userIds.length > 0) {
118173
await prisma.user.deleteMany({
119-
where: {
120-
id: {
121-
in: [user1?.id, user2?.id].filter(Boolean),
122-
},
123-
},
174+
where: { id: { in: userIds } },
124175
});
125176
}
126177
} catch (error) {
127178
console.warn("Test cleanup failed:", error);
128179
}
129180
});
130181

131-
it("should return user's own event types and event types of teams they are a member of", async () => {
182+
it("should return user's own event types and team event types they are a member of", async () => {
132183
const result = await listWithTeamHandler({
133-
ctx: {
134-
// we only need the id from the user object
135-
user: { id: user1.id } as any,
136-
},
184+
ctx: { user: { id: user1.id } },
137185
});
138186

139-
expect(result).toHaveLength(2);
140-
141187
const resultIds = result.map((e) => e.id);
142188
expect(resultIds).toContain(eventType1.id);
143189
expect(resultIds).toContain(eventType2.id);
144190

145-
const eventType1Result = result.find((e) => e.id === eventType1.id);
146-
expect(eventType1Result).toBeDefined();
147-
expect(eventType1Result?.title).toBe(eventType1.title);
148-
expect(eventType1Result?.slug).toBe(eventType1.slug);
149-
expect(eventType1Result?.team).toBeNull();
150-
151-
const eventType2Result = result.find((e) => e.id === eventType2.id);
152-
expect(eventType2Result).toBeDefined();
153-
expect(eventType2Result?.title).toBe(eventType2.title);
154-
expect(eventType2Result?.slug).toBe(eventType2.slug);
155-
expect(eventType2Result?.team).toBeDefined();
156-
expect(eventType2Result?.team?.id).toBe(team1.id);
157-
expect(eventType2Result?.team?.name).toBe(team1.name);
191+
const individualResult = result.find((e) => e.id === eventType1.id);
192+
expect(individualResult).toBeDefined();
193+
expect(individualResult?.title).toBe(eventType1.title);
194+
expect(individualResult?.slug).toBe(eventType1.slug);
195+
expect(individualResult?.team).toBeNull();
196+
197+
const teamResult = result.find((e) => e.id === eventType2.id);
198+
expect(teamResult).toBeDefined();
199+
expect(teamResult?.title).toBe(eventType2.title);
200+
expect(teamResult?.slug).toBe(eventType2.slug);
201+
expect(teamResult?.team).toBeDefined();
202+
expect(teamResult?.team?.id).toBe(team1.id);
203+
expect(teamResult?.team?.name).toBe(team1.name);
204+
});
205+
206+
it("should not return event types belonging to other users", async () => {
207+
const result = await listWithTeamHandler({
208+
ctx: { user: { id: user1.id } },
209+
});
210+
211+
const resultIds = result.map((e) => e.id);
212+
expect(resultIds).not.toContain(eventType3.id);
213+
expect(resultIds).not.toContain(eventType4.id);
214+
});
215+
216+
it("should return team event types where userId is set but user has no membership", async () => {
217+
const result = await listWithTeamHandler({
218+
ctx: { user: { id: user2.id } },
219+
});
220+
221+
const resultIds = result.map((e) => e.id);
222+
expect(resultIds).toContain(eventType3.id);
223+
expect(resultIds).toContain(eventType4.id);
224+
225+
const teamEventResult = result.find((e) => e.id === eventType4.id);
226+
expect(teamEventResult).toBeDefined();
227+
expect(teamEventResult?.team?.id).toBe(team2.id);
228+
expect(teamEventResult?.team?.name).toBe(team2.name);
229+
});
230+
231+
it("should not duplicate events when user is both owner and team member", async () => {
232+
const result = await listWithTeamHandler({
233+
ctx: { user: { id: user1.id } },
234+
});
235+
236+
const eventType2Occurrences = result.filter((e) => e.id === eventType2.id);
237+
expect(eventType2Occurrences).toHaveLength(1);
238+
});
239+
240+
it("should return managed event type children with userId set", async () => {
241+
const result = await listWithTeamHandler({
242+
ctx: { user: { id: user1.id } },
243+
});
244+
245+
const resultIds = result.map((e) => e.id);
246+
expect(resultIds).toContain(managedChild.id);
247+
248+
const managedChildResult = result.find((e) => e.id === managedChild.id);
249+
expect(managedChildResult).toBeDefined();
250+
expect(managedChildResult?.title).toBe(managedChild.title);
251+
expect(managedChildResult?.team).toBeNull();
252+
});
253+
254+
it("should return team event types without userId via membership", async () => {
255+
const result = await listWithTeamHandler({
256+
ctx: { user: { id: user1.id } },
257+
});
258+
259+
const resultIds = result.map((e) => e.id);
260+
expect(resultIds).toContain(teamEventNoUserId.id);
261+
262+
const noUserIdResult = result.find((e) => e.id === teamEventNoUserId.id);
263+
expect(noUserIdResult).toBeDefined();
264+
expect(noUserIdResult?.team?.id).toBe(team3.id);
265+
expect(noUserIdResult?.team?.name).toBe(team3.name);
266+
});
267+
268+
it("should populate username for individual event types and null for team event types", async () => {
269+
const result = await listWithTeamHandler({
270+
ctx: { user: { id: user1.id } },
271+
});
272+
273+
const individualResult = result.find((e) => e.id === eventType1.id);
274+
expect(individualResult?.username).toBe(user1.username);
275+
276+
const teamResult = result.find((e) => e.id === eventType2.id);
277+
expect(teamResult?.username).toBeNull();
278+
279+
const noUserIdTeamResult = result.find((e) => e.id === teamEventNoUserId.id);
280+
expect(noUserIdTeamResult?.username).toBeNull();
281+
});
282+
283+
it("should return username for user2 individual events", async () => {
284+
const result = await listWithTeamHandler({
285+
ctx: { user: { id: user2.id } },
286+
});
287+
288+
const individualResult = result.find((e) => e.id === eventType3.id);
289+
expect(individualResult?.username).toBe(user2.username);
290+
291+
const teamEventResult = result.find((e) => e.id === eventType4.id);
292+
expect(teamEventResult?.username).toBeNull();
293+
});
294+
295+
it("should return empty array for user with no event types", async () => {
296+
const result = await listWithTeamHandler({
297+
ctx: { user: { id: user3.id } },
298+
});
299+
300+
expect(result).toHaveLength(0);
301+
});
302+
303+
it("should not return team events for users with unaccepted membership", async () => {
304+
const timestamp = Date.now();
305+
const pendingTeam = await prisma.team.create({
306+
data: {
307+
name: "Pending Team lwt",
308+
slug: `pending-team-lwt-${timestamp}`,
309+
members: {
310+
create: {
311+
userId: user3.id,
312+
role: MembershipRole.MEMBER,
313+
accepted: false,
314+
},
315+
},
316+
},
317+
});
318+
319+
const pendingTeamEvent = await prisma.eventType.create({
320+
data: {
321+
title: "Pending Team Event",
322+
slug: `pending-team-event-lwt-${timestamp}`,
323+
length: 30,
324+
teamId: pendingTeam.id,
325+
},
326+
});
327+
328+
try {
329+
const result = await listWithTeamHandler({
330+
ctx: { user: { id: user3.id } },
331+
});
332+
333+
const resultIds = result.map((e) => e.id);
334+
expect(resultIds).not.toContain(pendingTeamEvent.id);
335+
} finally {
336+
await prisma.eventType.delete({ where: { id: pendingTeamEvent.id } });
337+
await prisma.team.delete({ where: { id: pendingTeam.id } });
338+
}
158339
});
159340
});

0 commit comments

Comments
 (0)