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