fix: Prevent invalid host assignment (#9441)
* Avoids invalid hosts * Fixed test selector, instead using react-select-2-listbox * Different way of selecting the managed user * Convert select-option- to value instead of label * Replaced all instances of networkidle * done rewriting test * Fix prisma is not defined error * Depend on inner text rather than testid * Fill of team create is unstable * Removed console.log --------- Co-authored-by: Efraín Rochín <roae.85@gmail.com>
This commit is contained in:
co-authored by
Efraín Rochín
parent
d9a7952467
commit
bb31c4ba14
@@ -12,33 +12,28 @@ test.describe("Managed Event Types tests", () => {
|
||||
const memberUser = await users.create();
|
||||
// First we work with owner user, logging in
|
||||
await adminUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
// Making sure page loads completely
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Let's create a team
|
||||
await page.goto("/teams");
|
||||
await page.goto("/settings/teams/new");
|
||||
|
||||
await test.step("Managed event option exists for team admin", async () => {
|
||||
// Proceed to create a team
|
||||
await page.locator("text=Create Team").click();
|
||||
await page.waitForURL("/settings/teams/new");
|
||||
|
||||
// Filling team creation form wizard
|
||||
await page.locator('input[name="name"]').waitFor();
|
||||
await page.locator('input[name="name"]').fill(`${adminUser.username}'s Team`);
|
||||
await page.locator("text=Continue").click();
|
||||
await page.waitForURL(/\/settings\/teams\/(\d+)\/onboard-members$/i);
|
||||
await page.locator('[data-testid="new-member-button"]').click();
|
||||
await page.getByTestId("new-member-button").click();
|
||||
await page.locator('[placeholder="email\\@example\\.com"]').fill(`${memberUser.username}@example.com`);
|
||||
await page.locator('[data-testid="invite-new-member-button"]').click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.getByTestId("invite-new-member-button").click();
|
||||
// wait for the second member to be added to the pending-member-list.
|
||||
await page.getByTestId("pending-member-list").locator("li:nth-child(2)").waitFor();
|
||||
// and publish
|
||||
await page.locator("text=Publish team").click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.waitForURL("/settings/teams/**");
|
||||
// Going to create an event type
|
||||
await page.goto("/event-types");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.click("[data-testid=new-event-type-dropdown]");
|
||||
await page.click("[data-testid=option-team-1]");
|
||||
await page.getByTestId("new-event-type-dropdown").click();
|
||||
await page.getByTestId("option-team-1").click();
|
||||
// Expecting we can add a managed event type as team owner
|
||||
await expect(page.locator('button[value="MANAGED"]')).toBeVisible();
|
||||
|
||||
@@ -46,51 +41,50 @@ test.describe("Managed Event Types tests", () => {
|
||||
await page.click('button[value="MANAGED"]');
|
||||
await page.fill("[name=title]", "managed");
|
||||
await page.click("[type=submit]");
|
||||
|
||||
await page.waitForURL("event-types/**");
|
||||
});
|
||||
|
||||
await test.step("Managed event type has unlocked fields for admin", async () => {
|
||||
await page.waitForSelector('[data-testid="update-eventtype"]');
|
||||
await page.getByTestId("update-eventtype").waitFor();
|
||||
await expect(page.locator('input[name="title"]')).toBeEditable();
|
||||
await expect(page.locator('input[name="slug"]')).toBeEditable();
|
||||
await expect(page.locator('input[name="length"]')).toBeEditable();
|
||||
await adminUser.logout();
|
||||
});
|
||||
|
||||
await test.step("Managed event type exists for added member", async () => {
|
||||
// Now we need to accept the invitation as member and come back in as admin to
|
||||
// assign the member in the managed event type
|
||||
await adminUser.logout();
|
||||
await memberUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
// We wait until loading is finished
|
||||
await page.waitForSelector('[data-testid="event-types"]');
|
||||
|
||||
await page.goto("/teams");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.locator('button[data-testid^="accept-invitation"]').click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.getByText("Member").waitFor();
|
||||
|
||||
await memberUser.logout();
|
||||
|
||||
// Coming back as team owner to assign member user to managed event
|
||||
await adminUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.locator('[data-testid="event-types"] a[title="managed"]').click();
|
||||
await page.locator('[data-testid="vertical-tab-assignment"]').click();
|
||||
await page.getByTestId("event-types").locator('a[title="managed"]').click();
|
||||
await page.getByTestId("vertical-tab-assignment").click();
|
||||
await page.locator('[class$="control"]').filter({ hasText: "Select..." }).click();
|
||||
await page.locator("#react-select-5-option-1").click();
|
||||
await page.getByTestId(`select-option-${memberUser.id}`).click();
|
||||
await page.locator('[type="submit"]').click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await adminUser.logout();
|
||||
await page.getByTestId("toast-success").waitFor();
|
||||
|
||||
// Coming back as member user to see if there is a managed event present after assignment
|
||||
await memberUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(page.locator('[data-testid="event-types"] a[title="managed"]')).toBeVisible();
|
||||
await adminUser.logout();
|
||||
});
|
||||
|
||||
await test.step("Managed event type has locked fields for added member", async () => {
|
||||
page.locator('[data-testid="event-types"] a[title="managed"]').click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
// Coming back as member user to see if there is a managed event present after assignment
|
||||
await memberUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
|
||||
await page.getByTestId("event-types").locator('a[title="managed"]').click();
|
||||
await page.waitForURL("event-types/**");
|
||||
|
||||
await expect(page.locator('input[name="title"]')).not.toBeEditable();
|
||||
await expect(page.locator('input[name="slug"]')).not.toBeEditable();
|
||||
await expect(page.locator('input[name="length"]')).not.toBeEditable();
|
||||
|
||||
@@ -463,7 +463,7 @@ async function addAllTypesOfFieldsAndSaveForm(
|
||||
// Click on the field type dropdown.
|
||||
await page.locator(".data-testid-field-type").nth(nth).click();
|
||||
// Click on the dropdown option.
|
||||
await page.locator(`[data-testid="select-option-${fieldTypeLabel}"]`).click();
|
||||
await page.locator(`[data-testid^="select-option-"]`).filter({ hasText: fieldTypeLabel }).click();
|
||||
} else {
|
||||
// Set the identifier manually for the first field to test out a case when identifier isn't computed from label automatically
|
||||
// First field type is by default selected. So, no need to choose from dropdown
|
||||
|
||||
@@ -45,13 +45,39 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
// Extract this from the input so it doesn't get saved in the db
|
||||
// eslint-disable-next-line
|
||||
userId,
|
||||
// eslint-disable-next-line
|
||||
teamId,
|
||||
bookingFields,
|
||||
offsetStart,
|
||||
...rest
|
||||
} = input;
|
||||
|
||||
const eventType = await ctx.prisma.eventType.findUniqueOrThrow({
|
||||
where: { id },
|
||||
select: {
|
||||
children: {
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
workflows: {
|
||||
select: {
|
||||
workflowId: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
name: true,
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (input.teamId && eventType.team?.id && input.teamId !== eventType.team.id) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
const teamId = input.teamId || eventType.team?.id;
|
||||
|
||||
ensureUniqueBookingFields(bookingFields);
|
||||
|
||||
const data: Prisma.EventTypeUpdateInput = {
|
||||
@@ -149,7 +175,23 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
};
|
||||
}
|
||||
|
||||
if (hosts) {
|
||||
if (teamId && hosts) {
|
||||
// check if all hosts can be assigned (memberships that have accepted invite)
|
||||
const memberships =
|
||||
(await ctx.prisma.membership.findMany({
|
||||
where: {
|
||||
teamId,
|
||||
accepted: true,
|
||||
},
|
||||
})) || [];
|
||||
const teamMemberIds = memberships.map((membership) => membership.userId);
|
||||
// guard against missing IDs, this may mean a member has just been removed
|
||||
// or this request was forged.
|
||||
if (!hosts.every((host) => teamMemberIds.includes(host.userId))) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
});
|
||||
}
|
||||
data.hosts = {
|
||||
deleteMany: {},
|
||||
create: hosts.map((host) => ({
|
||||
@@ -256,48 +298,26 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
const [oldEventType, eventType] = await ctx.prisma.$transaction([
|
||||
ctx.prisma.eventType.findFirst({
|
||||
where: { id },
|
||||
select: {
|
||||
children: {
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
workflows: {
|
||||
select: {
|
||||
workflowId: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
ctx.prisma.eventType.update({
|
||||
where: { id },
|
||||
data,
|
||||
}),
|
||||
]);
|
||||
const updatedEventType = await ctx.prisma.eventType.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
|
||||
// Handling updates to children event types (managed events types)
|
||||
await updateChildrenEventTypes({
|
||||
eventTypeId: id,
|
||||
currentUserId: ctx.user.id,
|
||||
oldEventType,
|
||||
oldEventType: eventType,
|
||||
hashedLink,
|
||||
connectedLink,
|
||||
updatedEventType: eventType,
|
||||
updatedEventType,
|
||||
children,
|
||||
prisma: ctx.prisma,
|
||||
});
|
||||
const res = ctx.res as NextApiResponse;
|
||||
if (typeof res?.revalidate !== "undefined") {
|
||||
try {
|
||||
await res?.revalidate(`/${ctx.user.username}/${eventType.slug}`);
|
||||
await res?.revalidate(`/${ctx.user.username}/${updatedEventType.slug}`);
|
||||
} catch (e) {
|
||||
// if reach this it is because the event type page has not been created, so it is not possible to revalidate it
|
||||
logger.debug((e as Error)?.message);
|
||||
|
||||
@@ -43,8 +43,8 @@ export const OptionComponent = <
|
||||
// This gets styled in the select classNames prop now - handles overrides with styles vs className here doesnt
|
||||
<reactSelectComponents.Option {...props}>
|
||||
<div className="flex">
|
||||
<span className="mr-auto" data-testid={`select-option-${props.label}`}>
|
||||
{props.label}
|
||||
<span className="mr-auto" data-testid={`select-option-${(props as unknown as ExtendedOption).value}`}>
|
||||
{props.label || <> </>}
|
||||
</span>
|
||||
{(props.data as unknown as ExtendedOption).needsUpgrade && <UpgradeTeamsBadge />}
|
||||
{props.isSelected && <Check className="ml-2 h-4 w-4" />}
|
||||
|
||||
Reference in New Issue
Block a user