From b6c8344fb5e4ad83246b740c8dd5914290b6fb51 Mon Sep 17 00:00:00 2001 From: Jon Koops Date: Thu, 13 Apr 2023 15:55:50 +0200 Subject: [PATCH] Ensure user details form state remains in sync (#19696) --- .../admin-ui/cypress/e2e/events_test.spec.ts | 2 +- .../admin-ui/cypress/e2e/group_test.spec.ts | 2 +- .../cypress/e2e/users_enable_disable.spec.ts | 80 +++++++++++++++++++ .../users/user_details/UserDetailsPage.ts | 26 ++++++ .../cypress/support/util/AdminClient.ts | 12 ++- js/apps/admin-ui/src/user/EditUser.tsx | 8 +- 6 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 js/apps/admin-ui/cypress/e2e/users_enable_disable.spec.ts diff --git a/js/apps/admin-ui/cypress/e2e/events_test.spec.ts b/js/apps/admin-ui/cypress/e2e/events_test.spec.ts index 798bbd4e70..5ac40241c5 100644 --- a/js/apps/admin-ui/cypress/e2e/events_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/events_test.spec.ts @@ -44,7 +44,7 @@ describe.skip("Events tests", () => { const result = await adminClient.createUser( eventsTestUser.userRepresentation ); - eventsTestUser.eventsTestUserId = result.id; + eventsTestUser.eventsTestUserId = result.id!; }); after(() => diff --git a/js/apps/admin-ui/cypress/e2e/group_test.spec.ts b/js/apps/admin-ui/cypress/e2e/group_test.spec.ts index 2fcc5014b1..cdd516dd0c 100644 --- a/js/apps/admin-ui/cypress/e2e/group_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/group_test.spec.ts @@ -42,7 +42,7 @@ describe("Group test", () => { enabled: true, }) .then((user) => { - return { id: user.id, username: username + index }; + return { id: user.id!, username: username + index }; }); return user; }) diff --git a/js/apps/admin-ui/cypress/e2e/users_enable_disable.spec.ts b/js/apps/admin-ui/cypress/e2e/users_enable_disable.spec.ts new file mode 100644 index 0000000000..4d9d552e3b --- /dev/null +++ b/js/apps/admin-ui/cypress/e2e/users_enable_disable.spec.ts @@ -0,0 +1,80 @@ +import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation"; +import LoginPage from "../support/pages/LoginPage"; +import Masthead from "../support/pages/admin-ui/Masthead"; +import SidebarPage from "../support/pages/admin-ui/SidebarPage"; +import UsersPage from "../support/pages/admin-ui/manage/users/UsersPage"; +import UserDetailsPage from "../support/pages/admin-ui/manage/users/user_details/UserDetailsPage"; +import adminClient from "../support/util/AdminClient"; +import { keycloakBefore } from "../support/util/keycloak_hooks"; + +const loginPage = new LoginPage(); +const sidebarPage = new SidebarPage(); +const usersPage = new UsersPage(); +const userDetailsPage = new UserDetailsPage(); +const masthead = new Masthead(); + +const createUser = (fields: UserRepresentation) => + cy + .wrap(null) + .then(() => + adminClient.createUser({ username: crypto.randomUUID(), ...fields }) + ); + +const deleteUser = (username: string) => + cy.wrap(null).then(() => adminClient.deleteUser(username)); + +describe("User enable/disable", () => { + beforeEach(() => { + loginPage.logIn(); + keycloakBefore(); + sidebarPage.goToUsers(); + }); + + it("disables a user", () => { + createUser({ enabled: true }).then(({ username }) => { + usersPage.goToUserDetailsPage(username!); + userDetailsPage.assertEnabled(username!); + + userDetailsPage.toggleEnabled(username!); + masthead.checkNotificationMessage("The user has been saved"); + cy.wait(1000); + userDetailsPage.assertDisabled(username!); + + return deleteUser(username!); + }); + }); + + it("enables a user", () => { + createUser({ enabled: false }).then(({ username }) => { + usersPage.goToUserDetailsPage(username!); + userDetailsPage.assertDisabled(username!); + + userDetailsPage.toggleEnabled(username!); + masthead.checkNotificationMessage("The user has been saved"); + cy.wait(1000); + userDetailsPage.assertEnabled(username!); + + return deleteUser(username!); + }); + }); + + // See: https://github.com/keycloak/keycloak/issues/19647 + it("ensures submitting doesn't reset the enabled state", () => { + createUser({ enabled: true }).then(({ username }) => { + usersPage.goToUserDetailsPage(username!); + userDetailsPage.assertEnabled(username!); + + userDetailsPage.toggleEnabled(username!); + masthead.checkNotificationMessage("The user has been saved"); + cy.wait(1000); + userDetailsPage.assertDisabled(username!); + + userDetailsPage.save(); + masthead.checkNotificationMessage("The user has been saved"); + cy.wait(1000); + userDetailsPage.assertDisabled(username!); + + return deleteUser(username!); + }); + }); +}); diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/user_details/UserDetailsPage.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/user_details/UserDetailsPage.ts index 929ac5eb14..6d02ffb7fc 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/user_details/UserDetailsPage.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/user_details/UserDetailsPage.ts @@ -69,4 +69,30 @@ export default class UserDetailsPage extends PageObject { cy.findByTestId(this.sessionsTab).click(); return this; } + + toggleEnabled(userName: string) { + this.#getEnabledSwitch(userName).click({ force: true }); + } + + assertEnabled(userName: string) { + this.#getEnabledSwitchLabel(userName) + .scrollIntoView() + .contains("Enabled") + .should("be.visible"); + } + + assertDisabled(userName: string) { + this.#getEnabledSwitchLabel(userName) + .scrollIntoView() + .contains("Disabled") + .should("be.visible"); + } + + #getEnabledSwitch(userName: string) { + return cy.findByTestId(`${userName}-switch`); + } + + #getEnabledSwitchLabel(userName: string) { + return this.#getEnabledSwitch(userName).closest("label"); + } } diff --git a/js/apps/admin-ui/cypress/support/util/AdminClient.ts b/js/apps/admin-ui/cypress/support/util/AdminClient.ts index 8b929323dc..2c23ac0bb1 100644 --- a/js/apps/admin-ui/cypress/support/util/AdminClient.ts +++ b/js/apps/admin-ui/cypress/support/util/AdminClient.ts @@ -95,7 +95,17 @@ class AdminClient { async createUser(user: UserRepresentation) { await this.login(); - return await this.client.users.create(user); + + const { id } = await this.client.users.create(user); + const createdUser = await this.client.users.findOne({ id }); + + if (!createdUser) { + throw new Error( + "Unable to create user, created user could not be found." + ); + } + + return createdUser; } async updateUser(id: string, payload: UserRepresentation) { diff --git a/js/apps/admin-ui/src/user/EditUser.tsx b/js/apps/admin-ui/src/user/EditUser.tsx index 95ec5de535..1e6bf06693 100644 --- a/js/apps/admin-ui/src/user/EditUser.tsx +++ b/js/apps/admin-ui/src/user/EditUser.tsx @@ -25,8 +25,7 @@ import { useAdminClient, useFetch } from "../context/auth/AdminClient"; import { useRealm } from "../context/realm-context/RealmContext"; import { UserProfileProvider } from "../realm-settings/user-profile/UserProfileContext"; import { useParams } from "../utils/useParams"; -import { toUser, UserParams, UserTab } from "./routes/User"; -import { toUsers } from "./routes/Users"; +import { useUpdateEffect } from "../utils/useUpdateEffect"; import { UserAttributes } from "./UserAttributes"; import { UserConsents } from "./UserConsents"; import { UserCredentials } from "./UserCredentials"; @@ -39,6 +38,8 @@ import { } from "./UserProfileFields"; import { UserRoleMapping } from "./UserRoleMapping"; import { UserSessions } from "./UserSessions"; +import { UserParams, UserTab, toUser } from "./routes/User"; +import { toUsers } from "./routes/Users"; import "./user-section.css"; @@ -121,6 +122,9 @@ const EditUserForm = ({ user, bruteForced, refresh }: EditUserFormProps) => { const identityProviderLinksTab = useTab("identity-provider-links"); const sessionsTab = useTab("sessions"); + // Ensure the form remains up-to-date when the user is updated. + useUpdateEffect(() => userForm.reset(user), [user]); + const save = async (formUser: UserRepresentation) => { try { await adminClient.users.update(