diff --git a/server-spi-private/src/main/java/org/keycloak/http/simple/SimpleHttpRequest.java b/server-spi-private/src/main/java/org/keycloak/http/simple/SimpleHttpRequest.java index 7991d017c6a..11f0f982ada 100755 --- a/server-spi-private/src/main/java/org/keycloak/http/simple/SimpleHttpRequest.java +++ b/server-spi-private/src/main/java/org/keycloak/http/simple/SimpleHttpRequest.java @@ -183,11 +183,15 @@ public class SimpleHttpRequest { } public String asString() throws IOException { - return asResponse().asString(); + try (SimpleHttpResponse response = makeRequest()) { + return response.asString(); + } } public int asStatus() throws IOException { - return asResponse().getStatus(); + try (SimpleHttpResponse response = asResponse()) { + return response.getStatus(); + } } public SimpleHttpResponse asResponse() throws IOException { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/HttpClientUtils.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/HttpClientUtils.java index 16c46ba1261..32e51c00d04 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/HttpClientUtils.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/HttpClientUtils.java @@ -1,6 +1,7 @@ package org.keycloak.testsuite.util; import org.apache.http.client.RedirectStrategy; +import org.apache.http.client.config.RequestConfig; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.DefaultRedirectStrategy; import org.apache.http.impl.client.HttpClientBuilder; @@ -9,11 +10,25 @@ public class HttpClientUtils { private static final boolean SSL_REQUIRED = Boolean.parseBoolean(System.getProperty("auth.server.ssl.required")); + // Add configurable socket timeout (default 30 seconds for better compatibility with slower environments like Windows) + private static final int SOCKET_TIMEOUT_MILLIS = Integer.parseInt( + System.getProperty("http.socket.timeout", "30000") + ); + + private static final int CONNECTION_TIMEOUT_MILLIS = Integer.parseInt( + System.getProperty("http.connection.timeout", "10000") + ); + public static CloseableHttpClient createDefault() { return createDefault(DefaultRedirectStrategy.INSTANCE); } public static CloseableHttpClient createDefault(RedirectStrategy redirectStrategy) { + RequestConfig requestConfig = RequestConfig.custom() + .setSocketTimeout(SOCKET_TIMEOUT_MILLIS) + .setConnectTimeout(CONNECTION_TIMEOUT_MILLIS) + .build(); + if (SSL_REQUIRED) { String keyStorePath = System.getProperty("client.certificate.keystore"); String keyStorePassword = System.getProperty("client.certificate.keystore.passphrase"); @@ -21,7 +36,11 @@ public class HttpClientUtils { String trustStorePassword = System.getProperty("client.truststore.passphrase"); return MutualTLSUtils.newCloseableHttpClient(keyStorePath, keyStorePassword, trustStorePath, trustStorePassword, redirectStrategy); } - return HttpClientBuilder.create().build(); + + return HttpClientBuilder.create() + .setDefaultRequestConfig(requestConfig) + .setRedirectStrategy(redirectStrategy) + .build(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/RepeatRule.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/RepeatRule.java new file mode 100644 index 00000000000..c3c853f46b9 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/RepeatRule.java @@ -0,0 +1,56 @@ +package org.keycloak.testsuite.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +public class RepeatRule implements TestRule { + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface Repeat { + int value() default 1; + } + + private static class RepeatStatement extends Statement { + private final Statement statement; + private final int repeat; + private final Description description; + + private RepeatStatement(Statement statement, int repeat, Description description) { + this.statement = statement; + this.repeat = repeat; + this.description = description; + } + + @Override + public void evaluate() throws Throwable { + for (int i = 1; i <= repeat; i++) { + System.out.println(String.format("Running iteration %d/%d of test: %s", + i, repeat, description.getMethodName())); + try { + statement.evaluate(); + } catch (Throwable t) { + System.err.println(String.format("Test failed on iteration %d/%d", i, repeat)); + throw t; + } + } + System.out.println(String.format("All %d iterations passed for test: %s", + repeat, description.getMethodName())); + } + } + + @Override + public Statement apply(Statement statement, Description description) { + Repeat repeat = description.getAnnotation(Repeat.class); + if (repeat != null && repeat.value() > 1) { + return new RepeatStatement(statement, repeat.value(), description); + } + return statement; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java index 0c92cce7b3c..e6d2c57b1c2 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java @@ -799,20 +799,22 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { .get(); // Test that current user can't update the credential, which belongs to the different user - SimpleHttpResponse response = SimpleHttpDefault + try (SimpleHttpResponse response = SimpleHttpDefault .doPut(getAccountUrl("credentials/" + otpCredential.getId() + "/label"), httpClient) .auth(tokenUtil.getToken()) .json("new-label") - .asResponse(); - assertEquals(404, response.getStatus()); + .asResponse()) { + assertEquals(404, response.getStatus()); + } // Test that current user can't delete the credential, which belongs to the different user - response = SimpleHttpDefault + try (SimpleHttpResponse response = SimpleHttpDefault .doDelete(getAccountUrl("credentials/" + otpCredential.getId()), httpClient) .acceptJson() .auth(tokenUtil.getToken()) - .asResponse(); - assertEquals(404, response.getStatus()); + .asResponse()) { + assertEquals(404, response.getStatus()); + } // Assert credential was not updated or removed CredentialRepresentation otpCredentialLoaded = user.credentials().stream() @@ -878,12 +880,13 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { .filter(credentialRep -> OTPCredentialModel.TYPE.equals(credentialRep.getType())) .findFirst() .get(); - SimpleHttpResponse response = SimpleHttpDefault + try (SimpleHttpResponse response = SimpleHttpDefault .doDelete(getAccountUrl("credentials/" + otpCredential.getId()), httpClient) .acceptJson() .auth(tokenUtil.getToken()) - .asResponse(); - assertEquals(204, response.getStatus()); + .asResponse()) { + assertEquals(204, response.getStatus()); + } events.poll(); events.expect(EventType.REMOVE_TOTP) @@ -1254,7 +1257,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { .doDelete(getAccountUrl("applications/" + appId + "/consent"), httpClient) .header("Accept", "application/json") .auth(token.getToken()) - .asResponse(); + .asResponse().close(); Map apps = applications.stream().collect(Collectors.toMap(x -> x.getClientId(), x -> x)); assertThat(apps.keySet(), containsInAnyOrder(appId, "always-display-client", "direct-grant")); @@ -1309,24 +1312,26 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { @Test public void listApplicationsWithoutPermission() throws IOException { TokenUtil token = new TokenUtil("no-account-access", "password"); - SimpleHttpResponse response = SimpleHttpDefault + try (SimpleHttpResponse response = SimpleHttpDefault .doGet(getAccountUrl("applications"), httpClient) .header("Accept", "application/json") .auth(token.getToken()) - .asResponse(); - assertEquals(403, response.getStatus()); + .asResponse()) { + assertEquals(403, response.getStatus()); + } } @Test public void getNotExistingApplication() throws IOException { TokenUtil token = new TokenUtil("view-applications-access", "password"); String appId = "not-existing"; - SimpleHttpResponse response = SimpleHttpDefault + try (SimpleHttpResponse response = SimpleHttpDefault .doGet(getAccountUrl("applications/" + appId), httpClient) .header("Accept", "application/json") .auth(token.getToken()) - .asResponse(); - assertEquals(404, response.getStatus()); + .asResponse()) { + assertEquals(404, response.getStatus()); + } } private ConsentRepresentation createRequestedConsent(List scopes) { @@ -1370,7 +1375,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { SimpleHttpDefault.doDelete(getAccountUrl("applications/" + appId + "/consent"), httpClient) .header("Accept", "application/json") .auth(tokenUtil.getToken()) - .asResponse(); + .asResponse().close(); } @Test @@ -1420,7 +1425,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { SimpleHttpDefault.doDelete(getAccountUrl("applications/" + appId + "/consent"), httpClient) .header("Accept", "application/json") .auth(tokenUtil.getToken()) - .asResponse(); + .asResponse().close(); } @Test @@ -1431,14 +1436,14 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { List requestedScopes = testRealm().clientScopes().findAll().subList(0,1); ConsentRepresentation requestedConsent = createRequestedConsent(requestedScopes); - SimpleHttpResponse response = SimpleHttpDefault + try (SimpleHttpResponse response = SimpleHttpDefault .doPost(getAccountUrl("applications/" + appId + "/consent"), httpClient) .header("Accept", "application/json") .json(requestedConsent) .auth(tokenUtil.getToken()) - .asResponse(); - - assertEquals(404, response.getStatus()); + .asResponse()) { + assertEquals(404, response.getStatus()); + } } @Test @@ -1449,14 +1454,14 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { List requestedScopes = testRealm().clientScopes().findAll().subList(0,1); ConsentRepresentation requestedConsent = createRequestedConsent(requestedScopes); - SimpleHttpResponse response = SimpleHttpDefault + try (SimpleHttpResponse response = SimpleHttpDefault .doPost(getAccountUrl("applications/" + appId + "/consent"), httpClient) .header("Accept", "application/json") .json(requestedConsent) .auth(tokenUtil.getToken()) - .asResponse(); - - assertEquals(403, response.getStatus()); + .asResponse()) { + assertEquals(403, response.getStatus()); + } } @Test @@ -1490,7 +1495,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { SimpleHttpDefault.doDelete(getAccountUrl("applications/" + appId + "/consent"), httpClient) .header("Accept", "application/json") .auth(tokenUtil.getToken()) - .asResponse(); + .asResponse().close(); } @Test @@ -1541,7 +1546,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { SimpleHttpDefault.doDelete(getAccountUrl("applications/" + appId + "/consent"), httpClient) .header("Accept", "application/json") .auth(tokenUtil.getToken()) - .asResponse(); + .asResponse().close(); } @Test @@ -1552,14 +1557,14 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { List requestedScopes = testRealm().clientScopes().findAll().subList(0,1); ConsentRepresentation requestedConsent = createRequestedConsent(requestedScopes); - SimpleHttpResponse response = SimpleHttpDefault + try (SimpleHttpResponse response = SimpleHttpDefault .doPut(getAccountUrl("applications/" + appId + "/consent"), httpClient) .header("Accept", "application/json") .json(requestedConsent) .auth(tokenUtil.getToken()) - .asResponse(); - - assertEquals(404, response.getStatus()); + .asResponse()) { + assertEquals(404, response.getStatus()); + } } @Test @@ -1570,14 +1575,14 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { List requestedScopes = testRealm().clientScopes().findAll().subList(0,1); ConsentRepresentation requestedConsent = createRequestedConsent(requestedScopes); - SimpleHttpResponse response = SimpleHttpDefault + try (SimpleHttpResponse response = SimpleHttpDefault .doPut(getAccountUrl("applications/" + appId + "/consent"), httpClient) .header("Accept", "application/json") .json(requestedConsent) .auth(tokenUtil.getToken()) - .asResponse(); - - assertEquals(403, response.getStatus()); + .asResponse()) { + assertEquals(403, response.getStatus()); + } } @Test @@ -1613,36 +1618,39 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { public void getConsentForNotExistingClient() throws IOException { tokenUtil = new TokenUtil("view-consent-access", "password"); String appId = "not-existing"; - SimpleHttpResponse response = SimpleHttpDefault + try (SimpleHttpResponse response = SimpleHttpDefault .doGet(getAccountUrl("applications/" + appId + "/consent"), httpClient) .header("Accept", "application/json") .auth(tokenUtil.getToken()) - .asResponse(); - assertEquals(404, response.getStatus()); + .asResponse()) { + assertEquals(404, response.getStatus()); + } } @Test public void getNotExistingConsentForClient() throws IOException { tokenUtil = new TokenUtil("view-consent-access", "password"); String appId = "security-admin-console"; - SimpleHttpResponse response = SimpleHttpDefault + try (SimpleHttpResponse response = SimpleHttpDefault .doGet(getAccountUrl("applications/" + appId + "/consent"), httpClient) .header("Accept", "application/json") .auth(tokenUtil.getToken()) - .asResponse(); - assertEquals(204, response.getStatus()); + .asResponse()) { + assertEquals(204, response.getStatus()); + } } @Test public void getConsentWithoutPermission() throws IOException { tokenUtil = new TokenUtil("no-account-access", "password"); String appId = "security-admin-console"; - SimpleHttpResponse response = SimpleHttpDefault + try (SimpleHttpResponse response = SimpleHttpDefault .doGet(getAccountUrl("applications/" + appId + "/consent"), httpClient) .header("Accept", "application/json") .auth(tokenUtil.getToken()) - .asResponse(); - assertEquals(403, response.getStatus()); + .asResponse()) { + assertEquals(403, response.getStatus()); + } } @Test @@ -1664,12 +1672,13 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { assertEquals(1, consentRepresentation.getGrantedScopes().size()); assertEquals(requestedScopes.get(0).getId(), consentRepresentation.getGrantedScopes().get(0).getId()); - SimpleHttpResponse response = SimpleHttpDefault + try (SimpleHttpResponse response = SimpleHttpDefault .doDelete(getAccountUrl("applications/" + appId + "/consent"), httpClient) .header("Accept", "application/json") .auth(tokenUtil.getToken()) - .asResponse(); - assertEquals(204, response.getStatus()); + .asResponse()) { + assertEquals(204, response.getStatus()); + } events.poll(); events.poll(); @@ -1679,36 +1688,39 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { .assertEvent(); events.assertEmpty(); - response = SimpleHttpDefault + try (SimpleHttpResponse response = SimpleHttpDefault .doDelete(getAccountUrl("applications/" + appId + "/consent"), httpClient) .header("Accept", "application/json") .auth(tokenUtil.getToken()) - .asResponse(); - assertEquals(204, response.getStatus()); + .asResponse()) { + assertEquals(204, response.getStatus()); + } } @Test public void deleteConsentForNotExistingClient() throws IOException { tokenUtil = new TokenUtil("manage-consent-access", "password"); String appId = "not-existing"; - SimpleHttpResponse response = SimpleHttpDefault + try (SimpleHttpResponse response = SimpleHttpDefault .doDelete(getAccountUrl("applications/" + appId + "/consent"), httpClient) .header("Accept", "application/json") .auth(tokenUtil.getToken()) - .asResponse(); - assertEquals(404, response.getStatus()); + .asResponse()) { + assertEquals(404, response.getStatus()); + } } @Test public void deleteConsentWithoutPermission() throws IOException { tokenUtil = new TokenUtil("view-consent-access", "password"); String appId = "security-admin-console"; - SimpleHttpResponse response = SimpleHttpDefault + try (SimpleHttpResponse response = SimpleHttpDefault .doDelete(getAccountUrl("applications/" + appId + "/consent"), httpClient) .header("Accept", "application/json") .auth(tokenUtil.getToken()) - .asResponse(); - assertEquals(403, response.getStatus()); + .asResponse()) { + assertEquals(403, response.getStatus()); + } } //KEYCLOAK-14344 @@ -1721,12 +1733,13 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { tokenUtil = new TokenUtil("view-applications-access", "password"); - SimpleHttpResponse response = SimpleHttpDefault + try (SimpleHttpResponse response = SimpleHttpDefault .doDelete(getAccountUrl("applications/offline-client/consent"), httpClient) .header("Accept", "application/json") .auth(tokenUtil.getToken()) - .asResponse(); - assertEquals(204, response.getStatus()); + .asResponse()) { + assertEquals(204, response.getStatus()); + } List applications = SimpleHttpDefault .doGet(getAccountUrl("applications"), httpClient) @@ -1755,9 +1768,10 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { public void testInvalidApiVersion() throws IOException { apiVersion = "v2-foo"; - SimpleHttpResponse response = SimpleHttpDefault.doGet(getAccountUrl("credentials"), httpClient).auth(tokenUtil.getToken()).asResponse(); - assertEquals("API version not found", response.asJson().get("error").textValue()); - assertEquals(404, response.getStatus()); + try (SimpleHttpResponse response = SimpleHttpDefault.doGet(getAccountUrl("credentials"), httpClient).auth(tokenUtil.getToken()).asResponse()) { + assertEquals("API version not found", response.asJson().get("error").textValue()); + assertEquals(404, response.getStatus()); + } } @Test @@ -1766,11 +1780,12 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { AccessTokenResponse tokenResponse = oauth.doPasswordGrantRequest("test-user@localhost", "password"); assertNull(tokenResponse.getErrorDescription()); - SimpleHttpResponse response = SimpleHttpDefault.doGet(getAccountUrl(null), httpClient) + try (SimpleHttpResponse response = SimpleHttpDefault.doGet(getAccountUrl(null), httpClient) .auth(tokenResponse.getAccessToken()) .header("Accept", "application/json") - .asResponse(); - assertEquals(401, response.getStatus()); + .asResponse()) { + assertEquals(401, response.getStatus()); + } // update to correct audience org.keycloak.representations.idm.ClientRepresentation clientRep = testRealm().clients().findByClientId("custom-audience").get(0); @@ -1782,11 +1797,12 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { tokenResponse = oauth.doPasswordGrantRequest("test-user@localhost", "password"); assertNull(tokenResponse.getErrorDescription()); - response = SimpleHttpDefault.doGet(getAccountUrl(null), httpClient) + try (SimpleHttpResponse response = SimpleHttpDefault.doGet(getAccountUrl(null), httpClient) .auth(tokenResponse.getAccessToken()) .header("Accept", "application/json") - .asResponse(); - assertEquals(200, response.getStatus()); + .asResponse()) { + assertEquals(200, response.getStatus()); + } // remove audience completely testRealm().clients().get(clientRep.getId()).getProtocolMappers().delete(mapperRep.getId()); @@ -1794,11 +1810,12 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { tokenResponse = oauth.doPasswordGrantRequest("test-user@localhost", "password"); assertNull(tokenResponse.getErrorDescription()); - response = SimpleHttpDefault.doGet(getAccountUrl(null), httpClient) + try (SimpleHttpResponse response = SimpleHttpDefault.doGet(getAccountUrl(null), httpClient) .auth(tokenResponse.getAccessToken()) .header("Accept", "application/json") - .asResponse(); - assertEquals(401, response.getStatus()); + .asResponse()) { + assertEquals(401, response.getStatus()); + } // custom-audience client is used only in this test so no need to revert the changes } @@ -1812,13 +1829,14 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { realmRep.setAccountTheme("custom-account-provider"); adminClient.realm("test").update(realmRep); - SimpleHttpResponse response = SimpleHttpDefault.doGet(getAccountUrl(null), httpClient) + try (SimpleHttpResponse response = SimpleHttpDefault.doGet(getAccountUrl(null), httpClient) .header("Accept", "text/html") - .asResponse(); - assertEquals(200, response.getStatus()); + .asResponse()) { + assertEquals(200, response.getStatus()); - String html = response.asString(); - assertTrue(html.contains("Custom Account Console")); + String html = response.asString(); + assertTrue(html.contains("Custom Account Console")); + } } finally { RealmRepresentation realmRep = testRealm().toRepresentation(); realmRep.setAccountTheme(accountTheme); @@ -1830,13 +1848,14 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { public void testUpdateProfileUnrecognizedPropertyInRepresentation() throws IOException { final UserRepresentation user = getUser(); final Map invalidRep = Map.of("id", user.getId(), "username", user.getUsername(), "invalid", "something"); - SimpleHttpResponse response = SimpleHttpDefault.doPost(getAccountUrl(null), httpClient) + try (SimpleHttpResponse response = SimpleHttpDefault.doPost(getAccountUrl(null), httpClient) .auth(tokenUtil.getToken()) .json(invalidRep) - .asResponse(); - assertEquals(400, response.getStatus()); - final OAuth2ErrorRepresentation error = response.asJson(OAuth2ErrorRepresentation.class); - assertThat(error.getError(), containsString("Invalid json representation for UserRepresentation. Unrecognized field \"invalid\" at line")); + .asResponse()) { + assertEquals(400, response.getStatus()); + final OAuth2ErrorRepresentation error = response.asJson(OAuth2ErrorRepresentation.class); + assertThat(error.getError(), containsString("Invalid json representation for UserRepresentation. Unrecognized field \"invalid\" at line")); + } } @Test