test: E2E for Orgs - The beginning (#12095)
parent
09ecd445bb
commit
426d31712e
|
@ -3,7 +3,10 @@ import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { orgDomainConfig, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
import {
|
||||||
|
getOrgDomainConfigFromHostname,
|
||||||
|
subdomainSuffix,
|
||||||
|
} from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { DOCS_URL, IS_CALCOM, JOIN_DISCORD, WEBSITE_URL } from "@calcom/lib/constants";
|
import { DOCS_URL, IS_CALCOM, JOIN_DISCORD, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { HeadSeo } from "@calcom/ui";
|
import { HeadSeo } from "@calcom/ui";
|
||||||
|
@ -50,7 +53,10 @@ export default function Custom404() {
|
||||||
|
|
||||||
const [url, setUrl] = useState(`${WEBSITE_URL}/signup`);
|
const [url, setUrl] = useState(`${WEBSITE_URL}/signup`);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(window.location.host);
|
const { isValidOrgDomain, currentOrgDomain } = getOrgDomainConfigFromHostname({
|
||||||
|
hostname: window.location.host,
|
||||||
|
});
|
||||||
|
|
||||||
const [routerUsername] = pathname?.replace("%20", "-").split(/[?#]/) ?? [];
|
const [routerUsername] = pathname?.replace("%20", "-").split(/[?#]/) ?? [];
|
||||||
if (routerUsername && (!isValidOrgDomain || !currentOrgDomain)) {
|
if (routerUsername && (!isValidOrgDomain || !currentOrgDomain)) {
|
||||||
const splitPath = routerUsername.split("/");
|
const splitPath = routerUsername.split("/");
|
||||||
|
|
|
@ -275,10 +275,7 @@ export type UserPageProps = {
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps<UserPageProps> = async (context) => {
|
export const getServerSideProps: GetServerSideProps<UserPageProps> = async (context) => {
|
||||||
const ssr = await ssrInit(context);
|
const ssr = await ssrInit(context);
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||||
context.req.headers.host ?? "",
|
|
||||||
context.params?.orgSlug
|
|
||||||
);
|
|
||||||
const usernameList = getUsernameList(context.query.user as string);
|
const usernameList = getUsernameList(context.query.user as string);
|
||||||
const isOrgContext = isValidOrgDomain && currentOrgDomain;
|
const isOrgContext = isValidOrgDomain && currentOrgDomain;
|
||||||
const dataFetchStart = Date.now();
|
const dataFetchStart = Date.now();
|
||||||
|
|
|
@ -72,10 +72,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||||
|
|
||||||
const { ssrInit } = await import("@server/lib/ssr");
|
const { ssrInit } = await import("@server/lib/ssr");
|
||||||
const ssr = await ssrInit(context);
|
const ssr = await ssrInit(context);
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||||
context.req.headers.host ?? "",
|
|
||||||
context.params?.orgSlug
|
|
||||||
);
|
|
||||||
|
|
||||||
const users = await prisma.user.findMany({
|
const users = await prisma.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
@ -148,10 +145,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||||
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
||||||
const username = usernames[0];
|
const username = usernames[0];
|
||||||
const { rescheduleUid, bookingUid, duration: queryDuration } = context.query;
|
const { rescheduleUid, bookingUid, duration: queryDuration } = context.query;
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||||
context.req.headers.host ?? "",
|
|
||||||
context.params?.orgSlug
|
|
||||||
);
|
|
||||||
|
|
||||||
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
||||||
|
|
||||||
|
|
|
@ -154,7 +154,7 @@ async function getTeamLogos(subdomain: string, isValidOrgDomain: boolean) {
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { query } = req;
|
const { query } = req;
|
||||||
const parsedQuery = logoApiSchema.parse(query);
|
const parsedQuery = logoApiSchema.parse(query);
|
||||||
const { isValidOrgDomain } = orgDomainConfig(req.headers.host ?? "");
|
const { isValidOrgDomain } = orgDomainConfig(req);
|
||||||
|
|
||||||
const hostname = req?.headers["host"];
|
const hostname = req?.headers["host"];
|
||||||
if (!hostname) throw new Error("No hostname");
|
if (!hostname) throw new Error("No hostname");
|
||||||
|
|
|
@ -29,7 +29,7 @@ const querySchema = z
|
||||||
|
|
||||||
async function getIdentityData(req: NextApiRequest) {
|
async function getIdentityData(req: NextApiRequest) {
|
||||||
const { username, teamname, orgId, orgSlug } = querySchema.parse(req.query);
|
const { username, teamname, orgId, orgSlug } = querySchema.parse(req.query);
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req.headers.host ?? "");
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req);
|
||||||
|
|
||||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ type Response = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<Response>): Promise<void> {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse<Response>): Promise<void> {
|
||||||
const { currentOrgDomain } = orgDomainConfig(req.headers.host ?? "");
|
const { currentOrgDomain } = orgDomainConfig(req);
|
||||||
const result = await checkUsername(req.body.username, currentOrgDomain);
|
const result = await checkUsername(req.body.username, currentOrgDomain);
|
||||||
return res.status(200).json(result);
|
return res.status(200).json(result);
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
|
|
||||||
const session = await getServerSession({ req, res });
|
const session = await getServerSession({ req, res });
|
||||||
const ssr = await ssrInit(context);
|
const ssr = await ssrInit(context);
|
||||||
const { currentOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
const { currentOrgDomain } = orgDomainConfig(context.req);
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
// Validating if username is Premium, while this is true an email its required for stripe user confirmation
|
// Validating if username is Premium, while this is true an email its required for stripe user confirmation
|
||||||
|
|
|
@ -61,7 +61,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||||
const session = await getServerSession(context);
|
const session = await getServerSession(context);
|
||||||
const { link, slug } = paramsSchema.parse(context.params);
|
const { link, slug } = paramsSchema.parse(context.params);
|
||||||
const { rescheduleUid, duration: queryDuration } = context.query;
|
const { rescheduleUid, duration: queryDuration } = context.query;
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req);
|
||||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||||
|
|
||||||
const { ssrInit } = await import("@server/lib/ssr");
|
const { ssrInit } = await import("@server/lib/ssr");
|
||||||
|
|
|
@ -269,10 +269,7 @@ function TeamPage({
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug;
|
const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug;
|
||||||
const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(
|
const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||||
context.req.headers.host ?? "",
|
|
||||||
context.params?.orgSlug
|
|
||||||
);
|
|
||||||
const isOrgContext = isValidOrgDomain && currentOrgDomain;
|
const isOrgContext = isValidOrgDomain && currentOrgDomain;
|
||||||
|
|
||||||
// Provided by Rewrite from next.config.js
|
// Provided by Rewrite from next.config.js
|
||||||
|
|
|
@ -74,10 +74,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
const { rescheduleUid, duration: queryDuration } = context.query;
|
const { rescheduleUid, duration: queryDuration } = context.query;
|
||||||
const { ssrInit } = await import("@server/lib/ssr");
|
const { ssrInit } = await import("@server/lib/ssr");
|
||||||
const ssr = await ssrInit(context);
|
const ssr = await ssrInit(context);
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||||
context.req.headers.host ?? "",
|
|
||||||
context.params?.orgSlug
|
|
||||||
);
|
|
||||||
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
||||||
|
|
||||||
if (!isOrgContext) {
|
if (!isOrgContext) {
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import type { Page } from "@playwright/test";
|
||||||
|
import type { Team } from "@prisma/client";
|
||||||
|
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
|
||||||
|
const getRandomSlug = () => `org-${Math.random().toString(36).substring(7)}`;
|
||||||
|
|
||||||
|
// creates a user fixture instance and stores the collection
|
||||||
|
export const createOrgsFixture = (page: Page) => {
|
||||||
|
const store = { orgs: [], page } as { orgs: Team[]; page: typeof page };
|
||||||
|
return {
|
||||||
|
create: async (opts: { name: string; slug?: string; requestedSlug?: string }) => {
|
||||||
|
const org = await createOrgInDb({
|
||||||
|
name: opts.name,
|
||||||
|
slug: opts.slug || getRandomSlug(),
|
||||||
|
requestedSlug: opts.requestedSlug,
|
||||||
|
});
|
||||||
|
store.orgs.push(org);
|
||||||
|
return org;
|
||||||
|
},
|
||||||
|
get: () => store.orgs,
|
||||||
|
deleteAll: async () => {
|
||||||
|
await prisma.team.deleteMany({ where: { id: { in: store.orgs.map((org) => org.id) } } });
|
||||||
|
store.orgs = [];
|
||||||
|
},
|
||||||
|
delete: async (id: number) => {
|
||||||
|
await prisma.team.delete({ where: { id } });
|
||||||
|
store.orgs = store.orgs.filter((b) => b.id !== id);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
async function createOrgInDb({
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
requestedSlug,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
slug: string | null;
|
||||||
|
requestedSlug?: string;
|
||||||
|
}) {
|
||||||
|
return await prisma.team.create({
|
||||||
|
data: {
|
||||||
|
name: name,
|
||||||
|
slug: slug,
|
||||||
|
metadata: {
|
||||||
|
isOrganization: true,
|
||||||
|
...(requestedSlug
|
||||||
|
? {
|
||||||
|
requestedSlug,
|
||||||
|
}
|
||||||
|
: null),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/avail
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||||
|
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import { selectFirstAvailableTimeSlotNextMonth, teamEventSlug, teamEventTitle } from "../lib/testUtils";
|
import { selectFirstAvailableTimeSlotNextMonth, teamEventSlug, teamEventTitle } from "../lib/testUtils";
|
||||||
import { TimeZoneEnum } from "./types";
|
import { TimeZoneEnum } from "./types";
|
||||||
|
@ -78,11 +79,13 @@ const createTeamAndAddUser = async (
|
||||||
isUnpublished,
|
isUnpublished,
|
||||||
isOrg,
|
isOrg,
|
||||||
hasSubteam,
|
hasSubteam,
|
||||||
|
organizationId,
|
||||||
}: {
|
}: {
|
||||||
user: { id: number; username: string | null; role?: MembershipRole };
|
user: { id: number; username: string | null; role?: MembershipRole };
|
||||||
isUnpublished?: boolean;
|
isUnpublished?: boolean;
|
||||||
isOrg?: boolean;
|
isOrg?: boolean;
|
||||||
hasSubteam?: true;
|
hasSubteam?: true;
|
||||||
|
organizationId?: number | null;
|
||||||
},
|
},
|
||||||
workerInfo: WorkerInfo
|
workerInfo: WorkerInfo
|
||||||
) => {
|
) => {
|
||||||
|
@ -101,6 +104,7 @@ const createTeamAndAddUser = async (
|
||||||
data.children = { connect: [{ id: team.id }] };
|
data.children = { connect: [{ id: team.id }] };
|
||||||
}
|
}
|
||||||
data.orgUsers = isOrg ? { connect: [{ id: user.id }] } : undefined;
|
data.orgUsers = isOrg ? { connect: [{ id: user.id }] } : undefined;
|
||||||
|
data.parent = organizationId ? { connect: { id: organizationId } } : undefined;
|
||||||
const team = await prisma.team.create({
|
const team = await prisma.team.create({
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
@ -114,6 +118,7 @@ const createTeamAndAddUser = async (
|
||||||
accepted: true,
|
accepted: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return team;
|
return team;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -282,6 +287,7 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn
|
||||||
isUnpublished: scenario.isUnpublished,
|
isUnpublished: scenario.isUnpublished,
|
||||||
isOrg: scenario.isOrg,
|
isOrg: scenario.isOrg,
|
||||||
hasSubteam: scenario.hasSubteam,
|
hasSubteam: scenario.hasSubteam,
|
||||||
|
organizationId: opts?.organizationId,
|
||||||
},
|
},
|
||||||
workerInfo
|
workerInfo
|
||||||
);
|
);
|
||||||
|
@ -399,11 +405,27 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
||||||
logout: async () => {
|
logout: async () => {
|
||||||
await page.goto("/auth/logout");
|
await page.goto("/auth/logout");
|
||||||
},
|
},
|
||||||
getTeam: async () => {
|
getFirstTeam: async () => {
|
||||||
return prisma.membership.findFirstOrThrow({
|
const memberships = await prisma.membership.findMany({
|
||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
include: { team: true },
|
include: { team: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const membership = memberships
|
||||||
|
.map((membership) => {
|
||||||
|
return {
|
||||||
|
...membership,
|
||||||
|
team: {
|
||||||
|
...membership.team,
|
||||||
|
metadata: teamMetadataSchema.parse(membership.team.metadata),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.find((membership) => !membership.team?.metadata?.isOrganization);
|
||||||
|
if (!membership) {
|
||||||
|
throw new Error("No team found for user");
|
||||||
|
}
|
||||||
|
return membership;
|
||||||
},
|
},
|
||||||
getOrg: async () => {
|
getOrg: async () => {
|
||||||
return prisma.membership.findFirstOrThrow({
|
return prisma.membership.findFirstOrThrow({
|
||||||
|
@ -453,16 +475,27 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
||||||
type SupportedTestEventTypes = PrismaType.EventTypeCreateInput & {
|
type SupportedTestEventTypes = PrismaType.EventTypeCreateInput & {
|
||||||
_bookings?: PrismaType.BookingCreateInput[];
|
_bookings?: PrismaType.BookingCreateInput[];
|
||||||
};
|
};
|
||||||
type CustomUserOptsKeys = "username" | "password" | "completedOnboarding" | "locale" | "name" | "email";
|
type CustomUserOptsKeys =
|
||||||
|
| "username"
|
||||||
|
| "password"
|
||||||
|
| "completedOnboarding"
|
||||||
|
| "locale"
|
||||||
|
| "name"
|
||||||
|
| "email"
|
||||||
|
| "organizationId";
|
||||||
type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & {
|
type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & {
|
||||||
timeZone?: TimeZoneEnum;
|
timeZone?: TimeZoneEnum;
|
||||||
eventTypes?: SupportedTestEventTypes[];
|
eventTypes?: SupportedTestEventTypes[];
|
||||||
// ignores adding the worker-index after username
|
// ignores adding the worker-index after username
|
||||||
useExactUsername?: boolean;
|
useExactUsername?: boolean;
|
||||||
|
roleInOrganization?: MembershipRole;
|
||||||
};
|
};
|
||||||
|
|
||||||
// creates the actual user in the db.
|
// creates the actual user in the db.
|
||||||
const createUser = (workerInfo: WorkerInfo, opts?: CustomUserOpts | null): PrismaType.UserCreateInput => {
|
const createUser = (
|
||||||
|
workerInfo: WorkerInfo,
|
||||||
|
opts?: CustomUserOpts | null
|
||||||
|
): PrismaType.UserUncheckedCreateInput => {
|
||||||
// build a unique name for our user
|
// build a unique name for our user
|
||||||
const uname =
|
const uname =
|
||||||
opts?.useExactUsername && opts?.username
|
opts?.useExactUsername && opts?.username
|
||||||
|
@ -478,6 +511,7 @@ const createUser = (workerInfo: WorkerInfo, opts?: CustomUserOpts | null): Prism
|
||||||
completedOnboarding: opts?.completedOnboarding ?? true,
|
completedOnboarding: opts?.completedOnboarding ?? true,
|
||||||
timeZone: opts?.timeZone ?? TimeZoneEnum.UK,
|
timeZone: opts?.timeZone ?? TimeZoneEnum.UK,
|
||||||
locale: opts?.locale ?? "en",
|
locale: opts?.locale ?? "en",
|
||||||
|
...getOrganizationRelatedProps({ organizationId: opts?.organizationId, role: opts?.roleInOrganization }),
|
||||||
schedules:
|
schedules:
|
||||||
opts?.completedOnboarding ?? true
|
opts?.completedOnboarding ?? true
|
||||||
? {
|
? {
|
||||||
|
@ -493,6 +527,42 @@ const createUser = (workerInfo: WorkerInfo, opts?: CustomUserOpts | null): Prism
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getOrganizationRelatedProps({
|
||||||
|
organizationId,
|
||||||
|
role,
|
||||||
|
}: {
|
||||||
|
organizationId: number | null | undefined;
|
||||||
|
role: MembershipRole | undefined;
|
||||||
|
}) {
|
||||||
|
if (!organizationId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!role) {
|
||||||
|
throw new Error("Missing role for user in organization");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
organizationId: organizationId || null,
|
||||||
|
...(organizationId
|
||||||
|
? {
|
||||||
|
teams: {
|
||||||
|
// Create membership
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
connect: {
|
||||||
|
id: organizationId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
accepted: true,
|
||||||
|
role: MembershipRole.ADMIN,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: null),
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function confirmPendingPayment(page: Page) {
|
async function confirmPendingPayment(page: Page) {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import prisma from "@calcom/prisma";
|
||||||
import type { ExpectedUrlDetails } from "../../../../playwright.config";
|
import type { ExpectedUrlDetails } from "../../../../playwright.config";
|
||||||
import { createBookingsFixture } from "../fixtures/bookings";
|
import { createBookingsFixture } from "../fixtures/bookings";
|
||||||
import { createEmbedsFixture } from "../fixtures/embeds";
|
import { createEmbedsFixture } from "../fixtures/embeds";
|
||||||
|
import { createOrgsFixture } from "../fixtures/orgs";
|
||||||
import { createPaymentsFixture } from "../fixtures/payments";
|
import { createPaymentsFixture } from "../fixtures/payments";
|
||||||
import { createBookingPageFixture } from "../fixtures/regularBookings";
|
import { createBookingPageFixture } from "../fixtures/regularBookings";
|
||||||
import { createRoutingFormsFixture } from "../fixtures/routingForms";
|
import { createRoutingFormsFixture } from "../fixtures/routingForms";
|
||||||
|
@ -17,6 +18,7 @@ import { createUsersFixture } from "../fixtures/users";
|
||||||
|
|
||||||
export interface Fixtures {
|
export interface Fixtures {
|
||||||
page: Page;
|
page: Page;
|
||||||
|
orgs: ReturnType<typeof createOrgsFixture>;
|
||||||
users: ReturnType<typeof createUsersFixture>;
|
users: ReturnType<typeof createUsersFixture>;
|
||||||
bookings: ReturnType<typeof createBookingsFixture>;
|
bookings: ReturnType<typeof createBookingsFixture>;
|
||||||
payments: ReturnType<typeof createPaymentsFixture>;
|
payments: ReturnType<typeof createPaymentsFixture>;
|
||||||
|
@ -48,6 +50,10 @@ declare global {
|
||||||
* @see https://playwright.dev/docs/test-fixtures
|
* @see https://playwright.dev/docs/test-fixtures
|
||||||
*/
|
*/
|
||||||
export const test = base.extend<Fixtures>({
|
export const test = base.extend<Fixtures>({
|
||||||
|
orgs: async ({ page }, use) => {
|
||||||
|
const orgsFixture = createOrgsFixture(page);
|
||||||
|
await use(orgsFixture);
|
||||||
|
},
|
||||||
users: async ({ page, context, emails }, use, workerInfo) => {
|
users: async ({ page, context, emails }, use, workerInfo) => {
|
||||||
const usersFixture = createUsersFixture(page, emails, workerInfo);
|
const usersFixture = createUsersFixture(page, emails, workerInfo);
|
||||||
await use(usersFixture);
|
await use(usersFixture);
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
|
import type { Page } from "@playwright/test";
|
||||||
import { expect } from "@playwright/test";
|
import { expect } from "@playwright/test";
|
||||||
|
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import { SchedulingType } from "@calcom/prisma/enums";
|
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import { test } from "./lib/fixtures";
|
import { test } from "./lib/fixtures";
|
||||||
import { bookTimeSlot, selectFirstAvailableTimeSlotNextMonth, testName, todo } from "./lib/testUtils";
|
import { bookTimeSlot, selectFirstAvailableTimeSlotNextMonth, testName, todo } from "./lib/testUtils";
|
||||||
|
|
||||||
test.describe.configure({ mode: "parallel" });
|
test.describe.configure({ mode: "parallel" });
|
||||||
|
|
||||||
test.afterEach(({ users }) => users.deleteAll());
|
test.describe("Teams - NonOrg", () => {
|
||||||
|
test.afterEach(({ users }) => users.deleteAll());
|
||||||
test.describe("Teams", () => {
|
|
||||||
test("Can create teams via Wizard", async ({ page, users }) => {
|
test("Can create teams via Wizard", async ({ page, users }) => {
|
||||||
const user = await users.create();
|
const user = await users.create();
|
||||||
const inviteeEmail = `${user.username}+invitee@example.com`;
|
const inviteeEmail = `${user.username}+invitee@example.com`;
|
||||||
|
@ -64,6 +64,7 @@ test.describe("Teams", () => {
|
||||||
// await expect(page.locator('[data-testid="empty-screen"]')).toBeVisible();
|
// await expect(page.locator('[data-testid="empty-screen"]')).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Can create a booking for Collective EventType", async ({ page, users }) => {
|
test("Can create a booking for Collective EventType", async ({ page, users }) => {
|
||||||
const ownerObj = { username: "pro-user", name: "pro-user" };
|
const ownerObj = { username: "pro-user", name: "pro-user" };
|
||||||
const teamMatesObj = [
|
const teamMatesObj = [
|
||||||
|
@ -78,7 +79,7 @@ test.describe("Teams", () => {
|
||||||
teammates: teamMatesObj,
|
teammates: teamMatesObj,
|
||||||
schedulingType: SchedulingType.COLLECTIVE,
|
schedulingType: SchedulingType.COLLECTIVE,
|
||||||
});
|
});
|
||||||
const { team } = await owner.getTeam();
|
const { team } = await owner.getFirstTeam();
|
||||||
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||||
|
|
||||||
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
||||||
|
@ -99,6 +100,7 @@ test.describe("Teams", () => {
|
||||||
|
|
||||||
// TODO: Assert whether the user received an email
|
// TODO: Assert whether the user received an email
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Can create a booking for Round Robin EventType", async ({ page, users }) => {
|
test("Can create a booking for Round Robin EventType", async ({ page, users }) => {
|
||||||
const ownerObj = { username: "pro-user", name: "pro-user" };
|
const ownerObj = { username: "pro-user", name: "pro-user" };
|
||||||
const teamMatesObj = [
|
const teamMatesObj = [
|
||||||
|
@ -113,7 +115,7 @@ test.describe("Teams", () => {
|
||||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { team } = await owner.getTeam();
|
const { team } = await owner.getFirstTeam();
|
||||||
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||||
|
|
||||||
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
||||||
|
@ -135,6 +137,7 @@ test.describe("Teams", () => {
|
||||||
expect(teamMatesObj.some(({ name }) => name === chosenUser)).toBe(true);
|
expect(teamMatesObj.some(({ name }) => name === chosenUser)).toBe(true);
|
||||||
// TODO: Assert whether the user received an email
|
// TODO: Assert whether the user received an email
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Non admin team members cannot create team in org", async ({ page, users }) => {
|
test("Non admin team members cannot create team in org", async ({ page, users }) => {
|
||||||
const teamMateName = "teammate-1";
|
const teamMateName = "teammate-1";
|
||||||
|
|
||||||
|
@ -169,6 +172,7 @@ test.describe("Teams", () => {
|
||||||
await prisma.team.delete({ where: { id: org.teamId } });
|
await prisma.team.delete({ where: { id: org.teamId } });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Can create team with same name as user", async ({ page, users }) => {
|
test("Can create team with same name as user", async ({ page, users }) => {
|
||||||
// Name to be used for both user and team
|
// Name to be used for both user and team
|
||||||
const uniqueName = "test-unique-name";
|
const uniqueName = "test-unique-name";
|
||||||
|
@ -210,6 +214,7 @@ test.describe("Teams", () => {
|
||||||
await prisma.team.delete({ where: { id: team?.id } });
|
await prisma.team.delete({ where: { id: team?.id } });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Can create a private team", async ({ page, users }) => {
|
test("Can create a private team", async ({ page, users }) => {
|
||||||
const ownerObj = { username: "pro-user", name: "pro-user" };
|
const ownerObj = { username: "pro-user", name: "pro-user" };
|
||||||
const teamMatesObj = [
|
const teamMatesObj = [
|
||||||
|
@ -226,7 +231,7 @@ test.describe("Teams", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
await owner.apiLogin();
|
await owner.apiLogin();
|
||||||
const { team } = await owner.getTeam();
|
const { team } = await owner.getFirstTeam();
|
||||||
|
|
||||||
// Mark team as private
|
// Mark team as private
|
||||||
await page.goto(`/settings/teams/${team.id}/members`);
|
await page.goto(`/settings/teams/${team.id}/members`);
|
||||||
|
@ -247,3 +252,180 @@ test.describe("Teams", () => {
|
||||||
todo("Reschedule a Collective EventType booking");
|
todo("Reschedule a Collective EventType booking");
|
||||||
todo("Reschedule a Round Robin EventType booking");
|
todo("Reschedule a Round Robin EventType booking");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe("Teams - Org", () => {
|
||||||
|
test.afterEach(({ orgs, users }) => {
|
||||||
|
orgs.deleteAll();
|
||||||
|
users.deleteAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Can create teams via Wizard", async ({ page, users, orgs }) => {
|
||||||
|
const org = await orgs.create({
|
||||||
|
name: "TestOrg",
|
||||||
|
});
|
||||||
|
const user = await users.create({
|
||||||
|
organizationId: org.id,
|
||||||
|
roleInOrganization: MembershipRole.ADMIN,
|
||||||
|
});
|
||||||
|
const inviteeEmail = `${user.username}+invitee@example.com`;
|
||||||
|
await user.apiLogin();
|
||||||
|
await page.goto("/teams");
|
||||||
|
|
||||||
|
await test.step("Can create team", async () => {
|
||||||
|
// Click text=Create Team
|
||||||
|
await page.locator("text=Create a new Team").click();
|
||||||
|
await page.waitForURL((url) => url.pathname === "/settings/teams/new");
|
||||||
|
// Fill input[name="name"]
|
||||||
|
await page.locator('input[name="name"]').fill(`${user.username}'s Team`);
|
||||||
|
// Click text=Continue
|
||||||
|
await page.locator("text=Continue").click();
|
||||||
|
await page.waitForURL(/\/settings\/teams\/(\d+)\/onboard-members$/i);
|
||||||
|
await page.waitForSelector('[data-testid="pending-member-list"]');
|
||||||
|
expect(await page.locator('[data-testid="pending-member-item"]').count()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Can add members", async () => {
|
||||||
|
// Click [data-testid="new-member-button"]
|
||||||
|
await page.locator('[data-testid="new-member-button"]').click();
|
||||||
|
// Fill [placeholder="email\@example\.com"]
|
||||||
|
await page.locator('[placeholder="email\\@example\\.com"]').fill(inviteeEmail);
|
||||||
|
// Click [data-testid="invite-new-member-button"]
|
||||||
|
await page.locator('[data-testid="invite-new-member-button"]').click();
|
||||||
|
await expect(page.locator(`li:has-text("${inviteeEmail}")`)).toBeVisible();
|
||||||
|
expect(await page.locator('[data-testid="pending-member-item"]').count()).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Can remove members", async () => {
|
||||||
|
expect(await page.locator('[data-testid="pending-member-item"]').count()).toBe(2);
|
||||||
|
|
||||||
|
const lastRemoveMemberButton = page.locator('[data-testid="remove-member-button"]').last();
|
||||||
|
await lastRemoveMemberButton.click();
|
||||||
|
await page.waitForLoadState("networkidle");
|
||||||
|
expect(await page.locator('[data-testid="pending-member-item"]').count()).toBe(1);
|
||||||
|
|
||||||
|
// Cleanup here since this user is created without our fixtures.
|
||||||
|
await prisma.user.delete({ where: { email: inviteeEmail } });
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Can finish team creation", async () => {
|
||||||
|
await page.locator("text=Finish").click();
|
||||||
|
await page.waitForURL("/settings/teams");
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Can disband team", async () => {
|
||||||
|
await page.locator('[data-testid="team-list-item-link"]').click();
|
||||||
|
await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i);
|
||||||
|
await page.locator("text=Disband Team").click();
|
||||||
|
await page.locator("text=Yes, disband team").click();
|
||||||
|
await page.waitForURL("/teams");
|
||||||
|
expect(await page.locator(`text=${user.username}'s Team`).count()).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Can create a booking for Collective EventType", async ({ page, users, orgs }) => {
|
||||||
|
const org = await orgs.create({
|
||||||
|
name: "TestOrg",
|
||||||
|
});
|
||||||
|
const teamMatesObj = [
|
||||||
|
{ name: "teammate-1" },
|
||||||
|
{ name: "teammate-2" },
|
||||||
|
{ name: "teammate-3" },
|
||||||
|
{ name: "teammate-4" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const owner = await users.create(
|
||||||
|
{
|
||||||
|
username: "pro-user",
|
||||||
|
name: "pro-user",
|
||||||
|
organizationId: org.id,
|
||||||
|
roleInOrganization: MembershipRole.MEMBER,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hasTeam: true,
|
||||||
|
teammates: teamMatesObj,
|
||||||
|
schedulingType: SchedulingType.COLLECTIVE,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const { team } = await owner.getFirstTeam();
|
||||||
|
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||||
|
|
||||||
|
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
||||||
|
|
||||||
|
await expect(page.locator('[data-testid="404-page"]')).toBeVisible();
|
||||||
|
await doOnOrgDomain(
|
||||||
|
{
|
||||||
|
orgSlug: org.slug,
|
||||||
|
page,
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
||||||
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||||
|
await bookTimeSlot(page);
|
||||||
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||||
|
|
||||||
|
// The title of the booking
|
||||||
|
const BookingTitle = `${teamEventTitle} between ${team.name} and ${testName}`;
|
||||||
|
await expect(page.locator("[data-testid=booking-title]")).toHaveText(BookingTitle);
|
||||||
|
// The booker should be in the attendee list
|
||||||
|
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
|
||||||
|
|
||||||
|
// All the teammates should be in the booking
|
||||||
|
for (const teammate of teamMatesObj) {
|
||||||
|
await expect(page.getByText(teammate.name, { exact: true })).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: Assert whether the user received an email
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Can create a booking for Round Robin EventType", async ({ page, users }) => {
|
||||||
|
const ownerObj = { username: "pro-user", name: "pro-user" };
|
||||||
|
const teamMatesObj = [
|
||||||
|
{ name: "teammate-1" },
|
||||||
|
{ name: "teammate-2" },
|
||||||
|
{ name: "teammate-3" },
|
||||||
|
{ name: "teammate-4" },
|
||||||
|
];
|
||||||
|
const owner = await users.create(ownerObj, {
|
||||||
|
hasTeam: true,
|
||||||
|
teammates: teamMatesObj,
|
||||||
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { team } = await owner.getFirstTeam();
|
||||||
|
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||||
|
|
||||||
|
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
||||||
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||||
|
await bookTimeSlot(page);
|
||||||
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||||
|
|
||||||
|
// The person who booked the meeting should be in the attendee list
|
||||||
|
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
|
||||||
|
|
||||||
|
// The title of the booking
|
||||||
|
const BookingTitle = `${teamEventTitle} between ${team.name} and ${testName}`;
|
||||||
|
await expect(page.locator("[data-testid=booking-title]")).toHaveText(BookingTitle);
|
||||||
|
|
||||||
|
// Since all the users have the same leastRecentlyBooked value
|
||||||
|
// Anyone of the teammates could be the Host of the booking.
|
||||||
|
const chosenUser = await page.getByTestId("booking-host-name").textContent();
|
||||||
|
expect(chosenUser).not.toBeNull();
|
||||||
|
expect(teamMatesObj.some(({ name }) => name === chosenUser)).toBe(true);
|
||||||
|
// TODO: Assert whether the user received an email
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function doOnOrgDomain(
|
||||||
|
{ orgSlug, page }: { orgSlug: string | null; page: Page },
|
||||||
|
callback: ({ page }: { page: Page }) => Promise<void>
|
||||||
|
) {
|
||||||
|
if (!orgSlug) {
|
||||||
|
throw new Error("orgSlug is not available");
|
||||||
|
}
|
||||||
|
page.setExtraHTTPHeaders({
|
||||||
|
"x-cal-force-slug": orgSlug,
|
||||||
|
});
|
||||||
|
await callback({ page });
|
||||||
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ test.afterAll(async ({ users }) => {
|
||||||
test.describe("Unpublished", () => {
|
test.describe("Unpublished", () => {
|
||||||
test("Regular team profile", async ({ page, users }) => {
|
test("Regular team profile", async ({ page, users }) => {
|
||||||
const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true });
|
const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true });
|
||||||
const { team } = await owner.getTeam();
|
const { team } = await owner.getFirstTeam();
|
||||||
const { requestedSlug } = team.metadata as { requestedSlug: string };
|
const { requestedSlug } = team.metadata as { requestedSlug: string };
|
||||||
await page.goto(`/team/${requestedSlug}`);
|
await page.goto(`/team/${requestedSlug}`);
|
||||||
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
|
expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1);
|
||||||
|
@ -33,7 +33,7 @@ test.describe("Unpublished", () => {
|
||||||
isUnpublished: true,
|
isUnpublished: true,
|
||||||
schedulingType: SchedulingType.COLLECTIVE,
|
schedulingType: SchedulingType.COLLECTIVE,
|
||||||
});
|
});
|
||||||
const { team } = await owner.getTeam();
|
const { team } = await owner.getFirstTeam();
|
||||||
const { requestedSlug } = team.metadata as { requestedSlug: string };
|
const { requestedSlug } = team.metadata as { requestedSlug: string };
|
||||||
const { slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
const { slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||||
await page.goto(`/team/${requestedSlug}/${teamEventSlug}`);
|
await page.goto(`/team/${requestedSlug}/${teamEventSlug}`);
|
||||||
|
|
|
@ -54,7 +54,7 @@ export const getServerSideProps = async function getServerSideProps(
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { form: formId, slug: _slug, pages: _pages, ...fieldsResponses } = queryParsed.data;
|
const { form: formId, slug: _slug, pages: _pages, ...fieldsResponses } = queryParsed.data;
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req);
|
||||||
|
|
||||||
const form = await prisma.app_RoutingForms_Form.findFirst({
|
const form = await prisma.app_RoutingForms_Form.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -248,7 +248,7 @@ export const getServerSideProps = async function getServerSideProps(
|
||||||
notFound: true,
|
notFound: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req);
|
||||||
|
|
||||||
const isEmbed = params.appPages[1] === "embed";
|
const isEmbed = params.appPages[1] === "embed";
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,33 @@
|
||||||
import type { Prisma } from "@prisma/client";
|
import type { Prisma } from "@prisma/client";
|
||||||
|
import type { IncomingMessage } from "http";
|
||||||
|
|
||||||
import { ALLOWED_HOSTNAMES, RESERVED_SUBDOMAINS, WEBAPP_URL } from "@calcom/lib/constants";
|
import { ALLOWED_HOSTNAMES, RESERVED_SUBDOMAINS, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import slugify from "@calcom/lib/slugify";
|
import slugify from "@calcom/lib/slugify";
|
||||||
|
|
||||||
|
const log = logger.getSubLogger({
|
||||||
|
prefix: ["orgDomains.ts"],
|
||||||
|
});
|
||||||
/**
|
/**
|
||||||
* return the org slug
|
* return the org slug
|
||||||
* @param hostname
|
* @param hostname
|
||||||
*/
|
*/
|
||||||
export function getOrgSlug(hostname: string) {
|
export function getOrgSlug(hostname: string, forcedSlug?: string) {
|
||||||
|
if (forcedSlug) {
|
||||||
|
if (process.env.NEXT_PUBLIC_IS_E2E) {
|
||||||
|
log.debug("Using provided forcedSlug in E2E", {
|
||||||
|
forcedSlug,
|
||||||
|
});
|
||||||
|
return forcedSlug;
|
||||||
|
}
|
||||||
|
log.debug("Ignoring forcedSlug in non-test mode", {
|
||||||
|
forcedSlug,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!hostname.includes(".")) {
|
if (!hostname.includes(".")) {
|
||||||
// A no-dot domain can never be org domain. It automatically handles localhost
|
log.warn('Org support not enabled for hostname without "."', { hostname });
|
||||||
|
// A no-dot domain can never be org domain. It automatically considers localhost to be non-org domain
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// Find which hostname is being currently used
|
// Find which hostname is being currently used
|
||||||
|
@ -19,24 +36,45 @@ export function getOrgSlug(hostname: string) {
|
||||||
const testHostname = `${url.hostname}${url.port ? `:${url.port}` : ""}`;
|
const testHostname = `${url.hostname}${url.port ? `:${url.port}` : ""}`;
|
||||||
return testHostname.endsWith(`.${ahn}`);
|
return testHostname.endsWith(`.${ahn}`);
|
||||||
});
|
});
|
||||||
logger.debug(`getOrgSlug: ${hostname} ${currentHostname}`, {
|
|
||||||
ALLOWED_HOSTNAMES,
|
if (!currentHostname) {
|
||||||
WEBAPP_URL,
|
log.warn("Match of WEBAPP_URL with ALLOWED_HOSTNAME failed", { WEBAPP_URL, ALLOWED_HOSTNAMES });
|
||||||
currentHostname,
|
return null;
|
||||||
hostname,
|
|
||||||
});
|
|
||||||
if (currentHostname) {
|
|
||||||
// Define which is the current domain/subdomain
|
|
||||||
const slug = hostname.replace(`.${currentHostname}` ?? "", "");
|
|
||||||
return slug.indexOf(".") === -1 ? slug : null;
|
|
||||||
}
|
}
|
||||||
|
// Define which is the current domain/subdomain
|
||||||
|
const slug = hostname.replace(`.${currentHostname}` ?? "", "");
|
||||||
|
const hasNoDotInSlug = slug.indexOf(".") === -1;
|
||||||
|
if (hasNoDotInSlug) {
|
||||||
|
return slug;
|
||||||
|
}
|
||||||
|
log.warn("Derived slug ended up having dots, so not considering it an org domain", { slug });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function orgDomainConfig(hostname: string, fallback?: string | string[]) {
|
export function orgDomainConfig(req: IncomingMessage | undefined, fallback?: string | string[]) {
|
||||||
const currentOrgDomain = getOrgSlug(hostname);
|
const forcedSlugHeader = req?.headers?.["x-cal-force-slug"];
|
||||||
|
|
||||||
|
const forcedSlug = forcedSlugHeader instanceof Array ? forcedSlugHeader[0] : forcedSlugHeader;
|
||||||
|
|
||||||
|
const hostname = req?.headers?.host || "";
|
||||||
|
return getOrgDomainConfigFromHostname({
|
||||||
|
hostname,
|
||||||
|
fallback,
|
||||||
|
forcedSlug,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOrgDomainConfigFromHostname({
|
||||||
|
hostname,
|
||||||
|
fallback,
|
||||||
|
forcedSlug,
|
||||||
|
}: {
|
||||||
|
hostname: string;
|
||||||
|
fallback?: string | string[];
|
||||||
|
forcedSlug?: string;
|
||||||
|
}) {
|
||||||
|
const currentOrgDomain = getOrgSlug(hostname, forcedSlug);
|
||||||
const isValidOrgDomain = currentOrgDomain !== null && !RESERVED_SUBDOMAINS.includes(currentOrgDomain);
|
const isValidOrgDomain = currentOrgDomain !== null && !RESERVED_SUBDOMAINS.includes(currentOrgDomain);
|
||||||
logger.debug(`orgDomainConfig: ${hostname} ${currentOrgDomain} ${isValidOrgDomain}`);
|
|
||||||
if (isValidOrgDomain || !fallback) {
|
if (isValidOrgDomain || !fallback) {
|
||||||
return {
|
return {
|
||||||
currentOrgDomain: isValidOrgDomain ? currentOrgDomain : null,
|
currentOrgDomain: isValidOrgDomain ? currentOrgDomain : null,
|
||||||
|
@ -100,6 +138,6 @@ export function whereClauseForOrgWithSlugOrRequestedSlug(slug: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function userOrgQuery(hostname: string, fallback?: string | string[]) {
|
export function userOrgQuery(hostname: string, fallback?: string | string[]) {
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(hostname, fallback);
|
const { currentOrgDomain, isValidOrgDomain } = getOrgDomainConfigFromHostname({ hostname, fallback });
|
||||||
return isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null;
|
return isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -183,6 +183,7 @@ export default function TeamListItem(props: Props) {
|
||||||
<div className={classNames("flex items-center justify-between", !isInvitee && "hover:bg-muted group")}>
|
<div className={classNames("flex items-center justify-between", !isInvitee && "hover:bg-muted group")}>
|
||||||
{!isInvitee ? (
|
{!isInvitee ? (
|
||||||
<Link
|
<Link
|
||||||
|
data-testid="team-list-item-link"
|
||||||
href={`/settings/teams/${team.id}/profile`}
|
href={`/settings/teams/${team.id}/profile`}
|
||||||
className="flex-grow cursor-pointer truncate text-sm"
|
className="flex-grow cursor-pointer truncate text-sm"
|
||||||
title={`${team.name}`}>
|
title={`${team.name}`}>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { orgDomainConfig, getOrgSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
import { getOrgSlug, getOrgDomainConfigFromHostname } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import * as constants from "@calcom/lib/constants";
|
import * as constants from "@calcom/lib/constants";
|
||||||
|
|
||||||
function setupEnvs({ WEBAPP_URL = "https://app.cal.com" } = {}) {
|
function setupEnvs({ WEBAPP_URL = "https://app.cal.com" } = {}) {
|
||||||
|
@ -35,10 +35,10 @@ function setupEnvs({ WEBAPP_URL = "https://app.cal.com" } = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Org Domains Utils", () => {
|
describe("Org Domains Utils", () => {
|
||||||
describe("orgDomainConfig", () => {
|
describe("getOrgDomainConfigFromHostname", () => {
|
||||||
it("should return a valid org domain", () => {
|
it("should return a valid org domain", () => {
|
||||||
setupEnvs();
|
setupEnvs();
|
||||||
expect(orgDomainConfig("acme.cal.com")).toEqual({
|
expect(getOrgDomainConfigFromHostname({ hostname: "acme.cal.com" })).toEqual({
|
||||||
currentOrgDomain: "acme",
|
currentOrgDomain: "acme",
|
||||||
isValidOrgDomain: true,
|
isValidOrgDomain: true,
|
||||||
});
|
});
|
||||||
|
@ -46,7 +46,7 @@ describe("Org Domains Utils", () => {
|
||||||
|
|
||||||
it("should return a non valid org domain", () => {
|
it("should return a non valid org domain", () => {
|
||||||
setupEnvs();
|
setupEnvs();
|
||||||
expect(orgDomainConfig("app.cal.com")).toEqual({
|
expect(getOrgDomainConfigFromHostname({ hostname: "app.cal.com" })).toEqual({
|
||||||
currentOrgDomain: null,
|
currentOrgDomain: null,
|
||||||
isValidOrgDomain: false,
|
isValidOrgDomain: false,
|
||||||
});
|
});
|
||||||
|
@ -54,7 +54,7 @@ describe("Org Domains Utils", () => {
|
||||||
|
|
||||||
it("should return a non valid org domain for localhost", () => {
|
it("should return a non valid org domain for localhost", () => {
|
||||||
setupEnvs();
|
setupEnvs();
|
||||||
expect(orgDomainConfig("localhost:3000")).toEqual({
|
expect(getOrgDomainConfigFromHostname({ hostname: "localhost:3000" })).toEqual({
|
||||||
currentOrgDomain: null,
|
currentOrgDomain: null,
|
||||||
isValidOrgDomain: false,
|
isValidOrgDomain: false,
|
||||||
});
|
});
|
||||||
|
|
|
@ -266,7 +266,7 @@ export function getRegularOrDynamicEventType(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAvailableSlots({ input, ctx }: GetScheduleOptions) {
|
export async function getAvailableSlots({ input, ctx }: GetScheduleOptions) {
|
||||||
const orgDetails = orgDomainConfig(ctx?.req?.headers.host ?? "");
|
const orgDetails = orgDomainConfig(ctx?.req);
|
||||||
if (process.env.INTEGRATION_TEST_MODE === "true") {
|
if (process.env.INTEGRATION_TEST_MODE === "true") {
|
||||||
logger.settings.minLevel = 2;
|
logger.settings.minLevel = 2;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue