Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 39 additions & 38 deletions apps/web/app/api/link/__tests__/route.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { NextRequest } from "next/server";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { confirmHandler } from "@calcom/trpc/server/routers/viewer/bookings/confirm.handler";
import type { Mock } from "vitest";

const mockConfirmHandler = confirmHandler as unknown as Mock<typeof confirmHandler>;

vi.mock("app/api/defaultResponderForAppDir", () => ({
defaultResponderForAppDir:
Expand Down Expand Up @@ -36,44 +40,47 @@ vi.mock("@calcom/lib/crypto", () => ({
),
}));

vi.mock("@calcom/prisma", () => ({
default: {
vi.mock("@calcom/prisma", () => {
const mockBookingFindUniqueOrThrow = vi.fn().mockResolvedValue({
id: 1,
uid: "test-booking-uid",
recurringEventId: null,
});
const mockUserFindUniqueOrThrow = vi.fn().mockResolvedValue({
id: 1,
uuid: "user-uuid",
email: "test@example.com",
username: "testuser",
role: "USER",
destinationCalendar: null,
});
const mockPrismaObj = {
booking: {
findUniqueOrThrow: vi.fn().mockResolvedValue({
id: 1,
uid: "test-booking-uid",
recurringEventId: null,
}),
findUniqueOrThrow: mockBookingFindUniqueOrThrow,
},
user: {
findUniqueOrThrow: vi.fn().mockResolvedValue({
id: 1,
locale: "en",
}),
findUniqueOrThrow: mockUserFindUniqueOrThrow,
},
},
}));

vi.mock("@calcom/trpc/server/createContext", () => ({
createContext: vi.fn().mockResolvedValue({}),
}));

vi.mock("@calcom/trpc/server/routers/viewer/bookings/_router", () => ({
bookingsRouter: {},
}));
};
return {
default: mockPrismaObj,
prisma: mockPrismaObj,
};
});

vi.mock("@calcom/trpc/server/trpc", () => ({
createCallerFactory: vi.fn().mockReturnValue(() => ({
confirm: vi.fn().mockResolvedValue({}),
})),
vi.mock("@calcom/trpc/server/routers/viewer/bookings/confirm.handler", () => ({
confirmHandler: vi.fn(),
}));

vi.mock("@lib/buildLegacyCtx", () => ({
buildLegacyRequest: vi.fn().mockReturnValue({}),
vi.mock("@calcom/lib/tracing/factory", () => ({
distributedTracing: {
createTrace: vi.fn().mockReturnValue({}),
},
}));

// Import after mocks are set up
import { GET } from "../route";
import prisma from "@calcom/prisma";

const createMockRequest = (url: string): NextRequest => {
const urlObj = new URL(url);
Expand Down Expand Up @@ -160,14 +167,11 @@ describe("link route", () => {
});

describe("GET handler - error handling", () => {
it("should redirect with error message when TRPC throws an error", async () => {
it("should redirect with error message when confirmHandler throws a TRPCError", async () => {
const { TRPCError } = await import("@trpc/server");

// Mock createCallerFactory to throw a TRPCError
const { createCallerFactory } = await import("@calcom/trpc/server/trpc");
vi.mocked(createCallerFactory).mockReturnValue(() => ({
confirm: vi.fn().mockRejectedValue(new TRPCError({ code: "BAD_REQUEST", message: "Custom error" })),
}));
// Mock confirmHandler to throw a TRPCError
mockConfirmHandler.mockRejectedValueOnce(new TRPCError({ code: "BAD_REQUEST", message: "Custom error" }));

const baseUrl = "https://app.example.com/api/link?action=accept&token=encrypted-token";
const req = createMockRequest(baseUrl);
Expand All @@ -186,11 +190,8 @@ describe("link route", () => {
it("should preserve origin in error redirect URL", async () => {
const { TRPCError } = await import("@trpc/server");

// Mock createCallerFactory to throw a TRPCError
const { createCallerFactory } = await import("@calcom/trpc/server/trpc");
vi.mocked(createCallerFactory).mockReturnValue(() => ({
confirm: vi.fn().mockRejectedValue(new TRPCError({ code: "INTERNAL_SERVER_ERROR" })),
}));
// Mock confirmHandler to throw a TRPCError
mockConfirmHandler.mockRejectedValueOnce(new TRPCError({ code: "INTERNAL_SERVER_ERROR" }));

const baseUrl = "https://self-hosted.company.org/api/link?action=accept&token=encrypted-token";
const req = createMockRequest(baseUrl);
Expand Down
104 changes: 40 additions & 64 deletions apps/web/app/api/link/route.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { cookies, headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";

import { symmetricDecrypt } from "@calcom/lib/crypto";
import { distributedTracing } from "@calcom/lib/tracing/factory";
import prisma from "@calcom/prisma";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { createContext } from "@calcom/trpc/server/createContext";
import { bookingsRouter } from "@calcom/trpc/server/routers/viewer/bookings/_router";
import { createCallerFactory } from "@calcom/trpc/server/trpc";
import type { UserProfile } from "@calcom/types/UserProfile";

import { buildLegacyRequest } from "@lib/buildLegacyCtx";

import { confirmHandler } from "@calcom/trpc/server/routers/viewer/bookings/confirm.handler";
import { TRPCError } from "@trpc/server";

enum DirectAction {
Expand All @@ -36,29 +29,6 @@ const decryptedSchema = z.object({
platformBookingUrl: z.string().optional(),
});

// Move the sessionGetter function outside the GET function
const createSessionGetter = (userId: number, userUuid: string) => async () => {
return {
user: {
id: userId,
uuid: userUuid,
username: "" /* Not used in this context */,
role: UserPermissionRole.USER,
/* Not used in this context */
profile: {
id: null,
organizationId: null,
organization: null,
username: "",
upId: "",
} satisfies UserProfile,
},
upId: "",
hasValidLicense: true,
expires: "" /* Not used in this context */,
};
};

async function handler(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;

Expand All @@ -83,45 +53,51 @@ async function handler(request: NextRequest) {

const user = await prisma.user.findUniqueOrThrow({
where: { id: userId },
select: {
id: true,
uuid: true,
email: true,
username: true,
role: true,
Comment on lines +56 to +61
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should move all the prisma queries to repository in follow up

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, agreed

destinationCalendar: true,
},
});

// Use the factory function instead of declaring inside the block
const sessionGetter = createSessionGetter(userId, user.uuid);

try {
/** @see https://trpc.io/docs/server-side-calls */
// Create a legacy request object for compatibility
const legacyReq = buildLegacyRequest(await headers(), await cookies());
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Response is mocked as it's not used in this context
const res = {} as any;

const ctx = await createContext({ req: legacyReq, res }, sessionGetter);
const createCaller = createCallerFactory(bookingsRouter);
const caller = createCaller({
...ctx,
req: legacyReq,
res,
user: { ...user, locale: user?.locale ?? "en" },
});

await caller.confirm({
bookingId: booking.id,
recurringEventId: booking.recurringEventId || undefined,
confirmed: action === DirectAction.ACCEPT,
reason,
platformClientParams: platformClientId
? {
platformClientId,
platformRescheduleUrl,
platformCancelUrl,
platformBookingUrl,
}
: undefined,
await confirmHandler({
ctx: {
user: {
id: user.id,
uuid: user.uuid,
email: user.email,
username: user.username ?? "",
role: user.role,
destinationCalendar: user.destinationCalendar ?? null,
},
traceContext: distributedTracing.createTrace("confirm_booking_magic_link"),
},
input: {
bookingId: booking.id,
recurringEventId: booking.recurringEventId || undefined,
confirmed: action === DirectAction.ACCEPT,
reason,
emailsEnabled: true,
platformClientParams: platformClientId
? {
platformClientId,
platformRescheduleUrl,
platformCancelUrl,
platformBookingUrl,
}
: undefined,
},
});
} catch (e) {
let message = "Error confirming booking";
if (e instanceof TRPCError) message = (e as TRPCError).message;
return NextResponse.redirect(new URL(`/booking/${bookingUid}?error=${encodeURIComponent(message)}`, request.url));
return NextResponse.redirect(
new URL(`/booking/${bookingUid}?error=${encodeURIComponent(message)}`, request.url)
);
}

return NextResponse.redirect(new URL(`/booking/${bookingUid}`, request.url));
Expand Down
Loading