KEYCLOAK-12106 UI tests for Device Activity page

This commit is contained in:
vmuzikar 2019-12-10 14:42:02 +01:00 committed by Bruno Oliveira da Silva
parent fb999d96a5
commit 4f7b56d227
14 changed files with 648 additions and 47 deletions

View file

@ -236,25 +236,6 @@
</artifactItems>
</configuration>
</execution>
<execution>
<id>unpack-undertow-server</id>
<phase>generate-test-resources</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-auth-server-undertow</artifactId>
<version>${project.version}</version>
<type>jar</type>
<outputDirectory>${containers.home}/auth-server-undertow</outputDirectory>
</artifactItem>
</artifactItems>
<includes>*.jks,*.crt,*.truststore,*.crl,*.key,certs/clients/*</includes>
</configuration>
</execution>
</executions>
</plugin>
<plugin>

View file

@ -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<String, String> getHeaders() {
return headers;
}

View file

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

View file

@ -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() {

View file

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

View file

@ -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 <vmuzikar@redhat.com>
*/
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<RealmRepresentation> 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<Browsers, String> 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<String> 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;
}
}
}

View file

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

View file

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

View file

@ -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 <vmuzikar@redhat.com>
*/
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<WebElement> 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;
}
}
}
}

View file

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

View file

