keycloak-aplcache

Details

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 0000000..86526b9
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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<Throwable> failed = new AtomicReference();
+        final List<Thread> 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/concurrency/ConcurrentLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java
new file mode 100644
index 0000000..ade3995
--- /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 <a href="mailto:vramik@redhat.com">Vlastislav Ramik</a>
+ */
+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("<title>AUTH_RESPONSE</title>"));
+
+            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("<title>AUTH_RESPONSE</title>"));
+                            
+                            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<NameValuePair> 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<String, String> getQueryFromUrl(String url) throws URISyntaxException {
+        Map<String, String> m = new HashMap<>();
+        List<NameValuePair> 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