Files
calendar/apps/web/lib/handleOrgRedirect.test.ts
T

705 lines
21 KiB
TypeScript

import type { GetServerSidePropsContext } from "next";
import type { ParsedUrlQuery } from "node:querystring";
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
import * as constants from "@calcom/lib/constants";
import { RedirectType } from "@calcom/prisma/enums";
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();
});
});