Files
calendar/apps/web/app/api/link/__tests__/route.test.ts
T
Benny JooGitHubbenny@cal.com <sldisek783@gmail.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
ab21c7f805 refactor: Cal.diy (#28903)
* feat: Cal.diy — community-driven MIT-licensed fork of Cal.com

This squashed commit contains all Cal.diy changes applied on top of calcom/cal.com main:

- Rebrand Cal.com to Cal.diy across the entire codebase
- Remove Enterprise Edition (EE) features, license checks, and AGPL restrictions
- Switch license from AGPL-3.0 to MIT
- Remove docs/ directory (migrated to Nextra at cal.diy)
- Remove dead code: org tests, EE tips, platform nav, premium username, SAML/SSO, etc.
- Clean up .env.example for self-hosted Cal.diy
- Update Docker image references to calcom/cal.diy
- Update README, CONTRIBUTING.md, and issue templates for Cal.diy community fork
- Add PR welcome bot for Cal.diy contributors
- Fix API v2 breaking changes oasdiff ignore entries
- Replace Blacksmith CI runners with default GitHub Actions

3893 files changed, 20789 insertions(+), 411020 deletions(-)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: remove org-specific /organizations/:orgId endpoints from API v2 atoms controllers (#1701)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: revert Cal.diy Inc to Cal.com, Inc. in license files, copyright notices, and package metadata (#1702)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* rip out org related comments in api v2

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-04-15 09:52:36 -03:00

334 lines
11 KiB
TypeScript

import { confirmHandler } from "@calcom/trpc/server/routers/viewer/bookings/confirm.handler";
import type { NextRequest } from "next/server";
import type { Mock } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const mockConfirmHandler = confirmHandler as unknown as Mock<typeof confirmHandler>;
vi.mock("app/api/defaultResponderForAppDir", () => ({
defaultResponderForAppDir:
(handler: (req: NextRequest) => Promise<Response>) =>
(req: NextRequest, _context: { params: Promise<Record<string, string>> }) =>
handler(req),
}));
vi.mock("next/headers", () => ({
headers: vi.fn().mockResolvedValue(new Headers()),
cookies: vi.fn().mockResolvedValue({ getAll: () => [] }),
}));
vi.mock("next/server", () => ({
NextResponse: {
redirect: vi.fn((url: string | URL, init?: { status?: number }) => {
const location = typeof url === "string" ? url : url.toString();
return {
status: init?.status ?? 302,
headers: {
get: (name: string) => (name.toLowerCase() === "location" ? location : null),
},
} as unknown as Response;
}),
},
}));
vi.mock("@calcom/lib/crypto", () => ({
symmetricDecrypt: vi.fn().mockReturnValue(
JSON.stringify({
bookingUid: "test-booking-uid",
userId: 1,
})
),
}));
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: mockBookingFindUniqueOrThrow,
},
user: {
findUniqueOrThrow: mockUserFindUniqueOrThrow,
},
};
return {
default: mockPrismaObj,
prisma: mockPrismaObj,
};
});
vi.mock("@calcom/trpc/server/routers/viewer/bookings/confirm.handler", () => ({
confirmHandler: vi.fn(),
}));
vi.mock("@calcom/lib/tracing/factory", () => ({
distributedTracing: {
createTrace: vi.fn().mockReturnValue({}),
},
}));
vi.mock("@calcom/features/booking-audit/lib/makeActor", () => ({
makeUserActor: vi.fn().mockReturnValue({ type: "user", id: "test-uuid" }),
}));
import prisma from "@calcom/prisma";
// Import after mocks are set up
import { GET } from "../route";
const createMockRequest = (url: string): NextRequest => {
const urlObj = new URL(url);
return {
method: "GET",
url,
nextUrl: {
searchParams: urlObj.searchParams,
},
} as unknown as NextRequest;
};
// Vitest sets NEXT_PUBLIC_WEBAPP_URL to http://app.cal.local:3000 (see vitest.config.mts)
const EXPECTED_REDIRECT_ORIGIN = "http://app.cal.local:3000";
describe("link route", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("GET handler - redirect URL construction", () => {
it("should redirect to booking page using WEBAPP_URL (fixes localhost redirect when behind proxy)", async () => {
const baseUrl = "https://app.example.com/api/link?action=accept&token=encrypted-token";
const req = createMockRequest(baseUrl);
const res = await GET(req, { params: Promise.resolve({}) });
const location = res.headers.get("location");
expect(location).toBeTruthy();
const redirectUrl = new URL(location!);
expect(redirectUrl.origin).toBe(EXPECTED_REDIRECT_ORIGIN);
expect(redirectUrl.pathname).toBe("/booking/test-booking-uid");
});
it("should use WEBAPP_URL for redirects, not request.url (avoids localhost when proxy sends localhost)", async () => {
const baseUrl = "https://custom-domain.company.com/api/link?action=accept&token=encrypted-token";
const req = createMockRequest(baseUrl);
const res = await GET(req, { params: Promise.resolve({}) });
const location = res.headers.get("location");
expect(location).toBeTruthy();
const redirectUrl = new URL(location!);
expect(redirectUrl.origin).toBe(EXPECTED_REDIRECT_ORIGIN);
expect(location).not.toContain("localhost");
});
it("should use WEBAPP_URL for self-hosted deployments", async () => {
const baseUrl = "https://calcom.internal.company.net/api/link?action=reject&token=encrypted-token";
const req = createMockRequest(baseUrl);
const res = await GET(req, { params: Promise.resolve({}) });
const location = res.headers.get("location");
expect(location).toBeTruthy();
const redirectUrl = new URL(location!);
expect(redirectUrl.origin).toBe(EXPECTED_REDIRECT_ORIGIN);
expect(redirectUrl.pathname).toBe("/booking/test-booking-uid");
});
it("should construct redirect URLs using WEBAPP_URL regardless of request origin", async () => {
const testOrigins = [
"https://app.cal.com",
"https://acme.cal.com",
"https://calcom.company.internal",
"http://192.168.1.100:3000",
];
for (const origin of testOrigins) {
vi.clearAllMocks();
const baseUrl = `${origin}/api/link?action=accept&token=encrypted-token`;
const req = createMockRequest(baseUrl);
const res = await GET(req, { params: Promise.resolve({}) });
const location = res.headers.get("location");
expect(location).toBeTruthy();
const redirectUrl = new URL(location!);
expect(redirectUrl.origin).toBe(EXPECTED_REDIRECT_ORIGIN);
expect(redirectUrl.pathname).toBe("/booking/test-booking-uid");
}
});
});
describe("GET handler - error handling", () => {
it("should redirect with error message when confirmHandler throws a TRPCError", async () => {
const { TRPCError } = await import("@trpc/server");
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);
const res = await GET(req, { params: Promise.resolve({}) });
const location = res.headers.get("location");
expect(location).toBeTruthy();
const redirectUrl = new URL(location!);
expect(redirectUrl.origin).toBe(EXPECTED_REDIRECT_ORIGIN);
expect(redirectUrl.pathname).toBe("/booking/test-booking-uid");
expect(redirectUrl.searchParams.get("error")).toBe("Custom error");
});
it("should use WEBAPP_URL for error redirects (not localhost when behind proxy)", async () => {
const { TRPCError } = await import("@trpc/server");
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);
const res = await GET(req, { params: Promise.resolve({}) });
const location = res.headers.get("location");
expect(location).toBeTruthy();
const redirectUrl = new URL(location!);
expect(redirectUrl.origin).toBe(EXPECTED_REDIRECT_ORIGIN);
expect(location).not.toContain("localhost");
});
});
describe("confirmHandler flow", () => {
it("should call confirmHandler with correct arguments for accept action", async () => {
const baseUrl = "https://app.example.com/api/link?action=accept&token=encrypted-token";
const req = createMockRequest(baseUrl);
await GET(req, { params: Promise.resolve({}) });
expect(mockConfirmHandler).toHaveBeenCalledWith(
expect.objectContaining({
input: expect.objectContaining({
bookingId: 1,
confirmed: true,
emailsEnabled: true,
}),
})
);
});
it("should call confirmHandler with confirmed=false for reject action", async () => {
const baseUrl = "https://app.example.com/api/link?action=reject&token=encrypted-token";
const req = createMockRequest(baseUrl);
await GET(req, { params: Promise.resolve({}) });
expect(mockConfirmHandler).toHaveBeenCalledWith(
expect.objectContaining({
input: expect.objectContaining({
bookingId: 1,
confirmed: false,
emailsEnabled: true,
}),
})
);
});
it("should call confirmHandler with reason when provided in query params", async () => {
const baseUrl =
"https://app.example.com/api/link?action=reject&token=encrypted-token&reason=test-reason";
const req = createMockRequest(baseUrl);
await GET(req, { params: Promise.resolve({}) });
expect(mockConfirmHandler).toHaveBeenCalledWith(
expect.objectContaining({
input: expect.objectContaining({
bookingId: 1,
confirmed: false,
reason: "test-reason",
emailsEnabled: true,
}),
})
);
});
it("should pass recurringEventId when booking has one", async () => {
// Update mock to return booking with recurringEventId
vi.mocked(prisma.booking.findUniqueOrThrow).mockResolvedValueOnce({
id: 1,
uid: "test-booking-uid",
recurringEventId: "recurring-123",
} as Awaited<ReturnType<typeof prisma.booking.findUniqueOrThrow>>);
const baseUrl = "https://app.example.com/api/link?action=accept&token=encrypted-token";
const req = createMockRequest(baseUrl);
await GET(req, { params: Promise.resolve({}) });
expect(mockConfirmHandler).toHaveBeenCalledWith(
expect.objectContaining({
input: expect.objectContaining({
bookingId: 1,
recurringEventId: "recurring-123",
confirmed: true,
}),
})
);
});
it("should pass user context to confirmHandler", async () => {
const baseUrl = "https://app.example.com/api/link?action=accept&token=encrypted-token";
const req = createMockRequest(baseUrl);
await GET(req, { params: Promise.resolve({}) });
expect(mockConfirmHandler).toHaveBeenCalledWith(
expect.objectContaining({
ctx: expect.objectContaining({
user: expect.objectContaining({
id: 1,
uuid: "user-uuid",
email: "test@example.com",
username: "testuser",
role: "USER",
}),
}),
})
);
});
it("should pass user context to confirmHandler input", async () => {
const baseUrl = "https://app.example.com/api/link?action=accept&token=encrypted-token";
const req = createMockRequest(baseUrl);
await GET(req, { params: Promise.resolve({}) });
// After EE removal, actor/actionSource are no longer passed to confirmHandler
expect(mockConfirmHandler).toHaveBeenCalledWith(
expect.objectContaining({
input: expect.objectContaining({
bookingId: 1,
confirmed: true,
}),
})
);
});
});
});