fix: restore listWithTeam query to include team events with userId set and add comprehensive tests (#27752)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Rajiv Sahal
2026-02-09 11:40:47 +00:00
committed by GitHub
co-authored by Devin AI
parent eba0635a07
commit d4a059033e
2 changed files with 243 additions and 61 deletions
@@ -1,23 +1,25 @@
import { describe, it, expect, afterAll, beforeAll } from "vitest";
import prisma from "@calcom/prisma";
import type { User, Team, EventType } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import type { EventType, Team, User } from "@calcom/prisma/client";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { listWithTeamHandler } from "./listWithTeam.handler";
let user1: User;
let user2: User;
let user3: User;
let team1: Team;
let team2: Team;
let team3: Team;
let eventType1: EventType;
let eventType2: EventType;
let eventType3: EventType;
let eventType4: EventType;
let managedParent: EventType;
let managedChild: EventType;
let teamEventNoUserId: EventType;
describe("listWithTeamHandler", () => {
beforeAll(async () => {
// Create users, teams and event types
const timestamp = Date.now();
user1 = await prisma.user.create({
data: {
@@ -33,6 +35,13 @@ describe("listWithTeamHandler", () => {
name: "Test User 2",
},
});
user3 = await prisma.user.create({
data: {
username: `testuser-lwt-3-${timestamp}`,
email: `testuser-lwt-3-${timestamp}@example.com`,
name: "Test User 3",
},
});
team1 = await prisma.team.create({
data: {
@@ -55,9 +64,23 @@ describe("listWithTeamHandler", () => {
},
});
team3 = await prisma.team.create({
data: {
name: "Team 3 lwt",
slug: `team-3-lwt-${timestamp}`,
members: {
create: {
userId: user1.id,
role: MembershipRole.ADMIN,
accepted: true,
},
},
},
});
eventType1 = await prisma.eventType.create({
data: {
title: "User 1 Event",
title: "User 1 Individual Event",
slug: `user1-event-lwt-${timestamp}`,
length: 30,
userId: user1.id,
@@ -66,7 +89,7 @@ describe("listWithTeamHandler", () => {
eventType2 = await prisma.eventType.create({
data: {
title: "Team 1 Event",
title: "Team 1 Event (user1 owner + member)",
slug: `team1-event-lwt-${timestamp}`,
length: 30,
teamId: team1.id,
@@ -76,7 +99,7 @@ describe("listWithTeamHandler", () => {
eventType3 = await prisma.eventType.create({
data: {
title: "User 2 Event",
title: "User 2 Individual Event",
slug: `user2-event-lwt-${timestamp}`,
length: 30,
userId: user2.id,
@@ -85,42 +108,70 @@ describe("listWithTeamHandler", () => {
eventType4 = await prisma.eventType.create({
data: {
title: "Team 2 Event",
title: "Team 2 Event (user2 owner, NOT member)",
slug: `team2-event-lwt-${timestamp}`,
length: 30,
teamId: team2.id,
userId: user2.id,
},
});
managedParent = await prisma.eventType.create({
data: {
title: "Managed Parent Event",
slug: `managed-parent-lwt-${timestamp}`,
length: 45,
teamId: team3.id,
schedulingType: SchedulingType.MANAGED,
},
});
managedChild = await prisma.eventType.create({
data: {
title: "Managed Child Event",
slug: `managed-child-lwt-${timestamp}`,
length: 45,
userId: user1.id,
parentId: managedParent.id,
},
});
teamEventNoUserId = await prisma.eventType.create({
data: {
title: "Team 3 Event (no userId)",
slug: `team3-nouserid-lwt-${timestamp}`,
length: 60,
teamId: team3.id,
},
});
});
afterAll(async () => {
try {
if (eventType1?.id || eventType2?.id || eventType3?.id || eventType4?.id) {
const eventTypeIds = [
eventType1?.id,
eventType2?.id,
eventType3?.id,
eventType4?.id,
managedChild?.id,
managedParent?.id,
teamEventNoUserId?.id,
].filter(Boolean);
if (eventTypeIds.length > 0) {
await prisma.eventType.deleteMany({
where: {
id: {
in: [eventType1?.id, eventType2?.id, eventType3?.id, eventType4?.id].filter(Boolean),
},
},
where: { id: { in: eventTypeIds } },
});
}
if (team1?.id || team2?.id) {
const teamIds = [team1?.id, team2?.id, team3?.id].filter(Boolean);
if (teamIds.length > 0) {
await prisma.team.deleteMany({
where: {
id: {
in: [team1?.id, team2?.id].filter(Boolean),
},
},
where: { id: { in: teamIds } },
});
}
if (user1?.id || user2?.id) {
const userIds = [user1?.id, user2?.id, user3?.id].filter(Boolean);
if (userIds.length > 0) {
await prisma.user.deleteMany({
where: {
id: {
in: [user1?.id, user2?.id].filter(Boolean),
},
},
where: { id: { in: userIds } },
});
}
} catch (error) {
@@ -128,32 +179,162 @@ describe("listWithTeamHandler", () => {
}
});
it("should return user's own event types and event types of teams they are a member of", async () => {
it("should return user's own event types and team event types they are a member of", async () => {
const result = await listWithTeamHandler({
ctx: {
// we only need the id from the user object
user: { id: user1.id } as any,
},
ctx: { user: { id: user1.id } },
});
expect(result).toHaveLength(2);
const resultIds = result.map((e) => e.id);
expect(resultIds).toContain(eventType1.id);
expect(resultIds).toContain(eventType2.id);
const eventType1Result = result.find((e) => e.id === eventType1.id);
expect(eventType1Result).toBeDefined();
expect(eventType1Result?.title).toBe(eventType1.title);
expect(eventType1Result?.slug).toBe(eventType1.slug);
expect(eventType1Result?.team).toBeNull();
const individualResult = result.find((e) => e.id === eventType1.id);
expect(individualResult).toBeDefined();
expect(individualResult?.title).toBe(eventType1.title);
expect(individualResult?.slug).toBe(eventType1.slug);
expect(individualResult?.team).toBeNull();
const eventType2Result = result.find((e) => e.id === eventType2.id);
expect(eventType2Result).toBeDefined();
expect(eventType2Result?.title).toBe(eventType2.title);
expect(eventType2Result?.slug).toBe(eventType2.slug);
expect(eventType2Result?.team).toBeDefined();
expect(eventType2Result?.team?.id).toBe(team1.id);
expect(eventType2Result?.team?.name).toBe(team1.name);
const teamResult = result.find((e) => e.id === eventType2.id);
expect(teamResult).toBeDefined();
expect(teamResult?.title).toBe(eventType2.title);
expect(teamResult?.slug).toBe(eventType2.slug);
expect(teamResult?.team).toBeDefined();
expect(teamResult?.team?.id).toBe(team1.id);
expect(teamResult?.team?.name).toBe(team1.name);
});
it("should not return event types belonging to other users", async () => {
const result = await listWithTeamHandler({
ctx: { user: { id: user1.id } },
});
const resultIds = result.map((e) => e.id);
expect(resultIds).not.toContain(eventType3.id);
expect(resultIds).not.toContain(eventType4.id);
});
it("should return team event types where userId is set but user has no membership", async () => {
const result = await listWithTeamHandler({
ctx: { user: { id: user2.id } },
});
const resultIds = result.map((e) => e.id);
expect(resultIds).toContain(eventType3.id);
expect(resultIds).toContain(eventType4.id);
const teamEventResult = result.find((e) => e.id === eventType4.id);
expect(teamEventResult).toBeDefined();
expect(teamEventResult?.team?.id).toBe(team2.id);
expect(teamEventResult?.team?.name).toBe(team2.name);
});
it("should not duplicate events when user is both owner and team member", async () => {
const result = await listWithTeamHandler({
ctx: { user: { id: user1.id } },
});
const eventType2Occurrences = result.filter((e) => e.id === eventType2.id);
expect(eventType2Occurrences).toHaveLength(1);
});
it("should return managed event type children with userId set", async () => {
const result = await listWithTeamHandler({
ctx: { user: { id: user1.id } },
});
const resultIds = result.map((e) => e.id);
expect(resultIds).toContain(managedChild.id);
const managedChildResult = result.find((e) => e.id === managedChild.id);
expect(managedChildResult).toBeDefined();
expect(managedChildResult?.title).toBe(managedChild.title);
expect(managedChildResult?.team).toBeNull();
});
it("should return team event types without userId via membership", async () => {
const result = await listWithTeamHandler({
ctx: { user: { id: user1.id } },
});
const resultIds = result.map((e) => e.id);
expect(resultIds).toContain(teamEventNoUserId.id);
const noUserIdResult = result.find((e) => e.id === teamEventNoUserId.id);
expect(noUserIdResult).toBeDefined();
expect(noUserIdResult?.team?.id).toBe(team3.id);
expect(noUserIdResult?.team?.name).toBe(team3.name);
});
it("should populate username for individual event types and null for team event types", async () => {
const result = await listWithTeamHandler({
ctx: { user: { id: user1.id } },
});
const individualResult = result.find((e) => e.id === eventType1.id);
expect(individualResult?.username).toBe(user1.username);
const teamResult = result.find((e) => e.id === eventType2.id);
expect(teamResult?.username).toBeNull();
const noUserIdTeamResult = result.find((e) => e.id === teamEventNoUserId.id);
expect(noUserIdTeamResult?.username).toBeNull();
});
it("should return username for user2 individual events", async () => {
const result = await listWithTeamHandler({
ctx: { user: { id: user2.id } },
});
const individualResult = result.find((e) => e.id === eventType3.id);
expect(individualResult?.username).toBe(user2.username);
const teamEventResult = result.find((e) => e.id === eventType4.id);
expect(teamEventResult?.username).toBeNull();
});
it("should return empty array for user with no event types", async () => {
const result = await listWithTeamHandler({
ctx: { user: { id: user3.id } },
});
expect(result).toHaveLength(0);
});
it("should not return team events for users with unaccepted membership", async () => {
const timestamp = Date.now();
const pendingTeam = await prisma.team.create({
data: {
name: "Pending Team lwt",
slug: `pending-team-lwt-${timestamp}`,
members: {
create: {
userId: user3.id,
role: MembershipRole.MEMBER,
accepted: false,
},
},
},
});
const pendingTeamEvent = await prisma.eventType.create({
data: {
title: "Pending Team Event",
slug: `pending-team-event-lwt-${timestamp}`,
length: 30,
teamId: pendingTeam.id,
},
});
try {
const result = await listWithTeamHandler({
ctx: { user: { id: user3.id } },
});
const resultIds = result.map((e) => e.id);
expect(resultIds).not.toContain(pendingTeamEvent.id);
} finally {
await prisma.eventType.delete({ where: { id: pendingTeamEvent.id } });
await prisma.team.delete({ where: { id: pendingTeam.id } });
}
});
});
@@ -1,6 +1,5 @@
import db from "@calcom/prisma";
import { Prisma } from "@calcom/prisma/client";
import type { TrpcSessionUser } from "../../../types";
type ListWithTeamOptions = {
@@ -15,25 +14,27 @@ export const listWithTeamHandler = async ({ ctx }: ListWithTeamOptions) => {
FROM "public"."EventType"
LEFT JOIN "public"."Team" AS "j1" ON ("j1"."id") = ("public"."EventType"."teamId")
LEFT JOIN "public"."users" AS "u" ON ("u"."id") = ("public"."EventType"."userId")
WHERE "public"."EventType"."userId" = ${userId} AND "public"."EventType"."teamId" IS NULL
WHERE "public"."EventType"."userId" = ${userId}
UNION
SELECT "public"."EventType"."id", "public"."EventType"."teamId", "public"."EventType"."title", "public"."EventType"."slug", "public"."EventType"."length", "j1"."name" as "teamName", NULL as "username"
SELECT "public"."EventType"."id", "public"."EventType"."teamId", "public"."EventType"."title", "public"."EventType"."slug", "public"."EventType"."length", "j1"."name" as "teamName", "u"."username" as "username"
FROM "public"."EventType"
INNER JOIN "public"."Team" AS "j1" ON ("j1"."id") = ("public"."EventType"."teamId")
INNER JOIN "public"."Membership" AS "t2" ON "t2"."teamId" = "j1"."id"
LEFT JOIN "public"."users" AS "u" ON ("u"."id") = ("public"."EventType"."userId")
WHERE "t2"."userId" = ${userId} AND "t2"."accepted" = true`;
const result = await db.$queryRaw<
{
id: number;
teamId: number | null;
title: string;
slug: string;
length: number;
teamName: string | null;
username: string | null;
}[]
>(query);
const result =
await db.$queryRaw<
{
id: number;
teamId: number | null;
title: string;
slug: string;
length: number;
teamName: string | null;
username: string | null;
}[]
>(query);
return result.map((row) => ({
id: row.id,