diff --git a/site/e2e/api.ts b/site/e2e/api.ts index 0494467799a97..2b4e54429c017 100644 --- a/site/e2e/api.ts +++ b/site/e2e/api.ts @@ -92,6 +92,43 @@ export const createOrganizationSyncSettings = async () => { return settings; }; +export const createCustomRole = async ( + orgId: string, + name: string, + displayName: string, +) => { + const role = await API.createOrganizationRole(orgId, { + name, + display_name: displayName, + organization_id: orgId, + site_permissions: [], + organization_permissions: [ + { + negate: false, + resource_type: "organization_member", + action: "create", + }, + { + negate: false, + resource_type: "organization_member", + action: "delete", + }, + { + negate: false, + resource_type: "organization_member", + action: "read", + }, + { + negate: false, + resource_type: "organization_member", + action: "update", + }, + ], + user_permissions: [], + }); + return role; +}; + export async function verifyConfigFlagBoolean( page: Page, config: DeploymentConfig, diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 8f69b90900538..1300f73fb643b 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -48,6 +48,10 @@ export function requiresLicense() { test.skip(!license); } +export function requiresUnlicensed() { + test.skip(license.length > 0); +} + /** * requireTerraformProvisioner by default is enabled. */ diff --git a/site/e2e/tests/deployment/idpOrgSync.spec.ts b/site/e2e/tests/deployment/idpOrgSync.spec.ts index 59fccb440400b..05ae54828fffc 100644 --- a/site/e2e/tests/deployment/idpOrgSync.spec.ts +++ b/site/e2e/tests/deployment/idpOrgSync.spec.ts @@ -5,7 +5,7 @@ import { deleteOrganization, setupApiCalls, } from "../../api"; -import { requiresLicense } from "../../helpers"; +import { randomName, requiresLicense } from "../../helpers"; import { beforeCoderTest } from "../../hooks"; test.describe("IdpOrgSyncPage", () => { @@ -117,7 +117,9 @@ test.describe("IdpOrgSyncPage", () => { requiresLicense(); await setupApiCalls(page); - await createOrganizationWithName("developers"); + const orgName = randomName(); + + await createOrganizationWithName(orgName); await page.goto("/deployment/idp-org-sync", { waitUntil: "domcontentloaded", @@ -135,7 +137,7 @@ test.describe("IdpOrgSyncPage", () => { // Select Coder organization from combobox await orgSelector.click(); - await page.getByRole("option", { name: "developers" }).click(); + await page.getByRole("option", { name: orgName }).click(); // Add button should now be enabled await expect(addButton).toBeEnabled(); @@ -146,12 +148,12 @@ test.describe("IdpOrgSyncPage", () => { const newRow = page.getByTestId("idp-org-new-idp-org"); await expect(newRow).toBeVisible(); await expect(newRow.getByText("new-idp-org")).toBeVisible(); - await expect(newRow.getByText("developers")).toBeVisible(); + await expect(newRow.getByText(orgName)).toBeVisible(); await expect( page.getByText("Organization sync settings updated."), ).toBeVisible(); - await deleteOrganization("developers"); + await deleteOrganization(orgName); }); }); diff --git a/site/e2e/tests/organizations/customRoles/customRoles.spec.ts b/site/e2e/tests/organizations/customRoles/customRoles.spec.ts new file mode 100644 index 0000000000000..99f8801de8141 --- /dev/null +++ b/site/e2e/tests/organizations/customRoles/customRoles.spec.ts @@ -0,0 +1,205 @@ +import { expect, test } from "@playwright/test"; +import { + createCustomRole, + createOrganizationWithName, + deleteOrganization, + setupApiCalls, +} from "../../../api"; +import { + randomName, + requiresLicense, + requiresUnlicensed, +} from "../../../helpers"; +import { beforeCoderTest } from "../../../hooks"; + +test.describe("CustomRolesPage", () => { + test.beforeEach(async ({ page }) => await beforeCoderTest(page)); + + test("create custom role and cancel edit changes", async ({ page }) => { + requiresLicense(); + await setupApiCalls(page); + + const org = await createOrganizationWithName(randomName()); + + const customRole = await createCustomRole( + org.id, + "custom-role-test-1", + "Custom Role Test 1", + ); + + await page.goto(`/organizations/${org.name}/roles`); + const roleRow = page.getByTestId(`role-${customRole.name}`); + await expect(roleRow.getByText(customRole.display_name)).toBeVisible(); + await expect(roleRow.getByText("organization_member")).toBeVisible(); + + await roleRow.getByRole("button", { name: "More options" }).click(); + const menu = page.locator("#more-options"); + await menu.getByText("Edit").click(); + + await expect(page).toHaveURL( + `/organizations/${org.name}/roles/${customRole.name}`, + ); + + const cancelButton = page.getByRole("button", { name: "Cancel" }).first(); + await expect(cancelButton).toBeVisible(); + await cancelButton.click(); + + await expect(page).toHaveURL(`/organizations/${org.name}/roles`); + + await deleteOrganization(org.name); + }); + + test("create custom role, edit role and save changes", async ({ page }) => { + requiresLicense(); + await setupApiCalls(page); + + const org = await createOrganizationWithName(randomName()); + + const customRole = await createCustomRole( + org.id, + "custom-role-test-1", + "Custom Role Test 1", + ); + + await page.goto(`/organizations/${org.name}/roles`); + const roleRow = page.getByTestId(`role-${customRole.name}`); + await expect(roleRow.getByText(customRole.display_name)).toBeVisible(); + await expect(roleRow.getByText("organization_member")).toBeVisible(); + + await page.goto(`/organizations/${org.name}/roles/${customRole.name}`); + + const displayNameInput = page.getByRole("textbox", { + name: "Display name", + }); + await displayNameInput.fill("Custom Role Test 2 Edited"); + + const groupCheckbox = page.getByTestId("group").getByRole("checkbox"); + await expect(groupCheckbox).toBeVisible(); + await groupCheckbox.click(); + + const organizationMemberCheckbox = page + .getByTestId("organization_member") + .getByRole("checkbox"); + await expect(organizationMemberCheckbox).toBeVisible(); + await organizationMemberCheckbox.click(); + + const saveButton = page.getByRole("button", { name: "Save" }).first(); + await expect(saveButton).toBeVisible(); + await saveButton.click(); + + await expect(roleRow.getByText("Custom Role Test 2 Edited")).toBeVisible(); + + const roleRow2 = page.getByTestId(`role-${customRole.name}`); + await expect(roleRow2.getByText("organization_member")).not.toBeVisible(); + await expect(roleRow2.getByText("group")).toBeVisible(); + + await expect(page).toHaveURL(`/organizations/${org.name}/roles`); + + await deleteOrganization(org.name); + }); + + test("displays built-in role without edit/delete options", async ({ + page, + }) => { + requiresLicense(); + await setupApiCalls(page); + + const org = await createOrganizationWithName(randomName()); + + await page.goto(`/organizations/${org.name}/roles`); + + const roleRow = page.getByTestId("role-organization-admin"); + await expect(roleRow).toBeVisible(); + + await expect(roleRow.getByText("Organization Admin")).toBeVisible(); + + // Verify that the more menu (three dots) is not present for built-in roles + await expect( + roleRow.getByRole("button", { name: "More options" }), + ).not.toBeVisible(); + + await deleteOrganization(org.name); + }); + + test("create custom role with UI", async ({ page }) => { + requiresLicense(); + await setupApiCalls(page); + + const org = await createOrganizationWithName(randomName()); + + await page.goto(`/organizations/${org.name}/roles`); + + await page + .getByRole("link", { name: "Create custom role" }) + .first() + .click(); + + await expect(page).toHaveURL(`/organizations/${org.name}/roles/create`); + + const customRoleName = "custom-role-test"; + const roleNameInput = page.getByRole("textbox", { + exact: true, + name: "Name", + }); + await roleNameInput.fill(customRoleName); + + const customRoleDisplayName = "Custom Role Test"; + const displayNameInput = page.getByRole("textbox", { + exact: true, + name: "Display Name", + }); + await displayNameInput.fill(customRoleDisplayName); + + await page.getByRole("button", { name: "Create Role" }).first().click(); + + await expect(page).toHaveURL(`/organizations/${org.name}/roles`); + + const roleRow = page.getByTestId(`role-${customRoleName}`); + await expect(roleRow.getByText(customRoleDisplayName)).toBeVisible(); + await expect(roleRow.getByText("None")).toBeVisible(); + + await deleteOrganization(org.name); + }); + + test("delete custom role", async ({ page }) => { + requiresLicense(); + await setupApiCalls(page); + + const org = await createOrganizationWithName(randomName()); + const customRole = await createCustomRole( + org.id, + "custom-role-test-1", + "Custom Role Test 1", + ); + await page.goto(`/organizations/${org.name}/roles`); + + const roleRow = page.getByTestId(`role-${customRole.name}`); + await roleRow.getByRole("button", { name: "More options" }).click(); + + const menu = page.locator("#more-options"); + await menu.getByText("Delete…").click(); + + const input = page.getByRole("textbox"); + await input.fill(customRole.name); + await page.getByRole("button", { name: "Delete" }).click(); + + await expect( + page.getByText("Custom role deleted successfully!"), + ).toBeVisible(); + + await deleteOrganization(org.name); + }); +}); + +test("custom roles disabled", async ({ page }) => { + requiresUnlicensed(); + await page.goto("/organizations/coder/roles"); + await expect(page).toHaveURL("/organizations/coder/roles"); + + await expect( + page.getByText("Upgrade to a premium license to create a custom role"), + ).toBeVisible(); + await expect( + page.getByRole("link", { name: "Create custom role" }), + ).not.toBeVisible(); +});