diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml index d160a7fddc..479aaf83c4 100644 --- a/testsuite/integration-arquillian/tests/base/pom.xml +++ b/testsuite/integration-arquillian/tests/base/pom.xml @@ -236,25 +236,6 @@ - - unpack-undertow-server - generate-test-resources - - unpack - - - - - org.keycloak.testsuite - integration-arquillian-servers-auth-server-undertow - ${project.version} - jar - ${containers.home}/auth-server-undertow - - - *.jks,*.crt,*.truststore,*.crl,*.key,certs/clients/* - - diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java index 0bc8de35ab..1a7291f12b 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java @@ -58,6 +58,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.IDToken; import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.RefreshToken; @@ -430,7 +431,7 @@ public class OAuthClient { return introspectTokenWithClientCredential(clientId, clientSecret, "refresh_token", tokenToIntrospect); } - public AccessTokenResponse doGrantAccessTokenRequest(String clientSecret, String username, String password) throws Exception { + public AccessTokenResponse doGrantAccessTokenRequest(String clientSecret, String username, String password) throws Exception { return doGrantAccessTokenRequest(realm, username, password, null, clientId, clientSecret); } @@ -440,6 +441,11 @@ public class OAuthClient { public AccessTokenResponse doGrantAccessTokenRequest(String realm, String username, String password, String totp, String clientId, String clientSecret) throws Exception { + return doGrantAccessTokenRequest(realm, username, password, totp, clientId, clientSecret, null); + } + + public AccessTokenResponse doGrantAccessTokenRequest(String realm, String username, String password, String totp, + String clientId, String clientSecret, String userAgent) throws Exception { try (CloseableHttpClient client = httpClient.get()) { HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm)); @@ -472,6 +478,10 @@ public class OAuthClient { parameters.add(new BasicNameValuePair(OAuth2Constants.SCOPE, scope)); } + if (userAgent != null) { + post.addHeader("User-Agent", userAgent); + } + UrlEncodedFormEntity formEntity; try { formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); @@ -1132,6 +1142,7 @@ public class OAuthClient { private String refreshToken; // OIDC Financial API Read Only Profile : scope MUST be returned in the response from Token Endpoint private String scope; + private String sessionState; private String error; private String errorDescription; @@ -1163,6 +1174,7 @@ public class OAuthClient { tokenType = (String) responseJson.get("token_type"); expiresIn = (Integer) responseJson.get("expires_in"); refreshExpiresIn = (Integer) responseJson.get("refresh_expires_in"); + sessionState = (String) responseJson.get("session_state"); // OIDC Financial API Read Only Profile : scope MUST be returned in the response from Token Endpoint if (responseJson.containsKey(OAuth2Constants.SCOPE)) { @@ -1222,6 +1234,10 @@ public class OAuthClient { return scope; } + public String getSessionState() { + return sessionState; + } + public Map getHeaders() { return headers; } diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/main/resources/themes/localized-theme/account/messages/messages_en.properties b/testsuite/integration-arquillian/tests/other/base-ui/src/main/resources/themes/localized-theme/account/messages/messages_en.properties index 8a13ded071..da90117b31 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/main/resources/themes/localized-theme/account/messages/messages_en.properties +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/main/resources/themes/localized-theme/account/messages/messages_en.properties @@ -1,3 +1,3 @@ #encoding: utf-8 locale_test=Přísný jazyk -client_fake-client-name=Referrer Test Client +client_localized-client=Přespříliš lokalizovaný klient diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/AbstractUiTest.java b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/AbstractUiTest.java index c185a93e07..b42249fcae 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/AbstractUiTest.java +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/AbstractUiTest.java @@ -39,6 +39,8 @@ public abstract class AbstractUiTest extends AbstractAuthTest { public static final String CUSTOM_LOCALE_NAME = "Přísný jazyk"; public static final String DEFAULT_LOCALE="en"; public static final String DEFAULT_LOCALE_NAME = "English"; + public static final String LOCALE_CLIENT_NAME = "${client_localized-client}"; + public static final String LOCALE_CLIENT_NAME_LOCALIZED = "Přespříliš lokalizovaný klient"; @BeforeClass public static void assumeSupportedBrowser() { diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/BaseAccountPageTest.java b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/BaseAccountPageTest.java index f12fad6491..64af58062c 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/BaseAccountPageTest.java +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/BaseAccountPageTest.java @@ -21,6 +21,7 @@ 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; /** @@ -49,4 +50,15 @@ public abstract class BaseAccountPageTest extends AbstractAccountTest { getAccountPage().sidebar().isNavSubsectionExpanded(getAccountPage().getParentPageId())); } } + + protected void testModalDialog(Runnable triggerModal, Runnable onCancel) { + triggerModal.run(); + getAccountPage().modal().assertIsDisplayed(); + getAccountPage().modal().clickCancel(); + getAccountPage().modal().assertIsNotDisplayed(); + onCancel.run(); + triggerModal.run(); + getAccountPage().modal().clickConfirm(); + getAccountPage().modal().assertIsNotDisplayed(); + } } diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/DeviceActivityTest.java b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/DeviceActivityTest.java index 1c3f83d8c5..323209743c 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/DeviceActivityTest.java +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/DeviceActivityTest.java @@ -18,13 +18,51 @@ package org.keycloak.testsuite.ui.account2; import org.jboss.arquillian.graphene.page.Page; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.common.util.Time; +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.ui.account2.page.AbstractLoggedInPage; import org.keycloak.testsuite.ui.account2.page.DeviceActivityPage; +import org.keycloak.testsuite.util.ClientBuilder; +import org.keycloak.testsuite.util.OAuthClient; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD; +import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; /** * @author Vaclav Muzikar */ public class DeviceActivityTest extends BaseAccountPageTest { + public static final String TEST_CLIENT_ID = "test-client"; + public static final String TEST_CLIENT_SECRET = "top secret stuff"; + public static final String TEST_CLIENT2_ID = "test-client2"; + public static final String TEST_CLIENT2_SECRET = "even more top secret stuff"; + public static final String TEST_CLIENT3_ID = "test-client3"; + public static final String TEST_CLIENT3_SECRET = "dunno"; + public static final String TEST_CLIENT3_NAME = "Příliš žluťoučký kůň"; + @Page private DeviceActivityPage deviceActivityPage; @@ -33,5 +71,310 @@ public class DeviceActivityTest extends BaseAccountPageTest { return deviceActivityPage; } - // TODO implement this! (KEYCLOAK-12106) + @Override + public void addTestRealms(List testRealms) { + super.addTestRealms(testRealms); + RealmRepresentation realm = testRealms.get(0); + + realm.setClients(Arrays.asList( + ClientBuilder + .create() + .clientId(TEST_CLIENT_ID) // client with no name + .secret(TEST_CLIENT_SECRET) + .directAccessGrants() + .build(), + ClientBuilder + .create(). + clientId(TEST_CLIENT2_ID) + .name(LOCALE_CLIENT_NAME) // client with localized name + .secret(TEST_CLIENT2_SECRET) + .directAccessGrants().build(), + ClientBuilder + .create(). + clientId(TEST_CLIENT3_ID) + .name(TEST_CLIENT3_NAME) // client without localized name + .secret(TEST_CLIENT3_SECRET) + .directAccessGrants().build() + + )); + + realm.setAccountTheme(LOCALIZED_THEME); // using localized custom theme for the client localized name + } + + @Before + public void beforeDeviceActivityTest() { + oauth.clientId(TEST_CLIENT3_ID); + } + + @Test + public void browsersTest() { + Map browserSessions = new HashMap<>(); + Arrays.stream(Browsers.values()).forEach(b -> { + browserSessions.put(b, createSession(b)); + }); + + deviceActivityPage.clickRefreshPage(); + + browserSessions.forEach((browser, sessionId) -> { + assertSession(browser, deviceActivityPage.getSession(sessionId)); + }); + + assertEquals(Browsers.values().length + 1, deviceActivityPage.getSessionsCount()); // + 1 for the current session + } + + @Test + public void currentSessionTest() { + createSession(Browsers.CHROME); + createSession(Browsers.SAFARI); + + deviceActivityPage.clickRefreshPage(); + + assertEquals(3, deviceActivityPage.getSessionsCount()); + + DeviceActivityPage.Session currentSession = deviceActivityPage.getSessionByIndex(0); // current session should be first + assertSessionRowsAreNotEmpty(currentSession, false); + assertTrue("Browser identification should be present", currentSession.isBrowserDisplayed()); + assertTrue("Current session badge should be present", currentSession.hasCurrentBadge()); + assertFalse("Icon should be present", currentSession.getBrowserIconName().isEmpty()); + } + + @Test + public void signOutTest() { + assertFalse("Sign out all shouldn't be displayed", deviceActivityPage.isSignOutAllDisplayed()); + DeviceActivityPage.Session chromeSession = deviceActivityPage.getSession(createSession(Browsers.CHROME)); + createSession(Browsers.SAFARI); + deviceActivityPage.clickRefreshPage(); + assertTrue("Sign out all should be displayed", deviceActivityPage.isSignOutAllDisplayed()); + assertEquals(3, testUserResource().getUserSessions().size()); + assertThat(testUserResource().getUserSessions(), + hasItem(hasProperty("id", is(chromeSession.getFullSessionId())))); + + // sign out one session + testModalDialog(chromeSession::clickSignOut, () -> { + assertEquals(3, testUserResource().getUserSessions().size()); // no change, all sessions still present + }); + deviceActivityPage.alert().assertSuccess(); + assertFalse("Chrome session should be gone", chromeSession.isPresent()); + assertEquals(2, testUserResource().getUserSessions().size()); + assertThat(testUserResource().getUserSessions(), + not(hasItem(hasProperty("id", is(chromeSession.getFullSessionId()))))); + + // sign out all sessions + testModalDialog(deviceActivityPage::clickSignOutAll, () -> { + assertEquals(2, testUserResource().getUserSessions().size()); // no change + }); + accountWelcomeScreen.assertCurrent(); + assertEquals(0, testUserResource().getUserSessions().size()); + } + + @Test + public void clientsTest() { + String sessionId = createSession(Browsers.CHROME); + + // attach more clients to the session + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName(TEST); + UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId); + + ClientModel client2 = session.clientLocalStorage().getClientByClientId(TEST_CLIENT2_ID, realm); + ClientModel client3 = session.clientLocalStorage().getClientByClientId(TEST_CLIENT3_ID, realm); + + session.sessions().createClientSession(realm, client2, userSession); + session.sessions().createClientSession(realm, client3, userSession); + }); + + deviceActivityPage.clickRefreshPage(); + + List expectedClients = Arrays.asList(TEST_CLIENT_ID, LOCALE_CLIENT_NAME_LOCALIZED, TEST_CLIENT3_NAME); + String[] actualClients = deviceActivityPage.getSession(sessionId).getClients().split(", "); + assertThat(expectedClients, containsInAnyOrder(actualClients)); + + assertEquals("Account Console", deviceActivityPage.getSessionByIndex(0).getClients()); + } + + @Test + public void timesTests() { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEEE, MMMM d, yyyy h:mm a", Locale.ENGLISH); + LocalDateTime now = LocalDateTime.now(); + String nowStr = now.format(formatter); + + String sessionId = createSession(Browsers.CHROME); + + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName(TEST); + UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId); + + userSession.setLastSessionRefresh(Time.currentTime() + 120); + }); + + deviceActivityPage.clickRefreshPage(); + + DeviceActivityPage.Session session = deviceActivityPage.getSession(sessionId); + + String startedAtStr = session.getStarted(); + LocalDateTime startedAt = LocalDateTime.parse(startedAtStr, formatter); + LocalDateTime lastAccessed = LocalDateTime.parse(session.getLastAccess(), formatter); + LocalDateTime expiresAt = LocalDateTime.parse(session.getExpires(), formatter); + + assertTrue("Last access should be after started at", lastAccessed.isAfter(startedAt)); + assertTrue("Expires at should be after last access", expiresAt.isAfter(lastAccessed)); + assertTrue("Last accessed should be in the future", lastAccessed.isAfter(now)); + assertEquals(nowStr, startedAtStr); + + int ssoLifespan = testRealmResource().toRepresentation().getSsoSessionMaxLifespan(); + assertEquals(startedAt.plusSeconds(ssoLifespan), expiresAt); + } + + @Test + public void ipTest() { + final String ip = "146.58.69.12"; + + String sessionId = "abcdefg"; + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName(TEST); + ClientModel client = session.clientLocalStorage().getClientByClientId(TEST_CLIENT_ID, realm); + UserModel user = session.users().getUserByUsername("test", realm); // cannot use testUser.getUsername() because it throws NotSerializableException for no apparent reason (or maybe I'm just stupid :D) + + UserSessionModel userSession = session.sessions().createUserSession(sessionId, realm, user, "test", ip, "form", false, null, null); + session.sessions().createClientSession(realm, client, userSession); + }); + + deviceActivityPage.clickRefreshPage(); + + assertEquals(ip, deviceActivityPage.getSession(sessionId).getIp()); + } + + private String createSession(Browsers browser) { + log.info("Creating session for " + browser); + OAuthClient.AccessTokenResponse res; + try { + // using direct grant not to use current browser + res = oauth.doGrantAccessTokenRequest( + TEST, testUser.getUsername(), PASSWORD, null, TEST_CLIENT_ID, TEST_CLIENT_SECRET, browser.userAgent); + } + catch (Exception e) { + throw new RuntimeException(e); + } + return res.getSessionState(); // session id + } + + private void assertSession(Browsers browser, DeviceActivityPage.Session session) { + log.infof("Asserting %s (session %s)", browser, session.getSessionId()); + assertTrue("Session should be present", session.isPresent()); + if (browser.sessionBrowser != null) { + assertEquals(browser.sessionBrowser, session.getBrowser()); + } + else { + assertFalse("Browser identification shouldn't be present", session.isBrowserDisplayed()); + } + assertEquals(browser.iconName, session.getBrowserIconName()); + assertFalse("Session shouldn't have current badge", session.hasCurrentBadge()); // we don't test current session + assertSessionRowsAreNotEmpty(session, true); + } + + private void assertSessionRowsAreNotEmpty(DeviceActivityPage.Session session, boolean expectSignOutPresent){ + assertFalse("IP address shouldn't be empty", session.getIp().isEmpty()); + assertFalse("Last accessed shouldn't be empty", session.getLastAccess().isEmpty()); + assertFalse("Started shouldn't be empty", session.getStarted().isEmpty()); + assertFalse("Expires shouldn't be empty", session.getExpires().isEmpty()); + assertFalse("Clients shouldn't be empty", session.getClients().isEmpty()); + assertEquals("Sign out button visibility", expectSignOutPresent, session.isSignOutDisplayed()); + } + + public enum Browsers { + CHROME( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36", + "Chrome/78.0.3904 / Windows 10", + "chrome" + ), + CHROMIUM( + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.30 (KHTML, like Gecko) Ubuntu/11.04 Chromium/12.0.742.112 Chrome/12.0.742.112 Safari/534.30", + "Chromium/12.0.742 / Ubuntu 11.04", + "chrome" + ), + FIREFOX( + "Mozilla/5.0 (X11; Fedora;Linux x86; rv:60.0) Gecko/20100101 Firefox/60.0", + "Firefox/60.0 / Fedora", + "firefox" + ), + EDGE( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763", + "Edge/18.17763 / Windows 10", + "edge" + ), + // TODO uncomment this once KEYCLOAK-12445 is resolved +// CHREDGE( // Edge based on Chromium +// "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.74 Safari/537.36 Edg/79.0.309.43", +// "Edge/79.0.309 / Mac OS X 10.15.1", +// "edge" +// ), + IE( + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko", + "IE/11.0 / Windows 7", + "ie" + ), + SAFARI( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Safari/605.1.15", + "Safari/13.0.3 / Mac OS X 10.15.1", + "safari" + ), + OPERA( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36 OPR/56.0.3051.52", + "Opera/56.0.3051 / Windows 10", + "opera" + ), + YANDEX( + "Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 YaBrowser/17.6.1.749 Yowser/2.5 Safari/537.36", + "Yandex Browser/17.6.1 / Windows 8.1", + "yandex" + ), + CHROME_ANDROID( + "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Mobile Safari/537.36", + "Chrome Mobile/68.0.3440 / Android 6.0", + "chrome" + ), + SAFARI_IOS( + "Mozilla/5.0 (iPhone; CPU iPhone OS 13_1_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.1 Mobile/15E148 Safari/604.1", + "Mobile Safari/13.0.1 / iOS 13.1.3", + "safari" + ), + UNKNOWN_BROWSER( + "Top-secret government browser running on top-secret OS", + null, + "default" + ), + UNKNOWN_OS( + "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36", + "Chrome/78.0.3904 / Other", // "Unknown Operating System" is actually never displayed (even though it's implemented) + "chrome" + ), + UNKNOWN_OS_VERSION( + "Mozilla/5.0 (Windows 256.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36", + "Chrome/78.0.3904 / Windows", + "chrome" + ); + // not sure what "Amazon" browser is supposed to be (it's specified in DeviceActivityPage.tsx) + + private String userAgent; + private String sessionBrowser; // how the browser is interpreted by the sessions endpoint + private String iconName; + + Browsers(String userAgent, String sessionBrowser, String iconName) { + this.userAgent = userAgent; + this.sessionBrowser = sessionBrowser; + this.iconName = iconName; + } + + public String userAgent() { + return userAgent; + } + + public String sessionBrowser() { + return sessionBrowser; + } + + public String iconName() { + return iconName; + } + } } diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/ReferrerTest.java b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/ReferrerTest.java index 78560ae527..b4258f2033 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/ReferrerTest.java +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/ReferrerTest.java @@ -36,8 +36,7 @@ import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals; */ public class ReferrerTest extends AbstractAccountTest { public static final String FAKE_CLIENT_ID = "fake-client-name"; - public static final String FAKE_CLIENT_NAME_LOCALIZE = "Referrer Test Client"; - public static final String REFERRER_LINK_TEXT = "Back to " + FAKE_CLIENT_NAME_LOCALIZE; + public static final String REFERRER_LINK_TEXT = "Back to " + LOCALE_CLIENT_NAME_LOCALIZED; @Page private WelcomeScreen welcomeScreen; @@ -52,7 +51,7 @@ public class ReferrerTest extends AbstractAccountTest { ClientRepresentation testClient = new ClientRepresentation(); testClient.setClientId(FAKE_CLIENT_ID); - testClient.setName("${client_" + FAKE_CLIENT_ID + "}"); + testClient.setName(LOCALE_CLIENT_NAME); testClient.setRedirectUris(Collections.singletonList(getFakeClientUrl())); testClient.setEnabled(true); diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/AbstractLoggedInPage.java b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/AbstractLoggedInPage.java index e08080f12e..44a1387690 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/AbstractLoggedInPage.java +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/AbstractLoggedInPage.java @@ -19,6 +19,7 @@ package org.keycloak.testsuite.ui.account2.page; import org.jboss.arquillian.graphene.page.Page; import org.keycloak.testsuite.ui.account2.page.fragment.ContentAlert; +import org.keycloak.testsuite.ui.account2.page.fragment.ContinueCancelModal; import org.keycloak.testsuite.ui.account2.page.fragment.LoggedInPageHeader; import org.keycloak.testsuite.ui.account2.page.fragment.Sidebar; import org.openqa.selenium.WebElement; @@ -26,6 +27,7 @@ import org.openqa.selenium.support.FindBy; import java.util.LinkedList; +import static org.keycloak.testsuite.util.UIUtils.clickLink; import static org.keycloak.testsuite.util.UIUtils.getTextFromElement; /** @@ -43,9 +45,15 @@ public abstract class AbstractLoggedInPage extends AbstractAccountPage { @Page private ContentAlert alert; + @Page + private ContinueCancelModal modal; + @FindBy(xpath = ".//*[@id='page-heading']//h1") private WebElement pageTitle; + @FindBy(id = "refresh-page") + private WebElement refreshPageBtn; + public AbstractLoggedInPage() { hashPath = new LinkedList<>(); if (getParentPageId() != null) hashPath.add(getParentPageId()); @@ -93,10 +101,18 @@ public abstract class AbstractLoggedInPage extends AbstractAccountPage { return alert; } + public ContinueCancelModal modal() { + return modal; + } + public String getPageTitle() { return getTextFromElement(pageTitle); } + public void clickRefreshPage() { + clickLink(refreshPageBtn); + } + @Override public boolean isCurrent() { return super.isCurrent() && getPageId().equals(sidebar().getActiveNavId()); diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/DeviceActivityPage.java b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/DeviceActivityPage.java index fb1f08131b..f13264ca85 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/DeviceActivityPage.java +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/DeviceActivityPage.java @@ -17,10 +17,27 @@ package org.keycloak.testsuite.ui.account2.page; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +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; + /** * @author Vaclav Muzikar */ public class DeviceActivityPage extends AbstractLoggedInPage { + @FindBy(id = "sign-out-all") + private WebElement signOutAllBtn; + + @FindBy(xpath = "//div[@rowid='sessions']/div[contains(@class,'-m-3-')]") + private List sessionsFirstCol; // this represents first column of each session (which contains the browser icon) + @Override public String getPageId() { return "device-activity"; @@ -31,5 +48,128 @@ public class DeviceActivityPage extends AbstractLoggedInPage { return ACCOUNT_SECURITY_ID; } - // TODO implement this! (KEYCLOAK-12106) + public boolean isSignOutAllDisplayed() { + return isElementVisible(signOutAllBtn); + } + + public void clickSignOutAll() { + clickLink(signOutAllBtn); + } + + public int getSessionsCount() { + return sessionsFirstCol.size(); + } + + public Session getSessionByIndex(int index) { + // get the session ID from browser icon (which we know is always present) + String sessionId = sessionsFirstCol.get(index) + .findElement(By.xpath("//*[contains(@id,'-icon-')]")) + .getAttribute("id") + .split("-")[1]; + + return getSession(sessionId); + } + + public Session getSession(String sessionId) { + return new Session(sessionId); + } + + // We cannot use standard Page Fragment as there's no root element. Even though the sessions are placed in rows, + // there's no element that would encapsulate it. Hence we cannot simply use e.g. @FindBy annotations. + public class Session { + private static final String SESSION = "session"; + private static final String BROWSER = "browser"; + private static final String IP = "ip"; + private static final String SIGN_OUT = "sign-out"; + + private final String sessionId; + private final String fullSessionId; + + // we don't want Session to be instantiated outside DeviceActivityPage + private Session(String sessionId) { + this.fullSessionId = sessionId; + this.sessionId = sessionId.substring(0,7); + } + + public String getSessionId() { + return sessionId; + } + + public String getFullSessionId() { + return fullSessionId; + } + + public boolean isPresent() { + return isItemDisplayed(IP); // no root element hence this workaround + } + + public String getBrowserIconName() { + String id = driver + .findElement(By.xpath(String.format("//*[contains(@id,'%s')]", getFullItemId("icon")))) + .getAttribute("id"); + + return id.split("-")[3]; // the id looks like session-71891504-icon-chrome + } + + public String getIp() { + return getTextFromItem(IP); + } + + public boolean hasCurrentBadge() { + return isItemDisplayed("current-badge"); + } + + public boolean isBrowserDisplayed() { + return isItemDisplayed(BROWSER); + } + + public String getBrowser() { + return getTextFromItem(BROWSER); + } + + public String getLastAccess() { + return getTextFromItem("last-access").split("Last accessed on ")[1]; + } + + public String getClients() { + return getTextFromItem("clients").split("Clients ")[1]; + } + + public String getStarted() { + return getTextFromItem("started").split("Started at ")[1]; + } + + public String getExpires() { + return getTextFromItem("expires").split("Expires at ")[1]; + } + + public boolean isSignOutDisplayed() { + return isItemDisplayed(SIGN_OUT); + } + + public void clickSignOut() { + clickLink(getItemElement(SIGN_OUT)); + } + + private String getFullItemId(String itemId) { + return String.format("%s-%s-%s", SESSION, sessionId, itemId); + } + + private WebElement getItemElement(String itemId) { + return driver.findElement(By.id(getFullItemId(itemId))); + } + + private String getTextFromItem(String itemId) { + return getTextFromElement(getItemElement(itemId)); + } + + private boolean isItemDisplayed(String itemId) { + try { + return getItemElement(itemId).isDisplayed(); + } + catch (NoSuchElementException e) { + return false; + } + } + } } diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/ContinueCancelModal.java b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/ContinueCancelModal.java new file mode 100644 index 0000000000..83e4c8174d --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/ContinueCancelModal.java @@ -0,0 +1,66 @@ +/* + * Copyright 2019 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.fragment; + +import org.jboss.arquillian.drone.api.annotation.Drone; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.keycloak.testsuite.util.UIUtils.clickLink; +import static org.keycloak.testsuite.util.UIUtils.isElementVisible; + +/** + * @author Vaclav Muzikar + */ +public class ContinueCancelModal { + public static final String ROOT_XPATH = "//div[@role='dialog']"; + + @Drone + private WebDriver driver; + + @FindBy(xpath = ROOT_XPATH) + private WebElement root; + + @FindBy(xpath = ROOT_XPATH + "//*[@id='modal-confirm']") + private WebElement confirmBtn; + @FindBy(xpath = ROOT_XPATH + "//*[@id='modal-cancel']") + private WebElement cancelBtn; + + public boolean isDisplayed() { + return isElementVisible(root); + } + + public void assertIsDisplayed() { + assertTrue("Modal dialog should be displayed", isDisplayed()); + } + + public void assertIsNotDisplayed() { + assertFalse("Modal dialog should not be displayed", isDisplayed()); + } + + public void clickConfirm() { + clickLink(confirmBtn); + } + + public void clickCancel() { + clickLink(cancelBtn); + } +} diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml index 3cb2f5b427..2d315f720d 100755 --- a/testsuite/integration-arquillian/tests/pom.xml +++ b/testsuite/integration-arquillian/tests/pom.xml @@ -225,6 +225,25 @@ ${auth.server.jboss.skip.unpack} + + unpack-undertow-server + generate-test-resources + + unpack + + + + + org.keycloak.testsuite + integration-arquillian-servers-auth-server-undertow + ${project.version} + jar + ${containers.home}/auth-server-undertow + + + *.jks,*.crt,*.truststore,*.crl,*.key,certs/clients/* + + unpack-app-server generate-test-resources diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/ContentPage.tsx b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/ContentPage.tsx index 5687922475..c855cea220 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/ContentPage.tsx +++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/ContentPage.tsx @@ -46,7 +46,7 @@ export class ContentPage extends React.Component { <strong><Msg msgKey={this.props.title}/></strong> {this.props.onRefresh && - }> + }> } {this.props.introMessage && } diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/device-activity-page/DeviceActivityPage.tsx b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/device-activity-page/DeviceActivityPage.tsx index e5de3d0e49..0064d1c0ba 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/device-activity-page/DeviceActivityPage.tsx +++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/device-activity-page/DeviceActivityPage.tsx @@ -158,18 +158,22 @@ export class DeviceActivityPage extends React.Component); // chrome or chromium - if (browserName.includes("firefox")) return (); - if (browserName.includes("edge")) return (); - if (browserName.startsWith("ie/")) return (); - if (browserName.includes("safari")) return (); - if (browserName.includes("opera")) return (); - if (browserName.includes("yandex")) return (); - if (browserName.includes("amazon")) return (); + if (browserName.includes("chrom")) return (); // chrome or chromium + if (browserName.includes("firefox")) return (); + if (browserName.includes("edge")) return (); + if (browserName.startsWith("ie/")) return (); + if (browserName.includes("safari")) return (); + if (browserName.includes("opera")) return (); + if (browserName.includes("yandex")) return (); + if (browserName.includes("amazon")) return (); - return (); + return (); } private findOS(device: Device): string { @@ -232,6 +236,7 @@ export class DeviceActivityPage extends React.Component {this.isShowSignOutAll(this.state.devices) && {!this.state.devices[0].mobile && - {session.ipAddress} + {session.ipAddress} } {session.current && - + } {!session.browser.toLowerCase().includes('unknown') && -

{session.browser} / {this.findOS(device)} {this.findOSVersion(device)}

+

{session.browser} / {this.findOS(device)} {this.findOSVersion(device)}

} {this.state.devices[0].mobile && -

{Msg.localize('ipAddress')} {session.ipAddress}

+

{Msg.localize('ipAddress')} {session.ipAddress}

} -

{Msg.localize('lastAccessedOn')} {this.time(session.lastAccess)}

-

{Msg.localize('clients')} {this.makeClientsString(session.clients)}

-

{Msg.localize('startedAt')} {this.time(session.started)}

-

{Msg.localize('expiresAt')} {this.time(session.expires)}

+

{Msg.localize('lastAccessedOn')} {this.time(session.lastAccess)}

+

{Msg.localize('clients')} {this.makeClientsString(session.clients)}

+

{Msg.localize('startedAt')} {this.time(session.started)}

+

{Msg.localize('expiresAt')} {this.time(session.expires)}

{!session.current && - + , - ]}