keycloak-aplcache
Changes
.travis.yml 10(+10 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml 31(+31 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestJavascriptResource.java 53(+53 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java 6(+6 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/javascript/index.html 76(+76 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/javascript/keycloak.json 2(+1 -1)
testsuite/integration-arquillian/test-apps/js-database/src/main/java/org/keycloak/example/oauth/CustomerService.java 61(+0 -61)
testsuite/integration-arquillian/test-apps/js-database/src/main/webapp/WEB-INF/keycloak.json 8(+0 -8)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/JSConsoleTestApp.java 245(+0 -245)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/JSDatabaseTestApp.java 46(+0 -46)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/OAuthGrant.java 9(+8 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java 10(+10 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/JavascriptBrowser.java 20(+12 -8)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java 12(+12 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractJSConsoleExampleAdapterTest.java 594(+0 -594)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/AbstractJavascriptTest.java 180(+180 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptAdapterTest.java 517(+517 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptStateValidator.java 15(+15 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptTestExecutor.java 251(+251 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JSObjectBuilder.java 90(+90 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/ResponseValidator.java 15(+15 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/XMLHttpRequest.java 77(+77 -0)
testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/example/EAPJSConsoleExampleAdapterTest.java 11(+0 -11)
testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/EAP6JSConsoleExampleAdapterTest.java 11(+0 -11)
testsuite/integration-arquillian/tests/other/adapters/jboss/relative/eap/src/test/java/org/keycloak/testsuite/adapter/example/RelativeEAPJSConsoleExampleAdapterTest.java 9(+0 -9)
testsuite/integration-arquillian/tests/other/adapters/jboss/relative/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/RelativeWildflyJSConsoleExampleAdapterTest.java 9(+0 -9)
testsuite/integration-arquillian/tests/other/adapters/jboss/remote/src/test/java/org/keycloak/testsuite/adapter/example/RemoteJSConsoleExampleAdapterTest.java 11(+0 -11)
testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyJSConsoleExampleAdapterTest.java 13(+0 -13)
Details
.travis.yml 10(+10 -0)
diff --git a/.travis.yml b/.travis.yml
index 3a494b4..ee4b9ec 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -22,6 +22,16 @@ jdk:
install: true
+before_install:
+ - "export PHANTOMJS_VERSION=2.1.1"
+ - "phantomjs --version"
+ - "export PATH=$PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64/bin:$PATH"
+ - "phantomjs --version"
+ - "if [ $(phantomjs --version) != '$PHANTOMJS_VERSION' ]; then rm -rf $PWD/travis_phantomjs; mkdir -p $PWD/travis_phantomjs; fi"
+ - "if [ $(phantomjs --version) != '$PHANTOMJS_VERSION' ]; then wget https://github.com/Medium/phantomjs/releases/download/v$PHANTOMJS_VERSION/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -O $PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2; fi"
+ - "if [ $(phantomjs --version) != '$PHANTOMJS_VERSION' ]; then tar -xvf $PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -C $PWD/travis_phantomjs; fi"
+ - "phantomjs --version"
+
script:
- ./travis-run-tests.sh $TESTS
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml
index 9c3b005..c0445c1 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml
@@ -30,6 +30,10 @@
<artifactId>integration-arquillian-testsuite-providers</artifactId>
<name>Auth Server Services - Testsuite Providers</name>
+ <properties>
+ <js-adapter.version>${project.version}</js-adapter.version>
+ </properties>
+
<dependencies>
<!-- Keycloak deps for tests -->
@@ -81,5 +85,32 @@
<filtering>true</filtering>
</resource>
</resources>
+
+ <plugins>
+ <plugin>
+ <artifactId>maven-dependency-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>unpack-javascript-adapter</id>
+ <phase>generate-resources</phase>
+ <goals>
+ <goal>unpack</goal>
+ </goals>
+ <configuration>
+ <artifactItems>
+ <artifactItem>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-js-adapter</artifactId>
+ <version>${js-adapter.version}</version>
+ <type>jar</type>
+ <outputDirectory>${pom.basedir}/src/main/resources/javascript</outputDirectory>
+ </artifactItem>
+ </artifactItems>
+ <includes>**/keycloak.js</includes>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
</build>
</project>
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestJavascriptResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestJavascriptResource.java
new file mode 100644
index 0000000..cb7ed2c
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestJavascriptResource.java
@@ -0,0 +1,53 @@
+package org.keycloak.testsuite.rest.resource;
+
+import org.keycloak.testsuite.rest.TestingResourceProvider;
+
+import javax.print.attribute.standard.Media;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+/**
+ * @author mhajas
+ */
+public class TestJavascriptResource {
+
+ @GET
+ @Path("/js/keycloak.js")
+ @Produces("application/javascript")
+ public String getJavascriptAdapter() throws IOException {
+ return resourceToString("/javascript/keycloak.js");
+ }
+
+ @GET
+ @Path("/index.html")
+ @Produces(MediaType.TEXT_HTML)
+ public String getJavascriptTestingEnvironment() throws IOException {
+ return resourceToString("/javascript/index.html");
+ }
+
+ @GET
+ @Path("/keycloak.json")
+ @Produces(MediaType.APPLICATION_JSON)
+ public String getKeycloakJSON() throws IOException {
+ return resourceToString("/javascript/keycloak.json");
+ }
+
+ private String resourceToString(String path) throws IOException {
+ InputStream is = TestingResourceProvider.class.getResourceAsStream(path);
+ BufferedReader buf = new BufferedReader(new InputStreamReader(is));
+ String line = buf.readLine();
+ StringBuilder sb = new StringBuilder();
+ while (line != null) {
+ sb.append(line).append("\n");
+ line = buf.readLine();
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
index 7b76083..1e5b9ab 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
@@ -62,6 +62,7 @@ import org.keycloak.testsuite.forms.PassThroughAuthenticator;
import org.keycloak.testsuite.forms.PassThroughClientAuthenticator;
import org.keycloak.testsuite.rest.representation.AuthenticatorState;
import org.keycloak.testsuite.rest.resource.TestCacheResource;
+import org.keycloak.testsuite.rest.resource.TestJavascriptResource;
import org.keycloak.testsuite.rest.resource.TestingExportImportResource;
import org.keycloak.testsuite.runonserver.ModuleUtil;
import org.keycloak.testsuite.runonserver.FetchOnServer;
@@ -759,6 +760,11 @@ public class TestingResourceProvider implements RealmResourceProvider {
}
}
+ @Path("/javascript")
+ public TestJavascriptResource getJavascriptResource() {
+ return new TestJavascriptResource();
+ }
+
private RealmModel getRealmByName(String realmName) {
RealmProvider realmProvider = session.getProvider(RealmProvider.class);
return realmProvider.getRealmByName(realmName);
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/javascript/index.html b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/javascript/index.html
new file mode 100644
index 0000000..b352c08
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/javascript/index.html
@@ -0,0 +1,76 @@
+<!--
+ ~ Copyright 2016 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.
+ -->
+
+<html>
+<head>
+ <script src="js/keycloak.js"></script>
+</head>
+<body>
+
+<h2>Result</h2>
+<pre style="background-color: #ddd; border: 1px solid #ccc; padding: 10px;" id="output"></pre>
+
+<h2>Events</h2>
+<pre style="background-color: #ddd; border: 1px solid #ccc; padding: 10px;" id="events"></pre>
+
+
+<script>
+ function showExpires() {
+ if (!keycloak.tokenParsed) {
+ output("Not authenticated");
+ return;
+ }
+
+ var o = 'Token Expires:\t\t' + new Date((keycloak.tokenParsed.exp + keycloak.timeSkew) * 1000).toLocaleString() + '\n';
+ o += 'Token Expires in:\t' + Math.round(keycloak.tokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds\n';
+
+ if (keycloak.refreshTokenParsed) {
+ o += 'Refresh Token Expires:\t' + new Date((keycloak.refreshTokenParsed.exp + keycloak.timeSkew) * 1000).toLocaleString() + '\n';
+ o += 'Refresh Expires in:\t' + Math.round(keycloak.refreshTokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds';
+ }
+
+ output(o);
+ }
+
+ function showError() {
+ output("Error: " + getParameterByName("error") + "\n" + "Error description: " + getParameterByName("error_description"));
+ }
+
+ function getParameterByName(name, url) {
+ if (!url) url = window.location.href;
+ name = name.replace(/[\[\]]/g, "\\$&");
+ var regex = new RegExp("[?&#]" + name + "(=([^&#]*)|&|#|$)"),
+ results = regex.exec(url);
+ if (!results) return null;
+ if (!results[2]) return '';
+ return decodeURIComponent(results[2].replace(/\+/g, " "));
+ }
+
+ function output(data) {
+ if (typeof data === 'object') {
+ data = JSON.stringify(data, null, ' ');
+ }
+ document.getElementById('output').innerHTML = data;
+ }
+
+ function event(event) {
+ var e = document.getElementById('events').innerHTML;
+ document.getElementById('events').innerHTML = new Date().toLocaleString() + "\t" + event + "\n" + e;
+ }
+</script>
+</body>
+</html>
diff --git a/testsuite/integration-arquillian/test-apps/pom.xml b/testsuite/integration-arquillian/test-apps/pom.xml
index 5a33513..e6c76e8 100644
--- a/testsuite/integration-arquillian/test-apps/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/pom.xml
@@ -15,9 +15,7 @@
<name>Test apps</name>
<modules>
- <module>js-console</module>
<module>test-apps-dist</module>
- <module>js-database</module>
<module>photoz</module>
<module>hello-world-authz-service</module>
<module>servlet-authz</module>
diff --git a/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml b/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml
index e3200c5..a9cd806 100755
--- a/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml
+++ b/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml
@@ -19,14 +19,6 @@
<target name="all">
<delete dir="target/test-apps"/>
- <copy todir="target/test-apps/js-console" overwrite="true">
- <fileset dir="../js-console">
- <exclude name="**/target/**"/>
- <exclude name="**/*.iml"/>
- <exclude name="**/*.unconfigured"/>
- <exclude name="**/subsystem-config.xml"/>
- </fileset>
- </copy>
<copy todir="target/test-apps/photoz" overwrite="true">
<fileset dir="../photoz">
<exclude name="**/target/**"/>
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/OAuthGrant.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/OAuthGrant.java
index 55b9d3a..f3312d9 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/OAuthGrant.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/OAuthGrant.java
@@ -16,6 +16,7 @@
*/
package org.keycloak.testsuite.auth.page.login;
+import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@@ -38,8 +39,14 @@ public class OAuthGrant extends LoginActions {
cancelButton.click();
}
+
+ public boolean isCurrent(WebDriver driver1) {
+ if (driver1 == null) driver1 = driver;
+ return driver1.getPageSource().contains("Do you grant these access privileges");
+ }
+
@Override
public boolean isCurrent() {
- return driver.getPageSource().contains("Do you grant these access privileges");
+ return isCurrent(null);
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
index dc6bfaf..04daf8d 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
@@ -286,4 +286,14 @@ public interface TestingResource {
@Produces(MediaType.TEXT_PLAIN_UTF_8)
String runOnServer(String runOnServer);
+ @GET
+ @Path("js/keycloak.js")
+ @Produces(MediaType.TEXT_HTML_UTF_8)
+ String getJavascriptAdapter();
+
+ @GET
+ @Path("/get-javascript-testing-environment")
+ @Produces(MediaType.TEXT_HTML_UTF_8)
+ String getJavascriptTestingEnvironment();
+
}
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 6ea17c6..497947d 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
@@ -50,6 +50,7 @@ import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.idm.KeysMetadataRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
import org.keycloak.util.BasicAuthHelper;
import org.keycloak.util.JsonSerialization;
@@ -69,6 +70,8 @@ import java.security.KeyStore;
import java.security.PublicKey;
import java.util.*;
+import static org.keycloak.testsuite.admin.Users.getPasswordOf;
+
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
@@ -197,6 +200,10 @@ public class OAuthClient {
origin = null;
}
+ public void setDriver(WebDriver driver) {
+ this.driver = driver;
+ }
+
public AuthorizationEndpointResponse doLogin(String username, String password) {
openLoginForm();
fillLoginForm(username, password);
@@ -204,6 +211,11 @@ public class OAuthClient {
return new AuthorizationEndpointResponse(this);
}
+ public AuthorizationEndpointResponse doLogin(UserRepresentation user) {
+
+ return doLogin(user.getUsername(), getPasswordOf(user));
+ }
+
public void fillLoginForm(String username, String password) {
WaitUtils.waitForPageToLoad();
String src = driver.getPageSource();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/AbstractJavascriptTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/AbstractJavascriptTest.java
new file mode 100644
index 0000000..fabedb8
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/AbstractJavascriptTest.java
@@ -0,0 +1,180 @@
+package org.keycloak.testsuite.adapter.javascript;
+
+import org.jboss.arquillian.drone.api.annotation.Drone;
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Before;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.AbstractAuthTest;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.auth.page.login.OIDCLogin;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.JavascriptBrowser;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.RolesBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.openqa.selenium.By;
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.collection.IsMapContaining.hasEntry;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
+import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
+
+/**
+ * @author mhajas
+ */
+public abstract class AbstractJavascriptTest extends AbstractAuthTest {
+
+ @FunctionalInterface
+ interface QuadFunction<T, U, V, W> {
+ void apply(T a, U b, V c, W d);
+ }
+
+ public static final String CLIENT_ID = "js-console";
+ public static final String REALM_NAME = "test";
+ public static final String SPACE_REALM_NAME = "Example realm";
+ public static final String JAVASCRIPT_URL = "/auth/realms/" + REALM_NAME + "/testing/javascript";
+ public static final String JAVASCRIPT_ENCODED_SPACE_URL = "/auth/realms/Example%20realm/testing/javascript";
+ public static final String JAVASCRIPT_SPACE_URL = "/auth/realms/Example realm/testing/javascript";
+ public static int TOKEN_LIFESPAN_LEEWAY = 3; // seconds
+
+
+ @Drone
+ @JavascriptBrowser
+ protected WebDriver jsDriver;
+
+ protected JavascriptExecutor jsExecutor;
+
+ @Page
+ @JavascriptBrowser
+ protected OIDCLogin testRealmLoginPage;
+
+ @FindBy(id = "output")
+ @JavascriptBrowser
+ protected WebElement outputArea;
+
+ @FindBy(id = "events")
+ @JavascriptBrowser
+ protected WebElement eventsArea;
+
+ public static final UserRepresentation testUser;
+ public static final UserRepresentation unauthorizedUser;
+
+ static {
+ testUser = UserBuilder.create().username("test-user@localhost").password("password").build();
+ unauthorizedUser = UserBuilder.create().username("unauthorized").password("password").build();
+ }
+
+
+ @Before
+ public void beforeJavascriptTest() {
+ jsExecutor = (JavascriptExecutor) jsDriver;
+ }
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ testRealms.add(updateRealm(RealmBuilder.create()
+ .name(REALM_NAME)
+ .roles(
+ RolesBuilder.create()
+ .realmRole(new RoleRepresentation("user", "", false))
+ .realmRole(new RoleRepresentation("admin", "", false))
+ )
+ .user(
+ UserBuilder.create()
+ .username("test-user@localhost").password("password")
+ .addRoles("user")
+ .role("realm-management", "view-realm")
+ .role("realm-management", "manage-users")
+ .role("account", "view-profile")
+ .role("account", "manage-account")
+ )
+ .user(
+ UserBuilder.create()
+ .username("unauthorized").password("password")
+ )
+ .client(
+ ClientBuilder.create()
+ .clientId(CLIENT_ID)
+ .redirectUris(JAVASCRIPT_URL + "/*", JAVASCRIPT_ENCODED_SPACE_URL + "/*")
+ .publicClient()
+ )
+ .accessTokenLifespan(30 + TOKEN_LIFESPAN_LEEWAY)
+ .testEventListener()
+ ));
+ }
+
+ protected <T> JavascriptStateValidator buildFunction(QuadFunction<T, WebDriver, Object, WebElement> f, T x) {
+ return (y,z,w) -> f.apply(x, y, z, w);
+ }
+
+ protected void setImplicitFlowForClient() {
+ ClientResource clientResource = ApiUtil.findClientResourceByClientId(adminClient.realms().realm(REALM_NAME), CLIENT_ID);
+ ClientRepresentation client = clientResource.toRepresentation();
+ client.setImplicitFlowEnabled(true);
+ client.setStandardFlowEnabled(false);
+ clientResource.update(client);
+ }
+
+ protected void setStandardFlowForClient() {
+ ClientResource clientResource = ApiUtil.findClientResourceByClientId(adminClient.realms().realm(REALM_NAME), CLIENT_ID);
+ ClientRepresentation client = clientResource.toRepresentation();
+ client.setImplicitFlowEnabled(false);
+ client.setStandardFlowEnabled(true);
+ clientResource.update(client);
+ }
+
+ protected abstract RealmRepresentation updateRealm(RealmBuilder builder);
+
+ protected void assertSuccessfullyLoggedIn(WebDriver driver1, Object output, WebElement events) {
+ buildFunction(this::assertOutputContains, "Init Success (Authenticated)").validate(driver1, output, events);
+ waitUntilElement(events).text().contains("Auth Success");
+ }
+
+ protected void assertInitNotAuth(WebDriver driver1, Object output, WebElement events) {
+ buildFunction(this::assertOutputContains, "Init Success (Not Authenticated)").validate(driver1, output, events);
+ }
+
+ protected void assertOnLoginPage(WebDriver driver1, Object output, WebElement events) {
+ waitUntilElement(By.tagName("body")).is().present();
+ assertCurrentUrlStartsWith(testRealmLoginPage, driver1);
+ }
+
+ public void assertOutputWebElementContains(String value, WebDriver driver1, Object output, WebElement events) {
+ waitUntilElement((WebElement) output).text().contains(value);
+ }
+
+ public void assertOutputContains(String value, WebDriver driver1, Object output, WebElement events) {
+ if (output instanceof WebElement) {
+ waitUntilElement((WebElement) output).text().contains(value);
+ } else {
+ Assert.assertThat((String) output, containsString(value));
+ }
+ }
+
+ public void assertEventsWebElementContains(String value, WebDriver driver1, Object output, WebElement events) {
+ waitUntilElement(events).text().contains(value);
+ }
+
+ public ResponseValidator assertResponseStatus(long status) {
+ return output -> Assert.assertThat(output, hasEntry("status", status));
+ }
+
+ public JavascriptStateValidator assertOutputContains(String text) {
+ return buildFunction(this::assertOutputContains, text);
+ }
+
+ public JavascriptStateValidator assertEventsContains(String text) {
+ return buildFunction(this::assertEventsWebElementContains, text);
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptAdapterTest.java
new file mode 100644
index 0000000..10361a6
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptAdapterTest.java
@@ -0,0 +1,517 @@
+package org.keycloak.testsuite.adapter.javascript;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.events.Details;
+import org.keycloak.events.EventType;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.auth.page.account.Applications;
+import org.keycloak.testsuite.auth.page.login.OAuthGrant;
+import org.keycloak.testsuite.util.JavascriptBrowser;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.openqa.selenium.TimeoutException;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebDriverException;
+import org.openqa.selenium.WebElement;
+
+import java.net.MalformedURLException;
+import java.util.List;
+import java.util.Map;
+
+import static java.lang.Math.toIntExact;
+import static org.hamcrest.CoreMatchers.both;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.lessThan;
+import static org.hamcrest.collection.IsMapContaining.hasEntry;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
+import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
+
+/**
+ * @author mhajas
+ */
+public class JavascriptAdapterTest extends AbstractJavascriptTest {
+
+ private String testAppUrl;
+ private JavascriptTestExecutor testExecutor;
+ private static int TIME_SKEW_TOLERANCE = 3;
+
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
+ @Page
+ @JavascriptBrowser
+ private Applications applicationsPage;
+
+ @Page
+ @JavascriptBrowser
+ private OAuthGrant oAuthGrantPage;
+
+ @Override
+ protected RealmRepresentation updateRealm(RealmBuilder builder) {
+ return builder.accessTokenLifespan(30 + TOKEN_LIFESPAN_LEEWAY).build();
+ }
+
+ @Before
+ public void setDefaultEnvironment() {
+ testAppUrl = authServerContextRootPage + JAVASCRIPT_URL + "/index.html";
+
+ testRealmLoginPage.setAuthRealm(REALM_NAME);
+ oAuthGrantPage.setAuthRealm(REALM_NAME);
+ applicationsPage.setAuthRealm(REALM_NAME);
+
+ jsDriver.navigate().to(testAppUrl);
+ testExecutor = JavascriptTestExecutor.create(jsDriver, testRealmLoginPage);
+
+ waitUntilElement(outputArea).is().present();
+ assertCurrentUrlStartsWith(testAppUrl, jsDriver);
+
+ jsDriver.manage().deleteAllCookies();
+ }
+
+ private JSObjectBuilder defaultArguments() {
+ return JSObjectBuilder.create().defaultSettings();
+ }
+
+ private void assertOnTestAppUrl(WebDriver jsDriver, Object output, WebElement events) {
+ assertCurrentUrlStartsWith(testAppUrl, jsDriver);
+ }
+
+ @Test
+ public void testJSConsoleAuth() {
+ testExecutor.init(defaultArguments(), this::assertInitNotAuth)
+ .login(this::assertOnLoginPage)
+ .loginForm( UserBuilder.create().username("user").password("invalid-password").build(),
+ (driver1, output, events) -> assertCurrentUrlDoesntStartWith(testAppUrl, driver1))
+ .loginForm(UserBuilder.create().username("invalid-user").password("password").build(),
+ (driver1, output, events) -> assertCurrentUrlDoesntStartWith(testAppUrl, driver1))
+ .loginForm(testUser, this::assertOnTestAppUrl)
+ .init(defaultArguments(), this::assertSuccessfullyLoggedIn)
+ .logout(this::assertOnTestAppUrl)
+ .init(defaultArguments(), this::assertInitNotAuth);
+ }
+
+ @Test
+ public void testRefreshToken() {
+ testExecutor.init(defaultArguments(), this::assertInitNotAuth)
+ .refreshToken(9999, assertOutputContains("Failed to refresh token"))
+ .login(this::assertOnLoginPage)
+ .loginForm(testUser, this::assertOnTestAppUrl)
+ .init(defaultArguments(), this::assertSuccessfullyLoggedIn)
+ .refreshToken(9999, assertEventsContains("Auth Refresh Success"));
+ }
+
+ @Test
+ public void testRefreshTokenIfUnder30s() {
+ testExecutor.init(defaultArguments(), this::assertInitNotAuth)
+ .login(this::assertOnLoginPage)
+ .loginForm(testUser, this::assertOnTestAppUrl)
+ .init(defaultArguments(), this::assertSuccessfullyLoggedIn)
+ .refreshToken(30, assertOutputContains("Token not refreshed, valid for"))
+ .addTimeSkew(-5) // instead of wait move in time
+ .refreshToken(30, assertEventsContains("Auth Refresh Success"));
+ }
+
+ @Test
+ public void testGetProfile() {
+ testExecutor.init(defaultArguments(), this::assertInitNotAuth)
+ .getProfile(assertOutputContains("Failed to load profile"))
+ .login(this::assertOnLoginPage)
+ .loginForm(testUser, this::assertOnTestAppUrl)
+ .init(defaultArguments(), this::assertSuccessfullyLoggedIn)
+ .getProfile((driver1, output, events) -> Assert.assertThat((Map<String, String>) output, hasEntry("username", testUser.getUsername())));
+ }
+
+ @Test
+ public void grantBrowserBasedApp() {
+ ClientResource clientResource = ApiUtil.findClientResourceByClientId(adminClient.realm(REALM_NAME), CLIENT_ID);
+ ClientRepresentation client = clientResource.toRepresentation();
+ client.setConsentRequired(true);
+ clientResource.update(client);
+
+ testExecutor.init(defaultArguments(), this::assertInitNotAuth)
+ .login(this::assertOnLoginPage)
+ .loginForm(testUser, (driver1, output, events) -> assertTrue(oAuthGrantPage.isCurrent(driver1))
+ // I am not sure why is this driver1 argument to isCurrent necessary, but I got exception without it
+ );
+
+ oAuthGrantPage.accept();
+
+ EventRepresentation loginEvent = events.expectLogin()
+ .client(CLIENT_ID)
+ .detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED)
+ .detail(Details.REDIRECT_URI, testAppUrl)
+ .detail(Details.USERNAME, testUser.getUsername())
+ .assertEvent();
+ String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+ testExecutor.init(defaultArguments(), this::assertSuccessfullyLoggedIn);
+
+ applicationsPage.navigateTo();
+ events.expectCodeToToken(codeId, loginEvent.getSessionId()).client(CLIENT_ID).assertEvent();
+
+ applicationsPage.revokeGrantForApplication(CLIENT_ID);
+ events.expect(EventType.REVOKE_GRANT)
+ .client("account")
+ .detail(Details.REVOKED_CLIENT, CLIENT_ID)
+ .assertEvent();
+
+ jsDriver.navigate().to(testAppUrl);
+ testExecutor.configure() // need to configure because we refreshed page
+ .init(defaultArguments(), this::assertInitNotAuth)
+ .login((driver1, output, events) -> assertTrue(oAuthGrantPage.isCurrent(driver1)));
+
+ // Clean
+ client.setConsentRequired(false);
+ clientResource.update(client);
+ }
+
+ @Test
+ public void implicitFlowTest() {
+ testExecutor.init(defaultArguments().implicitFlow(), this::assertInitNotAuth)
+ .login(this::assertOnTestAppUrl)
+ .errorResponse(assertOutputContains("Implicit flow is disabled for the client"));
+
+ setImplicitFlowForClient();
+ jsDriver.navigate().to(testAppUrl);
+
+ testExecutor.init(defaultArguments(), this::assertInitNotAuth)
+ .login(this::assertOnTestAppUrl)
+ .errorResponse(assertOutputContains("Standard flow is disabled for the client"));
+ jsDriver.navigate().to(testAppUrl);
+
+ testExecutor.init(defaultArguments().implicitFlow(), this::assertInitNotAuth)
+ .login(this::assertOnLoginPage)
+ .loginForm(testUser, this::assertOnTestAppUrl)
+ .init(defaultArguments().implicitFlow(), this::assertSuccessfullyLoggedIn);
+
+ setStandardFlowForClient();
+ }
+
+ @Test
+ public void testCertEndpoint() {
+ testExecutor.logInAndInit(defaultArguments(), testUser, this::assertSuccessfullyLoggedIn)
+ .sendXMLHttpRequest(XMLHttpRequest.create()
+ .url(authServerContextRootPage + "/auth/realms/" + REALM_NAME + "/protocol/openid-connect/certs")
+ .method("GET")
+ .addHeader("Accept", "application/json")
+ .addHeader("Authorization", "Bearer ' + keycloak.token + '"),
+ assertResponseStatus(200));
+ }
+
+ @Test
+ public void implicitFlowQueryTest() {
+ setImplicitFlowForClient();
+ testExecutor.init(defaultArguments().implicitFlow().queryResponse(), this::assertInitNotAuth)
+ .login(((driver1, output, events) ->
+ Assert.assertThat(driver1.getCurrentUrl(), containsString("Response_mode+%27query%27+not+allowed"))));
+ setStandardFlowForClient();
+ }
+
+ @Test
+ public void implicitFlowRefreshTokenTest() {
+ setImplicitFlowForClient();
+ testExecutor.logInAndInit(defaultArguments().implicitFlow(), testUser, this::assertSuccessfullyLoggedIn)
+ .refreshToken(9999, assertOutputContains("Failed to refresh token"));
+ setStandardFlowForClient();
+ }
+
+ @Test
+ public void implicitFlowOnTokenExpireTest() {
+ RealmRepresentation realm = adminClient.realms().realm(REALM_NAME).toRepresentation();
+ Integer storeAccesTokenLifespan = realm.getAccessTokenLifespanForImplicitFlow();
+ realm.setAccessTokenLifespanForImplicitFlow(5);
+ adminClient.realms().realm(REALM_NAME).update(realm);
+
+ setImplicitFlowForClient();
+ testExecutor.logInAndInit(defaultArguments().implicitFlow(), testUser, this::assertSuccessfullyLoggedIn)
+ .addTimeSkew(-5); // Move in time instead of wait
+
+ waitUntilElement(eventsArea).text().contains("Access token expired");
+
+ // Get to origin state
+ realm.setAccessTokenLifespanForImplicitFlow(storeAccesTokenLifespan);
+ adminClient.realms().realm(REALM_NAME).update(realm);
+ setStandardFlowForClient();
+ }
+
+ @Test
+ public void implicitFlowCertEndpoint() {
+ setImplicitFlowForClient();
+ testExecutor.logInAndInit(defaultArguments().implicitFlow(), testUser, this::assertSuccessfullyLoggedIn)
+ .sendXMLHttpRequest(XMLHttpRequest.create()
+ .url(authServerContextRootPage + "/auth/realms/" + REALM_NAME + "/protocol/openid-connect/certs")
+ .method("GET")
+ .addHeader("Accept", "application/json")
+ .addHeader("Authorization", "Bearer ' + keycloak.token + '"),
+ assertResponseStatus(200));
+ setStandardFlowForClient();
+ }
+
+ @Test
+ public void testBearerRequest() {
+ XMLHttpRequest request = XMLHttpRequest.create()
+ .url(authServerContextRootPage + "/auth/admin/realms/" + REALM_NAME + "/roles")
+ .method("GET")
+ .addHeader("Accept", "application/json")
+ .addHeader("Authorization", "Bearer ' + keycloak.token + '");
+
+ testExecutor.init(defaultArguments())
+ .sendXMLHttpRequest(request, assertResponseStatus(401))
+ .refresh();
+ if (!"phantomjs".equals(System.getProperty("js.browser"))) {
+ // I have no idea why, but this request doesn't work with phantomjs, it works in chrome
+ testExecutor.logInAndInit(defaultArguments(), unauthorizedUser, this::assertSuccessfullyLoggedIn)
+ .sendXMLHttpRequest(request, output -> Assert.assertThat(output, hasEntry("status", 403L)))
+ .logout(this::assertOnTestAppUrl)
+ .refresh();
+ }
+ testExecutor.logInAndInit(defaultArguments(), testUser, this::assertSuccessfullyLoggedIn)
+ .sendXMLHttpRequest(request, assertResponseStatus(200));
+ }
+
+ @Test
+ public void loginRequiredAction() {
+ try {
+ testExecutor.init(defaultArguments().loginRequiredOnLoad());
+ // This throws exception because when JavascriptExecutor waits for AsyncScript to finish
+ // it is redirected to login page and executor gets no response
+
+ throw new RuntimeException("Probably the login-required OnLoad mode doesn't work, because testExecutor should fail with error that page was redirected.");
+ } catch (WebDriverException ex) {
+ // should happen
+ }
+
+ testExecutor.loginForm(testUser, this::assertOnTestAppUrl)
+ .init(defaultArguments(), this::assertSuccessfullyLoggedIn);
+ }
+
+ @Test
+ public void testUpdateToken() {
+ XMLHttpRequest request = XMLHttpRequest.create()
+ .url(authServerContextRootPage + "/auth/admin/realms/" + REALM_NAME + "/roles")
+ .method("GET")
+ .addHeader("Accept", "application/json")
+ .addHeader("Authorization", "Bearer ' + keycloak.token + '");
+
+ testExecutor.logInAndInit(defaultArguments(), testUser, this::assertSuccessfullyLoggedIn)
+ .addTimeSkew(-33);
+ setTimeOffset(33);
+ testExecutor.refreshToken(5, assertEventsContains("Auth Refresh Success"));
+
+ setTimeOffset(67);
+ testExecutor.addTimeSkew(-34)
+ .sendXMLHttpRequest(request, assertResponseStatus(401))
+ .refreshToken(5, assertEventsContains("Auth Refresh Success"))
+ .sendXMLHttpRequest(request, assertResponseStatus(200));
+
+ setTimeOffset(0);
+ }
+
+ @Test
+ public void timeSkewTest() {
+ testExecutor.logInAndInit(defaultArguments(), testUser, this::assertSuccessfullyLoggedIn)
+ .checkTimeSkew((driver1, output, events) -> assertThat(toIntExact((long) output),
+ is(
+ both(greaterThan(0 - TIME_SKEW_TOLERANCE))
+ .and(lessThan(TIME_SKEW_TOLERANCE))
+ )
+ ));
+
+ setTimeOffset(40);
+
+ testExecutor.refreshToken(9999, assertEventsContains("Auth Refresh Success"))
+ .checkTimeSkew((driver1, output, events) -> assertThat(toIntExact((long) output),
+ is(
+ both(greaterThan(-40 - TIME_SKEW_TOLERANCE))
+ .and(lessThan(-40 + TIME_SKEW_TOLERANCE))
+ )
+ ));
+ }
+
+ @Test
+ public void testOneSecondTimeSkewTokenUpdate() {
+ setTimeOffset(1);
+
+ testExecutor.logInAndInit(defaultArguments(), testUser, this::assertSuccessfullyLoggedIn)
+ .refreshToken(9999, assertEventsContains("Auth Refresh Success"));
+
+ try {
+ // The events element should contain "Auth logout" but we need to wait for it
+ // and text().not().contains() doesn't wait. With KEYCLOAK-4179 it took some time for "Auth Logout" to be present
+ waitUntilElement(eventsArea).text().contains("Auth Logout");
+
+ throw new RuntimeException("The events element shouldn't contain \"Auth Logout\" text");
+ } catch (TimeoutException e) {
+ // OK
+ }
+ }
+
+ @Test
+ public void testLocationHeaderInResponse() {
+ XMLHttpRequest request = XMLHttpRequest.create()
+ .url(authServerContextRootPage + "/auth/admin/realms/" + REALM_NAME + "/users")
+ .method("POST")
+ .content("JSON.stringify(JSON.parse('{\"emailVerified\" : false, \"enabled\" : true, \"username\": \"mhajas\", \"firstName\" :\"First\", \"lastName\":\"Last\",\"email\":\"email@redhat.com\", \"attributes\": {}}'))")
+ .addHeader("Accept", "application/json")
+ .addHeader("Authorization", "Bearer ' + keycloak.token + '")
+ .addHeader("Content-Type", "application/json; charset=UTF-8");
+
+ testExecutor.logInAndInit(defaultArguments(), testUser, this::assertSuccessfullyLoggedIn)
+ .sendXMLHttpRequest(request, response -> {
+ List<UserRepresentation> users = adminClient.realm(REALM_NAME).users().search("mhajas", 0, 1);
+ assertEquals("There should be created user mhajas", 1, users.size());
+
+ assertThat((String) response.get("responseHeaders"), containsString("location: " + authServerContextRootPage.toString() + "/auth/admin/realms/" + REALM_NAME + "/users/" + users.get(0).getId()));
+ });
+ }
+
+ @Test
+ public void spaceInRealmNameTest() {
+ // Unfortunately this test doesn't work on phantomjs
+ // it looks like phantomjs double encode %20 => %25%20
+ Assume.assumeTrue("This test doesn't work with phantomjs", !"phantomjs".equals(System.getProperty("js.browser")));
+
+ adminClient.realm(REALM_NAME).update(RealmBuilder.edit(adminClient.realm(REALM_NAME).toRepresentation()).name(SPACE_REALM_NAME).build());
+
+ JSObjectBuilder configuration = JSObjectBuilder.create()
+ .add("url", authServerContextRootPage + "/auth")
+ .add("realm", SPACE_REALM_NAME)
+ .add("clientId", CLIENT_ID);
+
+ testAppUrl = authServerContextRootPage + JAVASCRIPT_SPACE_URL + "/index.html";
+ jsDriver.navigate().to(testAppUrl);
+ testRealmLoginPage.setAuthRealm(SPACE_REALM_NAME);
+
+ testExecutor.configure(configuration)
+ .init(defaultArguments(), this::assertInitNotAuth)
+ .login(this::assertOnLoginPage)
+ .loginForm(testUser, this::assertOnTestAppUrl)
+ .configure(configuration)
+ .init(defaultArguments(), this::assertSuccessfullyLoggedIn);
+
+ // Clean
+ adminClient.realm(SPACE_REALM_NAME).update(RealmBuilder.edit(adminClient.realm(SPACE_REALM_NAME).toRepresentation()).name(REALM_NAME).build());
+ testRealmLoginPage.setAuthRealm(REALM_NAME);
+ }
+
+ @Test
+ public void initializeWithTokenTest() {
+ oauth.setDriver(jsDriver);
+
+ oauth.realm(REALM_NAME);
+ oauth.clientId(CLIENT_ID);
+ oauth.redirectUri(testAppUrl);
+ oauth.doLogin(testUser);
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+ String token = tokenResponse.getAccessToken();
+ String refreshToken = tokenResponse.getRefreshToken();
+
+ testExecutor.init(JSObjectBuilder.create()
+ .add("token", token)
+ .add("refreshToken", refreshToken)
+ , this::assertSuccessfullyLoggedIn)
+ .refreshToken(9999, assertEventsContains("Auth Refresh Success"));
+
+
+ oauth.setDriver(driver);
+ }
+
+ @Test
+ public void initializeWithTimeSkew() {
+ oauth.setDriver(jsDriver); // Oauth need to login with jsDriver
+
+ // Get access token and refresh token to initialize with
+ setTimeOffset(600);
+ oauth.realm(REALM_NAME);
+ oauth.clientId(CLIENT_ID);
+ oauth.redirectUri(testAppUrl);
+ oauth.doLogin(testUser);
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+ String token = tokenResponse.getAccessToken();
+ String refreshToken = tokenResponse.getRefreshToken();
+
+ // Perform test
+ testExecutor.init(JSObjectBuilder.create()
+ .add("token", token)
+ .add("refreshToken", refreshToken)
+ .add("timeSkew", -600)
+ , this::assertSuccessfullyLoggedIn)
+ .checkTimeSkew((driver1, output, events) -> assertThat(output, equalTo(-600L)))
+ .refreshToken(9999, assertEventsContains("Auth Refresh Success"))
+ .checkTimeSkew((driver1, output, events) -> assertThat(output, equalTo(-600L)));
+
+ setTimeOffset(0);
+
+ oauth.setDriver(driver); // Clean
+ }
+
+ @Test
+ // KEYCLOAK-4503
+ public void initializeWithRefreshToken() {
+ oauth.setDriver(jsDriver); // Oauth need to login with jsDriver
+
+ oauth.realm(REALM_NAME);
+ oauth.clientId(CLIENT_ID);
+ oauth.redirectUri(testAppUrl);
+ oauth.doLogin(testUser);
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+ String token = tokenResponse.getAccessToken();
+ String refreshToken = tokenResponse.getRefreshToken();
+
+ testExecutor.init(JSObjectBuilder.create()
+ .add("refreshToken", refreshToken)
+ , (driver1, output, events) -> {
+ assertInitNotAuth(driver1, output, events);
+ waitUntilElement(events).text().not().contains("Auth Success");
+ });
+
+ oauth.setDriver(driver); // Clean
+ }
+
+ @Test
+ public void reentrancyCallbackTest() {
+ testExecutor.logInAndInit(defaultArguments(), testUser, this::assertSuccessfullyLoggedIn)
+ .executeAsyncScript(
+ "var callback = arguments[arguments.length - 1];" +
+ "keycloak.updateToken(60).success(function () {" +
+ " event(\"First callback\");" +
+ " keycloak.updateToken(60).success(function () {" +
+ " event(\"Second callback\");" +
+ " callback(\"Success\");" +
+ " });" +
+ " }" +
+ ");"
+ , (driver1, output, events) -> {
+ waitUntilElement(events).text().contains("First callback");
+ waitUntilElement(events).text().contains("Second callback");
+ waitUntilElement(events).text().not().contains("Auth Logout");
+ }
+ );
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptStateValidator.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptStateValidator.java
new file mode 100644
index 0000000..3c9f1ea
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptStateValidator.java
@@ -0,0 +1,15 @@
+package org.keycloak.testsuite.adapter.javascript;
+
+import org.keycloak.models.KeycloakSession;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+import java.io.Serializable;
+
+/**
+ * @author mhajas
+ */
+public interface JavascriptStateValidator extends Serializable {
+
+ void validate(WebDriver driver, Object output, WebElement events);
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptTestExecutor.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptTestExecutor.java
new file mode 100644
index 0000000..8db7dee
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptTestExecutor.java
@@ -0,0 +1,251 @@
+package org.keycloak.testsuite.adapter.javascript;
+
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.auth.page.login.OIDCLogin;
+import org.openqa.selenium.By;
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * @author mhajas
+ */
+public class JavascriptTestExecutor {
+ private WebDriver jsDriver;
+ private JavascriptExecutor jsExecutor;
+ private WebElement output;
+ private WebElement events;
+ private OIDCLogin loginPage;
+ private boolean configured;
+
+ public static JavascriptTestExecutor create(WebDriver driver, OIDCLogin loginPage) {
+ return new JavascriptTestExecutor(driver, loginPage);
+ }
+
+ private JavascriptTestExecutor(WebDriver driver, OIDCLogin loginPage) {
+ this.jsDriver = driver;
+ driver.manage().timeouts().setScriptTimeout(10, TimeUnit.SECONDS);
+ jsExecutor = (JavascriptExecutor) driver;
+ events = driver.findElement(By.id("events"));
+ output = driver.findElement(By.id("output"));
+ this.loginPage = loginPage;
+ configured = false;
+ }
+
+ public JavascriptTestExecutor login() {
+ return login(null);
+ }
+
+ public JavascriptTestExecutor login(JavascriptStateValidator validator) {
+ jsExecutor.executeScript("keycloak.login()");
+
+ if (validator != null) {
+ validator.validate(jsDriver, output, events);
+ }
+
+ configured = false; // Getting out of testApp page => loosing keycloak variable etc.
+
+ return this;
+ }
+
+ public JavascriptTestExecutor loginForm(UserRepresentation user) {
+ return loginForm(user, null);
+ }
+
+ public JavascriptTestExecutor loginForm(UserRepresentation user, JavascriptStateValidator validator) {
+ loginPage.form().login(user);
+
+ if (validator != null) {
+ validator.validate(jsDriver, null, events);
+ }
+
+ configured = false; // Getting out of testApp page => loosing keycloak variable etc.
+ // this is necessary in case we skipped login button for example in login-required mode
+
+ return this;
+ }
+
+ public JavascriptTestExecutor logout() {
+ return logout(null);
+ }
+
+ public JavascriptTestExecutor logout(JavascriptStateValidator validator) {
+ jsExecutor.executeScript("keycloak.logout()");
+ if (validator != null) {
+ validator.validate(jsDriver, output, events);
+ }
+
+ configured = false; // Loosing keycloak variable so we need to create it when init next session
+
+ return this;
+ }
+
+ public JavascriptTestExecutor configure() {
+ return configure(null);
+ }
+
+ public JavascriptTestExecutor configure(JSObjectBuilder argumentsBuilder) {
+ if (argumentsBuilder == null) {
+ jsExecutor.executeScript("keycloak = Keycloak()");
+ } else {
+ String configArguments = argumentsBuilder.build();
+ jsExecutor.executeScript("keycloak = Keycloak(" + configArguments + ")");
+ }
+
+ jsExecutor.executeScript("keycloak.onAuthSuccess = function () {event('Auth Success')}"); // event function is declared in index.html
+ jsExecutor.executeScript("keycloak.onAuthError = function () {event('Auth Error')}");
+ jsExecutor.executeScript("keycloak.onAuthRefreshSuccess = function () {event('Auth Refresh Success')}");
+ jsExecutor.executeScript("keycloak.onAuthRefreshError = function () {event('Auth Refresh Error')}");
+ jsExecutor.executeScript("keycloak.onAuthLogout = function () {event('Auth Logout')}");
+ jsExecutor.executeScript("keycloak.onTokenExpired = function () {event('Access token expired.')}");
+
+ configured = true;
+
+ return this;
+ }
+
+ public JavascriptTestExecutor init(JSObjectBuilder argumentsBuilder) {
+ return init(argumentsBuilder, null);
+ }
+
+ public JavascriptTestExecutor init(JSObjectBuilder argumentsBuilder, JavascriptStateValidator validator) {
+ if(!configured) {
+ configure();
+ }
+
+ String arguments = argumentsBuilder.build();
+
+ Object output = jsExecutor.executeAsyncScript(
+ "var callback = arguments[arguments.length - 1];" +
+ " keycloak.init(" + arguments + ").success(function (authenticated) {" +
+ " callback(\"Init Success (\" + (authenticated ? \"Authenticated\" : \"Not Authenticated\") + \")\");" +
+ " }).error(function () {" +
+ " callback(\"Init Error\");" +
+ " });");
+
+ if (validator != null) {
+ validator.validate(jsDriver, output, events);
+ }
+
+ return this;
+ }
+
+ public JavascriptTestExecutor logInAndInit(JSObjectBuilder argumentsBuilder,
+ UserRepresentation user, JavascriptStateValidator validator) {
+ init(argumentsBuilder);
+ login();
+ loginForm(user);
+ init(argumentsBuilder, validator);
+ return this;
+ }
+
+ public JavascriptTestExecutor refreshToken(int value) {
+ return refreshToken(value, null);
+ }
+
+ public JavascriptTestExecutor refreshToken(int value, JavascriptStateValidator validator) {
+ Object output = jsExecutor.executeAsyncScript(
+ "var callback = arguments[arguments.length - 1];" +
+ " keycloak.updateToken(" + Integer.toString(value) + ").success(function (refreshed) {" +
+ " if (refreshed) {" +
+ " callback(keycloak.tokenParsed);" +
+ " } else {" +
+ " callback('Token not refreshed, valid for ' + Math.round(keycloak.tokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds');" +
+ " }" +
+ " }).error(function () {" +
+ " callback('Failed to refresh token');" +
+ " });");
+
+ if(validator != null) {
+ validator.validate(jsDriver, output, events);
+ }
+
+ return this;
+ }
+
+ public JavascriptTestExecutor getProfile() {
+ return getProfile(null);
+ }
+
+ public JavascriptTestExecutor getProfile(JavascriptStateValidator validator) {
+
+ Object output = jsExecutor.executeAsyncScript(
+ "var callback = arguments[arguments.length - 1];" +
+ " keycloak.loadUserProfile().success(function (profile) {" +
+ " callback(profile);" +
+ " }).error(function () {" +
+ " callback('Failed to load profile');" +
+ " });");
+
+ if(validator != null) {
+ validator.validate(jsDriver, output, events);
+ }
+ return this;
+ }
+
+ public JavascriptTestExecutor sendXMLHttpRequest(XMLHttpRequest request, ResponseValidator validator) {
+ validator.validate(request.send(jsExecutor));
+
+ return this;
+ }
+
+ public JavascriptTestExecutor refresh() {
+ jsDriver.navigate().refresh();
+ configured = false; // Refreshing webpage => Loosing keycloak variable
+
+ return this;
+ }
+
+ public JavascriptTestExecutor addTimeSkew(int addition) {
+ jsExecutor.executeScript("keycloak.timeSkew += " + Integer.toString(addition));
+
+ return this;
+ }
+
+ public JavascriptTestExecutor checkTimeSkew(JavascriptStateValidator validator) {
+ Object timeSkew = jsExecutor.executeScript("return keycloak.timeSkew");
+
+ validator.validate(jsDriver, timeSkew, events);
+
+ return this;
+ }
+
+ public JavascriptTestExecutor executeScript(String script) {
+ return executeScript(script, null);
+ }
+
+ public JavascriptTestExecutor executeScript(String script, JavascriptStateValidator validator) {
+ Object output = jsExecutor.executeScript(script);
+
+ if(validator != null) {
+ validator.validate(jsDriver, output, events);
+ }
+
+ return this;
+ }
+
+ public JavascriptTestExecutor executeAsyncScript(String script) {
+ return executeAsyncScript(script, null);
+ }
+
+ public JavascriptTestExecutor executeAsyncScript(String script, JavascriptStateValidator validator) {
+ Object output = jsExecutor.executeAsyncScript(script);
+
+ if(validator != null) {
+ validator.validate(jsDriver, output, events);
+ }
+
+ return this;
+ }
+
+ public JavascriptTestExecutor errorResponse(JavascriptStateValidator validator) {
+ Object output = jsExecutor.executeScript("return \"Error: \" + getParameterByName(\"error\") + \"\\n\" + \"Error description: \" + getParameterByName(\"error_description\")");
+
+ validator.validate(jsDriver, output, events);
+ return this;
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JSObjectBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JSObjectBuilder.java
new file mode 100644
index 0000000..f0d549b
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JSObjectBuilder.java
@@ -0,0 +1,90 @@
+package org.keycloak.testsuite.adapter.javascript;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author mhajas
+ */
+public class JSObjectBuilder {
+
+ private Map<String, Object> arguments;
+
+
+ public static JSObjectBuilder create() {
+ return new JSObjectBuilder();
+ }
+
+ private JSObjectBuilder() {
+ arguments = new HashMap<>();
+ }
+
+ public JSObjectBuilder defaultSettings() {
+ standardFlow();
+ fragmentResponse();
+ return this;
+ }
+
+ public JSObjectBuilder standardFlow() {
+ arguments.put("flow", "standard");
+ return this;
+ }
+
+ public JSObjectBuilder implicitFlow() {
+ arguments.put("flow", "implicit");
+ return this;
+ }
+
+ public JSObjectBuilder fragmentResponse() {
+ arguments.put("responseMode", "fragment");
+ return this;
+ }
+
+ public JSObjectBuilder queryResponse() {
+ arguments.put("responseMode", "query");
+ return this;
+ }
+
+ public JSObjectBuilder checkSSOOnLoad() {
+ arguments.put("onLoad", "check-sso");
+ return this;
+ }
+
+ public JSObjectBuilder loginRequiredOnLoad() {
+ arguments.put("onLoad", "login-required");
+ return this;
+ }
+
+ public JSObjectBuilder add(String key, Object value) {
+ arguments.put(key, value);
+ return this;
+ }
+
+ public boolean isLoginRequired() {
+ return arguments.get("onLoad").equals("login-required");
+ }
+
+
+ public String build() {
+ StringBuilder argument = new StringBuilder("{");
+ String comma = "";
+ for (Map.Entry<String, Object> option : arguments.entrySet()) {
+ argument.append(comma)
+ .append(option.getKey())
+ .append(" : ");
+
+ if (!(option.getValue() instanceof Integer)) argument.append("\"");
+
+ argument.append(option.getValue());
+
+ if (!(option.getValue() instanceof Integer)) argument.append("\"");
+ comma = ",";
+ }
+
+ argument.append("}");
+
+ return argument.toString();
+ }
+
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/ResponseValidator.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/ResponseValidator.java
new file mode 100644
index 0000000..8587d25
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/ResponseValidator.java
@@ -0,0 +1,15 @@
+package org.keycloak.testsuite.adapter.javascript;
+
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+import java.io.Serializable;
+import java.util.Map;
+
+/**
+ * @author mhajas
+ */
+public interface ResponseValidator extends Serializable {
+
+ void validate(Map<String, Object> response);
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/XMLHttpRequest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/XMLHttpRequest.java
new file mode 100644
index 0000000..1a16611
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/XMLHttpRequest.java
@@ -0,0 +1,77 @@
+package org.keycloak.testsuite.adapter.javascript;
+
+import org.openqa.selenium.JavascriptExecutor;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author mhajas
+ */
+public class XMLHttpRequest {
+
+ private String url;
+ private String method;
+ private Map<String, String> headers;
+ private String content;
+
+ public static XMLHttpRequest create() {
+ return new XMLHttpRequest();
+ }
+
+ private XMLHttpRequest() {}
+
+ public XMLHttpRequest url(String url) {
+ this.url = url;
+ return this;
+ }
+
+ public XMLHttpRequest method(String method) {
+ this.method = method;
+ return this;
+ }
+
+ public XMLHttpRequest content(String content) {
+ this.content = content;
+ return this;
+ }
+
+ public XMLHttpRequest addHeader(String key, String value) {
+ if (headers == null) {
+ headers = new HashMap<>();
+ }
+
+ headers.put(key, value);
+
+ return this;
+ }
+
+ public Map<String, Object> send(JavascriptExecutor jsExecutor) {
+ String requestCode = "var callback = arguments[arguments.length - 1];" +
+ "var req = new XMLHttpRequest();" +
+ " req.open('" + method + "', '" + url + "', true);" +
+ getHeadersString() +
+ " req.onreadystatechange = function () {" +
+ " if (req.readyState == 4) {" +
+ " callback({\"status\" : req.status, \"reponseText\" : req.reponseText, \"responseHeaders\" : req.getAllResponseHeaders().toLowerCase(), \"res\" : req.response})" +
+ " }" +
+ " };" +
+ " req.send(" + content + ");";
+
+ return (Map<String, Object>) jsExecutor.executeAsyncScript(requestCode);
+ }
+
+ private String getHeadersString() {
+ StringBuilder builder = new StringBuilder();
+ for (Map.Entry<String, String> entry : headers.entrySet()) {
+ builder.append("req.setRequestHeader('")
+ .append(entry.getKey())
+ .append("', '")
+ .append(entry.getValue())
+ .append("');");
+ }
+
+ return builder.toString();
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
index 93ade25..d81d5cc 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
@@ -44,14 +44,23 @@
<!-- chrome -->
<property name="chromeArguments">${chromeArguments}</property>
</extension>
-
+
<extension qualifier="graphene">
<property name="waitGuiInterval">5</property>
<property name="waitAjaxInterval">5</property>
<property name="waitModelInterval">10</property>
<property name="waitGuardInterval">5</property>
</extension>
-
+
+ <extension qualifier="webdriver-javascriptbrowser">
+ <property name="browser">${js.browser}</property>
+ <property name="htmlUnit.version">${htmlUnitBrowserVersion}</property>
+ <property name="firefox_binary">${firefox_binary}</property>
+ <property name="chromeDriverBinary">${webdriver.chrome.driver}</property>
+ <property name="chromeArguments">${js.chromeArguments}</property>
+ <property name="phantomjs.cli.args">${phantomjs.cli.args} --ssl-certificates-path=${client.certificate.ca.path} --ssl-client-certificate-file=${client.certificate.file} --ssl-client-key-file=${client.key.file} --ssl-client-key-passphrase=${client.key.passphrase}</property>
+ </extension>
+
<extension qualifier="graphene-secondbrowser">
<property name="browser">${browser}</property>
<property name="firefox_binary">${firefox_binary}</property>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/pom.xml
index 2fc66a4..b7e9732 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/pom.xml
@@ -269,18 +269,6 @@
<artifactItems>
<artifactItem>
<groupId>org.keycloak.testsuite</groupId>
- <artifactId>integration-arquillian-test-apps-js-console</artifactId>
- <version>${project.version}</version>
- <type>war</type>
- </artifactItem>
- <artifactItem>
- <groupId>org.keycloak.testsuite</groupId>
- <artifactId>integration-arquillian-test-apps-js-database</artifactId>
- <version>${project.version}</version>
- <type>war</type>
- </artifactItem>
- <artifactItem>
- <groupId>org.keycloak.testsuite</groupId>
<artifactId>hello-world-authz-service</artifactId>
<version>${project.version}</version>
<type>war</type>
diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml
index 2af22be..1e29644 100755
--- a/testsuite/integration-arquillian/tests/pom.xml
+++ b/testsuite/integration-arquillian/tests/pom.xml
@@ -103,6 +103,8 @@
<github.username/>
<github.secretToken/>
<ieDriverArch/>
+ <js.browser>phantomjs</js.browser>
+ <js.chromeArguments>--headless</js.chromeArguments>
<htmlUnitBrowserVersion>chrome</htmlUnitBrowserVersion>
<phantomjs.cli.args>--ignore-ssl-errors=true --web-security=false --ssl-certificates-path=${client.certificate.ca.path} --ssl-client-certificate-file=${client.certificate.file} --ssl-client-key-file=${client.key.file} --ssl-client-key-passphrase=${client.key.passphrase}</phantomjs.cli.args>
<firefox_binary>/usr/bin/firefox</firefox_binary>
@@ -268,6 +270,8 @@
<test.intermittent>${test.intermittent}</test.intermittent>
<browser>${browser}</browser>
+ <js.browser>${js.browser}</js.browser>
+ <js.chromeArguments>${js.chromeArguments}</js.chromeArguments>
<htmlUnitBrowserVersion>${htmlUnitBrowserVersion}</htmlUnitBrowserVersion>
<webdriverDownloadBinaries>${webdriverDownloadBinaries}</webdriverDownloadBinaries>