diff --git a/cypress/e2e/users_test.spec.ts b/cypress/e2e/users_test.spec.ts index c5cf1f8033..d0433e8f39 100644 --- a/cypress/e2e/users_test.spec.ts +++ b/cypress/e2e/users_test.spec.ts @@ -3,13 +3,15 @@ import LoginPage from "../support/pages/LoginPage"; import CreateUserPage from "../support/pages/admin_console/manage/users/CreateUserPage"; import Masthead from "../support/pages/admin_console/Masthead"; import ListingPage from "../support/pages/admin_console/ListingPage"; -import UserDetailsPage from "../support/pages/admin_console/manage/users/UserDetailsPage"; +import UserDetailsPage from "../support/pages/admin_console/manage/users/user_details/UserDetailsPage"; import AttributesTab from "../support/pages/admin_console/manage/AttributesTab"; import ModalUtils from "../support/util/ModalUtils"; import { keycloakBefore } from "../support/util/keycloak_hooks"; import UserGroupsPage from "../support/pages/admin_console/manage/users/UserGroupsPage"; import adminClient from "../support/util/AdminClient"; import CredentialsPage from "../support/pages/admin_console/manage/users/CredentialsPage"; +import UsersPage from "cypress/support/pages/admin_console/manage/users/UsersPage"; +import IdentityProviderLinksTab from "cypress/support/pages/admin_console/manage/users/user_details/tabs/IdentityProviderLinksTab"; let groupName = "group"; let groupsList: string[] = []; @@ -25,6 +27,8 @@ describe("User creation", () => { const userDetailsPage = new UserDetailsPage(); const credentialsPage = new CredentialsPage(); const attributesTab = new AttributesTab(); + const usersPage = new UsersPage(); + const identityProviderLinksTab = new IdentityProviderLinksTab(); let itemId = "user_crud"; let itemIdWithGroups = "user_with_groups_crud"; @@ -37,7 +41,6 @@ describe("User creation", () => { adminClient.createGroup(groupName); groupsList = [...groupsList, groupName]; } - keycloakBefore(); loginPage.logIn(); }); @@ -207,6 +210,129 @@ describe("User creation", () => { cy.findByTestId("empty-state").contains("No consents"); }); + describe("Identity provider links", () => { + const usernameIdpLinksTest = "user_idp_link_test"; + const identityProviders = [ + { testName: "Bitbucket", displayName: "BitBucket", alias: "bitbucket" }, + { testName: "Facebook", displayName: "Facebook", alias: "facebook" }, + { + testName: "Keycloak-oidc", + displayName: "Keycloak OpenID Connect", + alias: "keycloak-oidc", + }, + ]; + + before(async () => { + await Promise.all([ + adminClient.createUser({ + username: usernameIdpLinksTest, + enabled: true, + }), + identityProviders.forEach((idp) => + adminClient.createIdentityProvider(idp.displayName, idp.alias) + ), + ]); + }); + + after(() => adminClient.deleteUser(usernameIdpLinksTest)); + + beforeEach(() => { + usersPage.goToUserListTab().goToUserDetailsPage(usernameIdpLinksTest); + userDetailsPage.goToIdentityProviderLinksTab(); + }); + + identityProviders.forEach(($idp, linkedIdpsCount) => { + it(`Link account to IdP: ${$idp.testName}`, () => { + const availableIdpsCount = identityProviders.length - linkedIdpsCount; + + if (linkedIdpsCount == 0) { + identityProviderLinksTab.assertNoIdentityProvidersLinkedMessageExist( + true + ); + } + identityProviderLinksTab + .assertAvailableIdentityProvidersItemsEqual(availableIdpsCount) + .clickLinkAccount($idp.testName) + .assertLinkAccountModalTitleEqual($idp.testName) + .assertLinkAccountModalIdentityProviderInputEqual($idp.testName) + .typeLinkAccountModalUserId("testUserId") + .typeLinkAccountModalUsername("testUsername") + .clickLinkAccountModalLinkBtn() + .assertNotificationIdentityProviderLinked() + .assertLinkedIdentityProvidersItemsEqual(linkedIdpsCount + 1) + .assertAvailableIdentityProvidersItemsEqual(availableIdpsCount - 1) + .assertLinkedIdentityProviderExist($idp.testName, true) + .assertAvailableIdentityProviderExist($idp.testName, false); + if (availableIdpsCount - 1 == 0) { + identityProviderLinksTab.assertNoAvailableIdentityProvidersMessageExist( + true + ); + } + masthead.closeAllAlertMessages(); + }); + }); + + it("Link account to already added IdP fail", () => { + cy.wrap(null).then(() => + adminClient.unlinkAccountIdentityProvider( + usernameIdpLinksTest, + identityProviders[0].displayName + ) + ); + + sidebarPage.goToUsers(); + usersPage.goToUserListTab().goToUserDetailsPage(usernameIdpLinksTest); + userDetailsPage.goToIdentityProviderLinksTab(); + + cy.wrap(null).then(() => + adminClient.linkAccountIdentityProvider( + usernameIdpLinksTest, + identityProviders[0].displayName + ) + ); + + identityProviderLinksTab + .clickLinkAccount(identityProviders[0].testName) + .assertLinkAccountModalTitleEqual(identityProviders[0].testName) + .assertLinkAccountModalIdentityProviderInputEqual( + identityProviders[0].testName + ) + .typeLinkAccountModalUserId("testUserId") + .typeLinkAccountModalUsername("testUsername") + .clickLinkAccountModalLinkBtn() + .assertNotificationAlreadyLinkedError(); + modalUtils.cancelModal(); + }); + + identityProviders.forEach(($idp, availableIdpsCount) => { + it(`Unlink account from IdP: ${$idp.testName}`, () => { + const linkedIdpsCount = identityProviders.length - availableIdpsCount; + + if (availableIdpsCount == 0) { + identityProviderLinksTab.assertNoAvailableIdentityProvidersMessageExist( + true + ); + } + identityProviderLinksTab + .assertAvailableIdentityProvidersItemsEqual(availableIdpsCount) + .clickUnlinkAccount($idp.testName) + .assertUnLinkAccountModalTitleEqual($idp.testName) + .clickUnlinkAccountModalUnlinkBtn() + .assertNotificationPoviderLinkRemoved() + .assertLinkedIdentityProvidersItemsEqual(linkedIdpsCount - 1) + .assertAvailableIdentityProvidersItemsEqual(availableIdpsCount + 1) + .assertLinkedIdentityProviderExist($idp.testName, false) + .assertAvailableIdentityProviderExist($idp.testName, true); + if (linkedIdpsCount - 1 == 0) { + identityProviderLinksTab.assertNoIdentityProvidersLinkedMessageExist( + true + ); + } + masthead.closeAllAlertMessages(); + }); + }); + }); + it("Reset credential of User with empty state", () => { listingPage.goToItemDetails(itemId); credentialsPage diff --git a/cypress/support/pages/admin_console/manage/users/UsersPage.ts b/cypress/support/pages/admin_console/manage/users/UsersPage.ts new file mode 100644 index 0000000000..365137e0de --- /dev/null +++ b/cypress/support/pages/admin_console/manage/users/UsersPage.ts @@ -0,0 +1,28 @@ +import PageObject from "../../components/PageObject"; +import ListingPage from "../../ListingPage"; + +const listingPage = new ListingPage(); + +export default class UsersPage extends PageObject { + private userListTabLink = "listTab"; + private permissionsTabLink = "permissionsTab"; + + public goToUserListTab() { + cy.findByTestId(this.userListTabLink).click(); + + return this; + } + + public goToPermissionsTab() { + cy.findByTestId(this.permissionsTabLink).click(); + + return this; + } + + public goToUserDetailsPage(username: string) { + listingPage.searchItem(username); + listingPage.goToItemDetails(username); + + return this; + } +} diff --git a/cypress/support/pages/admin_console/manage/users/UserDetailsPage.ts b/cypress/support/pages/admin_console/manage/users/user_details/UserDetailsPage.ts similarity index 76% rename from cypress/support/pages/admin_console/manage/users/UserDetailsPage.ts rename to cypress/support/pages/admin_console/manage/users/user_details/UserDetailsPage.ts index 8bccba7d6c..93d6428846 100644 --- a/cypress/support/pages/admin_console/manage/users/UserDetailsPage.ts +++ b/cypress/support/pages/admin_console/manage/users/user_details/UserDetailsPage.ts @@ -1,6 +1,7 @@ import { RequiredActionAlias } from "@keycloak/keycloak-admin-client/lib/defs/requiredActionProviderRepresentation"; +import PageObject from "../../../components/PageObject"; -export default class UserDetailsPage { +export default class UserDetailsPage extends PageObject { saveBtn: string; cancelBtn: string; emailInput: string; @@ -12,8 +13,10 @@ export default class UserDetailsPage { enabledSwitch: string; enabledValue: boolean; requiredUserActions: RequiredActionAlias[]; + identityProviderLinksTab: string; constructor() { + super(); this.saveBtn = "save-user"; this.cancelBtn = "cancel-create-user"; this.emailInput = "email-input"; @@ -29,6 +32,15 @@ export default class UserDetailsPage { this.enabledSwitch = "user-enabled-switch"; this.enabledValue = true; this.requiredUserActions = [RequiredActionAlias.UPDATE_PASSWORD]; + this.identityProviderLinksTab = "identity-provider-links-tab"; + } + + public goToIdentityProviderLinksTab() { + cy.findByTestId(this.identityProviderLinksTab).click(); + cy.intercept("/admin/realms/master").as("load"); + cy.wait(["@load"]); + + return this; } fillUserData() { diff --git a/cypress/support/pages/admin_console/manage/users/user_details/tabs/IdentityProviderLinksTab.ts b/cypress/support/pages/admin_console/manage/users/user_details/tabs/IdentityProviderLinksTab.ts new file mode 100644 index 0000000000..f0f1af4843 --- /dev/null +++ b/cypress/support/pages/admin_console/manage/users/user_details/tabs/IdentityProviderLinksTab.ts @@ -0,0 +1,162 @@ +import Masthead from "cypress/support/pages/admin_console/Masthead"; +import ModalUtils from "cypress/support/util/ModalUtils"; + +const modalUtils = new ModalUtils(); +const masthead = new Masthead(); + +export default class IdentityProviderLinksTab { + private linkedProvidersSection = ".kc-linked-idps"; + private availableProvidersSection = ".kc-available-idps"; + private linkAccountBtn = ".pf-c-button.pf-m-link"; + private linkAccountModalIdentityProviderInput = "idpNameInput"; + private linkAccountModalUserIdInput = "userIdInput"; + private linkAccountModalUsernameInput = "usernameInput"; + private linkAccountModalLinkBtn = "Link"; + + public clickLinkAccount(idpName: string) { + cy.get(this.availableProvidersSection + " tr") + .contains(idpName) + .parent() + .find(this.linkAccountBtn) + .click(); + + return this; + } + + public clickUnlinkAccount(idpName: string) { + cy.get(this.linkedProvidersSection + " tr") + .contains(idpName) + .parent() + .parent() + .find(this.linkAccountBtn) + .click(); + + return this; + } + + public typeLinkAccountModalUserId(userId: string) { + cy.findByTestId(this.linkAccountModalUserIdInput).type(userId); + + return this; + } + + public typeLinkAccountModalUsername(username: string) { + cy.findByTestId(this.linkAccountModalUsernameInput).type(username); + + return this; + } + + public clickLinkAccountModalLinkBtn() { + cy.findByTestId(this.linkAccountModalLinkBtn).click(); + cy.intercept("/admin/realms/master").as("load"); + cy.wait(["@load"]); + return this; + } + + public clickUnlinkAccountModalUnlinkBtn() { + modalUtils.confirmModal(); + cy.intercept("/admin/realms/master").as("load"); + cy.wait(["@load"]); + return this; + } + + public assertNoIdentityProvidersLinkedMessageExist(exist: boolean) { + cy.get(this.linkedProvidersSection).should( + (exist ? "" : "not.") + "contain.text", + "No identity providers linked. Choose one from the list below." + ); + + return this; + } + + public assertNoAvailableIdentityProvidersMessageExist(exist: boolean) { + cy.get(this.availableProvidersSection).should( + (exist ? "" : "not.") + "contain.text", + "No available identity providers." + ); + + return this; + } + + public assertLinkAccountModalTitleEqual(idpName: string) { + modalUtils.assertModalTitleEqual(`Link account to ${idpName}`); + + return this; + } + + public assertUnLinkAccountModalTitleEqual(idpName: string) { + modalUtils.assertModalTitleEqual(`Unlink account from ${idpName}?`); + + return this; + } + + public assertLinkAccountModalIdentityProviderInputEqual(idpName: string) { + cy.findByTestId(this.linkAccountModalIdentityProviderInput).should( + "have.value", + idpName + ); + + return this; + } + + public assertNotificationIdentityProviderLinked() { + masthead.checkNotificationMessage("Identity provider has been linked"); + + return this; + } + + public assertNotificationAlreadyLinkedError() { + masthead.checkNotificationMessage( + "Could not link identity provider User is already linked with provider" + ); + + return this; + } + + public assertNotificationPoviderLinkRemoved() { + masthead.checkNotificationMessage("The provider link has been removed"); + + return this; + } + + public assertLinkedIdentityProvidersItemsEqual(count: number) { + if (count > 0) { + cy.get(this.linkedProvidersSection + " tbody") + .find("tr") + .should("have.length", count); + } else { + cy.get(this.linkedProvidersSection + " tbody").should("not.exist"); + } + + return this; + } + + public assertAvailableIdentityProvidersItemsEqual(count: number) { + if (count > 0) { + cy.get(this.availableProvidersSection + " tbody") + .find("tr") + .should("have.length", count); + } else { + cy.get(this.availableProvidersSection + " tbody").should("not.exist"); + } + return this; + } + + public assertLinkedIdentityProviderExist(idpName: string, exist: boolean) { + cy.get(this.linkedProvidersSection).should( + (exist ? "" : "not.") + "contain.text", + idpName + ); + + return this; + } + + public assertAvailableIdentityProviderExist(idpName: string, exist: boolean) { + cy.get(this.availableProvidersSection).should( + (exist ? "" : "not.") + "contain.text", + idpName + ); + + return this; + } +} diff --git a/cypress/support/util/AdminClient.ts b/cypress/support/util/AdminClient.ts index 9d4f4db8ad..de12ca9092 100644 --- a/cypress/support/util/AdminClient.ts +++ b/cypress/support/util/AdminClient.ts @@ -184,6 +184,51 @@ class AdminClient { await this.login(); return await this.client.roles.delByName({ name }); } + + async createIdentityProvider(idpDisplayName: string, alias: string) { + await this.login(); + const identityProviders = + (await this.client.serverInfo.find()).identityProviders || []; + const idp = identityProviders.find(({ name }) => name === idpDisplayName); + await this.client.identityProviders.create({ + providerId: idp?.id!, + displayName: idpDisplayName, + alias: alias, + }); + } + + async unlinkAccountIdentityProvider( + username: string, + idpDisplayName: string + ) { + await this.login(); + const user = await this.client.users.find({ username }); + const identityProviders = + (await this.client.serverInfo.find()).identityProviders || []; + const idp = identityProviders.find(({ name }) => name === idpDisplayName); + await this.client.users.delFromFederatedIdentity({ + id: user[0].id!, + federatedIdentityId: idp?.id!, + }); + } + + async linkAccountIdentityProvider(username: string, idpDisplayName: string) { + await this.login(); + const user = await this.client.users.find({ username }); + const identityProviders = + (await this.client.serverInfo.find()).identityProviders || []; + const idp = identityProviders.find(({ name }) => name === idpDisplayName); + const fedIdentity = { + identityProvider: idp?.id, + userId: "testUserIdApi", + userName: "testUserNameApi", + }; + await this.client.users.addToFederatedIdentity({ + id: user[0].id!, + federatedIdentityId: idp?.id!, + federatedIdentity: fedIdentity, + }); + } } const adminClient = new AdminClient();