Skip to content

chore: add e2e tests for organization members #15807

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions coderd/database/dbmem/dbmem.go
Original file line number Diff line number Diff line change
Expand Up @@ -8556,6 +8556,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
Expand Down
50 changes: 50 additions & 0 deletions site/e2e/helpers.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -850,6 +851,7 @@ export const fillParameters = async (

export const updateTemplate = async (
page: Page,
organization: string,
templateName: string,
responses?: EchoProvisionerResponses,
) => {
Expand All @@ -868,6 +870,8 @@ export const updateTemplate = async (
"-y",
"-d",
"-",
"-O",
organization,
templateName,
],
{
Expand All @@ -880,6 +884,7 @@ export const updateTemplate = async (
);

const uploaded = new Awaiter();

child.on("exit", (code) => {
if (code === 0) {
uploaded.done();
Expand Down Expand Up @@ -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();
Comment on lines +1009 to +1035
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all taken verbatim from the existing createUserWithPassword test. It just seems like the kind of thing that's helpful to turn into a reusable function.


await page.goto(returnTo, { waitUntil: "domcontentloaded" });
return { name, username, email, password };
}
50 changes: 50 additions & 0 deletions site/e2e/tests/organizationMembers.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
20 changes: 11 additions & 9 deletions site/e2e/tests/organizations.spec.ts
Original file line number Diff line number Diff line change
@@ -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 }) => {
Expand All @@ -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();
});
70 changes: 6 additions & 64 deletions site/e2e/tests/users/createUserWithPassword.spec.ts
Original file line number Diff line number Diff line change
@@ -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: "" });
});
2 changes: 2 additions & 0 deletions site/e2e/tests/workspaces/updateWorkspace.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ test("update workspace, new optional, immutable parameter added", async ({
const updatedRichParameters = [...richParameters, fifthParameter];
await updateTemplate(
page,
"coder",
template,
echoResponsesWithParameters(updatedRichParameters),
);
Expand Down Expand Up @@ -79,6 +80,7 @@ test("update workspace, new required, mutable parameter added", async ({
const updatedRichParameters = [...richParameters, sixthParameter];
await updateTemplate(
page,
"coder",
template,
echoResponsesWithParameters(updatedRichParameters),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: (
Expand All @@ -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}
/>
)}

Expand All @@ -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
Expand All @@ -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(
Expand All @@ -126,15 +136,15 @@ 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 />
</MoreMenuTrigger>
<MoreMenuContent>
<MoreMenuItem
danger
onClick={() => props.removeMember(member)}
onClick={() => removeMember(member)}
>
Remove
</MoreMenuItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Loading
Loading