Files
calendar/apps/web/lib/handleOrgRedirect.test.ts
T
6c6cf0433a fix: handle TempOrgRedirect when NEXT_PUBLIC_SINGLE_ORG_SLUG forces org context (#23009)
* fix: rename getTemporaryOrgRedirect to handleOrgRedirect and prevent duplicate orgRedirection query parameter

* refactor: improve handleOrgRedirect test robustness with scenario-based mocking

- Replace brittle mockResolvedValue with mockImplementation that derives behavior from input
- Create createRedirectScenario function that sets up mocks based on actual query parameters
- Add expectRedirectUsesData helper to verify redirects use the correct data
- Add comprehensive mock verification tests to ensure different inputs return different outputs
- Ensure tests verify the actual usage of Prisma results, not just that Prisma was called

This prevents false positives where tests pass even if the code doesn't use the database results correctly.

* Fix bug with Private links redirecting an exposing users booking page URL

* Narrow the type to what is being used

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-08-12 08:47:26 -03:00

705 lines
21 KiB
TypeScript

import type { GetServerSidePropsContext } from "next";
import type { ParsedUrlQuery } from "querystring";
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
import * as constants from "@calcom/lib/constants";
import { RedirectType } from "@calcom/prisma/client";
import { handleOrgRedirect, getRedirectWithOriginAndSearchString } from "./handleOrgRedirect";
// Mock prisma for all tests
const prismaMock = vi.hoisted(() => ({
tempOrgRedirect: {
findMany: vi.fn(),
},
}));
vi.mock("@calcom/prisma", () => ({
default: prismaMock,
}));
const createTestContext = (overrides?: {
host?: string;
query?: ParsedUrlQuery;
}): GetServerSidePropsContext =>
({
req: {
headers: {
host: overrides?.host || "cal.local:3000",
},
},
query: overrides?.query || {},
} as unknown as GetServerSidePropsContext);
const createTestRedirectParams = (overrides?: {
slugs?: string[];
redirectType?: RedirectType;
eventTypeSlug?: string | null;
context?: GetServerSidePropsContext;
currentOrgDomain?: string | null;
}) => ({
slugs: overrides?.slugs || ["pro"],
redirectType: overrides?.redirectType || RedirectType.User,
eventTypeSlug: overrides?.eventTypeSlug ?? null,
context: overrides?.context || createTestContext(),
currentOrgDomain: overrides?.currentOrgDomain ?? null,
});
interface RedirectScenario {
redirects: Array<{
from: string;
toUrl: string;
type?: RedirectType;
}>;
}
const createRedirectScenario = (scenario: RedirectScenario) => {
// Create a map for quick lookup based on input parameters
const redirectMap = new Map<string, (typeof scenario.redirects)[0]>();
scenario.redirects.forEach((redirect) => {
const key = `${redirect.type || RedirectType.User}-${redirect.from}`;
redirectMap.set(key, redirect);
});
// Return a mock implementation that returns data based on the actual query
prismaMock.tempOrgRedirect.findMany.mockImplementation(async (params) => {
const { type, from } = params?.where || {};
const slugsToFind = from?.in || [];
// Return redirects that match the requested type and slugs
const matchingRedirects = slugsToFind
.map((slug: string) => {
const key = `${type}-${slug}`;
const redirect = redirectMap.get(key);
if (redirect) {
return {
from: redirect.from,
toUrl: redirect.toUrl,
type: redirect.type || type,
fromOrgId: 0,
};
}
return null;
})
.filter(Boolean);
return matchingRedirects;
});
};
const expectRedirectTo = (result: any, expectedDestination: string) => {
expect(result).toEqual({
redirect: {
permanent: false,
destination: expectedDestination,
},
});
};
const expectNoRedirect = (result: any) => {
expect(result).toBeNull();
};
const expectPrismaCalledWith = (expectedParams: { type: RedirectType; slugs: string[] }) => {
expect(prismaMock.tempOrgRedirect.findMany).toHaveBeenCalledWith({
where: {
type: expectedParams.type,
from: {
in: expectedParams.slugs,
},
fromOrgId: 0,
},
});
};
const expectPrismaNotCalled = () => {
expect(prismaMock.tempOrgRedirect.findMany).not.toHaveBeenCalled();
};
// Verify that the redirect was actually built from the data returned by Prisma
const expectRedirectUsesData = (
result: any,
expectedSlug: string,
expectedOrigin = "https://acme.cal.local:3000"
) => {
if (!result || !result.redirect) {
throw new Error("Expected a redirect result but got null/undefined");
}
const url = new URL(result.redirect.destination);
expect(url.origin).toBe(expectedOrigin);
expect(url.pathname).toContain(expectedSlug);
};
describe("handleOrgRedirect", () => {
beforeEach(() => {
vi.clearAllMocks();
// Default to no redirects - tests will override with specific scenarios
createRedirectScenario({ redirects: [] });
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("when not in organization context", () => {
it("should redirect to organization URL when redirect exists", async () => {
// Setup scenario with unique test data to verify correct usage
createRedirectScenario({
redirects: [
{
from: "pro",
toUrl: "https://acme.cal.local:3000/pro-example",
type: RedirectType.User,
},
// Add a decoy redirect to ensure the code picks the right one
{
from: "other-user",
toUrl: "https://acme.cal.local:3000/wrong-redirect",
type: RedirectType.User,
},
],
});
const params = createTestRedirectParams({
slugs: ["pro"],
});
const result = await handleOrgRedirect(params);
expectPrismaCalledWith({ type: RedirectType.User, slugs: ["pro"] });
expectRedirectTo(result, "https://acme.cal.local:3000/pro-example?orgRedirection=true");
expectRedirectUsesData(result, "pro-example");
});
it("should return null when no redirect exists", async () => {
// Explicitly set up no redirects scenario
createRedirectScenario({ redirects: [] });
const params = createTestRedirectParams({
slugs: ["pro"],
});
const result = await handleOrgRedirect(params);
expectPrismaCalledWith({ type: RedirectType.User, slugs: ["pro"] });
expectNoRedirect(result);
});
it("should handle Team redirect type", async () => {
createRedirectScenario({
redirects: [
{
from: "sales-team",
toUrl: "https://acme.cal.local:3000/sales",
type: RedirectType.Team,
},
// Add a User redirect with same slug to verify type filtering
{
from: "sales-team",
toUrl: "https://acme.cal.local:3000/wrong-user-redirect",
type: RedirectType.User,
},
],
});
const params = createTestRedirectParams({
slugs: ["sales-team"],
redirectType: RedirectType.Team,
});
const result = await handleOrgRedirect(params);
expectPrismaCalledWith({ type: RedirectType.Team, slugs: ["sales-team"] });
expectRedirectTo(result, "https://acme.cal.local:3000/sales?orgRedirection=true");
expectRedirectUsesData(result, "sales");
});
});
describe("when SINGLE_ORG_SLUG mode is enabled", () => {
beforeEach(() => {
vi.spyOn(constants, "SINGLE_ORG_SLUG", "get").mockReturnValue("acme");
});
it("should redirect using relative path when accessing from base domain", async () => {
createRedirectScenario({
redirects: [
{
from: "pro",
toUrl: "https://acme.cal.local:3000/pro-example",
type: RedirectType.User,
},
],
});
const params = createTestRedirectParams({
context: createTestContext({ host: "cal.local:3000" }),
currentOrgDomain: "acme",
});
const result = await handleOrgRedirect(params);
expectPrismaCalledWith({ type: RedirectType.User, slugs: ["pro"] });
// Should use relative path to stay on same domain
expectRedirectTo(result, "/pro-example?orgRedirection=true");
});
it("should redirect john87 to john using relative path in single org mode", async () => {
createRedirectScenario({
redirects: [
{
from: "john87",
toUrl: "https://acme.cal.local:3000/john",
type: RedirectType.User,
},
],
});
const params = createTestRedirectParams({
slugs: ["john87"],
context: createTestContext({
host: "my-instance.com",
}),
currentOrgDomain: "acme",
});
const result = await handleOrgRedirect(params);
expectPrismaCalledWith({ type: RedirectType.User, slugs: ["john87"] });
// Should redirect to relative path /john to stay on my-instance.com
expectRedirectTo(result, "/john?orgRedirection=true");
});
describe("when orgRedirection parameter is present", () => {
it("should not redirect to prevent loops", async () => {
const params = createTestRedirectParams({
context: createTestContext({ query: { orgRedirection: "true" } }),
currentOrgDomain: "acme",
});
const result = await handleOrgRedirect(params);
expectPrismaNotCalled();
expectNoRedirect(result);
});
});
});
describe("when event type slug is provided", () => {
it("should include event type slug in redirect destination", async () => {
createRedirectScenario({
redirects: [
{
from: "pro",
toUrl: "https://acme.cal.local:3000/pro-example",
type: RedirectType.User,
},
],
});
const params = createTestRedirectParams({
eventTypeSlug: "30min",
});
const result = await handleOrgRedirect(params);
expectPrismaCalledWith({ type: RedirectType.User, slugs: ["pro"] });
expectRedirectTo(result, "https://acme.cal.local:3000/pro-example/30min?orgRedirection=true");
// Verify the event type slug was properly appended
const url = new URL(result?.redirect.destination || "");
expect(url.pathname).toBe("/pro-example/30min");
});
it("should handle empty event type slug", async () => {
createRedirectScenario({
redirects: [
{
from: "pro",
toUrl: "https://acme.cal.local:3000/pro-example",
type: RedirectType.User,
},
],
});
const params = createTestRedirectParams({
eventTypeSlug: null,
});
const result = await handleOrgRedirect(params);
expectRedirectTo(result, "https://acme.cal.local:3000/pro-example?orgRedirection=true");
expectRedirectUsesData(result, "pro-example");
});
});
describe("when handling multiple slugs (group bookings)", () => {
it("should preserve order of slugs in redirect", async () => {
createRedirectScenario({
redirects: [
{
from: "user1",
toUrl: "https://acme.cal.local:3000/org-user1",
type: RedirectType.User,
},
{
from: "user2",
toUrl: "https://acme.cal.local:3000/org-user2",
type: RedirectType.User,
},
{
from: "user3",
toUrl: "https://acme.cal.local:3000/org-user3",
type: RedirectType.User,
},
],
});
const params = createTestRedirectParams({
slugs: ["user1", "user2", "user3"],
});
const result = await handleOrgRedirect(params);
expectRedirectTo(
result,
"https://acme.cal.local:3000/org-user1+org-user2+org-user3?orgRedirection=true"
);
// Verify order is preserved from input slugs
const url = new URL(result?.redirect.destination || "");
expect(url.pathname).toBe("/org-user1+org-user2+org-user3");
});
it("should handle partial redirects in group bookings", async () => {
createRedirectScenario({
redirects: [
{
from: "user1",
toUrl: "https://acme.cal.local:3000/org-user1",
type: RedirectType.User,
},
// user2 has no redirect - it should stay as-is
{
from: "user3",
toUrl: "https://acme.cal.local:3000/org-user3",
type: RedirectType.User,
},
],
});
const params = createTestRedirectParams({
slugs: ["user1", "user2", "user3"],
});
const result = await handleOrgRedirect(params);
expectRedirectTo(result, "https://acme.cal.local:3000/org-user1+user2+org-user3?orgRedirection=true");
// Verify that user2 remains unchanged while others are redirected
const url = new URL(result?.redirect.destination || "");
expect(url.pathname).toBe("/org-user1+user2+org-user3");
});
it("should handle different origins in redirects", async () => {
createRedirectScenario({
redirects: [
{
from: "user1",
toUrl: "https://org1.cal.com/john",
type: RedirectType.User,
},
{
from: "user2",
toUrl: "https://org1.cal.com/jane",
type: RedirectType.User,
},
],
});
const params = createTestRedirectParams({
slugs: ["user1", "user2"],
});
const result = await handleOrgRedirect(params);
// Should use the first redirect's origin
expectRedirectTo(result, "https://org1.cal.com/john+jane?orgRedirection=true");
expectRedirectUsesData(result, "john+jane", "https://org1.cal.com");
});
});
describe("when handling query parameters", () => {
it("should add orgRedirection=true to empty query", async () => {
createRedirectScenario({
redirects: [
{
from: "pro",
toUrl: "https://acme.cal.local:3000/pro-example",
type: RedirectType.User,
},
],
});
const params = createTestRedirectParams({
context: createTestContext({ query: {} }),
});
const result = await handleOrgRedirect(params);
expectRedirectTo(result, "https://acme.cal.local:3000/pro-example?orgRedirection=true");
expectRedirectUsesData(result, "pro-example");
});
it("should preserve existing query parameters", async () => {
createRedirectScenario({
redirects: [
{
from: "pro",
toUrl: "https://acme.cal.local:3000/pro-example",
type: RedirectType.User,
},
],
});
const params = createTestRedirectParams({
context: createTestContext({
query: { abc: "1", xyz: "test" },
}),
});
const result = await handleOrgRedirect(params);
expectRedirectTo(result, "https://acme.cal.local:3000/pro-example?abc=1&xyz=test&orgRedirection=true");
// Verify query parameters are preserved
const url = new URL(result?.redirect.destination || "");
expect(url.searchParams.get("abc")).toBe("1");
expect(url.searchParams.get("xyz")).toBe("test");
});
it("should not duplicate orgRedirection parameter", async () => {
createRedirectScenario({
redirects: [
{
from: "pro",
toUrl: "https://acme.cal.local:3000/pro-example",
type: RedirectType.User,
},
],
});
const params = createTestRedirectParams({
context: createTestContext({
query: { orgRedirection: "true", abc: "1" },
}),
});
const result = await handleOrgRedirect(params);
expectRedirectTo(result, "https://acme.cal.local:3000/pro-example?abc=1&orgRedirection=true");
// Verify only one orgRedirection parameter exists
const url = new URL(result?.redirect.destination || "");
const orgRedirectionParams = url.searchParams.getAll("orgRedirection");
expect(orgRedirectionParams).toHaveLength(1);
expect(orgRedirectionParams[0]).toBe("true");
});
it("should handle array query parameters", async () => {
createRedirectScenario({
redirects: [
{
from: "pro",
toUrl: "https://acme.cal.local:3000/pro-example",
type: RedirectType.User,
},
],
});
const params = createTestRedirectParams({
context: createTestContext({
query: { tags: ["tag1", "tag2"], filter: "active" },
}),
});
const result = await handleOrgRedirect(params);
const url = new URL(result?.redirect.destination || "");
expect(url.searchParams.getAll("tags")).toEqual(["tag1", "tag2"]);
expect(url.searchParams.get("filter")).toBe("active");
expect(url.searchParams.get("orgRedirection")).toBe("true");
});
});
describe("edge cases and error scenarios", () => {
it("should handle empty slugs array", async () => {
createRedirectScenario({ redirects: [] });
const params = createTestRedirectParams({
slugs: [],
});
const result = await handleOrgRedirect(params);
expectPrismaCalledWith({ type: RedirectType.User, slugs: [] });
expectNoRedirect(result);
});
it("should handle redirects with trailing slashes", async () => {
createRedirectScenario({
redirects: [
{
from: "pro",
toUrl: "https://acme.cal.local:3000/pro-example/",
type: RedirectType.User,
},
],
});
const params = createTestRedirectParams();
const result = await handleOrgRedirect(params);
expectRedirectTo(result, "https://acme.cal.local:3000/pro-example/?orgRedirection=true");
// Verify trailing slash is preserved
const url = new URL(result?.redirect.destination || "");
expect(url.pathname).toBe("/pro-example/");
});
it("should handle redirects to root path", async () => {
createRedirectScenario({
redirects: [
{
from: "pro",
toUrl: "https://acme.cal.local:3000",
type: RedirectType.User,
},
],
});
const params = createTestRedirectParams();
const result = await handleOrgRedirect(params);
expectRedirectTo(result, "https://acme.cal.local:3000?orgRedirection=true");
// Verify root path redirect
const url = new URL(result?.redirect.destination || "");
expect(url.pathname).toBe("/");
});
it("should handle special characters in slugs", async () => {
createRedirectScenario({
redirects: [
{
from: "user-name.test",
toUrl: "https://acme.cal.local:3000/user_name_test",
type: RedirectType.User,
},
],
});
const params = createTestRedirectParams({
slugs: ["user-name.test"],
});
const result = await handleOrgRedirect(params);
expectRedirectTo(result, "https://acme.cal.local:3000/user_name_test?orgRedirection=true");
expectRedirectUsesData(result, "user_name_test");
});
});
describe("when SINGLE_ORG_SLUG is not set", () => {
beforeEach(() => {
vi.spyOn(constants, "SINGLE_ORG_SLUG", "get").mockReturnValue(undefined);
});
it("should not check for redirects when in org context", async () => {
const params = createTestRedirectParams({
currentOrgDomain: "acme",
});
const result = await handleOrgRedirect(params);
expectPrismaNotCalled();
expectNoRedirect(result);
});
});
});
describe("getRedirectWithOriginAndSearchString", () => {
beforeEach(() => {
vi.spyOn(constants, "SINGLE_ORG_SLUG", "get").mockReturnValue(undefined);
});
afterEach(() => {
prismaMock.tempOrgRedirect.findMany.mockReset();
});
it("should return origin and search string for absolute URL redirects", async () => {
createRedirectScenario({
redirects: [
{
from: "john",
toUrl: "https://acme.cal.local:3000/john-org",
type: RedirectType.User,
},
],
});
const params = {
slugs: ["john"],
redirectType: RedirectType.User,
context: createTestContext(),
currentOrgDomain: null,
};
const result = await getRedirectWithOriginAndSearchString(params);
expect(result).not.toBeNull();
expect(result?.origin).toBe("https://acme.cal.local:3000");
expect(result?.searchString).toBe("?orgRedirection=true");
});
it("should return null origin for relative path redirects", async () => {
// Enable SINGLE_ORG_SLUG for this test to get relative paths
vi.spyOn(constants, "SINGLE_ORG_SLUG", "get").mockReturnValue("acme");
createRedirectScenario({
redirects: [
{
from: "john",
toUrl: "https://acme.cal.local:3000/john-org",
type: RedirectType.User,
},
],
});
const params = {
slugs: ["john"],
redirectType: RedirectType.User,
context: createTestContext({ host: "my-instance.com" }),
currentOrgDomain: "acme",
};
const result = await getRedirectWithOriginAndSearchString(params);
expect(result).not.toBeNull();
expect(result?.origin).toBeNull(); // Relative path has no origin
expect(result?.searchString).toBe("?orgRedirection=true");
});
it("should preserve existing query parameters in search string", async () => {
createRedirectScenario({
redirects: [
{
from: "john",
toUrl: "https://acme.cal.local:3000/john-org",
type: RedirectType.User,
},
],
});
const params = {
slugs: ["john"],
redirectType: RedirectType.User,
context: createTestContext({ query: { foo: "bar", baz: "qux" } }),
currentOrgDomain: null,
};
const result = await getRedirectWithOriginAndSearchString(params);
expect(result).not.toBeNull();
expect(result?.origin).toBe("https://acme.cal.local:3000");
expect(result?.searchString).toBe("?foo=bar&baz=qux&orgRedirection=true");
});
it("should return null when no redirect exists", async () => {
createRedirectScenario({ redirects: [] });
const params = {
slugs: ["john"],
redirectType: RedirectType.User,
context: createTestContext(),
currentOrgDomain: null,
};
const result = await getRedirectWithOriginAndSearchString(params);
expect(result).toBeNull();
});
});