KEYCLOAK-12105 Add UI tests for Single page to manage credentials
This commit is contained in:
parent
d417639cb8
commit
0801cfb01f
19 changed files with 654 additions and 29 deletions
|
@ -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_,
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ public class OAuthGrant extends RequiredActions {
|
|||
clickLink(acceptButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
clickLink(cancelButton);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
#encoding: utf-8
|
||||
locale_test=Přísný jazyk
|
||||
client_localized-client=Přespříliš lokalizovaný klient
|
|
@ -0,0 +1,4 @@
|
|||
#encoding: utf-8
|
||||
locale_test=Přísný jazyk
|
||||
accountManagementWelcomeMessage=Vítejte v Keycloaku
|
||||
personalInfoHtmlTitle=Osobní údaje
|
|
@ -0,0 +1,2 @@
|
|||
parent=${theme-default-name}-preview
|
||||
locales=en,lang01,lang02,lang03,lang04,lang05,test,lang06,lang07,lang08,lang09,lang10
|
|
@ -1,2 +1,2 @@
|
|||
parent=keycloak-preview
|
||||
parent=${theme-default-name}
|
||||
locales=en,lang01,lang02,lang03,lang04,lang05,test,lang06,lang07,lang08,lang09,lang10
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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)}
|
||||
|
|
Loading…
Reference in a new issue