fix: Dynamic group booking org-migration redirect (#13223)

* fix: Dynamic group booking redirect

* Fix flaky private team test
This commit is contained in:
Hariom Balhara
2024-01-15 21:38:43 +05:30
committed by GitHub
parent 24607873c0
commit 93dc1247f1
7 changed files with 278 additions and 73 deletions
+188 -17
View File
@@ -1,11 +1,19 @@
import prismaMock from "../../../tests/libs/__mocks__/prismaMock";
import { describe, it, expect } from "vitest";
import { describe, it, expect, beforeEach } from "vitest";
import { RedirectType } from "@calcom/prisma/client";
import { getTemporaryOrgRedirect } from "./getTemporaryOrgRedirect";
const mockData = {
redirects: [] as {
toUrl: string;
from: string;
redirectType: RedirectType;
}[],
};
function mockARedirectInDB({
toUrl,
slug,
@@ -15,28 +23,38 @@ function mockARedirectInDB({
slug: string;
redirectType: RedirectType;
}) {
mockData.redirects.push({
toUrl,
from: slug,
redirectType,
});
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
prismaMock.tempOrgRedirect.findUnique.mockImplementation(({ where }) => {
prismaMock.tempOrgRedirect.findMany.mockImplementation(({ where }) => {
return new Promise((resolve) => {
if (
where.from_type_fromOrgId.type === redirectType &&
where.from_type_fromOrgId.from === slug &&
where.from_type_fromOrgId.fromOrgId === 0
) {
resolve({ toUrl });
} else {
resolve(null);
}
const tempOrgRedirects: typeof mockData.redirects = [];
where.from.in.forEach((whereSlug: string) => {
const matchingRedirect = mockData.redirects.find((redirect) => {
return where.type === redirect.redirectType && whereSlug === redirect.from && where.fromOrgId === 0;
});
if (matchingRedirect) {
tempOrgRedirects.push(matchingRedirect);
}
});
resolve(tempOrgRedirects);
});
});
}
beforeEach(() => {
mockData.redirects = [];
});
describe("getTemporaryOrgRedirect", () => {
it("should generate event-type URL without existing query params", async () => {
mockARedirectInDB({ slug: "slug", toUrl: "https://calcom.cal.com", redirectType: RedirectType.User });
const redirect = await getTemporaryOrgRedirect({
slug: "slug",
slugs: "slug",
redirectType: RedirectType.User,
eventTypeSlug: "30min",
currentQuery: {},
@@ -54,7 +72,7 @@ describe("getTemporaryOrgRedirect", () => {
mockARedirectInDB({ slug: "slug", toUrl: "https://calcom.cal.com", redirectType: RedirectType.User });
const redirect = await getTemporaryOrgRedirect({
slug: "slug",
slugs: "slug",
redirectType: RedirectType.User,
eventTypeSlug: "30min",
currentQuery: {
@@ -74,7 +92,7 @@ describe("getTemporaryOrgRedirect", () => {
mockARedirectInDB({ slug: "slug", toUrl: "https://calcom.cal.com", redirectType: RedirectType.User });
const redirect = await getTemporaryOrgRedirect({
slug: "slug",
slugs: "slug",
redirectType: RedirectType.User,
eventTypeSlug: null,
currentQuery: {
@@ -98,7 +116,7 @@ describe("getTemporaryOrgRedirect", () => {
});
const redirect = await getTemporaryOrgRedirect({
slug: "seeded-team",
slugs: "seeded-team",
redirectType: RedirectType.Team,
eventTypeSlug: null,
currentQuery: {
@@ -122,7 +140,7 @@ describe("getTemporaryOrgRedirect", () => {
});
const redirect = await getTemporaryOrgRedirect({
slug: "seeded-team",
slugs: "seeded-team",
redirectType: RedirectType.Team,
eventTypeSlug: "30min",
currentQuery: {
@@ -146,7 +164,7 @@ describe("getTemporaryOrgRedirect", () => {
});
const redirect = await getTemporaryOrgRedirect({
slug: "seeded-team",
slugs: "seeded-team",
redirectType: RedirectType.Team,
eventTypeSlug: "30min",
currentQuery: {},
@@ -159,4 +177,157 @@ describe("getTemporaryOrgRedirect", () => {
},
});
});
it("should generate Dynamic Group Booking Profile Url", async () => {
mockARedirectInDB({
slug: "first",
toUrl: "https://calcom.cal.com/first-in-org1",
redirectType: RedirectType.Team,
});
mockARedirectInDB({
slug: "second",
toUrl: "https://calcom.cal.com/second-in-org1",
redirectType: RedirectType.Team,
});
const redirect = await getTemporaryOrgRedirect({
slugs: ["first", "second"],
redirectType: RedirectType.Team,
eventTypeSlug: null,
currentQuery: {},
});
expect(redirect).toEqual({
redirect: {
permanent: false,
destination: "https://calcom.cal.com/first-in-org1+second-in-org1",
},
});
});
it("should generate Dynamic Group Booking Profile Url - same order", async () => {
mockARedirectInDB({
slug: "second",
toUrl: "https://calcom.cal.com/second-in-org1",
redirectType: RedirectType.Team,
});
mockARedirectInDB({
slug: "first",
toUrl: "https://calcom.cal.com/first-in-org1",
redirectType: RedirectType.Team,
});
const redirect = await getTemporaryOrgRedirect({
slugs: ["first", "second"],
redirectType: RedirectType.Team,
eventTypeSlug: null,
currentQuery: {},
});
expect(redirect).toEqual({
redirect: {
permanent: false,
destination: "https://calcom.cal.com/first-in-org1+second-in-org1",
},
});
const redirect1 = await getTemporaryOrgRedirect({
slugs: ["second", "first"],
redirectType: RedirectType.Team,
eventTypeSlug: null,
currentQuery: {},
});
expect(redirect1).toEqual({
redirect: {
permanent: false,
destination: "https://calcom.cal.com/second-in-org1+first-in-org1",
},
});
});
it("should generate Dynamic Group Booking Profile Url with query params", async () => {
mockARedirectInDB({
slug: "first",
toUrl: "https://calcom.cal.com/first-in-org1",
redirectType: RedirectType.Team,
});
mockARedirectInDB({
slug: "second",
toUrl: "https://calcom.cal.com/second-in-org1",
redirectType: RedirectType.Team,
});
const redirect = await getTemporaryOrgRedirect({
slugs: ["first", "second"],
redirectType: RedirectType.Team,
eventTypeSlug: null,
currentQuery: {
abc: "1",
},
});
expect(redirect).toEqual({
redirect: {
permanent: false,
destination: "https://calcom.cal.com/first-in-org1+second-in-org1?abc=1",
},
});
});
it("should generate Dynamic Group Booking EventType Url", async () => {
mockARedirectInDB({
slug: "first",
toUrl: "https://calcom.cal.com/first-in-org1",
redirectType: RedirectType.Team,
});
mockARedirectInDB({
slug: "second",
toUrl: "https://calcom.cal.com/second-in-org1",
redirectType: RedirectType.Team,
});
const redirect = await getTemporaryOrgRedirect({
slugs: ["first", "second"],
redirectType: RedirectType.Team,
eventTypeSlug: "30min",
currentQuery: {},
});
expect(redirect).toEqual({
redirect: {
permanent: false,
destination: "https://calcom.cal.com/first-in-org1+second-in-org1/30min",
},
});
});
it("should generate Dynamic Group Booking EventType Url with query params", async () => {
mockARedirectInDB({
slug: "first",
toUrl: "https://calcom.cal.com/first-in-org1",
redirectType: RedirectType.Team,
});
mockARedirectInDB({
slug: "second",
toUrl: "https://calcom.cal.com/second-in-org1",
redirectType: RedirectType.Team,
});
const redirect = await getTemporaryOrgRedirect({
slugs: ["first", "second"],
redirectType: RedirectType.Team,
eventTypeSlug: "30min",
currentQuery: {
abc: "1",
},
});
expect(redirect).toEqual({
redirect: {
permanent: false,
destination: "https://calcom.cal.com/first-in-org1+second-in-org1/30min?abc=1",
},
});
});
});
+39 -19
View File
@@ -7,45 +7,65 @@ import type { RedirectType } from "@calcom/prisma/client";
const log = logger.getSubLogger({ prefix: ["lib", "getTemporaryOrgRedirect"] });
export const getTemporaryOrgRedirect = async ({
slug,
slugs,
redirectType,
eventTypeSlug,
currentQuery,
}: {
slug: string;
slugs: string[] | string;
redirectType: RedirectType;
eventTypeSlug: string | null;
currentQuery: ParsedUrlQuery;
}) => {
const prisma = (await import("@calcom/prisma")).default;
slugs = slugs instanceof Array ? slugs : [slugs];
log.debug(
`Looking for redirect for`,
safeStringify({
slug,
slugs,
redirectType,
eventTypeSlug,
})
);
const redirect = await prisma.tempOrgRedirect.findUnique({
const redirects = await prisma.tempOrgRedirect.findMany({
where: {
from_type_fromOrgId: {
type: redirectType,
from: slug,
fromOrgId: 0,
type: redirectType,
from: {
in: slugs,
},
fromOrgId: 0,
},
});
if (redirect) {
log.debug(`Redirecting ${slug} to ${redirect.toUrl}`);
const newDestinationWithoutQuery = eventTypeSlug ? `${redirect.toUrl}/${eventTypeSlug}` : redirect.toUrl;
const currentQueryString = stringify(currentQuery);
return {
redirect: {
permanent: false,
destination: `${newDestinationWithoutQuery}${currentQueryString ? `?${currentQueryString}` : ""}`,
},
} as const;
const currentQueryString = stringify(currentQuery);
if (!redirects.length) {
return null;
}
return null;
// Use the first redirect origin as the new origin as we aren't supposed to handle different org usernames in a group
const newOrigin = new URL(redirects[0].toUrl).origin;
const query = currentQueryString ? `?${currentQueryString}` : "";
// Use the same order as in input slugs - It is important from Dynamic Group perspective as the first user's settings are used for various things
const newSlugs = slugs.map((slug) => {
const redirect = redirects.find((redirect) => redirect.from === slug);
if (!redirect) {
return slug;
}
const newSlug = new URL(redirect.toUrl).pathname.slice(1);
return newSlug;
});
const newSlug = newSlugs.join("+");
const newPath = newSlug ? `/${newSlug}` : "";
const newDestination = `${newOrigin}${newPath}${eventTypeSlug ? `/${eventTypeSlug}` : ""}${query}`;
log.debug(`Suggesting redirect from ${slugs} to ${newDestination}`);
return {
redirect: {
permanent: false,
destination: newDestination,
},
} as const;
};
+28 -27
View File
@@ -318,7 +318,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
const isOrgContext = isValidOrgDomain && currentOrgDomain;
const dataFetchStart = Date.now();
let outOfOffice = false;
const isDynamicGroup = usernameList.length > 1;
if (usernameList.length === 1) {
const result = await handleUserRedirection({ username: usernameList[0] });
if (result && result.outOfOffice) {
@@ -329,6 +329,19 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
}
}
if (!isOrgContext) {
const redirect = await getTemporaryOrgRedirect({
slugs: usernameList,
redirectType: RedirectType.User,
eventTypeSlug: null,
currentQuery: context.query,
});
if (redirect) {
return redirect;
}
}
const usersWithoutAvatar = await prisma.user.findMany({
where: {
username: {
@@ -362,21 +375,6 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
},
});
const isDynamicGroup = usersWithoutAvatar.length > 1;
if (isDynamicGroup) {
return {
redirect: {
permanent: false,
destination: `/${usernameList.join("+")}/dynamic`,
},
} as {
redirect: {
permanent: false;
destination: string;
};
};
}
const users = usersWithoutAvatar.map((user) => ({
...user,
organization: {
@@ -386,17 +384,20 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
avatar: `/${user.username}/avatar.png`,
}));
if (!isOrgContext) {
const redirect = await getTemporaryOrgRedirect({
slug: usernameList[0],
redirectType: RedirectType.User,
eventTypeSlug: null,
currentQuery: context.query,
});
if (redirect) {
return redirect;
}
if (isDynamicGroup) {
const destinationUrl = `/${usernameList.join("+")}/dynamic`;
logger.debug(`Dynamic group detected, redirecting to ${destinationUrl}`);
return {
redirect: {
permanent: false,
destination: destinationUrl,
},
} as {
redirect: {
permanent: false;
destination: string;
};
};
}
if (!users.length || (!isValidOrgDomain && !users.some((user) => user.organizationId === null))) {
+14 -3
View File
@@ -90,6 +90,19 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
const org = isValidOrgDomain ? currentOrgDomain : null;
if (!org) {
const redirect = await getTemporaryOrgRedirect({
slugs: usernames,
redirectType: RedirectType.User,
eventTypeSlug: slug,
currentQuery: context.query,
});
if (redirect) {
return redirect;
}
}
const users = await prisma.user.findMany({
where: {
@@ -112,7 +125,6 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
notFound: true,
} as const;
}
const org = isValidOrgDomain ? currentOrgDomain : null;
let booking: GetBookingType | null = null;
if (rescheduleUid) {
@@ -167,10 +179,9 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
let outOfOffice = false;
const isOrgContext = currentOrgDomain && isValidOrgDomain;
if (!isOrgContext) {
const redirect = await getTemporaryOrgRedirect({
slug: usernames[0],
slugs: usernames,
redirectType: RedirectType.User,
eventTypeSlug: slug,
currentQuery: context.query,
+1 -1
View File
@@ -294,7 +294,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
if (!isOrgContext && slug) {
const redirect = await getTemporaryOrgRedirect({
slug: slug,
slugs: slug,
redirectType: RedirectType.Team,
eventTypeSlug: null,
currentQuery: context.query,
+1 -1
View File
@@ -81,7 +81,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
if (!isOrgContext) {
const redirect = await getTemporaryOrgRedirect({
slug: teamSlug,
slugs: teamSlug,
redirectType: RedirectType.Team,
eventTypeSlug: meetingSlug,
currentQuery: context.query,
+7 -5
View File
@@ -268,11 +268,13 @@ test.describe("Teams - NonOrg", () => {
// Mark team as private
await page.goto(`/settings/teams/${team.id}/members`);
await page.click("[data-testid=make-team-private-check]");
await expect(page.locator(`[data-testid=make-team-private-check][data-state="checked"]`)).toBeVisible();
// according to switch implementation, checked state can be set before mutation is resolved
// so we need to await for req to resolve
await page.waitForResponse((res) => res.url().includes("/api/trpc/teams/update"));
await Promise.all([
page.click("[data-testid=make-team-private-check]"),
expect(page.locator(`[data-testid=make-team-private-check][data-state="checked"]`)).toBeVisible(),
// according to switch implementation, checked state can be set before mutation is resolved
// so we need to await for req to resolve
page.waitForResponse((res) => res.url().includes("/api/trpc/teams/update")),
]);
// Go to Team's page
await page.goto(`/team/${team.slug}`);