@ -225,6 +225,25 @@
<skip>${auth.server.jboss.skip.unpack}</skip>
</configuration>
</execution>
<execution>
<id>unpack-undertow-server</id>
<phase>generate-test-resources</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-auth-server-undertow</artifactId>
<version>${project.version}</version>
<type>jar</type>
<outputDirectory>${containers.home}/auth-server-undertow</outputDirectory>
</artifactItem>
</artifactItems>
<includes>*.jks,*.crt,*.truststore,*.crl,*.key,certs/clients/*</includes>
</configuration>
</execution>
<execution>
<id>unpack-app-server</id>
<phase>generate-test-resources</phase>

View file

@ -46,7 +46,7 @@ export class ContentPage extends React.Component<ContentPageProps> {
<GridItem span={11}><Title headingLevel='h1' size='3xl'><strong><Msg msgKey={this.props.title}/></strong></Title></GridItem>
{this.props.onRefresh &&
<GridItem span={1}>
<Tooltip content={<Msg msgKey='refreshPage'/>}><Button variant='plain' onClick={this.props.onRefresh}><RedoIcon size='sm'/></Button></Tooltip>
<Tooltip content={<Msg msgKey='refreshPage'/>}><Button id='refresh-page' variant='plain' onClick={this.props.onRefresh}><RedoIcon size='sm'/></Button></Tooltip>
</GridItem>
}
{this.props.introMessage && <GridItem span={12}> <Msg msgKey={this.props.introMessage}/></GridItem>}

View file

@ -158,18 +158,22 @@ export class DeviceActivityPage extends React.Component<DeviceActivityPageProps,
return moment(time * 1000).format('LLLL');
}
private elementId(item: string, session: Session) : string {
return `session-${session.id.substring(0,7)}-${item}`;
}
private findBrowserIcon(session: Session): React.ReactNode {
const browserName: string = session.browser.toLowerCase();
if (browserName.includes("chrom")) return (<ChromeIcon size='lg'/>); // chrome or chromium
if (browserName.includes("firefox")) return (<FirefoxIcon size='lg'/>);
if (browserName.includes("edge")) return (<EdgeIcon size='lg'/>);
if (browserName.startsWith("ie/")) return (<InternetExplorerIcon size='lg'/>);
if (browserName.includes("safari")) return (<SafariIcon size='lg'/>);
if (browserName.includes("opera")) return (<OperaIcon size='lg'/>);
if (browserName.includes("yandex")) return (<YandexInternationalIcon size='lg'/>);
if (browserName.includes("amazon")) return (<AmazonIcon size='lg'/>);
if (browserName.includes("chrom")) return (<ChromeIcon id={this.elementId('icon-chrome', session)} size='lg'/>); // chrome or chromium
if (browserName.includes("firefox")) return (<FirefoxIcon id={this.elementId('icon-firefox', session)} size='lg'/>);
if (browserName.includes("edge")) return (<EdgeIcon id={this.elementId('icon-edge', session)} size='lg'/>);
if (browserName.startsWith("ie/")) return (<InternetExplorerIcon id={this.elementId('icon-ie', session)} size='lg'/>);
if (browserName.includes("safari")) return (<SafariIcon id={this.elementId('icon-safari', session)} size='lg'/>);
if (browserName.includes("opera")) return (<OperaIcon id={this.elementId('icon-opera', session)} size='lg'/>);
if (browserName.includes("yandex")) return (<YandexInternationalIcon id={this.elementId('icon-yandex', session)} size='lg'/>);
if (browserName.includes("amazon")) return (<AmazonIcon id={this.elementId('icon-amazon', session)} size='lg'/>);
return (<GlobeIcon size='lg'/>);
return (<GlobeIcon id={this.elementId('icon-default', session)} size='lg'/>);
}
private findOS(device: Device): string {
@ -232,6 +236,7 @@ export class DeviceActivityPage extends React.Component<DeviceActivityPageProps,
<DataListCell key='signOutAllButton' width={1}>
{this.isShowSignOutAll(this.state.devices) &&
<ContinueCancelModal buttonTitle='signOutAllDevices'
buttonId='sign-out-all'
modalTitle='signOutAllDevices'
modalMessage='signOutAllDevicesWarning'
onContinue={this.signOutAll}
@ -260,29 +265,30 @@ export class DeviceActivityPage extends React.Component<DeviceActivityPageProps,
</StackItem>
{!this.state.devices[0].mobile &&
<StackItem isFilled={false}>
<Bullseye>{session.ipAddress}</Bullseye>
<Bullseye id={this.elementId('ip', session)}>{session.ipAddress}</Bullseye>
</StackItem>
}
{session.current &&
<StackItem isFilled={false}>
<Bullseye><strong className='pf-c-badge pf-m-read'><Msg msgKey="currentSession"/></strong></Bullseye>
<Bullseye id={this.elementId('current-badge', session)}><strong className='pf-c-badge pf-m-read'><Msg msgKey="currentSession"/></strong></Bullseye>
</StackItem>
}
</Stack>
</GridItem>
<GridItem span={9}>
{!session.browser.toLowerCase().includes('unknown') &&
<p><strong>{session.browser} / {this.findOS(device)} {this.findOSVersion(device)}</strong></p>
<p id={this.elementId('browser', session)}><strong>{session.browser} / {this.findOS(device)} {this.findOSVersion(device)}</strong></p>
}
{this.state.devices[0].mobile &&
<p><strong>{Msg.localize('ipAddress')} </strong> {session.ipAddress}</p>
<p id={this.elementId('ip', session)}><strong>{Msg.localize('ipAddress')} </strong> {session.ipAddress}</p>
}
<p><strong>{Msg.localize('lastAccessedOn')}</strong> {this.time(session.lastAccess)}</p>
<p><strong>{Msg.localize('clients')}</strong> {this.makeClientsString(session.clients)}</p>
<p><strong>{Msg.localize('startedAt')}</strong> {this.time(session.started)}</p>
<p><strong>{Msg.localize('expiresAt')}</strong> {this.time(session.expires)}</p>
<p id={this.elementId('last-access', session)}><strong>{Msg.localize('lastAccessedOn')}</strong> {this.time(session.lastAccess)}</p>
<p id={this.elementId('clients', session)}><strong>{Msg.localize('clients')}</strong> {this.makeClientsString(session.clients)}</p>
<p id={this.elementId('started', session)}><strong>{Msg.localize('startedAt')}</strong> {this.time(session.started)}</p>
<p id={this.elementId('expires', session)}><strong>{Msg.localize('expiresAt')}</strong> {this.time(session.expires)}</p>
{!session.current &&
<ContinueCancelModal buttonTitle='doSignOut'
buttonId={this.elementId('sign-out', session)}
modalTitle='doSignOut'
buttonVariant='secondary'
modalMessage='signOutWarning'

View file

@ -25,6 +25,7 @@ import {Msg} from './Msg';
interface ContinueCancelModalProps {
buttonTitle: string;
buttonVariant?: ButtonProps['variant'];
buttonId?: string;
modalTitle: string;
modalMessage: string;
modalContinueButtonLabel?: string;
@ -74,7 +75,7 @@ export class ContinueCancelModal extends React.Component<ContinueCancelModalProp
return (
<React.Fragment>
<Button variant={this.props.buttonVariant} onClick={this.handleModalToggle} isDisabled={this.props.isDisabled}>
<Button id={this.props.buttonId} variant={this.props.buttonVariant} onClick={this.handleModalToggle} isDisabled={this.props.isDisabled}>
<Msg msgKey={this.props.buttonTitle}/>
</Button>
<Modal
@ -83,10 +84,10 @@ export class ContinueCancelModal extends React.Component<ContinueCancelModalProp
isOpen={isModalOpen}
onClose={this.handleModalToggle}
actions={[
<Button key="cancel" variant="secondary" onClick={this.handleModalToggle}>
<Button id='modal-cancel' key="cancel" variant="secondary" onClick={this.handleModalToggle}>
<Msg msgKey={this.props.modalCancelButtonLabel!}/>
</Button>,
<Button key="confirm" variant="primary" onClick={this.handleContinue}>
<Button id='modal-confirm' key="confirm" variant="primary" onClick={this.handleContinue}>
<Msg msgKey={this.props.modalContinueButtonLabel!}/>
</Button>
]}