chore: add e2e tests for organization members (#15807)

This commit is contained in:
ケイラ
2024-12-11 15:48:48 -07:00
committed by GitHub
parent 29c9bbf2bb
commit 06e7739e7d
11 changed files with 154 additions and 107 deletions
+4
View File
@@ -8621,6 +8621,10 @@ func (q *FakeQuerier) OrganizationMembers(_ context.Context, arg database.Organi
tmp = append(tmp, database.OrganizationMembersRow{
OrganizationMember: organizationMember,
Username: user.Username,
AvatarURL: user.AvatarURL,
Name: user.Name,
Email: user.Email,
GlobalRoles: user.RBACRoles,
})
}
return tmp, nil
+50
View File
@@ -1,5 +1,6 @@
import { type ChildProcess, exec, spawn } from "node:child_process";
import { randomUUID } from "node:crypto";
import * as fs from "node:fs";
import net from "node:net";
import path from "node:path";
import { Duplex } from "node:stream";
@@ -850,6 +851,7 @@ export const fillParameters = async (
export const updateTemplate = async (
page: Page,
organization: string,
templateName: string,
responses?: EchoProvisionerResponses,
) => {
@@ -868,6 +870,8 @@ export const updateTemplate = async (
"-y",
"-d",
"-",
"-O",
organization,
templateName,
],
{
@@ -880,6 +884,7 @@ export const updateTemplate = async (
);
const uploaded = new Awaiter();
child.on("exit", (code) => {
if (code === 0) {
uploaded.done();
@@ -987,3 +992,48 @@ export async function openTerminalWindow(
return terminal;
}
type UserValues = {
name: string;
username: string;
email: string;
password: string;
};
export async function createUser(
page: Page,
userValues: Partial<UserValues> = {},
): Promise<UserValues> {
const returnTo = page.url();
await page.goto("/deployment/users", { waitUntil: "domcontentloaded" });
await expect(page).toHaveTitle("Users - Coder");
await page.getByRole("button", { name: "Create user" }).click();
await expect(page).toHaveTitle("Create User - Coder");
const username = userValues.username ?? randomName();
const name = userValues.name ?? username;
const email = userValues.email ?? `${username}@coder.com`;
const password = userValues.password || "s3cure&password!";
await page.getByLabel("Username").fill(username);
if (name) {
await page.getByLabel("Full name").fill(name);
}
await page.getByLabel("Email").fill(email);
await page.getByLabel("Login Type").click();
await page.getByRole("option", { name: "Password", exact: false }).click();
// Using input[name=password] due to the select element utilizing 'password'
// as the label for the currently active option.
const passwordField = page.locator("input[name=password]");
await passwordField.fill(password);
await page.getByRole("button", { name: "Create user" }).click();
await expect(page.getByText("Successfully created user.")).toBeVisible();
await expect(page).toHaveTitle("Users - Coder");
await expect(page.locator("tr", { hasText: email })).toBeVisible();
await page.goto(returnTo, { waitUntil: "domcontentloaded" });
return { name, username, email, password };
}
@@ -0,0 +1,50 @@
import { expect, test } from "@playwright/test";
import { setupApiCalls } from "../api";
import { expectUrl } from "../expectUrl";
import { createUser, randomName, requiresLicense } from "../helpers";
import { beforeCoderTest } from "../hooks";
test.beforeEach(async ({ page }) => {
await beforeCoderTest(page);
await setupApiCalls(page);
});
test("add and remove organization member", async ({ page }) => {
requiresLicense();
// Create a new organization to test
await page.goto("/organizations/new", { waitUntil: "domcontentloaded" });
const name = randomName();
await page.getByLabel("Slug").fill(name);
await page.getByLabel("Display name").fill(`Org ${name}`);
await page.getByLabel("Description").fill(`Org description ${name}`);
await page.getByLabel("Icon", { exact: true }).fill("/emojis/1f957.png");
await page.getByRole("button", { name: "Submit" }).click();
// Navigate to members page
await expectUrl(page).toHavePathName(`/organizations/${name}`);
await expect(page.getByText("Organization created.")).toBeVisible();
await page.getByText("Members").click();
// Add a user to the org
const personToAdd = await createUser(page);
await page.getByPlaceholder("User email or username").fill(personToAdd.email);
await page.getByRole("option", { name: personToAdd.email }).click();
await page.getByRole("button", { name: "Add user" }).click();
const addedRow = page.locator("tr", { hasText: personToAdd.email });
await expect(addedRow).toBeVisible();
// Give them a role
await addedRow.getByLabel("Edit user roles").click();
await page.getByText("Organization User Admin").click();
await page.getByText("Organization Template Admin").click();
await page.mouse.click(10, 10); // close the popover by clicking outside of it
await expect(addedRow.getByText("Organization User Admin")).toBeVisible();
await expect(addedRow.getByText("+1 more")).toBeVisible();
// Remove them from the org
await addedRow.getByLabel("More options").click();
await page.getByText("Remove").click(); // Click the "Remove" option
await page.getByRole("button", { name: "Remove" }).click(); // Click "Remove" in the confirmation dialog
await expect(addedRow).not.toBeVisible();
});
+11 -9
View File
@@ -1,7 +1,7 @@
import { expect, test } from "@playwright/test";
import { setupApiCalls } from "../api";
import { expectUrl } from "../expectUrl";
import { requiresLicense } from "../helpers";
import { randomName, requiresLicense } from "../helpers";
import { beforeCoderTest } from "../hooks";
test.beforeEach(async ({ page }) => {
@@ -17,27 +17,29 @@ test("create and delete organization", async ({ page }) => {
waitUntil: "domcontentloaded",
});
await page.getByLabel("Slug").fill("floop");
await page.getByLabel("Display name").fill("Floop");
await page.getByLabel("Description").fill("Org description floop");
const name = randomName();
await page.getByLabel("Slug").fill(name);
await page.getByLabel("Display name").fill(`Org ${name}`);
await page.getByLabel("Description").fill(`Org description ${name}`);
await page.getByLabel("Icon", { exact: true }).fill("/emojis/1f957.png");
await page.getByRole("button", { name: "Submit" }).click();
// Expect to be redirected to the new organization
await expectUrl(page).toHavePathName("/organizations/floop");
await expectUrl(page).toHavePathName(`/organizations/${name}`);
await expect(page.getByText("Organization created.")).toBeVisible();
await page.getByLabel("Slug").fill("wibble");
await page.getByLabel("Description").fill("Org description wibble");
const newName = randomName();
await page.getByLabel("Slug").fill(newName);
await page.getByLabel("Description").fill(`Org description ${newName}`);
await page.getByRole("button", { name: "Submit" }).click();
// Expect to be redirected when renaming the organization
await expectUrl(page).toHavePathName("/organizations/wibble");
await expectUrl(page).toHavePathName(`/organizations/${newName}`);
await expect(page.getByText("Organization settings updated.")).toBeVisible();
await page.getByRole("button", { name: "Delete this organization" }).click();
const dialog = page.getByTestId("dialog");
await dialog.getByLabel("Name").fill("wibble");
await dialog.getByLabel("Name").fill(newName);
await dialog.getByRole("button", { name: "Delete" }).click();
await expect(page.getByText("Organization deleted.")).toBeVisible();
});
@@ -1,71 +1,13 @@
import { expect, test } from "@playwright/test";
import { randomName } from "../../helpers";
import { test } from "@playwright/test";
import { createUser } from "../../helpers";
import { beforeCoderTest } from "../../hooks";
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
test("create user with password", async ({ page, baseURL }) => {
await page.goto(`${baseURL}/deployment/users`, {
waitUntil: "domcontentloaded",
});
await expect(page).toHaveTitle("Users - Coder");
await page.getByRole("button", { name: "Create user" }).click();
await expect(page).toHaveTitle("Create User - Coder");
const name = randomName();
const userValues = {
username: name,
name: name,
email: `${name}@coder.com`,
loginType: "password",
password: "s3cure&password!",
};
await page.getByLabel("Username").fill(userValues.username);
await page.getByLabel("Full name").fill(userValues.username);
await page.getByLabel("Email").fill(userValues.email);
await page.getByLabel("Login Type").click();
await page.getByRole("option", { name: "Password", exact: false }).click();
// Using input[name=password] due to the select element utilizing 'password'
// as the label for the currently active option.
const passwordField = page.locator("input[name=password]");
await passwordField.fill(userValues.password);
await page.getByRole("button", { name: "Create user" }).click();
await expect(page.getByText("Successfully created user.")).toBeVisible();
await expect(page).toHaveTitle("Users - Coder");
await expect(page.locator("tr", { hasText: userValues.email })).toBeVisible();
test("create user with password", async ({ page }) => {
await createUser(page);
});
test("create user without full name is optional", async ({ page, baseURL }) => {
await page.goto(`${baseURL}/deployment/users`, {
waitUntil: "domcontentloaded",
});
await expect(page).toHaveTitle("Users - Coder");
await page.getByRole("button", { name: "Create user" }).click();
await expect(page).toHaveTitle("Create User - Coder");
const name = randomName();
const userValues = {
username: name,
email: `${name}@coder.com`,
loginType: "password",
password: "s3cure&password!",
};
await page.getByLabel("Username").fill(userValues.username);
await page.getByLabel("Email").fill(userValues.email);
await page.getByLabel("Login Type").click();
await page.getByRole("option", { name: "Password", exact: false }).click();
// Using input[name=password] due to the select element utilizing 'password'
// as the label for the currently active option.
const passwordField = page.locator("input[name=password]");
await passwordField.fill(userValues.password);
await page.getByRole("button", { name: "Create user" }).click();
await expect(page.getByText("Successfully created user.")).toBeVisible();
await expect(page).toHaveTitle("Users - Coder");
await expect(page.locator("tr", { hasText: userValues.email })).toBeVisible();
test("create user without full name", async ({ page }) => {
await createUser(page, { name: "" });
});
@@ -41,6 +41,7 @@ test("update workspace, new optional, immutable parameter added", async ({
const updatedRichParameters = [...richParameters, fifthParameter];
await updateTemplate(
page,
"coder",
template,
echoResponsesWithParameters(updatedRichParameters),
);
@@ -79,6 +80,7 @@ test("update workspace, new required, mutable parameter added", async ({
const updatedRichParameters = [...richParameters, sixthParameter];
await updateTemplate(
page,
"coder",
template,
echoResponsesWithParameters(updatedRichParameters),
);
@@ -77,7 +77,6 @@ const OrganizationMembersPage: FC = () => {
isUpdatingMemberRoles={updateMemberRolesMutation.isLoading}
me={me}
members={members}
groupsByUserId={groupsByUserIdQuery.data}
addMember={async (user: User) => {
await addMemberMutation.mutateAsync(user.id);
void membersQuery.refetch();
@@ -42,7 +42,6 @@ interface OrganizationMembersPageViewProps {
isUpdatingMemberRoles: boolean;
me: User;
members: Array<OrganizationMemberTableEntry> | undefined;
groupsByUserId: GroupsByUserId | undefined;
addMember: (user: User) => Promise<void>;
removeMember: (member: OrganizationMemberWithUserData) => void;
updateMemberRoles: (
@@ -57,17 +56,28 @@ interface OrganizationMemberTableEntry extends OrganizationMemberWithUserData {
export const OrganizationMembersPageView: FC<
OrganizationMembersPageViewProps
> = (props) => {
> = ({
allAvailableRoles,
canEditMembers,
error,
isAddingMember,
isUpdatingMemberRoles,
me,
members,
addMember,
removeMember,
updateMemberRoles,
}) => {
return (
<div>
<SettingsHeader title="Members" />
<Stack>
{Boolean(props.error) && <ErrorAlert error={props.error} />}
{Boolean(error) && <ErrorAlert error={error} />}
{props.canEditMembers && (
{canEditMembers && (
<AddOrganizationMember
isLoading={props.isAddingMember}
onSubmit={props.addMember}
isLoading={isAddingMember}
onSubmit={addMember}
/>
)}
@@ -92,7 +102,7 @@ export const OrganizationMembersPageView: FC<
</TableRow>
</TableHead>
<TableBody>
{props.members?.map((member) => (
{members?.map((member) => (
<TableRow key={member.user_id}>
<TableCell>
<AvatarData
@@ -109,13 +119,13 @@ export const OrganizationMembersPageView: FC<
<UserRoleCell
inheritedRoles={member.global_roles}
roles={member.roles}
allAvailableRoles={props.allAvailableRoles}
allAvailableRoles={allAvailableRoles}
oidcRoleSyncEnabled={false}
isLoading={props.isUpdatingMemberRoles}
canEditUsers={props.canEditMembers}
isLoading={isUpdatingMemberRoles}
canEditUsers={canEditMembers}
onEditRoles={async (roles) => {
try {
await props.updateMemberRoles(member, roles);
await updateMemberRoles(member, roles);
displaySuccess("Roles updated successfully.");
} catch (error) {
displayError(
@@ -126,7 +136,7 @@ export const OrganizationMembersPageView: FC<
/>
<UserGroupsCell userGroups={member.groups} />
<TableCell>
{member.user_id !== props.me.id && props.canEditMembers && (
{member.user_id !== me.id && canEditMembers && (
<MoreMenu>
<MoreMenuTrigger>
<ThreeDotsButton />
@@ -134,7 +144,7 @@ export const OrganizationMembersPageView: FC<
<MoreMenuContent>
<MoreMenuItem
danger
onClick={() => props.removeMember(member)}
onClick={() => removeMember(member)}
>
Remove
</MoreMenuItem>
@@ -128,7 +128,7 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
</Tooltip>
</PopoverTrigger>
<PopoverContent classes={{ paper }}>
<PopoverContent classes={{ paper }} disablePortal={false}>
<fieldset
css={styles.fieldset}
disabled={isLoading}
@@ -136,14 +136,8 @@ const OverflowRolePill: FC<OverflowRolePillProps> = ({ roles }) => {
minWidth: "auto",
},
}}
anchorOrigin={{
vertical: -4,
horizontal: "center",
}}
transformOrigin={{
vertical: "bottom",
horizontal: "center",
}}
anchorOrigin={{ vertical: -4, horizontal: "center" }}
transformOrigin={{ vertical: "bottom", horizontal: "center" }}
>
{roles.map((role) => (
<Pill
+5 -11
View File
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { spyOn, userEvent, within } from "@storybook/test";
import { screen, spyOn, userEvent, within } from "@storybook/test";
import { API } from "api/api";
import { deploymentConfigQueryKey } from "api/queries/deployment";
import { groupsQueryKey } from "api/queries/groups";
@@ -368,12 +368,8 @@ export const UpdateUserRoleSuccess: Story = {
});
await user.click(within(userRow).getByLabelText("Edit user roles"));
await user.click(
within(userRow).getByLabelText("Auditor", { exact: false }),
);
await within(document.body).findByText(
"Successfully updated the user roles.",
);
await user.click(screen.getByLabelText("Auditor", { exact: false }));
await screen.findByText("Successfully updated the user roles.");
},
};
@@ -388,10 +384,8 @@ export const UpdateUserRoleError: Story = {
spyOn(API, "updateUserRoles").mockRejectedValue({});
await user.click(within(userRow).getByLabelText("Edit user roles"));
await user.click(
within(userRow).getByLabelText("Auditor", { exact: false }),
);
await within(document.body).findByText("Error on updating the user roles.");
await user.click(screen.getByLabelText("Auditor", { exact: false }));
await screen.findByText("Error on updating the user roles.");
},
};