From 4c17fa86640e8336dd0080fc0cf1aba6aeeeabb7 Mon Sep 17 00:00:00 2001 From: vmuzikar Date: Thu, 5 Dec 2019 13:44:10 +0100 Subject: [PATCH] KEYCLOAK-12104 UI tests for Linked Accounts Page --- .../ui/account2/LinkedAccountsTest.java | 174 +++++++++++++++++- .../ui/account2/WelcomeScreenTest.java | 4 + .../ui/account2/page/LinkedAccountsPage.java | 111 ++++++++++- .../ui/account2/page/WelcomeScreen.java | 4 +- .../ui/account2/page/fragment/Sidebar.java | 11 ++ .../LinkedAccountsPage.tsx | 43 ++--- 6 files changed, 318 insertions(+), 29 deletions(-) diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/LinkedAccountsTest.java b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/LinkedAccountsTest.java index 837ae77c02b..cd6f2490168 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/LinkedAccountsTest.java +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/LinkedAccountsTest.java @@ -18,26 +18,192 @@ package org.keycloak.testsuite.ui.account2; import org.jboss.arquillian.graphene.page.Page; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.broker.oidc.OIDCIdentityProviderFactory; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.FederatedIdentityRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.social.google.GoogleIdentityProviderFactory; +import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.ui.account2.page.AbstractLoggedInPage; import org.keycloak.testsuite.ui.account2.page.LinkedAccountsPage; +import org.keycloak.testsuite.util.ClientBuilder; + +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; /** * @author Vaclav Muzikar */ public class LinkedAccountsTest extends BaseAccountPageTest { + public static final String SOCIAL_IDP_ALIAS = "fake-google-account"; + public static final String SYSTEM_IDP_ALIAS = "kc-to-kc-account"; + + public static final String REALM2_NAME = "test-realm2"; + public static final String CLIENT_ID = "cross-realm-client"; + public static final String CLIENT_SECRET = "top secret"; + + private UserRepresentation homerUser; + + private LinkedAccountsPage.IdentityProvider socialIdp; + private LinkedAccountsPage.IdentityProvider systemIdp; + @Page private LinkedAccountsPage linkedAccountsPage; + @Page + private LoginPage loginPageWithSocialBtns; + + public LinkedAccountsTest() { + // needs to be done here (setting fields in addTestRealms acts really weird resulting in Homer being null) + homerUser = createUserRepresentation("hsimpson", "hsimpson@keycloak.org", + "Homer", "Simpson", true, "Mmm donuts"); + } + @Override protected AbstractLoggedInPage getAccountPage() { return linkedAccountsPage; } @Override - protected void afterAbstractKeycloakTestRealmImport() { - super.afterAbstractKeycloakTestRealmImport(); - testRealmResource().identityProviders().create(createIdentityProviderRepresentation("test-idp", "test-provider")); + public void addTestRealms(List testRealms) { + super.addTestRealms(testRealms); + RealmRepresentation realm1 = testRealms.get(0); + + realm1.addIdentityProvider(createIdentityProviderRepresentation(SOCIAL_IDP_ALIAS, + GoogleIdentityProviderFactory.PROVIDER_ID)); + + String oidcRoot = getAuthServerRoot() + "realms/" + REALM2_NAME + "/protocol/openid-connect/"; + + IdentityProviderRepresentation systemIdp = createIdentityProviderRepresentation(SYSTEM_IDP_ALIAS, + OIDCIdentityProviderFactory.PROVIDER_ID); + systemIdp.getConfig().put("clientId", CLIENT_ID); + systemIdp.getConfig().put("clientSecret", CLIENT_SECRET); + systemIdp.getConfig().put("clientAuthMethod", OIDCLoginProtocol.CLIENT_SECRET_POST); + systemIdp.getConfig().put("authorizationUrl", oidcRoot + "auth"); + systemIdp.getConfig().put("tokenUrl", oidcRoot + "token"); + realm1.addIdentityProvider(systemIdp); + + ClientRepresentation client = ClientBuilder.create() + .clientId(CLIENT_ID) + .secret(CLIENT_SECRET) + .redirectUris(getAuthServerRoot() + "realms/" + TEST + "/broker/" + SYSTEM_IDP_ALIAS + "/endpoint") + .build(); + + // using REALM2 as an identity provider + RealmRepresentation realm2 = new RealmRepresentation(); + realm2.setId(REALM2_NAME); + realm2.setRealm(REALM2_NAME); + realm2.setEnabled(true); + realm2.setClients(Collections.singletonList(client)); + realm2.setUsers(Collections.singletonList(homerUser)); + testRealms.add(realm2); } - // TODO implement this! (KEYCLOAK-12104) + @Before + public void beforeLinkedAccountsTest() { + socialIdp = linkedAccountsPage.getProvider(SOCIAL_IDP_ALIAS); + systemIdp = linkedAccountsPage.getProvider(SYSTEM_IDP_ALIAS); + assertProvidersCount(); + } + + @After + public void afterLinkedAccountsTest() { + assertProvidersCount(); + } + + @Test + public void linkAccountTest() { + assertEquals(0, testUserResource().getFederatedIdentity().size()); + + assertProvider(socialIdp, false, true, ""); + assertProvider(systemIdp, false, false, ""); + + systemIdp.clickLinkBtn(); + loginPage.form().login(homerUser); + linkedAccountsPage.assertCurrent(); + assertProvider(systemIdp, true, false, homerUser.getUsername()); + + assertProvider(socialIdp, false, true, ""); + + // check through admin REST endpoints + List fids = testUserResource().getFederatedIdentity(); + assertEquals(1, fids.size()); + FederatedIdentityRepresentation fid = fids.get(0); + assertEquals(SYSTEM_IDP_ALIAS, fid.getIdentityProvider()); + assertEquals(homerUser.getUsername(), fid.getUserName()); + + // try to login using IdP + deleteAllSessionsInTestRealm(); + linkedAccountsPage.navigateTo(); + loginPageWithSocialBtns.clickSocial(SYSTEM_IDP_ALIAS); + linkedAccountsPage.assertCurrent(); // no need for re-login to REALM2 + } + + @Test + public void unlinkAccountTest() { + FederatedIdentityRepresentation fid = new FederatedIdentityRepresentation(); + fid.setIdentityProvider(SOCIAL_IDP_ALIAS); + fid.setUserId("Homer lost his ID at Moe's last night"); + fid.setUserName(homerUser.getUsername()); + testUserResource().addFederatedIdentity(SOCIAL_IDP_ALIAS, fid); + assertEquals(1, testUserResource().getFederatedIdentity().size()); + linkedAccountsPage.navigateTo(); + + assertProvider(systemIdp, false, false, ""); + assertProvider(socialIdp, true, true, homerUser.getUsername()); + + socialIdp.clickUnlinkBtn(); + linkedAccountsPage.assertCurrent(); + assertProvider(systemIdp, false, false, ""); + assertProvider(socialIdp, false, true, ""); + + assertEquals(0, testUserResource().getFederatedIdentity().size()); + } + + private void assertProvider( + LinkedAccountsPage.IdentityProvider provider, + boolean expectLinked, + boolean expectSocial, + String expectedUsername + ) { + if (expectLinked) { + assertTrue("Account should be in the \"Linked\" list", provider.isLinked()); + assertTrue("Unlink button should be visible", provider.isUnlinkBtnVisible()); + assertFalse("Link button shouldn't be visible", provider.isLinkBtnVisible()); + } + else { + assertFalse("Account should be in the \"Unlinked\" list", provider.isLinked()); + assertFalse("Unlink button shouldn't be visible", provider.isUnlinkBtnVisible()); + assertTrue("Link button should be visible", provider.isLinkBtnVisible()); + } + + if (expectSocial) { + assertTrue("Social badge should be visible", provider.hasSocialLoginBadge()); + assertTrue("Social icon should be visible", provider.hasSocialIcon()); + assertFalse("Default icon shouldn't be visible", provider.hasDefaultIcon()); + } + else { + assertFalse("Social badge shouldn't be visible", provider.hasSocialLoginBadge()); + assertFalse("Social icon shouldn't be visible", provider.hasSocialIcon()); + assertTrue("Default icon should be visible", provider.hasDefaultIcon()); + } + + assertEquals(expectedUsername, provider.getUsername()); + } + + private void assertProvidersCount() { + assertEquals(2, + linkedAccountsPage.getLinkedProvidersCount() + linkedAccountsPage.getUnlinkedProvidersCount()); + } } diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/WelcomeScreenTest.java b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/WelcomeScreenTest.java index 6a5ecfb70ab..91aff7e3535 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/WelcomeScreenTest.java +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/WelcomeScreenTest.java @@ -93,6 +93,10 @@ public class WelcomeScreenTest extends AbstractAccountTest { loginToAccount(); deviceActivityPage.assertCurrent(); + // linked accounts nav item (this doesn't test welcome page directly but the sidebar after login) + personalInfoPage.navigateTo(); + personalInfoPage.sidebar().assertNavNotPresent(LinkedAccountsPage.LINKED_ACCOUNTS_ID); + // linked accounts link accountWelcomeScreen.navigateTo(); accountWelcomeScreen.assertLinkedAccountsLinkVisible(false); diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/LinkedAccountsPage.java b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/LinkedAccountsPage.java index cdd3e83a662..a95cd7e16f2 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/LinkedAccountsPage.java +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/LinkedAccountsPage.java @@ -17,13 +17,36 @@ package org.keycloak.testsuite.ui.account2.page; +import org.jboss.arquillian.graphene.Graphene; +import org.jboss.arquillian.graphene.fragment.Root; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +import java.util.List; + +import static org.keycloak.testsuite.util.UIUtils.clickLink; +import static org.keycloak.testsuite.util.UIUtils.getTextFromElement; +import static org.keycloak.testsuite.util.UIUtils.isElementVisible; +import static org.openqa.selenium.By.id; +import static org.openqa.selenium.By.xpath; + /** * @author Vaclav Muzikar */ public class LinkedAccountsPage extends AbstractLoggedInPage { + public static final String LINKED_ACCOUNTS_ID = "linked-accounts"; + public static final String LINKED_IDPS_LIST_ID = "linked-idps"; + public static final String UNLINKED_IDPS_LIST_ID = "unlinked-idps"; + + @FindBy(id = LINKED_IDPS_LIST_ID) + private List linkedIdPsList; + + @FindBy(id = UNLINKED_IDPS_LIST_ID) + private List unlinkedIdPsList; + @Override public String getPageId() { - return "linked-accounts"; + return LINKED_ACCOUNTS_ID; } @Override @@ -31,5 +54,89 @@ public class LinkedAccountsPage extends AbstractLoggedInPage { return ACCOUNT_SECURITY_ID; } - // TODO implement this! (KEYCLOAK-12104) + public IdentityProvider getProvider(String providerAlias) { + WebElement root = driver.findElement(id(providerAlias + "-idp")); + return Graphene.createPageFragment(IdentityProvider.class, root); + } + + public int getLinkedProvidersCount() { + return linkedIdPsList.size(); + } + + public int getUnlinkedProvidersCount() { + return unlinkedIdPsList.size(); + } + + public class IdentityProvider { + @Root + private WebElement root; + + @FindBy(xpath = ".//*[contains(@id,'idp-name')]") + private WebElement nameElement; + + @FindBy(xpath = ".//*[contains(@id,'idp-icon')]") + private WebElement iconElement; + + @FindBy(xpath = ".//*[contains(@id,'idp-badge')]") + private WebElement badgeElement; + + @FindBy(xpath = ".//*[contains(@id,'idp-username')]") + private WebElement usernameElement; + + @FindBy(xpath = ".//*[contains(@id,'idp-link')]") + private WebElement linkBtn; + + @FindBy(xpath = ".//*[contains(@id,'idp-unlink')]") + private WebElement unlinkBtn; + + public boolean isLinked() { + String parentListId = root.findElement(xpath("ancestor::ul")).getAttribute("id"); + + if (parentListId.equals(LINKED_IDPS_LIST_ID)) { + return true; + } + else if (parentListId.equals(UNLINKED_IDPS_LIST_ID)) { + return false; + } + else { + throw new IllegalStateException("Unexpected parent list ID: " + parentListId); + } + } + + public boolean hasSocialLoginBadge() { + return getTextFromElement(badgeElement).equals("Social Login"); + } + + public boolean hasSystemDefinedBadge() { + return getTextFromElement(badgeElement).equals("System Defined"); + } + + public boolean hasSocialIcon() { + return iconElement.getAttribute("id").contains("social"); + } + + public boolean hasDefaultIcon() { + return iconElement.getAttribute("id").contains("default"); + } + + public String getUsername() { + return getTextFromElement(usernameElement); + } + + public boolean isLinkBtnVisible() { + return isElementVisible(linkBtn); + } + + public boolean isUnlinkBtnVisible() { + return isElementVisible(unlinkBtn); + } + + public void clickLinkBtn() { + clickLink(linkBtn); + } + + public void clickUnlinkBtn() { + clickLink(unlinkBtn); + } + } } diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/WelcomeScreen.java b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/WelcomeScreen.java index 4003a4e2046..1ecac78a2ac 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/WelcomeScreen.java +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/WelcomeScreen.java @@ -70,8 +70,8 @@ public class WelcomeScreen extends AbstractAccountPage { } @Override - public UriBuilder createUriBuilder() { - UriBuilder uriBuilder = super.createUriBuilder(); + public UriBuilder getUriBuilder() { + UriBuilder uriBuilder = super.getUriBuilder(); if (referrer != null && referrerUri != null) { uriBuilder.queryParam("referrer", referrer); uriBuilder.queryParam("referrer_uri", referrerUri); diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/Sidebar.java b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/Sidebar.java index d74484df924..5a0ec0c7bfd 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/Sidebar.java +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/Sidebar.java @@ -19,6 +19,7 @@ package org.keycloak.testsuite.ui.account2.page.fragment; import org.jboss.arquillian.graphene.fragment.Root; import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; @@ -75,6 +76,16 @@ public class Sidebar extends AbstractFragmentWithMobileLayout { return sidebarRoot.findElement(By.id(NAV_ITEM_ID_PREFIX + id)); } + public void assertNavNotPresent(String id) { + try { + getNavElement(id).isDisplayed(); + throw new AssertionError("Nav element " + id + " shouldn't be present"); + } + catch (NoSuchElementException e) { + // ok + } + } + protected WebElement getNavSubsection(WebElement parent) { return parent.findElement(By.xpath("../section[@aria-labelledby='" + parent.getAttribute("id") + "']")); } diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/linked-accounts-page/LinkedAccountsPage.tsx b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/linked-accounts-page/LinkedAccountsPage.tsx index b34f7cf1609..561592a0cde 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/linked-accounts-page/LinkedAccountsPage.tsx +++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/linked-accounts-page/LinkedAccountsPage.tsx @@ -129,7 +129,7 @@ class LinkedAccountsPage extends React.Component - + {this.makeRows(this.state.linkedAccounts, true)} @@ -138,7 +138,7 @@ class LinkedAccountsPage extends React.Component - + {this.makeRows(this.state.unLinkedAccounts, false)} @@ -175,17 +175,17 @@ class LinkedAccountsPage extends React.Component { accounts.map( (account: LinkedAccount) => ( - + {this.findIcon(account)}

{account.displayName}

, - {this.badge(account)}, - {account.linkedUsername}, + {this.findIcon(account)}

{account.displayName}

, + {this.badge(account)}, + {account.linkedUsername}, ]}/> - {isLinked && } - {!isLinked && } + {isLinked && } + {!isLinked && }
@@ -205,20 +205,21 @@ class LinkedAccountsPage extends React.Component); - if (account.providerName.toLowerCase().includes('linkedin')) return (); - if (account.providerName.toLowerCase().includes('facebook')) return (); - if (account.providerName.toLowerCase().includes('google')) return (); - if (account.providerName.toLowerCase().includes('instagram')) return (); - if (account.providerName.toLowerCase().includes('microsoft')) return (); - if (account.providerName.toLowerCase().includes('bitbucket')) return (); - if (account.providerName.toLowerCase().includes('twitter')) return (); - if (account.providerName.toLowerCase().includes('openshift')) return (); - if (account.providerName.toLowerCase().includes('gitlab')) return (); - if (account.providerName.toLowerCase().includes('paypal')) return (); - if (account.providerName.toLowerCase().includes('stackoverflow')) return (); + const socialIconId = `${account.providerAlias}-idp-icon-social`; + if (account.providerName.toLowerCase().includes('github')) return (); + if (account.providerName.toLowerCase().includes('linkedin')) return (); + if (account.providerName.toLowerCase().includes('facebook')) return (); + if (account.providerName.toLowerCase().includes('google')) return (); + if (account.providerName.toLowerCase().includes('instagram')) return (); + if (account.providerName.toLowerCase().includes('microsoft')) return (); + if (account.providerName.toLowerCase().includes('bitbucket')) return (); + if (account.providerName.toLowerCase().includes('twitter')) return (); + if (account.providerName.toLowerCase().includes('openshift')) return (); + if (account.providerName.toLowerCase().includes('gitlab')) return (); + if (account.providerName.toLowerCase().includes('paypal')) return (); + if (account.providerName.toLowerCase().includes('stackoverflow')) return (); - return (); + return (); } };