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:
+228
-47
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user