KEYCLOAK-12105 Add UI tests for Single page to manage credentials

This commit is contained in:
vmuzikar 2020-01-27 17:10:15 +01:00 committed by Bruno Oliveira da Silva
parent d417639cb8
commit 0801cfb01f
19 changed files with 654 additions and 29 deletions

View file

@ -427,6 +427,9 @@ mvn -f testsuite/integration-arquillian/tests/other/base-ui/pom.xml \
-Pandroid \
-Dappium.avd=Nexus_5X_API_27
```
**Note:** Some of the tests are covering WebAuthn functionality. Such tests are ignored by default, to ensure that all
tests in the Base UI testsuite are executed please use `-DchromeArguments=--enable-web-authentication-testing-api` as
specified in [WebAuthn tests](#webauthn-tests).
## WebAuthN tests
The WebAuthN tests, in Keycloak, can be only executed with Chrome browser, because the Chrome has feature _WebAuthenticationTestingApi_,

View file

@ -6,11 +6,17 @@ import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.BrowserType;
import org.openqa.selenium.remote.RemoteWebDriver;
import static org.keycloak.testsuite.util.DroneUtils.getCurrentDriver;
public class WebAuthnAssume {
public static final String CHROME_NAME = BrowserType.CHROME;
public static final int CHROME_MIN_VERSION = 68;
public static void assumeChrome() {
assumeChrome(getCurrentDriver());
}
public static void assumeChrome(WebDriver driver) {
Assume.assumeNotNull(driver);
String chromeArguments = System.getProperty("chromeArguments");

View file

@ -39,10 +39,17 @@ public class LoginActions extends LoginBase {
@FindBy(css = "input[type='submit']")
private WebElement submitButton;
@FindBy(css = "button[type='submit']")
private WebElement cancelButton;
public void submit() {
clickLink(submitButton);
}
public void cancel() {
clickLink(cancelButton);
}
@Override
public boolean isCurrent() {
return URLUtils.currentUrlStartsWith(toString() + "?"); // ignore the query string

View file

@ -161,6 +161,9 @@ public class LoginForm extends Form {
@FindBy(id = "totp")
private WebElement totpInputField;
@FindBy(id = "userLabel")
private WebElement userLabelInputField;
@FindBy(id = "totpSecret")
private WebElement totpSecret;
@ -171,6 +174,10 @@ public class LoginForm extends Form {
UIUtils.setTextInputValue(totpInputField, value);
}
public void setUserLabel(String value) {
UIUtils.setTextInputValue(userLabelInputField, value);
}
public String getTotpSecret() {
return totpSecret.getAttribute(UIUtils.VALUE_ATTR_NAME);
}

View file

@ -52,6 +52,7 @@ public class OAuthGrant extends RequiredActions {
clickLink(acceptButton);
}
@Override
public void cancel() {
clickLink(cancelButton);
}

View file

@ -113,6 +113,10 @@ public class OTPSetup extends RequiredActions {
return otpCounter.getText();
}
public void setUserLabel(String value) {
form.setUserLabel(value);
}
@Override
public String getActionId() {
return UserModel.RequiredAction.CONFIGURE_TOTP.name();

View file

@ -0,0 +1,3 @@
#encoding: utf-8
locale_test=Přísný jazyk
client_localized-client=Přespříliš lokalizovaný klient

View file

@ -0,0 +1,4 @@
#encoding: utf-8
locale_test=Přísný jazyk
accountManagementWelcomeMessage=Vítejte v Keycloaku
personalInfoHtmlTitle=Osobní údaje

View file

@ -0,0 +1,2 @@
parent=${theme-default-name}-preview
locales=en,lang01,lang02,lang03,lang04,lang05,test,lang06,lang07,lang08,lang09,lang10

View file

@ -1,2 +1,2 @@
parent=keycloak-preview
parent=${theme-default-name}
locales=en,lang01,lang02,lang03,lang04,lang05,test,lang06,lang07,lang08,lang09,lang10

View file

@ -35,6 +35,7 @@ import static org.junit.Assume.assumeFalse;
*/
public abstract class AbstractUiTest extends AbstractAuthTest {
public static final String LOCALIZED_THEME = "localized-theme";
public static final String LOCALIZED_THEME_PREVIEW = "localized-theme-preview";
public static final String CUSTOM_LOCALE = "test";
public static final String CUSTOM_LOCALE_NAME = "Přísný jazyk";
public static final String DEFAULT_LOCALE="en";
@ -53,7 +54,13 @@ public abstract class AbstractUiTest extends AbstractAuthTest {
createTestUserWithAdminClient(false);
}
protected boolean isAccountPreviewTheme() {
return false;
}
protected void configureInternationalizationForRealm(RealmRepresentation realm) {
final String localizedTheme = isAccountPreviewTheme() ? LOCALIZED_THEME_PREVIEW : LOCALIZED_THEME;
// fetch the supported locales for the special test theme that includes some fake test locales
Set<String> supportedLocales = adminClient.serverInfo().getInfo().getThemes().get("login").stream()
.filter(x -> x.getName().equals(LOCALIZED_THEME))
@ -64,7 +71,7 @@ public abstract class AbstractUiTest extends AbstractAuthTest {
realm.setSupportedLocales(supportedLocales);
realm.setLoginTheme(LOCALIZED_THEME);
realm.setAdminTheme(LOCALIZED_THEME);
realm.setAccountTheme(LOCALIZED_THEME);
realm.setAccountTheme(localizedTheme);
realm.setEmailTheme(LOCALIZED_THEME);
}

View file

@ -19,10 +19,9 @@ package org.keycloak.testsuite.ui.account2;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
import org.junit.BeforeClass;
import org.keycloak.common.Profile;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.ProfileAssume;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.ui.AbstractUiTest;
import org.keycloak.testsuite.ui.account2.page.PageNotFound;
import org.keycloak.testsuite.ui.account2.page.WelcomeScreen;
@ -34,8 +33,11 @@ import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLo
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
@EnableFeature(value = Profile.Feature.ACCOUNT2, skipRestart = true)
@EnableFeature(value = Profile.Feature.ACCOUNT_API, skipRestart = true)
public abstract class AbstractAccountTest extends AbstractUiTest {
public static final String ACCOUNT_THEME_NAME = "keycloak-preview";
public static final String ACCOUNT_THEME_NAME_KC = "keycloak-preview";
public static final String ACCOUNT_THEME_NAME_RHSSO = "rh-sso-preview";
@Page
protected WelcomeScreen accountWelcomeScreen;
@ -47,22 +49,25 @@ public abstract class AbstractAccountTest extends AbstractUiTest {
public void addTestRealms(List<RealmRepresentation> testRealms) {
super.addTestRealms(testRealms);
RealmRepresentation testRealmRep = testRealms.get(0);
testRealmRep.setAccountTheme(ACCOUNT_THEME_NAME);
}
@BeforeClass
public static void assumeProfilesEnabled() {
ProfileAssume.assumeFeatureEnabled(Profile.Feature.ACCOUNT2);
ProfileAssume.assumeFeatureEnabled(Profile.Feature.ACCOUNT_API);
testRealmRep.setAccountTheme(getAccountThemeName());
}
@Before
public void beforeAccountTest() {
public void navigateBeforeTest() {
accountWelcomeScreen.navigateTo();
}
@Override
protected boolean isAccountPreviewTheme() {
return true;
}
protected void loginToAccount() {
assertCurrentUrlStartsWithLoginUrlOf(accountWelcomeScreen);
loginPage.form().login(testUser);
}
protected String getAccountThemeName() {
return suiteContext.getAuthServerInfo().isEAP() ? ACCOUNT_THEME_NAME_RHSSO : ACCOUNT_THEME_NAME_KC;
}
}

View file

@ -17,11 +17,9 @@
package org.keycloak.testsuite.ui.account2;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.testsuite.ui.account2.page.AbstractLoggedInPage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
@ -30,8 +28,8 @@ import static org.junit.Assert.assertTrue;
public abstract class BaseAccountPageTest extends AbstractAccountTest {
protected abstract AbstractLoggedInPage getAccountPage();
@Before
public void beforeBaseAccountPageTest() {
@Override
public void navigateBeforeTest() {
getAccountPage().navigateTo();
loginToAccount();
getAccountPage().assertCurrent();

View file

@ -98,7 +98,7 @@ public class DeviceActivityTest extends BaseAccountPageTest {
));
realm.setAccountTheme(LOCALIZED_THEME); // using localized custom theme for the client localized name
realm.setAccountTheme(LOCALIZED_THEME_PREVIEW); // using localized custom theme for the client localized name
}
@Before

View file

@ -56,7 +56,7 @@ public class ReferrerTest extends AbstractAccountTest {
testClient.setEnabled(true);
testRealm.setClients(Collections.singletonList(testClient));
testRealm.setAccountTheme(LOCALIZED_THEME); // using localized custom theme for the fake client localized name
testRealm.setAccountTheme(LOCALIZED_THEME_PREVIEW); // using localized custom theme for the fake client localized name
}
@Test

View file

@ -0,0 +1,344 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.ui.account2;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory;
import org.keycloak.authentication.authenticators.browser.WebAuthnPasswordlessAuthenticatorFactory;
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.models.credential.WebAuthnCredentialModel;
import org.keycloak.models.utils.Base32;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation;
import org.keycloak.testsuite.WebAuthnAssume;
import org.keycloak.testsuite.auth.page.login.OTPSetup;
import org.keycloak.testsuite.auth.page.login.UpdatePassword;
import org.keycloak.testsuite.pages.webauthn.WebAuthnRegisterPage;
import org.keycloak.testsuite.ui.account2.page.AbstractLoggedInPage;
import org.keycloak.testsuite.ui.account2.page.SigningInPage;
import java.time.LocalDateTime;
import java.util.List;
import static java.util.Collections.emptyList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED;
import static org.keycloak.models.UserModel.RequiredAction.CONFIGURE_TOTP;
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
import static org.keycloak.testsuite.util.UIUtils.refreshPageAndWaitForLoad;
import static org.keycloak.testsuite.util.WaitUtils.pause;
import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public class SigningInTest extends BaseAccountPageTest {
public static final String PASSWORD_LABEL = "Password";
public static final String WEBAUTHN_FLOW_ID = "75e2390e-f296-49e6-acf8-6d21071d7e10";
@Page
private SigningInPage signingInPage;
@Page
private UpdatePassword updatePasswordPage;
@Page
private OTPSetup otpSetupPage;
@Page
private WebAuthnRegisterPage webAuthnRegisterPage;
private SigningInPage.CredentialType passwordCredentialType;
private SigningInPage.CredentialType otpCredentialType;
private SigningInPage.CredentialType webAuthnCredentialType;
private SigningInPage.CredentialType webAuthnPwdlessCredentialType;
private TimeBasedOTP otpGenerator;
@Override
protected AbstractLoggedInPage getAccountPage() {
return signingInPage;
}
@Override
protected void afterAbstractKeycloakTestRealmImport() {
super.afterAbstractKeycloakTestRealmImport();
// configure WebAuthn
// we can't do this during the realm import because we'd need to specify all built-in flows as well
AuthenticationFlowRepresentation flow = new AuthenticationFlowRepresentation();
flow.setId(WEBAUTHN_FLOW_ID);
flow.setAlias("webauthn flow");
flow.setProviderId("basic-flow");
flow.setBuiltIn(false);
flow.setTopLevel(true);
testRealmResource().flows().createFlow(flow);
AuthenticationExecutionRepresentation execution = new AuthenticationExecutionRepresentation();
execution.setAuthenticator(WebAuthnAuthenticatorFactory.PROVIDER_ID);
execution.setPriority(10);
execution.setRequirement(REQUIRED.toString());
execution.setParentFlow(WEBAUTHN_FLOW_ID);
testRealmResource().flows().addExecution(execution);
execution.setAuthenticator(WebAuthnPasswordlessAuthenticatorFactory.PROVIDER_ID);
testRealmResource().flows().addExecution(execution);
RequiredActionProviderSimpleRepresentation requiredAction = new RequiredActionProviderSimpleRepresentation();
requiredAction.setProviderId(WebAuthnRegisterFactory.PROVIDER_ID);
requiredAction.setName("blahblah");
testRealmResource().flows().registerRequiredAction(requiredAction);
requiredAction.setProviderId(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID);
testRealmResource().flows().registerRequiredAction(requiredAction);
// no need to actually configure the authentication, in Account Console tests we just verify the registration
}
@Override
public void setDefaultPageUriParameters() {
super.setDefaultPageUriParameters();
updatePasswordPage.setAuthRealm(TEST);
otpSetupPage.setAuthRealm(TEST);
}
@Before
public void beforeSigningInTest() {
passwordCredentialType = signingInPage.getCredentialType(PasswordCredentialModel.TYPE);
otpCredentialType = signingInPage.getCredentialType(OTPCredentialModel.TYPE);
webAuthnCredentialType = signingInPage.getCredentialType(WebAuthnCredentialModel.TYPE_TWOFACTOR);
webAuthnPwdlessCredentialType = signingInPage.getCredentialType(WebAuthnCredentialModel.TYPE_PASSWORDLESS);
RealmRepresentation realm = testRealmResource().toRepresentation();
otpGenerator = new TimeBasedOTP(realm.getOtpPolicyAlgorithm(), realm.getOtpPolicyDigits(), realm.getOtpPolicyPeriod(), 0);
}
@Test
public void categoriesTest() {
testContext.setTestRealmReps(emptyList()); // reimport realm after this test
assertEquals(3, signingInPage.getCategoriesCount());
assertEquals("Password", signingInPage.getCategoryTitle("password"));
assertEquals("Two-Factor Authentication", signingInPage.getCategoryTitle("two-factor"));
assertEquals("Passwordless", signingInPage.getCategoryTitle("passwordless"));
// Delete WebAuthn flow ==> Passwordless category should disappear
testRealmResource().flows().deleteFlow(WEBAUTHN_FLOW_ID);
refreshPageAndWaitForLoad();
assertEquals(2, signingInPage.getCategoriesCount());
}
@Test
public void updatePasswordTest() {
SigningInPage.UserCredential passwordCred =
passwordCredentialType.getUserCredential(testUserResource().credentials().get(0).getId());
assertFalse(passwordCredentialType.isSetUpLinkVisible());
assertTrue(passwordCredentialType.isSetUp());
assertUserCredential(PASSWORD_LABEL, false, passwordCred);
LocalDateTime previousCreatedAt = passwordCred.getCreatedAt();
log.info("Waiting one minute to ensure createdAt will change");
pause(60000);
final String newPwd = "Keycloak is the best!";
passwordCred.clickUpdateBtn();
updatePasswordPage.assertCurrent();
updatePasswordPage.updatePasswords(newPwd, newPwd);
// TODO uncomment this once KEYCLOAK-12852 is resolved
// signingInPage.assertCurrent();
assertUserCredential(PASSWORD_LABEL, false, passwordCred);
assertNotEquals(previousCreatedAt, passwordCred.getCreatedAt());
// TODO KEYCLOAK-12875 try to update/set up password when user has no password configured
}
@Test
public void otpTest() {
assertFalse(otpCredentialType.isSetUp());
otpCredentialType.clickSetUpLink();
otpSetupPage.cancel();
// TODO uncomment this once KEYCLOAK-12852 is resolved
// signingInPage.assertCurrent();
assertFalse(otpCredentialType.isSetUp());
assertEquals("Authenticator Application", otpCredentialType.getTitle());
final String label1 = "OTP is secure";
final String label2 = "OTP is inconvenient";
SigningInPage.UserCredential otp1 = addOtpCredential(label1);
assertTrue(otpCredentialType.isSetUp());
assertEquals(1, otpCredentialType.getUserCredentialsCount());
assertUserCredential(label1, true, otp1);
SigningInPage.UserCredential otp2 = addOtpCredential(label2);
assertEquals(2, otpCredentialType.getUserCredentialsCount());
assertUserCredential(label2, true, otp2);
testRemoveCredential(otp1);
}
@Test
public void twoFactorWebAuthnTest() {
testWebAuthn(false);
}
@Test
public void passwordlessWebAuthnTest() {
testWebAuthn(true);
}
private void testWebAuthn(boolean passwordless) {
WebAuthnAssume.assumeChrome(driver); // we need some special flags to be able to register security key
SigningInPage.CredentialType credentialType;
final String expectedHelpText;
if (passwordless) {
credentialType = webAuthnPwdlessCredentialType;
expectedHelpText = "Use your security key for passwordless log in.";
}
else {
credentialType = webAuthnCredentialType;
expectedHelpText = "Use your security key to log in.";
}
assertFalse(credentialType.isSetUp());
// no way to simulate registration cancellation
assertEquals("Security Key", credentialType.getTitle());
assertEquals(expectedHelpText, credentialType.getHelpText());
final String label1 = "WebAuthn is convenient";
final String label2 = "but not yet widely adopted";
SigningInPage.UserCredential webAuthn1 = addWebAuthnCredential(label1, passwordless);
assertTrue(credentialType.isSetUp());
assertEquals(1, credentialType.getUserCredentialsCount());
assertUserCredential(label1, true, webAuthn1);
SigningInPage.UserCredential webAuthn2 = addWebAuthnCredential(label2, passwordless);
assertEquals(2, credentialType.getUserCredentialsCount());
assertUserCredential(label2, true, webAuthn2);
testRemoveCredential(webAuthn1);
}
@Test
public void setUpLinksTest() {
testSetUpLink(otpCredentialType, CONFIGURE_TOTP.name());
testSetUpLink(webAuthnCredentialType, WebAuthnRegisterFactory.PROVIDER_ID);
testSetUpLink(webAuthnPwdlessCredentialType, WebAuthnPasswordlessRegisterFactory.PROVIDER_ID);
}
private void testSetUpLink(SigningInPage.CredentialType credentialType, String requiredActionProviderId) {
assertTrue("Set up link is visible", credentialType.isSetUpLinkVisible());
RequiredActionProviderRepresentation requiredAction = new RequiredActionProviderRepresentation();
requiredAction.setEnabled(false);
testRealmResource().flows().updateRequiredAction(requiredActionProviderId, requiredAction);
refreshPageAndWaitForLoad();
assertFalse("Set up link is not visible", credentialType.isSetUpLinkVisible());
assertFalse("Credential type is not set up", credentialType.isSetUp()); // this also check the cred type is present
assertNotNull("Title is present", credentialType.getTitle());
}
private SigningInPage.UserCredential addOtpCredential(String label) {
otpCredentialType.clickSetUpLink();
otpSetupPage.assertCurrent();
otpSetupPage.clickManualMode();
String secret = new String(Base32.decode(otpSetupPage.getSecretKey()));
String code = otpGenerator.generateTOTP(secret);
otpSetupPage.setTotp(code);
otpSetupPage.setUserLabel(label);
otpSetupPage.submit();
// TODO uncomment this once KEYCLOAK-12852 is resolved
// signingInPage.assertCurrent();
return getNewestUserCredential(otpCredentialType);
}
private SigningInPage.UserCredential addWebAuthnCredential(String label, boolean passwordless) {
SigningInPage.CredentialType credentialType = passwordless ? webAuthnPwdlessCredentialType : webAuthnCredentialType;
credentialType.clickSetUpLink(true);
webAuthnRegisterPage.registerWebAuthnCredential(label);
waitForPageToLoad();
// TODO uncomment this once KEYCLOAK-12852 is resolved
// signingInPage.assertCurrent();
return getNewestUserCredential(credentialType);
}
private SigningInPage.UserCredential getNewestUserCredential(SigningInPage.CredentialType credentialType) {
List<CredentialRepresentation> credentials = testUserResource().credentials();
SigningInPage.UserCredential userCredential =
credentialType.getUserCredential(credentials.get(credentials.size() - 1).getId());
assertTrue(userCredential.isPresent());
return userCredential;
}
private void testRemoveCredential(SigningInPage.UserCredential userCredential) {
int countBeforeRemove = userCredential.getCredentialType().getUserCredentialsCount();
testModalDialog(userCredential::clickRemoveBtn, () -> {
assertTrue(userCredential.isPresent());
assertEquals(countBeforeRemove, userCredential.getCredentialType().getUserCredentialsCount());
});
assertFalse(userCredential.isPresent());
assertEquals(countBeforeRemove - 1, userCredential.getCredentialType().getUserCredentialsCount());
signingInPage.alert().assertSuccess();
}
private void assertUserCredential(String expectedUserLabel, boolean removable, SigningInPage.UserCredential userCredential) {
assertEquals(expectedUserLabel, userCredential.getUserLabel());
// we expect the credential was created/edited no longer that 2 minutes ago (1 minute might not be enough in some corner cases)
LocalDateTime beforeNow = LocalDateTime.now().minusMinutes(2);
LocalDateTime now = LocalDateTime.now();
// createdAt doesn't specify seconds so it should be something like 12:47:00
LocalDateTime createdAt = userCredential.getCreatedAt();
assertTrue("Creation time should be after time before update", createdAt.isAfter(beforeNow));
assertTrue("Creation time should be before now", createdAt.isBefore(now));
assertEquals("Remove button visible", removable, userCredential.isRemoveBtnDisplayed());
assertEquals("Update button visible", !removable, userCredential.isUpdateBtnDisplayed());
}
}

View file

@ -0,0 +1,228 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.ui.account2.page;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import static org.keycloak.testsuite.util.UIUtils.clickLink;
import static org.keycloak.testsuite.util.UIUtils.getTextFromElement;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public class SigningInPage extends AbstractLoggedInPage {
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("MMMM d, yyyy h:mm a", Locale.ENGLISH);
private static final String CATEG_TITLE = "-categ-title";
@Override
public String getPageId() {
return "signingin";
}
@Override
public String getParentPageId() {
return ACCOUNT_SECURITY_ID;
}
public CredentialType getCredentialType(String type) {
return new CredentialType(type);
}
public String getCategoryTitle(String categoryId) {
return getTextFromElement(driver.findElement(By.id(categoryId + CATEG_TITLE)));
}
public int getCategoriesCount() {
String xpath = String.format("//*[contains(@id,'%s')]", CATEG_TITLE);
return driver.findElements(By.xpath(xpath)).size();
}
public class CredentialType {
private static final String NOT_SET_UP = "not-set-up";
private static final String SET_UP = "set-up";
private static final String TITLE = "cred-title";
private static final String HELP = "cred-help";
private final String type;
private CredentialType(String type) {
this.type = type;
}
public String getType() {
return type;
}
private WebElement getItemElement(String item) {
String elementId = String.format("%s-%s", type, item);
return driver.findElement(By.id(elementId));
}
public int getUserCredentialsCount() {
String xpath = String.format("//li[starts-with(@id,'%s')]", type + "-" + UserCredential.ROW);
return driver.findElements(By.xpath(xpath)).size();
}
public UserCredential getUserCredential(String id) {
return new UserCredential(id, this);
}
public boolean isSetUp() {
boolean notSetUpLabelPresent;
try {
notSetUpLabelPresent = getItemElement(NOT_SET_UP).isDisplayed();
}
catch (NoSuchElementException e) {
notSetUpLabelPresent = false;
}
int userCredentialsCount = getUserCredentialsCount();
if (notSetUpLabelPresent && userCredentialsCount == 0) {
return false;
}
else if (!notSetUpLabelPresent && userCredentialsCount > 0) {
return true;
}
else {
throw new IllegalStateException("Unexpected \"not set up label\" state");
}
}
public void clickSetUpLink() {
clickSetUpLink(false);
}
public void clickSetUpLink(boolean skipWaits) {
WebElement element = getItemElement(SET_UP);
if (skipWaits) {
// this is for the very special case of registering webauthn; chromedriver doesn't seem to like requesting
// getCurrentUrl during security key registration
element.click();
}
else {
clickLink(element);
}
}
public boolean isSetUpLinkVisible() {
try {
return getItemElement(SET_UP).isDisplayed();
}
catch (NoSuchElementException e) {
return false;
}
}
public String getTitle() {
return getTextFromElement(getItemElement(TITLE));
}
public String getHelpText() {
return getTextFromElement(getItemElement(HELP));
}
}
public class UserCredential {
private static final String ROW = "row";
private static final String LABEL = "label";
private static final String CREATED_AT = "created-at";
private static final String UPDATE = "update";
private static final String REMOVE = "remove";
private final String fullId;
private final String id;
private final CredentialType credentialType;
private UserCredential(String id, CredentialType credentialType) {
this.fullId = id;
this.id = id.substring(0,8);
this.credentialType = credentialType;
}
public String getId() {
return fullId;
}
public CredentialType getCredentialType() {
return credentialType;
}
private WebElement getItemElement(String item) {
String elementId = String.format("%s-%s-%s", credentialType.getType(), item, id);
return driver.findElement(By.id(elementId));
}
private boolean isItemDisplayed(String item) {
try {
return getItemElement(item).isDisplayed();
}
catch (NoSuchElementException e) {
return false;
}
}
private String getTextFromItem(String item) {
return getTextFromElement(getItemElement(item));
}
public String getUserLabel() {
return getTextFromItem(LABEL);
}
public String getCreatedAtStr() {
return getTextFromItem(CREATED_AT).split("Created: ")[1];
}
public LocalDateTime getCreatedAt() {
return LocalDateTime.parse(getCreatedAtStr(), DATE_TIME_FORMATTER);
}
public void clickUpdateBtn() {
clickLink(getItemElement(UPDATE));
}
public void clickRemoveBtn() {
clickLink(getItemElement(REMOVE));
}
public boolean isUpdateBtnDisplayed() {
return isItemDisplayed(UPDATE);
}
public boolean isRemoveBtnDisplayed() {
return isItemDisplayed(REMOVE);
}
public boolean isPresent() {
try {
return getItemElement(ROW).isDisplayed();
}
catch (NoSuchElementException e) {
return false;
}
}
}
}

View file

@ -65,7 +65,7 @@ Running tests
1. Build the project (no need to build the whole distribution).
1. Run:
```
mvn clean verify -f testsuite/integration-arquillian/tests/other/base-ui -Dtest=**.account2.** -Dkeycloak.profile.feature.account2=enabled -Dkeycloak.profile.feature.account_api=enabled -Dbrowser=chrome
mvn clean verify -f testsuite/integration-arquillian/tests/other/base-ui -Dtest=**.account2.** -Dbrowser=chrome -DchromeArguments=--enable-web-authentication-testing-api
```
Use `chrome` or `firefox` as the browser, other browsers are currently broken for the testsuite.

View file

@ -125,6 +125,10 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
});
}
public static credElementId(credType: CredType, credId: string, item: string): string {
return `${credType}-${item}-${credId.substring(0,8)}`;
}
public render(): React.ReactNode {
return (
<ContentPage title="signingIn"
@ -140,7 +144,7 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
return (<> {
Array.from(this.state.credentialContainers.keys()).map(category => (
<StackItem key={category} isFilled>
<Title headingLevel={TitleLevel.h2} size='2xl'>
<Title id={`${category}-categ-title`} headingLevel={TitleLevel.h2} size='2xl'>
<strong><Msg msgKey={category}/></strong>
</Title>
<DataList aria-label='foo'>
@ -178,6 +182,7 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
const credContainer: CredentialContainer = credTypeMap.get(credType)!;
const userCredentials: UserCredential[] = credContainer.userCredentials;
const removeable: boolean = credContainer.removeable;
const type: string = credContainer.type;
const displayName: string = credContainer.displayName;
if (userCredentials.length === 0) {
@ -188,7 +193,7 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
<DataListItemCells
dataListCells={[
<DataListCell key={'no-credentials-cell-0'}/>,
<strong key={'no-credentials-cell-1'}><Msg msgKey='notSetUp' params={[localizedDisplayName]}/></strong>,
<strong id={`${type}-not-set-up`} key={'no-credentials-cell-1'}><Msg msgKey='notSetUp' params={[localizedDisplayName]}/></strong>,
<DataListCell key={'no-credentials-cell-2'}/>
]}/>
</DataListItemRow>
@ -209,12 +214,12 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
return (
<React.Fragment key='userCredentials'> {
userCredentials.map(credential => (
<DataListItem key={'credential-list-item-' + credential.id} aria-labelledby={'credential-list-item-' + credential.userLabel}>
<DataListItem id={`${SigningInPage.credElementId(type, credential.id, 'row')}`} key={'credential-list-item-' + credential.id} aria-labelledby={'credential-list-item-' + credential.userLabel}>
<DataListItemRow key={'userCredentialRow-' + credential.id}>
<DataListItemCells
dataListCells={[
<DataListCell key={'userLabel-' + credential.id}>{credential.userLabel}</DataListCell>,
<DataListCell key={'created-' + credential.id}><strong><Msg msgKey='credentialCreatedAt'/>: </strong>{credential.strCreatedDate}</DataListCell>,
<DataListCell id={`${SigningInPage.credElementId(type, credential.id, 'label')}`} key={'userLabel-' + credential.id}>{credential.userLabel}</DataListCell>,
<DataListCell id={`${SigningInPage.credElementId(type, credential.id, 'created-at')}`} key={'created-' + credential.id}><strong><Msg msgKey='credentialCreatedAt'/>: </strong>{credential.strCreatedDate}</DataListCell>,
<DataListCell key={'spacer-' + credential.id}/>
]}/>
@ -246,15 +251,15 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
dataListCells={[
<DataListCell width={5} key={'credTypeTitle-' + credContainer.type}>
<Title headingLevel={TitleLevel.h3} size='2xl'>
<strong><Msg msgKey={credContainer.displayName}/></strong>
<strong id={`${credContainer.type}-cred-title`}><Msg msgKey={credContainer.displayName}/></strong>
</Title>
<Msg msgKey={credContainer.helptext}/>
<span id={`${credContainer.type}-cred-help`}><Msg msgKey={credContainer.helptext}/></span>
</DataListCell>,
]}/>
{credContainer.createAction &&
<DataListAction aria-labelledby='foo' aria-label='foo action' id={'setUpAction-' + credContainer.type}>
<button className="pf-c-button pf-m-link" type="button" onClick={()=> setupAction.execute()}>
<button id={`${credContainer.type}-set-up`} className="pf-c-button pf-m-link" type="button" onClick={()=> setupAction.execute()}>
<span className="pf-c-button__icon">
<i className="fas fa-plus-circle" aria-hidden="true"></i>
</span>
@ -280,7 +285,7 @@ class CredentialAction extends React.Component<CredentialActionProps> {
if (this.props.updateAction) {
return (
<DataListAction aria-labelledby='foo' aria-label='foo action' id={'updateAction-' + this.props.credential.id}>
<Button variant='primary'onClick={()=> this.props.updateAction.execute()}><Msg msgKey='update'/></Button>
<Button id={`${SigningInPage.credElementId(this.props.credential.type, this.props.credential.id, 'update')}`} variant='primary'onClick={()=> this.props.updateAction.execute()}><Msg msgKey='update'/></Button>
</DataListAction>
)
}
@ -290,6 +295,7 @@ class CredentialAction extends React.Component<CredentialActionProps> {
return (
<DataListAction aria-labelledby='foo' aria-label='foo action' id={'removeAction-' + this.props.credential.id }>
<ContinueCancelModal buttonTitle='remove'
buttonId={`${SigningInPage.credElementId(this.props.credential.type, this.props.credential.id, 'remove')}`}
modalTitle={Msg.localize('removeCred', [userLabel])}
modalMessage={Msg.localize('stopUsingCred', [userLabel])}
onContinue={() => this.props.credRemover(this.props.credential.id, userLabel)}