diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java
new file mode 100644
index 0000000000..86526b9ac5
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.admin.concurrency;
+
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.admin.client.resource.RealmResource;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import org.keycloak.testsuite.admin.AbstractAdminTest;
+
+
+/**
+ * @author Stian Thorgersen
+ */
+public abstract class AbstractConcurrencyTest extends AbstractAdminTest {
+
+ private static final int DEFAULT_THREADS = 5;
+ private static final int DEFAULT_ITERATIONS = 20;
+
+ // If enabled only one request is allowed at the time. Useful for checking that test is working.
+ private static final boolean SYNCHRONIZED = false;
+
+ protected void run(final KeycloakRunnable runnable) throws Throwable {
+ run(runnable, DEFAULT_THREADS, DEFAULT_ITERATIONS);
+ }
+
+ protected void run(final KeycloakRunnable runnable, final int numThreads, final int numIterationsPerThread) throws Throwable {
+ final CountDownLatch latch = new CountDownLatch(numThreads);
+ final AtomicReference failed = new AtomicReference();
+ final List threads = new LinkedList<>();
+ final Lock lock = SYNCHRONIZED ? new ReentrantLock() : null;
+
+ for (int t = 0; t < numThreads; t++) {
+ final int threadNum = t;
+ Thread thread = new Thread() {
+ @Override
+ public void run() {
+ Keycloak keycloak = null;
+ try {
+ if (lock != null) {
+ lock.lock();
+ }
+
+ keycloak = Keycloak.getInstance(getAuthServerRoot().toString(), "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
+ RealmResource realm = keycloak.realm(REALM_NAME);
+ for (int i = 0; i < numIterationsPerThread && latch.getCount() > 0; i++) {
+ log.infov("thread {0}, iteration {1}", threadNum, i);
+ runnable.run(keycloak, realm, threadNum, i);
+ }
+ latch.countDown();
+ } catch (Throwable t) {
+ failed.compareAndSet(null, t);
+ while (latch.getCount() > 0) {
+ latch.countDown();
+ }
+ } finally {
+ keycloak.close();
+ if (lock != null) {
+ lock.unlock();
+ }
+ }
+ }
+ };
+ thread.start();
+ threads.add(thread);
+ }
+
+ latch.await();
+
+ for (Thread t : threads) {
+ t.join();
+ }
+
+ if (failed.get() != null) {
+ throw failed.get();
+ }
+ }
+
+ protected interface KeycloakRunnable {
+
+ void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum);
+
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ConcurrencyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrencyTest.java
similarity index 74%
rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ConcurrencyTest.java
rename to testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrencyTest.java
index 583ec1b2e5..a2f440932e 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ConcurrencyTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrencyTest.java
@@ -15,9 +15,8 @@
* limitations under the License.
*/
-package org.keycloak.testsuite.admin;
+package org.keycloak.testsuite.admin.concurrency;
-import org.jboss.logging.Logger;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
@@ -30,12 +29,7 @@ import org.keycloak.representations.idm.RoleRepresentation;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
+import org.keycloak.testsuite.admin.ApiUtil;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
@@ -43,15 +37,7 @@ import static org.junit.Assert.fail;
/**
* @author Stian Thorgersen
*/
-public class ConcurrencyTest extends AbstractAdminTest {
-
- private static final Logger log = Logger.getLogger(ConcurrencyTest.class);
-
- private static final int DEFAULT_THREADS = 5;
- private static final int DEFAULT_ITERATIONS = 20;
-
- // If enabled only one request is allowed at the time. Useful for checking that test is working.
- private static final boolean SYNCHRONIZED = false;
+public class ConcurrencyTest extends AbstractConcurrencyTest {
boolean passedCreateClient = false;
boolean passedCreateRole = false;
@@ -252,67 +238,4 @@ public class ConcurrencyTest extends AbstractAdminTest {
System.out.println("*********************************************");
}
-
- private void run(final KeycloakRunnable runnable) throws Throwable {
- run(runnable, DEFAULT_THREADS, DEFAULT_ITERATIONS);
- }
-
- private void run(final KeycloakRunnable runnable, final int numThreads, final int numIterationsPerThread) throws Throwable {
- final CountDownLatch latch = new CountDownLatch(numThreads);
- final AtomicReference failed = new AtomicReference();
- final List threads = new LinkedList<>();
- final Lock lock = SYNCHRONIZED ? new ReentrantLock() : null;
-
- for (int t = 0; t < numThreads; t++) {
- final int threadNum = t;
- Thread thread = new Thread() {
- @Override
- public void run() {
- Keycloak keycloak = null;
- try {
- if (lock != null) {
- lock.lock();
- }
-
- keycloak = Keycloak.getInstance(getAuthServerRoot().toString(), "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
- RealmResource realm = keycloak.realm(REALM_NAME);
- for (int i = 0; i < numIterationsPerThread && latch.getCount() > 0; i++) {
- log.infov("thread {0}, iteration {1}", threadNum, i);
- runnable.run(keycloak, realm, threadNum, i);
- }
- latch.countDown();
- } catch (Throwable t) {
- failed.compareAndSet(null, t);
- while (latch.getCount() > 0) {
- latch.countDown();
- }
- } finally {
- keycloak.close();
- if (lock != null) {
- lock.unlock();
- }
- }
- }
- };
- thread.start();
- threads.add(thread);
- }
-
- latch.await();
-
- for (Thread t : threads) {
- t.join();
- }
-
- if (failed.get() != null) {
- throw failed.get();
- }
- }
-
- interface KeycloakRunnable {
-
- void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum);
-
- }
-
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java
new file mode 100644
index 0000000000..ade3995369
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java
@@ -0,0 +1,239 @@
+/*
+ * 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.admin.concurrency;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.ws.rs.core.Response;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.LaxRedirectStrategy;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.testsuite.util.OAuthClient;
+
+
+
+/**
+ * @author Vlastislav Ramik
+ */
+public class ConcurrentLoginTest extends AbstractConcurrencyTest {
+
+ private static final int DEFAULT_THREADS = 10;
+ private static final int DEFAULT_ITERATIONS = 20;
+ private static final int CLIENTS_PER_THREAD = 10;
+ private static final int DEFAULT_CLIENTS_COUNT = CLIENTS_PER_THREAD * DEFAULT_THREADS;
+
+ @Before
+ public void beforeTest() {
+ for (int i = 0; i < DEFAULT_CLIENTS_COUNT; i++) {
+ ClientRepresentation client = new ClientRepresentation();
+ client.setClientId("client" + i);
+ client.setDirectAccessGrantsEnabled(true);
+ client.setRedirectUris(Arrays.asList("http://localhost:8180/auth/realms/master/app/*"));
+ client.setWebOrigins(Arrays.asList("http://localhost:8180"));
+ client.setSecret("password");
+
+ log.debug("creating " + client.getClientId());
+ Response create = adminClient.realm("test").clients().create(client);
+ Assert.assertEquals(Response.Status.CREATED, create.getStatusInfo());
+ create.close();
+ }
+ log.debug("clients created");
+ }
+
+ @Override
+ protected void run(final KeycloakRunnable runnable) throws Throwable {
+ run(runnable, DEFAULT_THREADS, DEFAULT_ITERATIONS);
+ }
+
+ @Test
+ public void concurrentLogin() throws Throwable {
+ System.out.println("*********************************************");
+ long start = System.currentTimeMillis();
+
+ try (CloseableHttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build()) {
+
+ HttpUriRequest request = handleLogin(getPageContent(oauth.getLoginFormUrl(), httpClient, null), "test-user@localhost", "password");
+
+ log.debug("Executing login request");
+
+ Assert.assertTrue(parseAndCloseResponse(httpClient.execute(request)).contains("AUTH_RESPONSE"));
+
+ run(new KeycloakRunnable() {
+ @Override
+ public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) {
+ OAuthClient oauth = new OAuthClient();
+ oauth.init(adminClient, driver);
+
+ int startIndex = CLIENTS_PER_THREAD * threadNum;
+ for (int i = startIndex; i < startIndex + CLIENTS_PER_THREAD; i++) {
+ oauth.clientId("client" + i);
+ log.trace("Accessing login page for " + oauth.getClientId() + " threat " + threadNum + " iteration " + iterationNum);
+ try {
+ final HttpClientContext context = HttpClientContext.create();
+
+ String pageContent = getPageContent(oauth.getLoginFormUrl(), httpClient, context);
+ String currentUrl = context.getRedirectLocations().get(0).toString();
+
+ Assert.assertTrue(pageContent.contains("AUTH_RESPONSE"));
+
+ String code = getQueryFromUrl(currentUrl).get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse accessRes = oauth.doAccessTokenRequest(code, "password");
+ Assert.assertEquals("AccessTokenResponse: error: '" + accessRes.getError() + "' desc: '" + accessRes.getErrorDescription() + "'",
+ 200, accessRes.getStatusCode());
+
+ OAuthClient.AccessTokenResponse refreshRes = oauth.doRefreshTokenRequest(accessRes.getRefreshToken(), "password");
+ Assert.assertEquals("AccessTokenResponse: error: '" + refreshRes.getError() + "' desc: '" + refreshRes.getErrorDescription() + "'",
+ 200, refreshRes.getStatusCode());
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+ }
+ });
+ }
+
+ long end = System.currentTimeMillis() - start;
+ System.out.println("concurrentLogin took " + (end/1000) + "s");
+ System.out.println("*********************************************");
+ }
+
+ private String getPageContent(String url, CloseableHttpClient httpClient, HttpClientContext context) throws Exception {
+
+ HttpGet request = new HttpGet(url);
+
+ request.setHeader("User-Agent", "Mozilla/5.0");
+ request.setHeader("Accept",
+ "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
+ request.setHeader("Accept-Language", "en-US,en;q=0.5");
+
+ if (context != null) {
+ return parseAndCloseResponse(httpClient.execute(request, context));
+ } else {
+ return parseAndCloseResponse(httpClient.execute(request));
+ }
+
+ }
+
+ private String parseAndCloseResponse(CloseableHttpResponse response) throws UnsupportedOperationException, IOException {
+ try {
+ int responseCode = response.getStatusLine().getStatusCode();
+ if (responseCode != 200) {
+ log.debug("Response Code : " + responseCode);
+ }
+ BufferedReader rd = new BufferedReader(
+ new InputStreamReader(response.getEntity().getContent()));
+ StringBuilder result = new StringBuilder();
+ String line;
+ while ((line = rd.readLine()) != null) {
+ result.append(line);
+ }
+ if (responseCode != 200) {
+ log.debug(result.toString());
+ }
+ return result.toString();
+ } catch (IOException | UnsupportedOperationException ex) {
+ throw new RuntimeException(ex);
+ } finally {
+ if (response != null) {
+ EntityUtils.consumeQuietly(response.getEntity());
+ try {
+ response.close();
+ } catch (IOException ex) { }
+ }
+ }
+ }
+
+ private HttpUriRequest handleLogin(String html, String username, String password) throws UnsupportedEncodingException {
+
+ System.out.println("Extracting form's data...");
+
+ // Keycloak form id
+ Element loginform = Jsoup.parse(html).getElementById("kc-form-login");
+ String method = loginform.attr("method");
+ String action = loginform.attr("action");
+
+ List paramList = new ArrayList<>();
+
+ for (Element inputElement : loginform.getElementsByTag("input")) {
+ String key = inputElement.attr("name");
+
+ if (key.equals("username")) {
+ paramList.add(new BasicNameValuePair(key, username));
+ } else if (key.equals("password")) {
+ paramList.add(new BasicNameValuePair(key, password));
+ }
+ }
+
+ boolean isPost = method != null && "post".equalsIgnoreCase(method);
+
+ if (isPost) {
+ HttpPost req = new HttpPost(action);
+
+ UrlEncodedFormEntity formEntity;
+ try {
+ formEntity = new UrlEncodedFormEntity(paramList, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ req.setEntity(formEntity);
+
+ return req;
+ } else {
+ throw new UnsupportedOperationException("not supported yet!");
+ }
+ }
+
+ private Map getQueryFromUrl(String url) throws URISyntaxException {
+ Map m = new HashMap<>();
+ List pairs = URLEncodedUtils.parse(new URI(url), "UTF-8");
+ for (NameValuePair p : pairs) {
+ m.put(p.getName(), p.getValue());
+ }
+ return m;
+ }
+
+
+}
\ No newline at end of file