From e2ad59a74da371ee5e713d4dff06bfce8f855b9b Mon Sep 17 00:00:00 2001 From: mhajas Date: Mon, 26 Feb 2018 10:49:05 +0100 Subject: [PATCH] KEYCLOAK-4816 KEYCLOAK-4817 Move javascript tests to base testsuite and (#4964) * KEYCLOAK-4816 KEYCLOAK-4817 Move javascript tests to base testsuite and use JavascriptExecutor * Use PhantomJS 2.1.1 instead of 1.9.8 in Travis CI --- .travis.yml | 10 + .../services/testsuite-providers/pom.xml | 31 + .../rest/TestingResourceProvider.java | 6 + .../rest/resource/TestJavascriptResource.java | 53 ++ .../src/main/resources/javascript/index.html | 76 +++ .../main/resources/javascript}/keycloak.json | 2 +- .../test-apps/js-console/README.md | 17 - .../test-apps/js-console/example-realm.json | 88 --- .../test-apps/js-console/pom.xml | 80 --- .../src/main/webapp/WEB-INF/web.xml | 25 - .../js-console/src/main/webapp/index.html | 363 ----------- .../test-apps/js-database/pom.xml | 74 --- .../example/oauth/CustomerService.java | 61 -- .../src/main/webapp/WEB-INF/keycloak.json | 8 - .../src/main/webapp/WEB-INF/web.xml | 46 -- .../integration-arquillian/test-apps/pom.xml | 2 - .../test-apps/test-apps-dist/build.xml | 8 - .../adapter/page/JSConsoleTestApp.java | 245 -------- .../adapter/page/JSDatabaseTestApp.java | 46 -- .../testsuite/auth/page/login/OAuthGrant.java | 9 +- .../client/resources/TestingResource.java | 10 + .../testsuite/util/JavascriptBrowser.java} | 20 +- .../keycloak/testsuite/util/OAuthClient.java | 12 + .../AbstractJSConsoleExampleAdapterTest.java | 594 ------------------ .../javascript/AbstractJavascriptTest.java | 180 ++++++ .../adapter/javascript/JSObjectBuilder.java | 90 +++ .../javascript/JavascriptAdapterTest.java | 517 +++++++++++++++ .../javascript/JavascriptStateValidator.java | 15 + .../javascript/JavascriptTestExecutor.java | 251 ++++++++ .../adapter/javascript/ResponseValidator.java | 15 + .../adapter/javascript/XMLHttpRequest.java | 77 +++ .../base/src/test/resources/arquillian.xml | 13 +- .../EAPJSConsoleExampleAdapterTest.java | 11 - .../EAP6JSConsoleExampleAdapterTest.java | 11 - ...elativeEAPJSConsoleExampleAdapterTest.java | 9 - ...iveWildflyJSConsoleExampleAdapterTest.java | 9 - .../RemoteJSConsoleExampleAdapterTest.java | 11 - .../WildflyJSConsoleExampleAdapterTest.java | 13 - .../Wildfly10JSConsoleExampleAdapterTest.java | 13 - .../tests/other/adapters/pom.xml | 12 - .../integration-arquillian/tests/pom.xml | 4 + 41 files changed, 1379 insertions(+), 1758 deletions(-) create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestJavascriptResource.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/javascript/index.html rename testsuite/integration-arquillian/{test-apps/js-console/src/main/webapp => servers/auth-server/services/testsuite-providers/src/main/resources/javascript}/keycloak.json (94%) delete mode 100644 testsuite/integration-arquillian/test-apps/js-console/README.md delete mode 100755 testsuite/integration-arquillian/test-apps/js-console/example-realm.json delete mode 100755 testsuite/integration-arquillian/test-apps/js-console/pom.xml delete mode 100644 testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/WEB-INF/web.xml delete mode 100755 testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/index.html delete mode 100644 testsuite/integration-arquillian/test-apps/js-database/pom.xml delete mode 100755 testsuite/integration-arquillian/test-apps/js-database/src/main/java/org/keycloak/example/oauth/CustomerService.java delete mode 100755 testsuite/integration-arquillian/test-apps/js-database/src/main/webapp/WEB-INF/keycloak.json delete mode 100755 testsuite/integration-arquillian/test-apps/js-database/src/main/webapp/WEB-INF/web.xml delete mode 100755 testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/JSConsoleTestApp.java delete mode 100644 testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/JSDatabaseTestApp.java rename testsuite/integration-arquillian/{test-apps/js-database/src/main/java/org/keycloak/example/oauth/DataApplication.java => tests/base/src/main/java/org/keycloak/testsuite/util/JavascriptBrowser.java} (59%) mode change 100755 => 100644 delete mode 100755 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractJSConsoleExampleAdapterTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/AbstractJavascriptTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JSObjectBuilder.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptAdapterTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptStateValidator.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptTestExecutor.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/ResponseValidator.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/XMLHttpRequest.java delete mode 100644 testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/example/EAPJSConsoleExampleAdapterTest.java delete mode 100644 testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/EAP6JSConsoleExampleAdapterTest.java delete mode 100644 testsuite/integration-arquillian/tests/other/adapters/jboss/relative/eap/src/test/java/org/keycloak/testsuite/adapter/example/RelativeEAPJSConsoleExampleAdapterTest.java delete mode 100644 testsuite/integration-arquillian/tests/other/adapters/jboss/relative/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/RelativeWildflyJSConsoleExampleAdapterTest.java delete mode 100644 testsuite/integration-arquillian/tests/other/adapters/jboss/remote/src/test/java/org/keycloak/testsuite/adapter/example/RemoteJSConsoleExampleAdapterTest.java delete mode 100644 testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyJSConsoleExampleAdapterTest.java delete mode 100644 testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/src/test/java/org/keycloak/testsuite/adapter/example/Wildfly10JSConsoleExampleAdapterTest.java diff --git a/.travis.yml b/.travis.yml index 3a494b4bd5..ee4b9ec2c6 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 9c3b005d81..c0445c1d42 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 @@ integration-arquillian-testsuite-providers Auth Server Services - Testsuite Providers + + ${project.version} + + @@ -81,5 +85,32 @@ true + + + + maven-dependency-plugin + + + unpack-javascript-adapter + generate-resources + + unpack + + + + + org.keycloak + keycloak-js-adapter + ${js-adapter.version} + jar + ${pom.basedir}/src/main/resources/javascript + + + **/keycloak.js + + + + + 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 7b76083ec0..1e5b9ab50b 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/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 0000000000..cb7ed2c69e --- /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/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 0000000000..b352c0846d --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/javascript/index.html @@ -0,0 +1,76 @@ + + + + + + + + +

Result

+

+
+

Events

+

+
+
+
+
+
diff --git a/testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/keycloak.json b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/javascript/keycloak.json
similarity index 94%
rename from testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/keycloak.json
rename to testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/javascript/keycloak.json
index 00f6c9e57e..989e71efe2 100644
--- a/testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/keycloak.json
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/javascript/keycloak.json
@@ -1,5 +1,5 @@
 {
-  "realm" : "example",
+  "realm" : "test",
   "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
   "auth-server-url" : "http://localhost:8180/auth",
   "ssl-required" : "external",
diff --git a/testsuite/integration-arquillian/test-apps/js-console/README.md b/testsuite/integration-arquillian/test-apps/js-console/README.md
deleted file mode 100644
index fa9a02d192..0000000000
--- a/testsuite/integration-arquillian/test-apps/js-console/README.md
+++ /dev/null
@@ -1,17 +0,0 @@
-Basic JavaScript Example
-========================
-
-Start and configure Keycloak
-----------------------------
-
-Start Keycloak:
-
-    bin/standalone.sh
-
-Open the Keycloak admin console, click on Add Realm, click on 'Choose a JSON file', selct example-realm.json and click Upload.
-
-Deploy the JS Console to Keycloak by running:
-
-    mvn install wildfly:deploy
-
-Open the console at http://localhost:8080/js-console and login with username: 'user', and password: 'password'.
diff --git a/testsuite/integration-arquillian/test-apps/js-console/example-realm.json b/testsuite/integration-arquillian/test-apps/js-console/example-realm.json
deleted file mode 100755
index 0a90d2ca03..0000000000
--- a/testsuite/integration-arquillian/test-apps/js-console/example-realm.json
+++ /dev/null
@@ -1,88 +0,0 @@
-{
-    "realm": "example",
-    "enabled": true,
-    "sslRequired": "external",
-    "registrationAllowed": true,
-    "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
-    "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
-    "requiredCredentials": [ "password" ],
-    "users" : [
-        {
-            "username" : "user",
-            "enabled": true,
-            "email" : "sample-user@example",
-            "firstName": "Sample",
-            "lastName": "User",
-            "credentials" : [
-                { "type" : "password",
-                  "value" : "password" }
-            ],
-            "realmRoles": [ "user" ],
-            "clientRoles": {
-                "realm-management" : [ "view-realm", "manage-users" ],
-                "account": ["view-profile", "manage-account"]
-            }
-        },{
-            "username" : "unauthorized",
-            "enabled": true,
-            "email" : "sample-user2@example",
-            "firstName": "Sample",
-            "lastName": "User",
-            "credentials" : [
-                { "type" : "password",
-                    "value" : "password" }
-            ],
-            "realmRoles": [],
-            "clientRoles": {
-                "account": ["view-profile", "manage-account"]
-            }
-        }
-    ],
-    "roles" : {
-        "realm" : [
-            {
-                "name": "user",
-                "description": "User privileges"
-            },
-            {
-                "name": "admin",
-                "description": "Administrator privileges"
-            }
-        ]
-    },
-    "scopeMappings": [
-        {
-            "client": "js-console",
-            "roles": ["user"]
-        }
-    ],
-    "clients": [
-        {
-            "clientId": "js-console",
-            "enabled": true,
-            "publicClient": true,
-            "baseUrl": "/js-console",
-            "redirectUris": [
-                "/js-console/*"
-            ],
-            "webOrigins": [
-                "http://localhost:8280",
-                "https://localhost:8643"
-            ]
-        },{
-            "clientId": "js-database",
-            "enabled": true,
-            "adminUrl": "/database",
-            "baseUrl": "/database",
-            "bearerOnly": true
-        }
-    ],
-    "clientScopeMappings": {
-        "account": [
-            {
-                "client": "js-console",
-                "roles": ["view-profile"]
-            }
-        ]
-    }
-}
diff --git a/testsuite/integration-arquillian/test-apps/js-console/pom.xml b/testsuite/integration-arquillian/test-apps/js-console/pom.xml
deleted file mode 100755
index e23f99bfaf..0000000000
--- a/testsuite/integration-arquillian/test-apps/js-console/pom.xml
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
-    
-        org.keycloak.testsuite
-        integration-arquillian-test-apps
-        4.0.0.CR1-SNAPSHOT
-    
-
-    4.0.0
-    integration-arquillian-test-apps-js-console
-    war
-    JS Console
-    
-
-    
-        ${project.version}
-    
-
-    
-        js-console
-        
-            
-                maven-dependency-plugin
-                
-                    
-                        unpack-javascript-adapter
-                        generate-resources
-                        
-                            unpack
-                        
-                        
-                            
-                                
-                                    org.keycloak
-                                    keycloak-js-adapter
-                                    ${js-adapter.version}
-                                    jar
-                                    ${pom.basedir}/target/js-console/js
-                                
-                            
-                            **/keycloak.js
-                        
-                    
-                
-            
-            
-                org.jboss.as.plugins
-                jboss-as-maven-plugin
-                
-                    false
-                
-            
-            
-                org.wildfly.plugins
-                wildfly-maven-plugin
-                
-                    false
-                
-            
-        
-    
-
diff --git a/testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/WEB-INF/web.xml b/testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/WEB-INF/web.xml
deleted file mode 100644
index 16e8b239d2..0000000000
--- a/testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/WEB-INF/web.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-    js-console
-
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/index.html b/testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/index.html
deleted file mode 100755
index 65d58f1575..0000000000
--- a/testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/index.html
+++ /dev/null
@@ -1,363 +0,0 @@
-
-
-
-
-    
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -

Result

-

-
-

Events

-

-
-

Info

-TimeSkew: -
- - - - - diff --git a/testsuite/integration-arquillian/test-apps/js-database/pom.xml b/testsuite/integration-arquillian/test-apps/js-database/pom.xml deleted file mode 100644 index 078d7be7f5..0000000000 --- a/testsuite/integration-arquillian/test-apps/js-database/pom.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - integration-arquillian-test-apps - org.keycloak.testsuite - 4.0.0.CR1-SNAPSHOT - - - 4.0.0 - integration-arquillian-test-apps-js-database - JAX-RS Database Service Using OAuth Bearer Tokens - war - - - - jboss - jboss repo - https://repository.jboss.org/nexus/content/groups/public/ - - false - - - - - - - org.jboss.resteasy - resteasy-jaxrs - provided - - - org.jboss.spec.javax.servlet - jboss-servlet-api_3.0_spec - provided - - - org.keycloak - keycloak-core - provided - - - org.keycloak - keycloak-adapter-core - provided - - - org.apache.httpcomponents - httpclient - provided - - - - - js-database - - - org.jboss.as.plugins - jboss-as-maven-plugin - - false - - - - org.wildfly.plugins - wildfly-maven-plugin - - false - - - - - diff --git a/testsuite/integration-arquillian/test-apps/js-database/src/main/java/org/keycloak/example/oauth/CustomerService.java b/testsuite/integration-arquillian/test-apps/js-database/src/main/java/org/keycloak/example/oauth/CustomerService.java deleted file mode 100755 index d2b193eb60..0000000000 --- a/testsuite/integration-arquillian/test-apps/js-database/src/main/java/org/keycloak/example/oauth/CustomerService.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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. - */ - -package org.keycloak.example.oauth; - -import org.jboss.resteasy.annotations.cache.NoCache; -import org.jboss.resteasy.spi.HttpRequest; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.representations.AccessToken; - -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import java.util.ArrayList; -import java.util.List; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -@Path("customers") -public class CustomerService { - - @Context - private HttpRequest httpRequest; - - @GET - @Produces("application/json") - @NoCache - public List getCustomers() { - // Just to show how to user info from access token in REST endpoint - KeycloakSecurityContext securityContext = (KeycloakSecurityContext) httpRequest.getAttribute(KeycloakSecurityContext.class.getName()); - AccessToken accessToken = securityContext.getToken(); - System.out.println(String.format("User '%s' with email '%s' made request to CustomerService REST endpoint", accessToken.getPreferredUsername(), accessToken.getEmail())); - - ArrayList rtn = new ArrayList(); - rtn.add("Bill Burke"); - rtn.add("Stian Thorgersen"); - rtn.add("Stan Silvert"); - rtn.add("Gabriel Cardoso"); - rtn.add("Viliam Rockai"); - rtn.add("Marek Posolda"); - rtn.add("Boleslaw Dawidowicz"); - return rtn; - } -} diff --git a/testsuite/integration-arquillian/test-apps/js-database/src/main/webapp/WEB-INF/keycloak.json b/testsuite/integration-arquillian/test-apps/js-database/src/main/webapp/WEB-INF/keycloak.json deleted file mode 100755 index 9f94148a6d..0000000000 --- a/testsuite/integration-arquillian/test-apps/js-database/src/main/webapp/WEB-INF/keycloak.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "realm" : "example", - "resource" : "js-database", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", - "auth-server-url": "http://localhost:8180/auth", - "bearer-only" : true, - "ssl-required" : "external" -} diff --git a/testsuite/integration-arquillian/test-apps/js-database/src/main/webapp/WEB-INF/web.xml b/testsuite/integration-arquillian/test-apps/js-database/src/main/webapp/WEB-INF/web.xml deleted file mode 100755 index 599f6679ed..0000000000 --- a/testsuite/integration-arquillian/test-apps/js-database/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - js-database - - - - /* - - - - user - - - - - KEYCLOAK - example - - - - user - - diff --git a/testsuite/integration-arquillian/test-apps/pom.xml b/testsuite/integration-arquillian/test-apps/pom.xml index 5a33513947..e6c76e8bc6 100644 --- a/testsuite/integration-arquillian/test-apps/pom.xml +++ b/testsuite/integration-arquillian/test-apps/pom.xml @@ -15,9 +15,7 @@ Test apps - js-console test-apps-dist - js-database photoz hello-world-authz-service servlet-authz 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 e3200c5a46..a9cd806112 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 @@ - - - - - - - - diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/JSConsoleTestApp.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/JSConsoleTestApp.java deleted file mode 100755 index 40063da936..0000000000 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/JSConsoleTestApp.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * 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. - */ - -package org.keycloak.testsuite.adapter.page; - -import org.jboss.arquillian.container.test.api.OperateOnDeployment; -import org.jboss.arquillian.test.api.ArquillianResource; -import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.ui.Select; - -import java.net.URL; - -/** - * - * @author tkyjovsk - */ -public class JSConsoleTestApp extends AbstractPageWithInjectedUrl { - - public static final String DEPLOYMENT_NAME = "js-console-example"; - public static final String CLIENT_ID = "integration-arquillian-test-apps-js-console"; - - @ArquillianResource - @OperateOnDeployment(DEPLOYMENT_NAME) - private URL url; - - @Override - public URL getInjectedUrl() { - //EAP6 URL fix - URL fixedUrl = createInjectedURL("js-console"); - return fixedUrl != null ? fixedUrl : url; - } - - @FindBy(xpath = "//button[text() = 'Init']") - private WebElement initButton; - @FindBy(xpath = "//button[text() = 'Init with both tokens']") - private WebElement initWithBothTokens; - @FindBy(xpath = "//button[text() = 'Init with refresh token']") - private WebElement initWithRefreshToken; - @FindBy(xpath = "//button[text() = 'Init with TimeSkew']") - private WebElement initWithTimeSkew; - @FindBy(xpath = "//button[text() = 'Init with different realm name']") - private WebElement initWithDifferentRealmName; - @FindBy(xpath = "//button[text() = 'Login']") - private WebElement logInButton; - @FindBy(xpath = "//button[text() = 'Logout']") - private WebElement logOutButton; - @FindBy(xpath = "//button[text() = 'Refresh Token']") - private WebElement refreshTokenButton; - @FindBy(xpath = "//button[contains(text(),'Refresh Token (if <30s')]") - private WebElement refreshTokenIfUnder30sButton; - @FindBy(xpath = "//button[contains(text(),'Refresh Token (if <5s')]") - private WebElement refreshTokenIfUnder5sButton; - @FindBy(xpath = "//button[text() = 'Get Profile']") - private WebElement getProfileButton; - - @FindBy(xpath = "//button[text() = 'Show Error Response']") - private WebElement showErrorButton; - @FindBy(xpath = "//button[text() = 'Show Token']") - private WebElement showTokenButton; - @FindBy(xpath = "//button[text() = 'Show Refresh Token']") - private WebElement showRefreshTokenButton; - @FindBy(xpath = "//button[text() = 'Show ID Token']") - private WebElement showIdTokenButton; - @FindBy(xpath = "//button[text() = 'Show Expires']") - private WebElement showExpiresButton; - @FindBy(xpath = "//button[text() = 'Show Details']") - private WebElement showDetailsButton; - @FindBy(xpath = "//button[text() = 'Create Bearer Request']") - private WebElement createBearerRequest; - @FindBy(xpath = "//button[text() = 'Bearer to keycloak']") - private WebElement createBearerRequestToKeycloakButton; - @FindBy(xpath = "//button[text() = 'Cert request']") - private WebElement certRequestButton; - @FindBy(xpath = "//button[text() = 'refresh timeSkew']") - private WebElement refreshTimeSkewButton; - @FindBy(xpath = "//button[text() = 'Create user']") - private WebElement createUserButton; - @FindBy(xpath = "//button[text() = 'Reentrancy callback']") - private WebElement reentrancyCallbackButton; - - @FindBy(id = "timeSkew") - private WebElement timeSkewValue; - @FindBy(id = "inputField") - private WebElement generalInput; - @FindBy(id = "inputField2") - private WebElement generalInput2; - @FindBy(id = "inputField3") - private WebElement generalInput3; - @FindBy(xpath = "//button[text() = 'timeSkew offset']") - private WebElement timeSkewButton; - - @FindBy(id = "flowSelect") - private Select flowSelect; - @FindBy(id = "responseModeSelect") - private Select responseModeSelect; - @FindBy(id = "onLoad") - private Select onLoad; - - - @FindBy(id = "output") - private WebElement outputArea; - - @FindBy(id = "events") - private WebElement eventsArea; - - public void logIn() { - logInButton.click(); - } - - public void logOut() { - logOutButton.click(); - } - - public void refreshToken() { - refreshTokenButton.click(); - } - - public void refreshTokenIfUnder30s() { - refreshTokenIfUnder30sButton.click(); - } - - public void refreshTokenIfUnder5s() { - refreshTokenIfUnder5sButton.click(); - } - - public void getProfile() { - getProfileButton.click(); - } - - public void setFlow(String value) { - flowSelect.selectByValue(value); - } - - public void setOnLoad(String value) { - onLoad.selectByValue(value); - } - - public void init() { - initButton.click(); - } - - public void initWithBothTokens() { - initWithBothTokens.click(); - } - - public void initWithRefreshToken() { - initWithRefreshToken.click(); - } - - public void initWithTimeSkew() { - initWithTimeSkew.click(); - } - - public void initWithDifferentRealmName() { - initWithDifferentRealmName.click(); - } - - public void createBearerRequest() { - createBearerRequest.click(); - } - - public void createBearerRequestToKeycloak() { - createBearerRequestToKeycloakButton.click(); - } - - public void setResponseMode(String value) { - responseModeSelect.selectByValue(value); - } - - public WebElement getOutputElement() { - return outputArea; - } - - public WebElement getEventsElement() { - return eventsArea; - } - - public WebElement getInitButtonElement() { - return initButton; - } - - public void showErrorResponse() { - showErrorButton.click(); - } - - public WebElement getTimeSkewValue() { - return timeSkewValue; - } - - public void setInput(String value) { - generalInput.clear(); - generalInput.sendKeys(value); - } - - public void setInput2(String value) { - generalInput2.clear(); - generalInput2.sendKeys(value); - } - - public void setInput3(String value) { - generalInput3.clear(); - generalInput3.sendKeys(value); - } - - public void setInput(int value) { - setInput(Integer.toString(value)); - } - - public void setTimeSkew(int value) { - setInput(value); - timeSkewButton.click(); - } - - public void refreshTimeSkew() { - refreshTimeSkewButton.click(); - } - - public void createUserRequest() { - createUserButton.click(); - } - - public void sendCertRequest() { - certRequestButton.click(); - } - - public void callReentrancyCallback() { - reentrancyCallbackButton.click(); - } -} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/JSDatabaseTestApp.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/JSDatabaseTestApp.java deleted file mode 100644 index 83afdb2893..0000000000 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/JSDatabaseTestApp.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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. - */ - -package org.keycloak.testsuite.adapter.page; - -import org.jboss.arquillian.container.test.api.OperateOnDeployment; -import org.jboss.arquillian.test.api.ArquillianResource; -import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl; - -import java.net.URL; - -/** - * - * @author tkyjovsk - */ -public class JSDatabaseTestApp extends AbstractPageWithInjectedUrl { - - public static final String DEPLOYMENT_NAME = "js-database-example"; - public static final String CLIENT_ID = "integration-arquillian-test-apps-js-database"; - - @ArquillianResource - @OperateOnDeployment(DEPLOYMENT_NAME) - private URL url; - - @Override - public URL getInjectedUrl() { - //EAP6 URL fix - URL fixedUrl = createInjectedURL("js-database"); - return fixedUrl != null ? fixedUrl : url; - } - -} \ No newline at end of file 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 55b9d3aa6d..f3312d9359 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 dc6bfaffd8..04daf8da81 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/test-apps/js-database/src/main/java/org/keycloak/example/oauth/DataApplication.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/JavascriptBrowser.java old mode 100755 new mode 100644 similarity index 59% rename from testsuite/integration-arquillian/test-apps/js-database/src/main/java/org/keycloak/example/oauth/DataApplication.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/JavascriptBrowser.java index 6456904af6..687d95126e --- a/testsuite/integration-arquillian/test-apps/js-database/src/main/java/org/keycloak/example/oauth/DataApplication.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/JavascriptBrowser.java @@ -14,17 +14,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package org.keycloak.testsuite.util; -package org.keycloak.example.oauth; +import org.jboss.arquillian.drone.api.annotation.Qualifier; -import javax.ws.rs.ApplicationPath; -import javax.ws.rs.core.Application; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; /** - * @author Bill Burke - * @version $Revision: 1 $ + * + * @author Petr Mensik */ -@ApplicationPath("/") -public class DataApplication extends Application -{ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Qualifier +public @interface JavascriptBrowser { } 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 6ea17c683a..497947d1f5 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 Stian Thorgersen * @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/example/AbstractJSConsoleExampleAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractJSConsoleExampleAdapterTest.java deleted file mode 100755 index fcc7ff3553..0000000000 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractJSConsoleExampleAdapterTest.java +++ /dev/null @@ -1,594 +0,0 @@ -/* - * 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. - */ - -package org.keycloak.testsuite.adapter.example; - -import org.jboss.arquillian.container.test.api.Deployment; -import org.jboss.arquillian.graphene.page.Page; -import org.jboss.shrinkwrap.api.spec.WebArchive; -import org.junit.Test; -import org.keycloak.OAuth2Constants; -import org.keycloak.admin.client.resource.ClientResource; -import org.keycloak.admin.client.resource.UsersResource; -import org.keycloak.representations.idm.ClientRepresentation; -import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest; -import org.keycloak.testsuite.adapter.page.JSConsoleTestApp; -import org.keycloak.testsuite.adapter.page.JSDatabaseTestApp; -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.console.page.events.Config; -import org.keycloak.testsuite.console.page.events.LoginEvents; -import org.keycloak.testsuite.util.OAuthClient; -import org.keycloak.testsuite.util.RealmBuilder; -import org.openqa.selenium.By; -import org.openqa.selenium.TimeoutException; -import org.openqa.selenium.WebElement; - -import java.io.File; -import java.io.IOException; -import java.util.Arrays; -import java.util.List; - -import static org.hamcrest.Matchers.containsString; -import static org.junit.Assert.*; -import static org.keycloak.testsuite.auth.page.AuthRealm.EXAMPLE; -import static org.keycloak.testsuite.util.IOUtil.loadRealm; -import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith; -import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; -import static org.keycloak.testsuite.util.WaitUtils.pause; -import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement; - -public abstract class AbstractJSConsoleExampleAdapterTest extends AbstractExampleAdapterTest { - - @Page - private JSConsoleTestApp jsConsoleTestAppPage; - - @Page - private Config configPage; - - @Page - private LoginEvents loginEventsPage; - - @Page - private OAuthGrant oAuthGrantPage; - - @Page - private Applications applicationsPage; - - private static int TIME_SKEW_TOLERANCE = 3; - - public static int TOKEN_LIFESPAN_LEEWAY = 3; // seconds - - @Deployment(name = JSConsoleTestApp.DEPLOYMENT_NAME) - private static WebArchive jsConsoleTestApp() throws IOException { - return exampleDeployment(JSConsoleTestApp.CLIENT_ID); - } - - @Deployment(name = JSDatabaseTestApp.DEPLOYMENT_NAME) - private static WebArchive jsDbApp() throws IOException { - return exampleDeployment(JSDatabaseTestApp.CLIENT_ID); - } - - @Override - public void addAdapterTestRealms(List testRealms) { - RealmRepresentation jsConsoleRealm = loadRealm(new File(TEST_APPS_HOME_DIR + "/js-console/example-realm.json")); - - fixClientUrisUsingDeploymentUrl(jsConsoleRealm, - JSConsoleTestApp.CLIENT_ID, jsConsoleTestAppPage.buildUri().toASCIIString()); - - jsConsoleRealm.setAccessTokenLifespan(30 + TOKEN_LIFESPAN_LEEWAY); // seconds - - testRealms.add(jsConsoleRealm); - } - - @Override - public void setDefaultPageUriParameters() { - super.setDefaultPageUriParameters(); - testRealmPage.setAuthRealm(EXAMPLE); - } - - @Test - public void testJSConsoleAuth() { - jsConsoleTestAppPage.navigateTo(); - assertCurrentUrlStartsWith(jsConsoleTestAppPage); - - waitUntilElement(jsConsoleTestAppPage.getInitButtonElement()).is().present(); - - jsConsoleTestAppPage.init(); - jsConsoleTestAppPage.logIn(); - testRealmLoginPage.form().login("user", "invalid-password"); - assertCurrentUrlDoesntStartWith(jsConsoleTestAppPage); - - testRealmLoginPage.form().login("invalid-user", "password"); - assertCurrentUrlDoesntStartWith(jsConsoleTestAppPage); - - testRealmLoginPage.form().login("user", "password"); - assertCurrentUrlStartsWith(jsConsoleTestAppPage); - jsConsoleTestAppPage.init(); - - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Init Success (Authenticated)"); - waitUntilElement(jsConsoleTestAppPage.getEventsElement()).text().contains("Auth Success"); - - jsConsoleTestAppPage.logOut(); - assertCurrentUrlStartsWith(jsConsoleTestAppPage); - waitUntilElement(jsConsoleTestAppPage.getInitButtonElement()).is().present(); - jsConsoleTestAppPage.init(); - - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Init Success (Not Authenticated)"); - } - - @Test - public void testRefreshToken() { - jsConsoleTestAppPage.navigateTo(); - assertCurrentUrlStartsWith(jsConsoleTestAppPage); - - jsConsoleTestAppPage.init(); - jsConsoleTestAppPage.refreshToken(); - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Failed to refresh token"); - - jsConsoleTestAppPage.logIn(); - testRealmLoginPage.form().login("user", "password"); - assertCurrentUrlStartsWith(jsConsoleTestAppPage); - jsConsoleTestAppPage.init(); - waitUntilElement(jsConsoleTestAppPage.getEventsElement()).text().contains("Auth Success"); - - jsConsoleTestAppPage.refreshToken(); - waitUntilElement(jsConsoleTestAppPage.getEventsElement()).text().contains("Auth Refresh Success"); - } - - @Test - public void testRefreshTokenIfUnder30s() { - jsConsoleTestAppPage.navigateTo(); - assertCurrentUrlStartsWith(jsConsoleTestAppPage); - jsConsoleTestAppPage.init(); - jsConsoleTestAppPage.refreshToken(); - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Failed to refresh token"); - - jsConsoleTestAppPage.logIn(); - testRealmLoginPage.form().login("user", "password"); - assertCurrentUrlStartsWith(jsConsoleTestAppPage); - jsConsoleTestAppPage.init(); - waitUntilElement(jsConsoleTestAppPage.getEventsElement()).text().contains("Auth Success"); - - jsConsoleTestAppPage.refreshTokenIfUnder30s(); - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Token not refreshed, valid for"); - - pause((TOKEN_LIFESPAN_LEEWAY + 2) * 1000); - - jsConsoleTestAppPage.refreshTokenIfUnder30s(); - waitUntilElement(jsConsoleTestAppPage.getEventsElement()).text().contains("Auth Refresh Success"); - } - - @Test - public void testGetProfile() { - jsConsoleTestAppPage.navigateTo(); - assertCurrentUrlStartsWith(jsConsoleTestAppPage); - - jsConsoleTestAppPage.init(); - jsConsoleTestAppPage.getProfile(); - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Failed to load profile"); - - jsConsoleTestAppPage.logIn(); - testRealmLoginPage.form().login("user", "password"); - assertCurrentUrlStartsWith(jsConsoleTestAppPage); - jsConsoleTestAppPage.init(); - waitUntilElement(jsConsoleTestAppPage.getEventsElement()).text().contains("Auth Success"); - - jsConsoleTestAppPage.getProfile(); - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("\"username\": \"user\""); - } - - @Test - public void testCertEndpoint() { - logInAndInit("standard"); - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Init Success (Authenticated)"); - - jsConsoleTestAppPage.sendCertRequest(); - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Success"); - } - - @Test - public void grantBrowserBasedApp() { - testRealmPage.setAuthRealm(EXAMPLE); - testRealmLoginPage.setAuthRealm(EXAMPLE); - configPage.setConsoleRealm(EXAMPLE); - loginEventsPage.setConsoleRealm(EXAMPLE); - applicationsPage.setAuthRealm(EXAMPLE); - - jsConsoleTestAppPage.navigateTo(); - driver.manage().deleteAllCookies(); - - ClientResource clientResource = ApiUtil.findClientResourceByClientId(testRealmResource(), "js-console"); - ClientRepresentation client = clientResource.toRepresentation(); - client.setConsentRequired(true); - clientResource.update(client); - - RealmRepresentation realm = testRealmResource().toRepresentation(); - realm.setEventsEnabled(true); - realm.setEnabledEventTypes(Arrays.asList("REVOKE_GRANT", "LOGIN")); - testRealmResource().update(realm); - - jsConsoleTestAppPage.navigateTo(); - jsConsoleTestAppPage.init(); - jsConsoleTestAppPage.logIn(); - - testRealmLoginPage.form().login("user", "password"); - - assertTrue(oAuthGrantPage.isCurrent()); - oAuthGrantPage.accept(); - - jsConsoleTestAppPage.init(); - - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Init Success (Authenticated)"); - - applicationsPage.navigateTo(); - applicationsPage.revokeGrantForApplication("js-console"); - - jsConsoleTestAppPage.navigateTo(); - jsConsoleTestAppPage.setOnLoad("login-required"); - jsConsoleTestAppPage.init(); - - waitUntilElement(By.tagName("body")).is().visible(); - assertTrue(oAuthGrantPage.isCurrent()); - - loginEventsPage.navigateTo(); - loginPage.form().login(adminUser); - loginEventsPage.table().filter(); - loginEventsPage.table().filterForm().addEventType("REVOKE_GRANT"); - loginEventsPage.table().update(); - - List resultList = loginEventsPage.table().rows(); - - assertEquals(1, resultList.size()); - - resultList.get(0).findElement(By.xpath(".//td[text()='REVOKE_GRANT']")); - resultList.get(0).findElement(By.xpath(".//td[text()='Client']/../td[text()='account']")); - resultList.get(0).findElement(By.xpath(".//td[text()='IP Address']/../td[text()='127.0.0.1' or text()='0:0:0:0:0:0:0:1']")); - resultList.get(0).findElement(By.xpath(".//td[text()='revoked_client']/../td[text()='js-console']")); - - loginEventsPage.table().reset(); - loginEventsPage.table().filterForm().addEventType("LOGIN"); - loginEventsPage.table().update(); - resultList = loginEventsPage.table().rows(); - - assertEquals(1, resultList.size()); - - resultList.get(0).findElement(By.xpath(".//td[text()='LOGIN']")); - resultList.get(0).findElement(By.xpath(".//td[text()='Client']/../td[text()='js-console']")); - resultList.get(0).findElement(By.xpath(".//td[text()='IP Address']/../td[text()='127.0.0.1' or text()='0:0:0:0:0:0:0:1']")); - resultList.get(0).findElement(By.xpath(".//td[text()='username']/../td[text()='user']")); - resultList.get(0).findElement(By.xpath(".//td[text()='consent']/../td[text()='consent_granted']")); - } - - - @Test - public void implicitFlowTest() { - jsConsoleTestAppPage.navigateTo(); - jsConsoleTestAppPage.setFlow("implicit"); - jsConsoleTestAppPage.init(); - - jsConsoleTestAppPage.logIn(); - assertResponseError("Implicit flow is disabled for the client"); - - setImplicitFlowForClient(); - - jsConsoleTestAppPage.navigateTo(); - jsConsoleTestAppPage.init(); - jsConsoleTestAppPage.logIn(); - assertResponseError("Standard flow is disabled for the client"); - - logInAndInit("implicit"); - - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Init Success (Authenticated)"); - } - - @Test - public void implicitFlowQueryTest() { - setImplicitFlowForClient(); - - jsConsoleTestAppPage.navigateTo(); - jsConsoleTestAppPage.setFlow("implicit"); - jsConsoleTestAppPage.setResponseMode("query"); - jsConsoleTestAppPage.init(); - jsConsoleTestAppPage.logIn(); - assertResponseError("Response_mode 'query' not allowed"); - } - - @Test - public void implicitFlowRefreshTokenTest() { - setImplicitFlowForClient(); - - logInAndInit("implicit"); - - jsConsoleTestAppPage.refreshToken(); - - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Failed to refresh token"); - } - - @Test - public void implicitFlowOnTokenExpireTest() { - RealmRepresentation realm = testRealmResource().toRepresentation(); - realm.setAccessTokenLifespanForImplicitFlow(5); - testRealmResource().update(realm); - - setImplicitFlowForClient(); - - logInAndInit("implicit"); - - pause(6000); - - waitUntilElement(jsConsoleTestAppPage.getEventsElement()).text().contains("Access token expired"); - } - - @Test - public void implicitFlowCertEndpoint() { - setImplicitFlowForClient(); - logInAndInit("implicit"); - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Init Success (Authenticated)"); - - jsConsoleTestAppPage.sendCertRequest(); - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Success"); - } - - @Test - public void testBearerRequest() { - jsConsoleTestAppPage.navigateTo(); - jsConsoleTestAppPage.init(); - jsConsoleTestAppPage.createBearerRequest(); - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Unauthorized"); - - logInAndInit("standard", "unauthorized"); - jsConsoleTestAppPage.createBearerRequest(); - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Forbidden"); - - jsConsoleTestAppPage.logOut(); - logInAndInit("standard"); - jsConsoleTestAppPage.createBearerRequest(); - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("[\"Bill Burke\",\"Stian Thorgersen\",\"Stan Silvert\",\"Gabriel Cardoso\",\"Viliam Rockai\",\"Marek Posolda\",\"Boleslaw Dawidowicz\"]"); - } - - @Test - public void loginRequiredAction() { - jsConsoleTestAppPage.navigateTo(); - jsConsoleTestAppPage.setOnLoad("login-required"); - jsConsoleTestAppPage.init(); - - assertCurrentUrlStartsWith(testRealmLoginPage); - testRealmLoginPage.form().login("user", "password"); - - waitUntilElement(jsConsoleTestAppPage.getInitButtonElement()).is().present(); - jsConsoleTestAppPage.init(); - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Init Success (Authenticated)"); - } - - @Test - public void testUpdateToken() { - logInAndInit("standard"); - - jsConsoleTestAppPage.setTimeSkew(-33); - setTimeOffset(33); - - jsConsoleTestAppPage.refreshTokenIfUnder5s(); - - jsConsoleTestAppPage.setTimeSkew(-34); - setTimeOffset(67); - - jsConsoleTestAppPage.refreshTokenIfUnder5s(); - jsConsoleTestAppPage.createBearerRequestToKeycloak(); - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Success"); - } - - @Test - public void timeSkewTest() { - logInAndInit("standard"); - - jsConsoleTestAppPage.refreshTimeSkew(); - - waitUntilElement(jsConsoleTestAppPage.getTimeSkewValue()).text().not().contains("undefined"); - - int timeSkew = Integer.parseInt(jsConsoleTestAppPage.getTimeSkewValue().getText()); - assertTrue("TimeSkew was: " + timeSkew + ", but should be ~0", timeSkew >= 0 - TIME_SKEW_TOLERANCE); - assertTrue("TimeSkew was: " + timeSkew + ", but should be ~0", timeSkew <= TIME_SKEW_TOLERANCE); - - setTimeOffset(40); - jsConsoleTestAppPage.refreshToken(); - jsConsoleTestAppPage.refreshTimeSkew(); - - waitUntilElement(jsConsoleTestAppPage.getTimeSkewValue()).text().not().contains("undefined"); - - timeSkew = Integer.parseInt(jsConsoleTestAppPage.getTimeSkewValue().getText()); - assertTrue("TimeSkew was: " + timeSkew + ", but should be ~-40", timeSkew + 40 >= 0 - TIME_SKEW_TOLERANCE); - assertTrue("TimeSkew was: " + timeSkew + ", but should be ~-40", timeSkew + 40 <= TIME_SKEW_TOLERANCE); - } - - // KEYCLOAK-4179 - @Test - public void testOneSecondTimeSkewTokenUpdate() { - setTimeOffset(1); - - logInAndInit("standard"); - - jsConsoleTestAppPage.refreshToken(); - - waitUntilElement(jsConsoleTestAppPage.getEventsElement()).text().contains("Auth Refresh Success"); - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().not().contains("Failed to refresh token"); - - 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(jsConsoleTestAppPage.getEventsElement()).text().contains("Auth Logout"); - - throw new RuntimeException("The events element shouldn't contain \"Auth Logout\" text"); - } catch (TimeoutException e) { - // OK - } - - } - - @Test - public void testLocationHeaderInResponse() { - logInAndInit("standard"); - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Init Success (Authenticated)"); - - jsConsoleTestAppPage.createUserRequest(); - - UsersResource userResource = testRealmResource().users(); - - List users = userResource.search("mhajas", 0, 1); - assertEquals("There should be created user mhajas", 1, users.size()); - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text() - .contains("location: " + authServerContextRootPage.toString() + "/auth/admin/realms/" + EXAMPLE + "/users/" + users.get(0).getId()); - } - - @Test - public void spaceInRealmNameTest() { - String SPACE_REALM_NAME = "Example realm"; - adminClient.realm(EXAMPLE).update(RealmBuilder.edit(adminClient.realm(EXAMPLE).toRepresentation()).name(SPACE_REALM_NAME).build()); - - jsConsoleTestAppPage.navigateTo(); - jsConsoleTestAppPage.setInput(SPACE_REALM_NAME); - jsConsoleTestAppPage.initWithDifferentRealmName(); - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Init Success (Not Authenticated)"); - jsConsoleTestAppPage.logIn(); - waitUntilElement(By.xpath("//body")).is().present(); - testRealmLoginPage.form().login("user", "password"); - jsConsoleTestAppPage.setInput(SPACE_REALM_NAME); - jsConsoleTestAppPage.initWithDifferentRealmName(); - - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Init Success (Authenticated)"); - waitUntilElement(jsConsoleTestAppPage.getEventsElement()).text().contains("Auth Success"); - } - - @Test - public void initializeWithTokenTest() { - oauth.realm(EXAMPLE); - oauth.clientId("js-console"); - oauth.redirectUri(appServerContextRootPage + "/js-console"); - oauth.doLogin("user", "password"); - - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); - String token = tokenResponse.getAccessToken(); - String refreshToken = tokenResponse.getRefreshToken(); - - //String refreshToken = oauth.doRefreshTokenRequest(token, "password").getRefreshToken(); - - jsConsoleTestAppPage.navigateTo(); - jsConsoleTestAppPage.setInput(token); - jsConsoleTestAppPage.setInput2(refreshToken); - - jsConsoleTestAppPage.initWithBothTokens(); - - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Init Success (Authenticated)"); - - jsConsoleTestAppPage.refreshToken(); - waitUntilElement(jsConsoleTestAppPage.getEventsElement()).text().contains("Auth Refresh Success"); - } - - @Test - public void initializeWithTimeSkew() { - setTimeOffset(600); - oauth.realm(EXAMPLE); - oauth.clientId("js-console"); - oauth.redirectUri(appServerContextRootPage + "/js-console"); - oauth.doLogin("user", "password"); - - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); - String token = tokenResponse.getAccessToken(); - String refreshToken = tokenResponse.getRefreshToken(); - - jsConsoleTestAppPage.navigateTo(); - jsConsoleTestAppPage.setInput(token); - jsConsoleTestAppPage.setInput2(refreshToken); - jsConsoleTestAppPage.setInput3("-600"); - - jsConsoleTestAppPage.initWithTimeSkew(); - - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Init Success (Authenticated)"); - - jsConsoleTestAppPage.refreshToken(); - waitUntilElement(jsConsoleTestAppPage.getEventsElement()).text().contains("Auth Refresh Success"); - - setTimeOffset(0); - } - - @Test - // KEYCLOAK-4503 - public void initializeWithRefreshToken() { - oauth.realm(EXAMPLE); - oauth.clientId("js-console"); - oauth.redirectUri("http://localhost:8280/js-console"); - oauth.doLogin("user", "password"); - - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - String token = oauth.doAccessTokenRequest(code, "password").getAccessToken(); - String refreshToken = oauth.doRefreshTokenRequest(token, "password").getRefreshToken(); - - jsConsoleTestAppPage.navigateTo(); - jsConsoleTestAppPage.setInput2(refreshToken); - - jsConsoleTestAppPage.initWithRefreshToken(); - - waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Init Success (Not Authenticated)"); - waitUntilElement(jsConsoleTestAppPage.getEventsElement()).text().not().contains("Auth Success"); - } - - @Test - public void reentrancyCallbackTest() { - logInAndInit("standard"); - - jsConsoleTestAppPage.callReentrancyCallback(); - - waitUntilElement(jsConsoleTestAppPage.getEventsElement()).text().contains("First callback"); - waitUntilElement(jsConsoleTestAppPage.getEventsElement()).text().contains("Second callback"); - - waitUntilElement(jsConsoleTestAppPage.getEventsElement()).text().not().contains("Auth Logout"); - } - - private void setImplicitFlowForClient() { - ClientResource clientResource = ApiUtil.findClientResourceByClientId(testRealmResource(), "js-console"); - ClientRepresentation client = clientResource.toRepresentation(); - client.setImplicitFlowEnabled(true); - client.setStandardFlowEnabled(false); - clientResource.update(client); - } - - private void logInAndInit(String flow, String user) { - jsConsoleTestAppPage.navigateTo(); - jsConsoleTestAppPage.setFlow(flow); - jsConsoleTestAppPage.init(); - jsConsoleTestAppPage.logIn(); - waitUntilElement(By.xpath("//body")).is().present(); - testRealmLoginPage.form().login(user, "password"); - jsConsoleTestAppPage.setFlow(flow); - jsConsoleTestAppPage.init(); - } - - private void logInAndInit(String flow) { - logInAndInit(flow, "user"); - } - - private void assertResponseError(String errorDescription) { - jsConsoleTestAppPage.showErrorResponse(); - assertThat(jsConsoleTestAppPage.getOutputElement().getText(), containsString(errorDescription)); - } - -} 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 0000000000..fabedb8fe8 --- /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 { + 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 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 JavascriptStateValidator buildFunction(QuadFunction 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/JSObjectBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JSObjectBuilder.java new file mode 100644 index 0000000000..f0d549b7d5 --- /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 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 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/JavascriptAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptAdapterTest.java new file mode 100644 index 0000000000..10361a6038 --- /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) 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 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 0000000000..3c9f1ea350 --- /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 0000000000..8db7dee03f --- /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/ResponseValidator.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/ResponseValidator.java new file mode 100644 index 0000000000..8587d25e56 --- /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 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 0000000000..1a166119d1 --- /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 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 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) jsExecutor.executeAsyncScript(requestCode); + } + + private String getHeadersString() { + StringBuilder builder = new StringBuilder(); + for (Map.Entry 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 93ade25522..d81d5cc9d1 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 @@ ${chromeArguments} - + 5 5 10 5 - + + + ${js.browser} + ${htmlUnitBrowserVersion} + ${firefox_binary} + ${webdriver.chrome.driver} + ${js.chromeArguments} + ${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} + + ${browser} ${firefox_binary} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/example/EAPJSConsoleExampleAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/example/EAPJSConsoleExampleAdapterTest.java deleted file mode 100644 index e9cabc4891..0000000000 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/example/EAPJSConsoleExampleAdapterTest.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.keycloak.testsuite.adapter.example; - -import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; - -/** - * @author tkyjovsk - */ -@AppServerContainer("app-server-eap") -public class EAPJSConsoleExampleAdapterTest extends AbstractJSConsoleExampleAdapterTest { - -} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/EAP6JSConsoleExampleAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/EAP6JSConsoleExampleAdapterTest.java deleted file mode 100644 index c2911b53b5..0000000000 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/EAP6JSConsoleExampleAdapterTest.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.keycloak.testsuite.adapter.example; - -import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; - -/** - * @author tkyjovsk - */ -@AppServerContainer("app-server-eap6") -public class EAP6JSConsoleExampleAdapterTest extends AbstractJSConsoleExampleAdapterTest { - -} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/eap/src/test/java/org/keycloak/testsuite/adapter/example/RelativeEAPJSConsoleExampleAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/eap/src/test/java/org/keycloak/testsuite/adapter/example/RelativeEAPJSConsoleExampleAdapterTest.java deleted file mode 100644 index 465ae456d2..0000000000 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/eap/src/test/java/org/keycloak/testsuite/adapter/example/RelativeEAPJSConsoleExampleAdapterTest.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.keycloak.testsuite.adapter.example; - -/** - * - * @author tkyjovsk - */ -public class RelativeEAPJSConsoleExampleAdapterTest extends AbstractJSConsoleExampleAdapterTest { - -} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/RelativeWildflyJSConsoleExampleAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/RelativeWildflyJSConsoleExampleAdapterTest.java deleted file mode 100644 index 7bf44bcca3..0000000000 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/RelativeWildflyJSConsoleExampleAdapterTest.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.keycloak.testsuite.adapter.example; - -/** - * - * @author tkyjovsk - */ -public class RelativeWildflyJSConsoleExampleAdapterTest extends AbstractJSConsoleExampleAdapterTest { - -} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/remote/src/test/java/org/keycloak/testsuite/adapter/example/RemoteJSConsoleExampleAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/remote/src/test/java/org/keycloak/testsuite/adapter/example/RemoteJSConsoleExampleAdapterTest.java deleted file mode 100644 index 36d5e4a3aa..0000000000 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/remote/src/test/java/org/keycloak/testsuite/adapter/example/RemoteJSConsoleExampleAdapterTest.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.keycloak.testsuite.adapter.example; - -import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; - -/** - * @author tkyjovsk - */ -@AppServerContainer("app-server-remote") -public class RemoteJSConsoleExampleAdapterTest extends AbstractJSConsoleExampleAdapterTest { - -} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyJSConsoleExampleAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyJSConsoleExampleAdapterTest.java deleted file mode 100644 index bc9c7ae949..0000000000 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyJSConsoleExampleAdapterTest.java +++ /dev/null @@ -1,13 +0,0 @@ - -package org.keycloak.testsuite.adapter.example; - -import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; - -/** - * - * @author tkyjovsk - */ -@AppServerContainer("app-server-wildfly") -public class WildflyJSConsoleExampleAdapterTest extends AbstractJSConsoleExampleAdapterTest { - -} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/src/test/java/org/keycloak/testsuite/adapter/example/Wildfly10JSConsoleExampleAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/src/test/java/org/keycloak/testsuite/adapter/example/Wildfly10JSConsoleExampleAdapterTest.java deleted file mode 100644 index 565ca53259..0000000000 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/src/test/java/org/keycloak/testsuite/adapter/example/Wildfly10JSConsoleExampleAdapterTest.java +++ /dev/null @@ -1,13 +0,0 @@ - -package org.keycloak.testsuite.adapter.example; - -import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; - -/** - * - * @author tkyjovsk - */ -@AppServerContainer("app-server-wildfly10") -public class Wildfly10JSConsoleExampleAdapterTest extends AbstractJSConsoleExampleAdapterTest { - -} diff --git a/testsuite/integration-arquillian/tests/other/adapters/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/pom.xml index 2fc66a4402..b7e973289f 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/pom.xml @@ -267,18 +267,6 @@ - - org.keycloak.testsuite - integration-arquillian-test-apps-js-console - ${project.version} - war - - - org.keycloak.testsuite - integration-arquillian-test-apps-js-database - ${project.version} - war - org.keycloak.testsuite hello-world-authz-service diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml index 2af22bea8d..1e296445cf 100755 --- a/testsuite/integration-arquillian/tests/pom.xml +++ b/testsuite/integration-arquillian/tests/pom.xml @@ -103,6 +103,8 @@ + phantomjs + --headless chrome --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} /usr/bin/firefox @@ -268,6 +270,8 @@ ${test.intermittent} ${browser} + ${js.browser} + ${js.chromeArguments} ${htmlUnitBrowserVersion} ${webdriverDownloadBinaries}