chore: team update handler refactor (#25332)

* chore: team update handler refactor

* add tests

* fix
This commit is contained in:
Anik Dhabal Babu
2025-11-25 11:27:26 +00:00
committed by GitHub
parent 9c2d7f99c2
commit 57476ea392
2 changed files with 242 additions and 20 deletions
@@ -0,0 +1,215 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { prisma } from "@calcom/prisma";
import { RRTimestampBasis } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "../../../types";
import { updateHandler } from "./update.handler";
import type { TUpdateInputSchema } from "./update.schema";
vi.mock("@calcom/prisma", () => ({
prisma: {
team: {
findUnique: vi.fn(),
update: vi.fn(),
updateMany: vi.fn(),
},
eventType: {
updateMany: vi.fn(),
},
tempOrgRedirect: {
updateMany: vi.fn(),
},
},
}));
vi.mock("@calcom/features/pbac/services/permission-check.service", () => ({
PermissionCheckService: vi.fn().mockImplementation(() => ({
checkPermission: vi.fn(),
})),
}));
vi.mock("@calcom/features/ee/teams/repositories/TeamRepository", () => ({
TeamRepository: vi.fn().mockImplementation(() => ({
isSlugAvailableForUpdate: vi.fn().mockResolvedValue(true),
})),
}));
vi.mock("@calcom/lib/server/avatar", () => ({
uploadLogo: vi.fn().mockResolvedValue("https://example.com/logo.png"),
}));
vi.mock("@calcom/lib/intervalLimits/validateIntervalLimitOrder", () => ({
validateIntervalLimitOrder: vi.fn().mockReturnValue(true),
}));
vi.mock("@calcom/lib/constants", () => ({
IS_TEAM_BILLING_ENABLED: false,
}));
vi.mock("@calcom/ee/organizations/lib/orgDomains", () => ({
getOrgFullOrigin: vi.fn((slug: string) => `https://${slug}.cal.com`),
}));
describe("updateHandler - Permission Check Tests", () => {
const mockPermissionCheckService = {
checkPermission: vi.fn(),
};
const mockTeamRepository = {
isSlugAvailableForUpdate: vi.fn().mockResolvedValue(true),
};
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(PermissionCheckService).mockImplementation(
() => mockPermissionCheckService as unknown as InstanceType<typeof PermissionCheckService>
);
vi.mocked(TeamRepository).mockImplementation(
() => mockTeamRepository as unknown as InstanceType<typeof TeamRepository>
);
});
describe("Permission Check Service", () => {
const user: NonNullable<TrpcSessionUser> = {
id: 1,
organizationId: 100,
} as NonNullable<TrpcSessionUser>;
it("should use permission check service for all users", async () => {
const input: TUpdateInputSchema = {
id: 50,
name: "Updated Team",
};
mockPermissionCheckService.checkPermission.mockResolvedValue(true);
vi.mocked(prisma.team.findUnique).mockResolvedValue({
id: 50,
parentId: 100,
slug: "test-team",
metadata: {},
rrTimestampBasis: RRTimestampBasis.CREATED_AT,
} as any);
vi.mocked(prisma.team.update).mockResolvedValue({
id: 50,
name: "Updated Team",
bio: null,
slug: "test-team",
theme: null,
brandColor: null,
darkBrandColor: null,
logoUrl: null,
bookingLimits: null,
includeManagedEventsInLimits: null,
rrResetInterval: null,
rrTimestampBasis: RRTimestampBasis.CREATED_AT,
} as any);
await updateHandler({
ctx: { user },
input,
});
expect(mockPermissionCheckService.checkPermission).toHaveBeenCalledWith({
userId: user.id,
teamId: input.id,
permission: "team.update",
fallbackRoles: expect.any(Array),
});
expect(prisma.team.update).toHaveBeenCalled();
});
it("should throw UNAUTHORIZED when permission check fails", async () => {
const input: TUpdateInputSchema = {
id: 50,
name: "Unauthorized Update",
};
mockPermissionCheckService.checkPermission.mockResolvedValue(false);
vi.mocked(prisma.team.findUnique).mockResolvedValue({
id: 50,
parentId: 100,
slug: "test-team",
metadata: {},
rrTimestampBasis: RRTimestampBasis.CREATED_AT,
} as any);
await expect(
updateHandler({
ctx: { user },
input,
})
).rejects.toMatchObject({
code: "UNAUTHORIZED",
});
expect(prisma.team.update).not.toHaveBeenCalled();
});
it("should throw UNAUTHORIZED when user has no id", async () => {
const userWithoutId = {
id: undefined,
organizationId: 100,
} as unknown as NonNullable<TrpcSessionUser>;
const input: TUpdateInputSchema = {
id: 50,
name: "Updated Team",
};
vi.mocked(prisma.team.findUnique).mockResolvedValue({
id: 50,
parentId: 100,
slug: "test-team",
metadata: {},
rrTimestampBasis: RRTimestampBasis.CREATED_AT,
} as any);
await expect(
updateHandler({
ctx: { user: userWithoutId },
input,
})
).rejects.toMatchObject({
code: "UNAUTHORIZED",
});
expect(mockPermissionCheckService.checkPermission).not.toHaveBeenCalled();
expect(prisma.team.update).not.toHaveBeenCalled();
});
});
describe("Team Not Found", () => {
const user: NonNullable<TrpcSessionUser> = {
id: 1,
organizationId: 100,
} as NonNullable<TrpcSessionUser>;
it("should throw NOT_FOUND when team does not exist", async () => {
const input: TUpdateInputSchema = {
id: 999,
name: "Non-existent Team",
};
vi.mocked(prisma.team.findUnique).mockResolvedValue(null);
await expect(
updateHandler({
ctx: { user },
input,
})
).rejects.toMatchObject({
code: "NOT_FOUND",
message: "Team not found.",
});
// Should not check permissions if team doesn't exist
expect(mockPermissionCheckService.checkPermission).not.toHaveBeenCalled();
});
});
});
@@ -23,20 +23,35 @@ type UpdateOptions = {
};
export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
const isOrgAdmin = ctx.user?.organization?.isOrgAdmin;
const prevTeam = await prisma.team.findUnique({
where: {
id: input.id,
},
select: {
id: true,
parentId: true,
slug: true,
metadata: true,
rrTimestampBasis: true,
},
});
if (!isOrgAdmin) {
const permissionCheckService = new PermissionCheckService();
const hasTeamUpdatePermission = await permissionCheckService.checkPermission({
userId: ctx.user?.id || 0,
teamId: input.id,
permission: "team.update",
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
});
if (!prevTeam) throw new TRPCError({ code: "NOT_FOUND", message: "Team not found." });
if (!hasTeamUpdatePermission) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
if (!ctx.user?.id) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const permissionCheckService = new PermissionCheckService();
const hasTeamUpdatePermission = await permissionCheckService.checkPermission({
userId: ctx.user.id,
teamId: input.id,
permission: "team.update",
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
});
if (!hasTeamUpdatePermission) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
if (input.slug) {
@@ -52,14 +67,6 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
}
}
const prevTeam = await prisma.team.findUnique({
where: {
id: input.id,
},
});
if (!prevTeam) throw new TRPCError({ code: "NOT_FOUND", message: "Team not found." });
if (input.bookingLimits) {
const isValid = validateIntervalLimitOrder(input.bookingLimits);
if (!isValid)