mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: add e2e tests for organization members (#15807)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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.");
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user