KEYCLOAK-12106 UI tests for Device Activity page
This commit is contained in:
parent
fb999d96a5
commit
4f7b56d227
14 changed files with 648 additions and 47 deletions
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
]}
|
||||
|
|
Loading…
Reference in a new issue