* fix: remove @calcom/web imports from packages/features to eliminate circular dependency - Migrate UserTableUser and MemberPermissions types to packages/features/users/types/user-table.ts - Migrate useGeo hook to packages/features/geo/GeoContext.tsx - Migrate buildLegacyRequest to packages/lib/buildLegacyCtx.ts - Migrate Calendar component to packages/features/calendars/weeklyview/components/ - Move test utilities (bookingScenario, fixtures) to packages/features/test/ - Update all imports in packages/features to use new locations - Add re-exports in apps/web for backward compatibility Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: delete original implementation files and fix type issues - Delete original calendar component files in apps/web (keep only re-export stubs) - Migrate OutOfOfficeInSlots to packages/features/bookings/components - Convert apps/web OutOfOfficeInSlots to re-export stub - Fix className vs class issue in Calendar.tsx - Fix @calcom/trpc import violation in user-table.ts by using structural type Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add missing isGroup and contains fields to UserTableUser attributes type Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: update customRole type to match actual Prisma Role model Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix build * fix * fix * cleanup weeklyview * fix * refactor to mv MemberPermissions to types package * add types dependency to features * fix * fix * fix * fix * fix * fix * rename * rename * migrate * migrate * migrate * fix * fix * fix * refactor: move test utilities from packages/features/test to tests/libs - Move bookingScenario utilities to tests/libs/bookingScenario - Move fixtures to tests/libs/fixtures - Update all imports in packages/features test files to use new location - Update all imports in apps/web test files to use new location - Eliminates duplication of test utilities between packages/features and apps/web Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: correct relative import paths for tests/libs in test files Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * refactor: replace test utility implementations with re-exports to tests/libs Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: fix test import paths and move signup handler tests to apps/web Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: move buildLegacyCtx to packages/lib and restore handlers to packages/features - Move buildLegacyCtx from apps/web/lib to packages/lib to break circular dependency - Update apps/web/lib/buildLegacyCtx.ts to re-export from @calcom/lib - Restore signup handlers and tests to packages/features/auth/signup/handlers - Update handler imports to use @calcom/lib/buildLegacyCtx instead of @calcom/web/lib/buildLegacyCtx Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: update recurring-event.test.ts imports to use tests/libs path Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * refactor: delete test re-export files and update imports to use tests/libs directly Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: update remaining test imports to use tests/libs directly Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: update handleRecurringEventBooking calls to match function signature (1 arg) Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * remove * migrate tests * migrate tests * refactor: update test mock imports by removing and using async for mock creators. * fix type errors * fix: add type assertion for MockUser in p2002.test-suite.ts Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: restore locale import in compareReminderBodyToTemplate.test.ts using relative path Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * feat: create @calcom/testing package and migrate tests from /tests directory - Created new @calcom/testing package in /packages/testing - Moved all files from /tests to /packages/testing - Updated all imports across the codebase to use @calcom/testing alias - Removed /tests directory at root level This allows other packages like @calcom/features and @calcom/web to import testing utilities using the @calcom/testing alias instead of relative paths. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix * fix * fix: add missing useBookings export to @calcom/atoms package Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add missing useCalendarsBusyTimes and useConnectedCalendars exports to @calcom/atoms Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * chore: add @calcom/testing as explicit devDependency to packages that use it Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * refactor: move setupVitest.ts into @calcom/testing package Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix * chore: add biome rules to restrict @calcom/testing imports Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix * rename libs to lib * rename libs to lib * add rule * add rule * refactor: remove @calcom/features imports from @calcom/testing - Move mockPaymentSuccessWebhookFromStripe to fresh-booking.test.ts - Replace ProfileRepository.generateProfileUid() with uuidv4() - Clone Tracking type into @calcom/testing/src/lib/types.ts - Update imports in expects.ts and getMockRequestDataForBooking.ts - Move source files into src/ folder - Move CalendarManager mock to @calcom/features Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add explicit exports for nested paths in @calcom/testing Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * improve * improve * fix * fix * fix type checks * fix type checks * fix type checks * fix tests --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
398 lines
14 KiB
TypeScript
398 lines
14 KiB
TypeScript
import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock";
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
|
import { updateNewTeamMemberEventTypes } from "@calcom/features/ee/teams/lib/queries";
|
|
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
|
|
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
|
|
import { createAProfileForAnExistingUser } from "@calcom/features/profile/lib/createAProfileForAnExistingUser";
|
|
import { deleteDomain } from "@calcom/lib/domainManager/organization";
|
|
import { ErrorCode } from "@calcom/lib/errorCodes";
|
|
import { ErrorWithCode } from "@calcom/lib/errors";
|
|
import type { Membership, Team, User, VerificationToken, Profile } from "@calcom/prisma/client";
|
|
import { MembershipRole } from "@calcom/prisma/enums";
|
|
|
|
import { TeamService } from "./teamService";
|
|
|
|
vi.mock("@calcom/ee/billing/di/containers/Billing");
|
|
vi.mock("@calcom/features/ee/teams/repositories/TeamRepository");
|
|
vi.mock("@calcom/features/ee/workflows/lib/service/WorkflowService");
|
|
vi.mock("@calcom/lib/domainManager/organization");
|
|
vi.mock("@calcom/features/ee/teams/lib/removeMember");
|
|
vi.mock("@calcom/features/profile/lib/createAProfileForAnExistingUser");
|
|
vi.mock("@calcom/features/ee/teams/lib/queries");
|
|
|
|
const mockTeamBilling = {
|
|
cancel: vi.fn(),
|
|
updateQuantity: vi.fn(),
|
|
publish: vi.fn(),
|
|
downgrade: vi.fn(),
|
|
};
|
|
|
|
const mockTeamBillingFactory = {
|
|
findAndInit: vi.fn().mockResolvedValue(mockTeamBilling),
|
|
findAndInitMany: vi.fn().mockResolvedValue([mockTeamBilling]),
|
|
};
|
|
|
|
describe("TeamService", () => {
|
|
beforeEach(async () => {
|
|
vi.resetAllMocks();
|
|
mockTeamBillingFactory.findAndInit.mockResolvedValue(mockTeamBilling);
|
|
mockTeamBillingFactory.findAndInitMany.mockResolvedValue([mockTeamBilling]);
|
|
|
|
const { getTeamBillingServiceFactory } = await import("@calcom/ee/billing/di/containers/Billing");
|
|
vi.mocked(getTeamBillingServiceFactory).mockReturnValue(mockTeamBillingFactory);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("delete", () => {
|
|
it("should delete team, cancel billing, and clean up", async () => {
|
|
const mockDeletedTeam = {
|
|
id: 1,
|
|
name: "Deleted Team",
|
|
isOrganization: true,
|
|
slug: "deleted-team",
|
|
};
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore
|
|
const mockTeamRepo = {
|
|
deleteById: vi.fn().mockResolvedValue(mockDeletedTeam),
|
|
} as Pick<TeamRepository, "deleteById">;
|
|
vi.mocked(TeamRepository).mockImplementation(function() { return mockTeamRepo; });
|
|
|
|
const result = await TeamService.delete({ id: 1 });
|
|
|
|
expect(mockTeamBillingFactory.findAndInit).toHaveBeenCalledWith(1);
|
|
expect(mockTeamBilling.cancel).toHaveBeenCalled();
|
|
expect(WorkflowService.deleteWorkflowRemindersOfRemovedTeam).toHaveBeenCalledWith(1);
|
|
expect(mockTeamRepo.deleteById).toHaveBeenCalledWith({ id: 1 });
|
|
expect(deleteDomain).toHaveBeenCalledWith("deleted-team");
|
|
expect(result).toEqual(mockDeletedTeam);
|
|
});
|
|
});
|
|
|
|
describe("inviteMemberByToken", () => {
|
|
it("should throw error if verification token is not found", async () => {
|
|
prismaMock.verificationToken.findFirst.mockResolvedValue(null);
|
|
await expect(TeamService.inviteMemberByToken("invalid-token", 1)).rejects.toThrow(ErrorWithCode);
|
|
});
|
|
|
|
it("should create provisional membership and update billing", async () => {
|
|
const mockToken = {
|
|
teamId: 1,
|
|
team: { name: "Test Team" },
|
|
expiresInDays: null,
|
|
expires: new Date(Date.now() + 86400000),
|
|
token: "valid-token",
|
|
identifier: "test@example.com",
|
|
id: "1",
|
|
};
|
|
prismaMock.verificationToken.findFirst.mockResolvedValue(mockToken);
|
|
prismaMock.membership.create.mockResolvedValue({} as Membership);
|
|
|
|
const result = await TeamService.inviteMemberByToken("valid-token", 1);
|
|
|
|
expect(prismaMock.membership.create).toHaveBeenCalledWith({
|
|
data: {
|
|
accepted: false,
|
|
createdAt: expect.any(Date),
|
|
role: MembershipRole.MEMBER,
|
|
teamId: 1,
|
|
userId: 1,
|
|
},
|
|
});
|
|
expect(mockTeamBilling.updateQuantity).toHaveBeenCalled();
|
|
expect(result).toBe("Test Team");
|
|
});
|
|
});
|
|
|
|
describe("acceptTeamMembership", () => {
|
|
it("should accept membership and update event types for regular team", async () => {
|
|
const mockMembership = {
|
|
team: { id: 1, parentId: null, isOrganization: false },
|
|
};
|
|
|
|
prismaMock.membership.update.mockResolvedValue(mockMembership as Membership & { team: Team });
|
|
vi.mocked(updateNewTeamMemberEventTypes).mockResolvedValue(undefined);
|
|
|
|
await TeamService.acceptTeamMembership({
|
|
userId: 1,
|
|
teamId: 1,
|
|
userEmail: "test@example.com",
|
|
username: "testuser",
|
|
});
|
|
|
|
expect(prismaMock.membership.update).toHaveBeenCalledWith({
|
|
where: { userId_teamId: { userId: 1, teamId: 1 } },
|
|
data: { accepted: true },
|
|
select: { team: true },
|
|
});
|
|
expect(updateNewTeamMemberEventTypes).toHaveBeenCalledWith(1, 1);
|
|
});
|
|
|
|
it("should accept membership and create profile for organization", async () => {
|
|
const mockMembership = {
|
|
team: { id: 1, parentId: null, isOrganization: true },
|
|
};
|
|
|
|
prismaMock.membership.update.mockResolvedValue(mockMembership as Membership & { team: Team });
|
|
vi.mocked(createAProfileForAnExistingUser).mockResolvedValue({} as Profile);
|
|
vi.mocked(updateNewTeamMemberEventTypes).mockResolvedValue(undefined);
|
|
|
|
await TeamService.acceptTeamMembership({
|
|
userId: 1,
|
|
teamId: 1,
|
|
userEmail: "test@example.com",
|
|
username: "testuser",
|
|
});
|
|
|
|
expect(createAProfileForAnExistingUser).toHaveBeenCalledWith({
|
|
user: {
|
|
id: 1,
|
|
email: "test@example.com",
|
|
currentUsername: "testuser",
|
|
},
|
|
organizationId: 1,
|
|
});
|
|
});
|
|
|
|
it("should accept membership and handle parent team for subteam", async () => {
|
|
const mockMembership = {
|
|
team: { id: 1, parentId: 2, isOrganization: false },
|
|
};
|
|
|
|
prismaMock.membership.update
|
|
.mockResolvedValueOnce(mockMembership as Membership & { team: Team })
|
|
.mockResolvedValueOnce({} as Membership);
|
|
vi.mocked(createAProfileForAnExistingUser).mockResolvedValue({} as Profile);
|
|
vi.mocked(updateNewTeamMemberEventTypes).mockResolvedValue(undefined);
|
|
|
|
await TeamService.acceptTeamMembership({
|
|
userId: 1,
|
|
teamId: 1,
|
|
userEmail: "test@example.com",
|
|
username: "testuser",
|
|
});
|
|
|
|
expect(prismaMock.membership.update).toHaveBeenCalledTimes(2);
|
|
expect(prismaMock.membership.update).toHaveBeenNthCalledWith(2, {
|
|
where: { userId_teamId: { userId: 1, teamId: 2 } },
|
|
data: { accepted: true },
|
|
});
|
|
expect(createAProfileForAnExistingUser).toHaveBeenCalledWith({
|
|
user: {
|
|
id: 1,
|
|
email: "test@example.com",
|
|
currentUsername: "testuser",
|
|
},
|
|
organizationId: 2,
|
|
});
|
|
});
|
|
});
|
|
describe("leaveTeamMembership", () => {
|
|
it("should delete membership when rejecting invitation", async () => {
|
|
const mockMembership = {
|
|
team: { id: 1, parentId: null },
|
|
};
|
|
|
|
prismaMock.membership.delete.mockResolvedValue(mockMembership as Membership & { team: Team });
|
|
|
|
await TeamService.leaveTeamMembership({
|
|
userId: 1,
|
|
teamId: 1,
|
|
});
|
|
|
|
expect(prismaMock.membership.delete).toHaveBeenCalledWith({
|
|
where: { userId_teamId: { userId: 1, teamId: 1 } },
|
|
select: { team: true },
|
|
});
|
|
});
|
|
|
|
it("should delete parent membership when rejecting subteam invitation", async () => {
|
|
const mockMembership = {
|
|
team: { id: 1, parentId: 2 },
|
|
};
|
|
|
|
prismaMock.membership.delete
|
|
.mockResolvedValueOnce(mockMembership as Membership & { team: Team })
|
|
.mockResolvedValueOnce({} as Membership);
|
|
|
|
await TeamService.leaveTeamMembership({
|
|
userId: 1,
|
|
teamId: 1,
|
|
});
|
|
|
|
expect(prismaMock.membership.delete).toHaveBeenCalledTimes(2);
|
|
expect(prismaMock.membership.delete).toHaveBeenNthCalledWith(2, {
|
|
where: { userId_teamId: { userId: 1, teamId: 2 } },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("acceptInvitationByToken", () => {
|
|
it("should throw error if verification token is not found", async () => {
|
|
prismaMock.verificationToken.findFirst.mockResolvedValue(null);
|
|
await expect(TeamService.acceptInvitationByToken("invalid-token", 1)).rejects.toThrow(ErrorWithCode);
|
|
});
|
|
|
|
it("should throw error if token is not associated with team", async () => {
|
|
const mockToken = {
|
|
teamId: null,
|
|
team: null,
|
|
identifier: "test@example.com",
|
|
id: "1",
|
|
};
|
|
|
|
prismaMock.verificationToken.findFirst.mockResolvedValue(
|
|
mockToken as VerificationToken & { team: Team | null }
|
|
);
|
|
|
|
await expect(TeamService.acceptInvitationByToken("valid-token", 1)).rejects.toThrow(
|
|
new ErrorWithCode(ErrorCode.NotFound, "Invite token is not associated with any team")
|
|
);
|
|
});
|
|
|
|
it("should throw error if user not found", async () => {
|
|
const mockToken = {
|
|
teamId: 1,
|
|
team: { name: "Test Team" },
|
|
identifier: "test@example.com",
|
|
id: "1",
|
|
};
|
|
|
|
prismaMock.verificationToken.findFirst.mockResolvedValue(
|
|
mockToken as VerificationToken & { team: Team }
|
|
);
|
|
prismaMock.user.findUnique.mockResolvedValue(null);
|
|
|
|
await expect(TeamService.acceptInvitationByToken("valid-token", 1)).rejects.toThrow(
|
|
new ErrorWithCode(ErrorCode.NotFound, "User not found")
|
|
);
|
|
});
|
|
|
|
it("should throw error if user email doesn't match token identifier", async () => {
|
|
const mockToken = {
|
|
teamId: 1,
|
|
team: { name: "Test Team" },
|
|
identifier: "invited@example.com",
|
|
id: "1",
|
|
};
|
|
|
|
const mockUser = {
|
|
email: "different@example.com",
|
|
username: "testuser",
|
|
};
|
|
|
|
prismaMock.verificationToken.findFirst.mockResolvedValue(
|
|
mockToken as VerificationToken & { team: Team }
|
|
);
|
|
prismaMock.user.findUnique.mockResolvedValue(mockUser as User);
|
|
|
|
await expect(TeamService.acceptInvitationByToken("valid-token", 1)).rejects.toThrow(
|
|
new ErrorWithCode(ErrorCode.Forbidden, "This invitation is not for your account")
|
|
);
|
|
});
|
|
|
|
it("should throw error if user username doesn't match token identifier", async () => {
|
|
const mockToken = {
|
|
teamId: 1,
|
|
team: { name: "Test Team" },
|
|
identifier: "inviteduser",
|
|
id: "1",
|
|
};
|
|
|
|
const mockUser = {
|
|
email: "test@example.com",
|
|
username: "differentuser",
|
|
};
|
|
|
|
prismaMock.verificationToken.findFirst.mockResolvedValue(
|
|
mockToken as VerificationToken & { team: Team }
|
|
);
|
|
prismaMock.user.findUnique.mockResolvedValue(mockUser as User);
|
|
|
|
await expect(TeamService.acceptInvitationByToken("valid-token", 1)).rejects.toThrow(
|
|
new ErrorWithCode(ErrorCode.Forbidden, "This invitation is not for your account")
|
|
);
|
|
});
|
|
|
|
it("should accept invitation when user email matches token identifier", async () => {
|
|
const mockToken = {
|
|
teamId: 1,
|
|
team: { name: "Test Team" },
|
|
identifier: "test@example.com",
|
|
id: "1",
|
|
};
|
|
|
|
const mockUser = {
|
|
email: "test@example.com",
|
|
username: "testuser",
|
|
};
|
|
|
|
const mockMembership = {
|
|
team: { id: 1, parentId: null, isOrganization: false },
|
|
};
|
|
|
|
prismaMock.verificationToken.findFirst.mockResolvedValue(
|
|
mockToken as VerificationToken & { team: Team }
|
|
);
|
|
prismaMock.user.findUnique.mockResolvedValue(mockUser as User);
|
|
prismaMock.membership.update.mockResolvedValue(mockMembership as Membership & { team: Team });
|
|
vi.mocked(updateNewTeamMemberEventTypes).mockResolvedValue(undefined);
|
|
|
|
await TeamService.acceptInvitationByToken("valid-token", 1);
|
|
|
|
expect(prismaMock.membership.update).toHaveBeenCalledWith({
|
|
where: { userId_teamId: { userId: 1, teamId: 1 } },
|
|
data: { accepted: true },
|
|
select: { team: true },
|
|
});
|
|
});
|
|
|
|
it("should accept invitation when user username matches token identifier", async () => {
|
|
const mockToken = {
|
|
teamId: 1,
|
|
team: { name: "Test Team" },
|
|
identifier: "testuser",
|
|
id: "1",
|
|
};
|
|
|
|
const mockUser = {
|
|
email: "testuser@example.com",
|
|
username: "testuser",
|
|
};
|
|
|
|
const mockMembership = {
|
|
team: { id: 1, parentId: null, isOrganization: false },
|
|
};
|
|
|
|
prismaMock.verificationToken.findFirst.mockResolvedValue(
|
|
mockToken as VerificationToken & { team: Team }
|
|
);
|
|
prismaMock.user.findUnique.mockResolvedValue(mockUser as User);
|
|
prismaMock.membership.update.mockResolvedValue(mockMembership as Membership & { team: Team });
|
|
vi.mocked(updateNewTeamMemberEventTypes).mockResolvedValue(undefined);
|
|
|
|
await TeamService.acceptInvitationByToken("valid-token", 1);
|
|
|
|
expect(prismaMock.membership.update).toHaveBeenCalledWith({
|
|
where: { userId_teamId: { userId: 1, teamId: 1 } },
|
|
data: { accepted: true },
|
|
select: { team: true },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("publish", () => {
|
|
it("should call publish on TeamBilling", async () => {
|
|
await TeamService.publish(1);
|
|
|
|
expect(mockTeamBillingFactory.findAndInit).toHaveBeenCalledWith(1);
|
|
expect(mockTeamBilling.publish).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|