KEYCLOAK-12104 UI tests for Linked Accounts Page

This commit is contained in:
vmuzikar 2019-12-05 13:44:10 +01:00 committed by Bruno Oliveira da Silva
parent 8efe89135e
commit 4c17fa8664
6 changed files with 318 additions and 29 deletions

View file

@ -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 <vmuzikar@redhat.com>
*/
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<RealmRepresentation> 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<FederatedIdentityRepresentation> 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());
}
}

View file

@ -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);

View file

@ -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 <vmuzikar@redhat.com>
*/
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<WebElement> linkedIdPsList;
@FindBy(id = UNLINKED_IDPS_LIST_ID)
private List<WebElement> 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);
}
}
}

View file

@ -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);

View file

@ -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") + "']"));
}

View file

@ -129,7 +129,7 @@ class LinkedAccountsPage extends React.Component<LinkedAccountsPageProps, Linked
<Title headingLevel={TitleLevel.h2} size='2xl'>
<Msg msgKey='linkedLoginProviders'/>
</Title>
<DataList aria-label='foo'>
<DataList id="linked-idps" aria-label='foo'>
{this.makeRows(this.state.linkedAccounts, true)}
</DataList>
</StackItem>
@ -138,7 +138,7 @@ class LinkedAccountsPage extends React.Component<LinkedAccountsPageProps, Linked
<Title headingLevel={TitleLevel.h2} size='2xl'>
<Msg msgKey='unlinkedLoginProviders'/>
</Title>
<DataList aria-label='foo'>
<DataList id="unlinked-idps" aria-label='foo'>
{this.makeRows(this.state.unLinkedAccounts, false)}
</DataList>
</StackItem>
@ -175,17 +175,17 @@ class LinkedAccountsPage extends React.Component<LinkedAccountsPageProps, Linked
<> {
accounts.map( (account: LinkedAccount) => (
<DataListItem key={account.providerName} aria-labelledby="simple-item1">
<DataListItem id={`${account.providerAlias}-idp`} key={account.providerName} aria-labelledby="simple-item1">
<DataListItemRow key={account.providerName}>
<DataListItemCells
dataListCells={[
<DataListCell key='idp'><Stack><StackItem isFilled>{this.findIcon(account)}</StackItem><StackItem isFilled><h2><strong>{account.displayName}</strong></h2></StackItem></Stack></DataListCell>,
<DataListCell key='badge'><Stack><StackItem isFilled/><StackItem isFilled>{this.badge(account)}</StackItem></Stack></DataListCell>,
<DataListCell key='username'><Stack><StackItem isFilled/><StackItem isFilled>{account.linkedUsername}</StackItem></Stack></DataListCell>,
<DataListCell key='idp'><Stack><StackItem isFilled>{this.findIcon(account)}</StackItem><StackItem id={`${account.providerAlias}-idp-name`} isFilled><h2><strong>{account.displayName}</strong></h2></StackItem></Stack></DataListCell>,
<DataListCell key='badge'><Stack><StackItem isFilled/><StackItem id={`${account.providerAlias}-idp-badge`} isFilled>{this.badge(account)}</StackItem></Stack></DataListCell>,
<DataListCell key='username'><Stack><StackItem isFilled/><StackItem id={`${account.providerAlias}-idp-username`} isFilled>{account.linkedUsername}</StackItem></Stack></DataListCell>,
]}/>
<DataListAction aria-labelledby='foo' aria-label='foo action' id='setPasswordAction'>
{isLinked && <Button variant='link' onClick={() => this.unLinkAccount(account)}><UnlinkIcon size='sm'/> <Msg msgKey='unLink'/></Button>}
{!isLinked && <Button variant='link' onClick={() => this.linkAccount(account)}><LinkIcon size='sm'/> <Msg msgKey='link'/></Button>}
{isLinked && <Button id={`${account.providerAlias}-idp-unlink`} variant='link' onClick={() => this.unLinkAccount(account)}><UnlinkIcon size='sm'/> <Msg msgKey='unLink'/></Button>}
{!isLinked && <Button id={`${account.providerAlias}-idp-link`} variant='link' onClick={() => this.linkAccount(account)}><LinkIcon size='sm'/> <Msg msgKey='link'/></Button>}
</DataListAction>
</DataListItemRow>
</DataListItem>
@ -205,20 +205,21 @@ class LinkedAccountsPage extends React.Component<LinkedAccountsPageProps, Linked
}
private findIcon(account: LinkedAccount): React.ReactNode {
if (account.providerName.toLowerCase().includes('github')) return (<GithubIcon size='xl'/>);
if (account.providerName.toLowerCase().includes('linkedin')) return (<LinkedinIcon size='xl'/>);
if (account.providerName.toLowerCase().includes('facebook')) return (<FacebookIcon size='xl'/>);
if (account.providerName.toLowerCase().includes('google')) return (<GoogleIcon size='xl'/>);
if (account.providerName.toLowerCase().includes('instagram')) return (<InstagramIcon size='xl'/>);
if (account.providerName.toLowerCase().includes('microsoft')) return (<MicrosoftIcon size='xl'/>);
if (account.providerName.toLowerCase().includes('bitbucket')) return (<BitbucketIcon size='xl'/>);
if (account.providerName.toLowerCase().includes('twitter')) return (<TwitterIcon size='xl'/>);
if (account.providerName.toLowerCase().includes('openshift')) return (<OpenshiftIcon size='xl'/>);
if (account.providerName.toLowerCase().includes('gitlab')) return (<GitlabIcon size='xl'/>);
if (account.providerName.toLowerCase().includes('paypal')) return (<PaypalIcon size='xl'/>);
if (account.providerName.toLowerCase().includes('stackoverflow')) return (<StackOverflowIcon size='xl'/>);
const socialIconId = `${account.providerAlias}-idp-icon-social`;
if (account.providerName.toLowerCase().includes('github')) return (<GithubIcon id={socialIconId} size='xl'/>);
if (account.providerName.toLowerCase().includes('linkedin')) return (<LinkedinIcon id={socialIconId} size='xl'/>);
if (account.providerName.toLowerCase().includes('facebook')) return (<FacebookIcon id={socialIconId} size='xl'/>);
if (account.providerName.toLowerCase().includes('google')) return (<GoogleIcon id={socialIconId} size='xl'/>);
if (account.providerName.toLowerCase().includes('instagram')) return (<InstagramIcon id={socialIconId} size='xl'/>);
if (account.providerName.toLowerCase().includes('microsoft')) return (<MicrosoftIcon id={socialIconId} size='xl'/>);
if (account.providerName.toLowerCase().includes('bitbucket')) return (<BitbucketIcon id={socialIconId} size='xl'/>);
if (account.providerName.toLowerCase().includes('twitter')) return (<TwitterIcon id={socialIconId} size='xl'/>);
if (account.providerName.toLowerCase().includes('openshift')) return (<OpenshiftIcon id={socialIconId} size='xl'/>);
if (account.providerName.toLowerCase().includes('gitlab')) return (<GitlabIcon id={socialIconId} size='xl'/>);
if (account.providerName.toLowerCase().includes('paypal')) return (<PaypalIcon id={socialIconId} size='xl'/>);
if (account.providerName.toLowerCase().includes('stackoverflow')) return (<StackOverflowIcon id={socialIconId} size='xl'/>);
return (<CubeIcon size='xl'/>);
return (<CubeIcon id={`${account.providerAlias}-idp-icon-default`} size='xl'/>);
}
};