Replace rest test client with low level RestClient

We still have a wrapper called RestTestClient that is very specific to Rest tests, as well as RestTestResponse etc. but all the low level bits around http connections etc. are now handled by RestClient.
This commit is contained in:
javanna 2016-05-19 15:18:17 +02:00 committed by Luca Cavanna
parent 325b723930
commit eae914ae8e
14 changed files with 258 additions and 712 deletions

View File

@ -1311,9 +1311,6 @@
<suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]engine[/\\]MockEngineSupport.java" checks="LineLength" /> <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]engine[/\\]MockEngineSupport.java" checks="LineLength" />
<suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]hamcrest[/\\]ElasticsearchAssertions.java" checks="LineLength" /> <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]hamcrest[/\\]ElasticsearchAssertions.java" checks="LineLength" />
<suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]junit[/\\]listeners[/\\]LoggingListener.java" checks="LineLength" /> <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]junit[/\\]listeners[/\\]LoggingListener.java" checks="LineLength" />
<suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]ESRestTestCase.java" checks="LineLength" />
<suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]RestTestExecutionContext.java" checks="LineLength" />
<suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]client[/\\]http[/\\]HttpRequestBuilder.java" checks="LineLength" />
<suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]json[/\\]JsonPath.java" checks="LineLength" /> <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]json[/\\]JsonPath.java" checks="LineLength" />
<suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]parser[/\\]GreaterThanEqualToParser.java" checks="LineLength" /> <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]parser[/\\]GreaterThanEqualToParser.java" checks="LineLength" />
<suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]parser[/\\]GreaterThanParser.java" checks="LineLength" /> <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]parser[/\\]GreaterThanParser.java" checks="LineLength" />
@ -1321,7 +1318,6 @@
<suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]parser[/\\]LessThanParser.java" checks="LineLength" /> <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]parser[/\\]LessThanParser.java" checks="LineLength" />
<suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]parser[/\\]RestTestSuiteParseContext.java" checks="LineLength" /> <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]parser[/\\]RestTestSuiteParseContext.java" checks="LineLength" />
<suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]parser[/\\]RestTestSuiteParser.java" checks="LineLength" /> <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]parser[/\\]RestTestSuiteParser.java" checks="LineLength" />
<suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]section[/\\]DoSection.java" checks="LineLength" />
<suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]section[/\\]GreaterThanAssertion.java" checks="LineLength" /> <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]section[/\\]GreaterThanAssertion.java" checks="LineLength" />
<suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]section[/\\]GreaterThanEqualToAssertion.java" checks="LineLength" /> <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]section[/\\]GreaterThanEqualToAssertion.java" checks="LineLength" />
<suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]section[/\\]LengthAssertion.java" checks="LineLength" /> <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]section[/\\]LengthAssertion.java" checks="LineLength" />

View File

@ -121,7 +121,6 @@ public final class RestClient implements Closeable {
ElasticsearchResponse elasticsearchResponse = new ElasticsearchResponse(request.getRequestLine(), ElasticsearchResponse elasticsearchResponse = new ElasticsearchResponse(request.getRequestLine(),
connection.getHost(), response); connection.getHost(), response);
int statusCode = response.getStatusLine().getStatusCode(); int statusCode = response.getStatusLine().getStatusCode();
//TODO make ignore status code configurable. rest-spec and tests support that parameter (ignore_missing)
if (statusCode < 300 || (request.getMethod().equals(HttpHead.METHOD_NAME) && statusCode == 404) ) { if (statusCode < 300 || (request.getMethod().equals(HttpHead.METHOD_NAME) && statusCode == 404) ) {
RequestLogger.log(logger, "request succeeded", request, connection.getHost(), response); RequestLogger.log(logger, "request succeeded", request, connection.getHost(), response);
onSuccess(connection); onSuccess(connection);

View File

@ -121,7 +121,6 @@ public class ExceptionSerializationTests extends ESTestCase {
.resolve("org").resolve("elasticsearch"); .resolve("org").resolve("elasticsearch");
final Set<? extends Class<?>> ignore = Sets.newHashSet( final Set<? extends Class<?>> ignore = Sets.newHashSet(
org.elasticsearch.test.rest.parser.RestTestParseException.class, org.elasticsearch.test.rest.parser.RestTestParseException.class,
org.elasticsearch.test.rest.client.RestException.class,
CancellableThreadsTests.CustomException.class, CancellableThreadsTests.CustomException.class,
org.elasticsearch.rest.BytesRestResponseTests.WithHeadersException.class, org.elasticsearch.rest.BytesRestResponseTests.WithHeadersException.class,
AbstractClientHeadersTestCase.InternalException.class); AbstractClientHeadersTestCase.InternalException.class);

View File

@ -19,6 +19,31 @@
package org.elasticsearch.test.rest; package org.elasticsearch.test.rest;
import com.carrotsearch.randomizedtesting.RandomizedTest;
import org.apache.lucene.util.IOUtils;
import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksAction;
import org.elasticsearch.client.ElasticsearchResponseException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.rest.client.RestTestResponse;
import org.elasticsearch.test.rest.parser.RestTestParseException;
import org.elasticsearch.test.rest.parser.RestTestSuiteParser;
import org.elasticsearch.test.rest.section.DoSection;
import org.elasticsearch.test.rest.section.ExecutableSection;
import org.elasticsearch.test.rest.section.RestTestSuite;
import org.elasticsearch.test.rest.section.SkipSection;
import org.elasticsearch.test.rest.section.TestSection;
import org.elasticsearch.test.rest.spec.RestApi;
import org.elasticsearch.test.rest.spec.RestSpec;
import org.elasticsearch.test.rest.support.FileUtils;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI; import java.net.URI;
@ -38,32 +63,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.apache.lucene.util.IOUtils;
import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksAction;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.rest.client.RestException;
import org.elasticsearch.test.rest.client.RestResponse;
import org.elasticsearch.test.rest.parser.RestTestParseException;
import org.elasticsearch.test.rest.parser.RestTestSuiteParser;
import org.elasticsearch.test.rest.section.DoSection;
import org.elasticsearch.test.rest.section.ExecutableSection;
import org.elasticsearch.test.rest.section.RestTestSuite;
import org.elasticsearch.test.rest.section.SkipSection;
import org.elasticsearch.test.rest.section.TestSection;
import org.elasticsearch.test.rest.spec.RestApi;
import org.elasticsearch.test.rest.spec.RestSpec;
import org.elasticsearch.test.rest.support.FileUtils;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import com.carrotsearch.randomizedtesting.RandomizedTest;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap; import static java.util.Collections.emptyMap;
import static java.util.Collections.sort; import static java.util.Collections.sort;
@ -98,8 +97,8 @@ public abstract class ESRestTestCase extends ESTestCase {
private static final String DEFAULT_SPEC_PATH = "/rest-api-spec/api"; private static final String DEFAULT_SPEC_PATH = "/rest-api-spec/api";
/** /**
* This separator pattern matches ',' except it is preceded by a '\'. This allows us to support ',' within paths when it is escaped with * This separator pattern matches ',' except it is preceded by a '\'.
* a slash. * This allows us to support ',' within paths when it is escaped with a slash.
* *
* For example, the path string "/a/b/c\,d/e/f,/foo/bar,/baz" is separated to "/a/b/c\,d/e/f", "/foo/bar" and "/baz". * For example, the path string "/a/b/c\,d/e/f,/foo/bar,/baz" is separated to "/a/b/c\,d/e/f", "/foo/bar" and "/baz".
* *
@ -233,7 +232,7 @@ public abstract class ESRestTestCase extends ESTestCase {
} }
@BeforeClass @BeforeClass
public static void initExecutionContext() throws IOException, RestException { public static void initExecutionContext() throws IOException {
String[] specPaths = resolvePathsProperty(REST_TESTS_SPEC, DEFAULT_SPEC_PATH); String[] specPaths = resolvePathsProperty(REST_TESTS_SPEC, DEFAULT_SPEC_PATH);
RestSpec restSpec = null; RestSpec restSpec = null;
FileSystem fileSystem = getFileSystem(); FileSystem fileSystem = getFileSystem();
@ -277,9 +276,9 @@ public abstract class ESRestTestCase extends ESTestCase {
deleteIndicesArgs.put("index", "*"); deleteIndicesArgs.put("index", "*");
try { try {
adminExecutionContext.callApi("indices.delete", deleteIndicesArgs, Collections.emptyList(), Collections.emptyMap()); adminExecutionContext.callApi("indices.delete", deleteIndicesArgs, Collections.emptyList(), Collections.emptyMap());
} catch (RestException e) { } catch (ElasticsearchResponseException e) {
// 404 here just means we had no indexes // 404 here just means we had no indexes
if (e.statusCode() != 404) { if (e.getElasticsearchResponse().getStatusLine().getStatusCode() != 404) {
throw e; throw e;
} }
} }
@ -300,8 +299,8 @@ public abstract class ESRestTestCase extends ESTestCase {
* other tests. * other tests.
*/ */
@After @After
public void logIfThereAreRunningTasks() throws InterruptedException, IOException, RestException { public void logIfThereAreRunningTasks() throws InterruptedException, IOException {
RestResponse tasks = adminExecutionContext.callApi("tasks.list", emptyMap(), emptyList(), emptyMap()); RestTestResponse tasks = adminExecutionContext.callApi("tasks.list", emptyMap(), emptyList(), emptyMap());
Set<String> runningTasks = runningTasks(tasks); Set<String> runningTasks = runningTasks(tasks);
// Ignore the task list API - it doens't count against us // Ignore the task list API - it doens't count against us
runningTasks.remove(ListTasksAction.NAME); runningTasks.remove(ListTasksAction.NAME);
@ -347,7 +346,7 @@ public abstract class ESRestTestCase extends ESTestCase {
} }
@Before @Before
public void reset() throws IOException, RestException { public void reset() throws IOException {
// admin context must be available for @After always, regardless of whether the test was blacklisted // admin context must be available for @After always, regardless of whether the test was blacklisted
adminExecutionContext.initClient(clusterUrls, restAdminSettings()); adminExecutionContext.initClient(clusterUrls, restAdminSettings());
adminExecutionContext.clear(); adminExecutionContext.clear();
@ -355,7 +354,8 @@ public abstract class ESRestTestCase extends ESTestCase {
//skip test if it matches one of the blacklist globs //skip test if it matches one of the blacklist globs
for (BlacklistedPathPatternMatcher blacklistedPathMatcher : blacklistPathMatchers) { for (BlacklistedPathPatternMatcher blacklistedPathMatcher : blacklistPathMatchers) {
String testPath = testCandidate.getSuitePath() + "/" + testCandidate.getTestSection().getName(); String testPath = testCandidate.getSuitePath() + "/" + testCandidate.getTestSection().getName();
assumeFalse("[" + testCandidate.getTestPath() + "] skipped, reason: blacklisted", blacklistedPathMatcher.isSuffixMatch(testPath)); assumeFalse("[" + testCandidate.getTestPath() + "] skipped, reason: blacklisted", blacklistedPathMatcher
.isSuffixMatch(testPath));
} }
//The client needs non static info to get initialized, therefore it can't be initialized in the before class //The client needs non static info to get initialized, therefore it can't be initialized in the before class
restTestExecutionContext.initClient(clusterUrls, restClientSettings()); restTestExecutionContext.initClient(clusterUrls, restClientSettings());
@ -374,7 +374,8 @@ public abstract class ESRestTestCase extends ESTestCase {
if (skipSection.isVersionCheck()) { if (skipSection.isVersionCheck()) {
messageBuilder.append("[").append(description).append("] skipped, reason: [").append(skipSection.getReason()).append("] "); messageBuilder.append("[").append(description).append("] skipped, reason: [").append(skipSection.getReason()).append("] ");
} else { } else {
messageBuilder.append("[").append(description).append("] skipped, reason: features ").append(skipSection.getFeatures()).append(" not supported"); messageBuilder.append("[").append(description).append("] skipped, reason: features ")
.append(skipSection.getFeatures()).append(" not supported");
} }
return messageBuilder.toString(); return messageBuilder.toString();
} }
@ -401,7 +402,7 @@ public abstract class ESRestTestCase extends ESTestCase {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public Set<String> runningTasks(RestResponse response) throws IOException { public Set<String> runningTasks(RestTestResponse response) throws IOException {
Set<String> runningTasks = new HashSet<>(); Set<String> runningTasks = new HashSet<>();
Map<String, Object> nodes = (Map<String, Object>) response.evaluate("nodes"); Map<String, Object> nodes = (Map<String, Object>) response.evaluate("nodes");
for (Map.Entry<String, Object> node : nodes.entrySet()) { for (Map.Entry<String, Object> node : nodes.entrySet()) {

View File

@ -19,18 +19,17 @@
package org.elasticsearch.test.rest; package org.elasticsearch.test.rest;
import org.elasticsearch.Version; import org.elasticsearch.Version;
import org.elasticsearch.client.ElasticsearchResponseException;
import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.test.rest.client.RestClient; import org.elasticsearch.test.rest.client.RestTestClient;
import org.elasticsearch.test.rest.client.RestException; import org.elasticsearch.test.rest.client.RestTestResponse;
import org.elasticsearch.test.rest.client.RestResponse;
import org.elasticsearch.test.rest.spec.RestSpec; import org.elasticsearch.test.rest.spec.RestSpec;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URL; import java.net.URL;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -50,9 +49,9 @@ public class RestTestExecutionContext implements Closeable {
private final RestSpec restSpec; private final RestSpec restSpec;
private RestClient restClient; private RestTestClient restTestClient;
private RestResponse response; private RestTestResponse response;
public RestTestExecutionContext(RestSpec restSpec) { public RestTestExecutionContext(RestSpec restSpec) {
this.restSpec = restSpec; this.restSpec = restSpec;
@ -61,10 +60,9 @@ public class RestTestExecutionContext implements Closeable {
/** /**
* Calls an elasticsearch api with the parameters and request body provided as arguments. * Calls an elasticsearch api with the parameters and request body provided as arguments.
* Saves the obtained response in the execution context. * Saves the obtained response in the execution context.
* @throws RestException if the returned status code is non ok
*/ */
public RestResponse callApi(String apiName, Map<String, String> params, List<Map<String, Object>> bodies, public RestTestResponse callApi(String apiName, Map<String, String> params, List<Map<String, Object>> bodies,
Map<String, String> headers) throws IOException, RestException { Map<String, String> headers) throws IOException {
//makes a copy of the parameters before modifying them for this specific request //makes a copy of the parameters before modifying them for this specific request
HashMap<String, String> requestParams = new HashMap<>(params); HashMap<String, String> requestParams = new HashMap<>(params);
for (Map.Entry<String, String> entry : requestParams.entrySet()) { for (Map.Entry<String, String> entry : requestParams.entrySet()) {
@ -80,8 +78,8 @@ public class RestTestExecutionContext implements Closeable {
//we always stash the last response body //we always stash the last response body
stash.stashResponse(response); stash.stashResponse(response);
return response; return response;
} catch(RestException e) { } catch(ElasticsearchResponseException e) {
response = e.restResponse(); response = new RestTestResponse(e);
throw e; throw e;
} }
} }
@ -106,8 +104,9 @@ public class RestTestExecutionContext implements Closeable {
return XContentFactory.jsonBuilder().map(body).string(); return XContentFactory.jsonBuilder().map(body).string();
} }
private RestResponse callApiInternal(String apiName, Map<String, String> params, String body, Map<String, String> headers) throws IOException, RestException { private RestTestResponse callApiInternal(String apiName, Map<String, String> params, String body, Map<String, String> headers)
return restClient.callApi(apiName, params, body, headers); throws IOException {
return restTestClient.callApi(apiName, params, body, headers);
} }
/** /**
@ -120,9 +119,9 @@ public class RestTestExecutionContext implements Closeable {
/** /**
* Creates the embedded REST client when needed. Needs to be called before each test. * Creates the embedded REST client when needed. Needs to be called before each test.
*/ */
public void initClient(URL[] urls, Settings settings) throws IOException, RestException { public void initClient(URL[] urls, Settings settings) throws IOException {
if (restClient == null) { if (restTestClient == null) {
restClient = new RestClient(restSpec, settings, urls); restTestClient = new RestTestClient(restSpec, settings, urls);
} }
} }
@ -143,7 +142,7 @@ public class RestTestExecutionContext implements Closeable {
* Returns the current es version as a string * Returns the current es version as a string
*/ */
public Version esVersion() { public Version esVersion() {
return restClient.getEsVersion(); return restTestClient.getEsVersion();
} }
/** /**
@ -151,8 +150,8 @@ public class RestTestExecutionContext implements Closeable {
*/ */
@Override @Override
public void close() { public void close() {
if (restClient != null) { if (restTestClient != null) {
restClient.close(); restTestClient.close();
} }
} }
} }

View File

@ -19,17 +19,17 @@
package org.elasticsearch.test.rest; package org.elasticsearch.test.rest;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.elasticsearch.common.Strings; import org.elasticsearch.common.Strings;
import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.test.rest.client.RestResponse; import org.elasticsearch.test.rest.client.RestTestResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** /**
* Allows to cache the last obtained test response and or part of it within variables * Allows to cache the last obtained test response and or part of it within variables
@ -42,7 +42,7 @@ public class Stash implements ToXContent {
public static final Stash EMPTY = new Stash(); public static final Stash EMPTY = new Stash();
private final Map<String, Object> stash = new HashMap<>(); private final Map<String, Object> stash = new HashMap<>();
private RestResponse response; private RestTestResponse response;
/** /**
* Allows to saved a specific field in the stash as key-value pair * Allows to saved a specific field in the stash as key-value pair
@ -55,7 +55,7 @@ public class Stash implements ToXContent {
} }
} }
public void stashResponse(RestResponse response) throws IOException { public void stashResponse(RestTestResponse response) throws IOException {
// TODO we can almost certainly save time by lazily evaluating the body // TODO we can almost certainly save time by lazily evaluating the body
stashValue("body", response.getBody()); stashValue("body", response.getBody());
this.response = response; this.response = response;

View File

@ -1,41 +0,0 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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.elasticsearch.test.rest.client;
/**
* Thrown when a status code that holds an error is received (unless needs to be ignored)
* Holds the original {@link RestResponse}
*/
public class RestException extends Exception {
private final RestResponse restResponse;
public RestException(String message, RestResponse restResponse) {
super(message);
this.restResponse = restResponse;
}
public RestResponse restResponse() {
return restResponse;
}
public int statusCode() {
return restResponse.getStatusCode();
}
}

View File

@ -19,17 +19,25 @@
package org.elasticsearch.test.rest.client; package org.elasticsearch.test.rest.client;
import com.carrotsearch.randomizedtesting.RandomizedTest; import com.carrotsearch.randomizedtesting.RandomizedTest;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.config.Registry; import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder; import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContexts; import org.apache.http.message.BasicHeader;
import org.apache.lucene.util.IOUtils; import org.apache.lucene.util.IOUtils;
import org.elasticsearch.Version; import org.elasticsearch.Version;
import org.elasticsearch.client.ElasticsearchResponse;
import org.elasticsearch.client.ElasticsearchResponseException;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.common.Strings; import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.common.io.PathUtils;
import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.ESLogger;
@ -37,8 +45,6 @@ import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.test.rest.client.http.HttpRequestBuilder;
import org.elasticsearch.test.rest.client.http.HttpResponse;
import org.elasticsearch.test.rest.spec.RestApi; import org.elasticsearch.test.rest.spec.RestApi;
import org.elasticsearch.test.rest.spec.RestSpec; import org.elasticsearch.test.rest.spec.RestSpec;
@ -46,6 +52,8 @@ import javax.net.ssl.SSLContext;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@ -55,61 +63,53 @@ import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
import static java.util.Objects.requireNonNull;
/** /**
* REST client used to test the elasticsearch REST layer * REST client used to test the elasticsearch REST layer
* Holds the {@link RestSpec} used to translate api calls into REST calls * Holds the {@link RestSpec} used to translate api calls into REST calls
*/ */
public class RestClient implements Closeable { public class RestTestClient implements Closeable {
public static final String PROTOCOL = "protocol"; public static final String PROTOCOL = "protocol";
public static final String TRUSTSTORE_PATH = "truststore.path"; public static final String TRUSTSTORE_PATH = "truststore.path";
public static final String TRUSTSTORE_PASSWORD = "truststore.password"; public static final String TRUSTSTORE_PASSWORD = "truststore.password";
private static final ESLogger logger = Loggers.getLogger(RestClient.class);
private static final ESLogger logger = Loggers.getLogger(RestTestClient.class);
//query_string params that don't need to be declared in the spec, thay are supported by default //query_string params that don't need to be declared in the spec, thay are supported by default
private static final Set<String> ALWAYS_ACCEPTED_QUERY_STRING_PARAMS = Sets.newHashSet("pretty", "source", "filter_path"); private static final Set<String> ALWAYS_ACCEPTED_QUERY_STRING_PARAMS = Sets.newHashSet("pretty", "source", "filter_path");
private final String protocol;
private final RestSpec restSpec; private final RestSpec restSpec;
private final CloseableHttpClient httpClient; private final RestClient restClient;
private final URL[] urls;
private final Version esVersion; private final Version esVersion;
private final ThreadContext threadContext;
public RestClient(RestSpec restSpec, Settings settings, URL[] urls) throws IOException, RestException { public RestTestClient(RestSpec restSpec, Settings settings, URL[] urls) throws IOException {
assert urls.length > 0; assert urls.length > 0;
this.restSpec = restSpec; this.restSpec = restSpec;
this.protocol = settings.get(PROTOCOL, "http"); this.restClient = createRestClient(urls, settings);
this.httpClient = createHttpClient(settings); this.esVersion = readAndCheckVersion(urls);
this.threadContext = new ThreadContext(settings);
this.urls = urls;
this.esVersion = readAndCheckVersion();
logger.info("REST client initialized {}, elasticsearch version: [{}]", urls, esVersion); logger.info("REST client initialized {}, elasticsearch version: [{}]", urls, esVersion);
} }
private Version readAndCheckVersion() throws IOException, RestException { private Version readAndCheckVersion(URL[] urls) throws IOException {
//we make a manual call here without using callApi method, mainly because we are initializing
//and the randomized context doesn't exist for the current thread (would be used to choose the method otherwise)
RestApi restApi = restApi("info"); RestApi restApi = restApi("info");
assert restApi.getPaths().size() == 1; assert restApi.getPaths().size() == 1;
assert restApi.getMethods().size() == 1; assert restApi.getMethods().size() == 1;
String version = null; String version = null;
for (URL url : urls) { for (URL ignored : urls) {
RestResponse restResponse = new RestResponse(httpRequestBuilder(url) //we don't really use the urls here, we rely on the client doing round-robin to touch all the nodes in the cluster
.path(restApi.getPaths().get(0)) String method = restApi.getMethods().get(0);
.method(restApi.getMethods().get(0)).execute()); String endpoint = restApi.getPaths().get(0);
checkStatusCode(restResponse); ElasticsearchResponse elasticsearchResponse = restClient.performRequest(method, endpoint, Collections.emptyMap(), null);
RestTestResponse restTestResponse = new RestTestResponse(elasticsearchResponse);
Object latestVersion = restResponse.evaluate("version.number"); Object latestVersion = restTestResponse.evaluate("version.number");
if (latestVersion == null) { if (latestVersion == null) {
throw new RuntimeException("elasticsearch version not found in the response"); throw new RuntimeException("elasticsearch version not found in the response");
} }
@ -130,77 +130,41 @@ public class RestClient implements Closeable {
/** /**
* Calls an api with the provided parameters and body * Calls an api with the provided parameters and body
* @throws RestException if the obtained status code is non ok, unless the specific error code needs to be ignored
* according to the ignore parameter received as input (which won't get sent to elasticsearch)
*/ */
public RestResponse callApi(String apiName, Map<String, String> params, String body, Map<String, String> headers) public RestTestResponse callApi(String apiName, Map<String, String> params, String body, Map<String, String> headers)
throws IOException, RestException { throws IOException {
List<Integer> ignores = new ArrayList<>();
Map<String, String> requestParams = null;
if (params != null) {
//makes a copy of the parameters before modifying them for this specific request
requestParams = new HashMap<>(params);
//ignore is a special parameter supported by the clients, shouldn't be sent to es
String ignoreString = requestParams.remove("ignore");
if (Strings.hasLength(ignoreString)) {
try {
ignores.add(Integer.valueOf(ignoreString));
} catch(NumberFormatException e) {
throw new IllegalArgumentException("ignore value should be a number, found [" + ignoreString + "] instead");
}
}
}
HttpRequestBuilder httpRequestBuilder = callApiBuilder(apiName, requestParams, body);
for (Map.Entry<String, String> header : headers.entrySet()) {
logger.error("Adding header {}\n with value {}", header.getKey(), header.getValue());
httpRequestBuilder.addHeader(header.getKey(), header.getValue());
}
logger.debug("calling api [{}]", apiName);
HttpResponse httpResponse = httpRequestBuilder.execute();
// http HEAD doesn't support response body
// For the few api (exists class of api) that use it we need to accept 404 too
if (!httpResponse.supportsBody()) {
ignores.add(404);
}
RestResponse restResponse = new RestResponse(httpResponse);
checkStatusCode(restResponse, ignores);
return restResponse;
}
private void checkStatusCode(RestResponse restResponse, List<Integer> ignores) throws RestException {
//ignore is a catch within the client, to prevent the client from throwing error if it gets non ok codes back
if (ignores.contains(restResponse.getStatusCode())) {
if (logger.isDebugEnabled()) {
logger.debug("ignored non ok status codes {} as requested", ignores);
}
return;
}
checkStatusCode(restResponse);
}
private void checkStatusCode(RestResponse restResponse) throws RestException {
if (restResponse.isError()) {
throw new RestException("non ok status code [" + restResponse.getStatusCode() + "] returned", restResponse);
}
}
private HttpRequestBuilder callApiBuilder(String apiName, Map<String, String> params, String body) {
if ("raw".equals(apiName)) { if ("raw".equals(apiName)) {
// Raw requests are bit simpler.... // Raw requests are bit simpler....
HttpRequestBuilder httpRequestBuilder = httpRequestBuilder(); HashMap<String, String> queryStringParams = new HashMap<>(params);
httpRequestBuilder.method(requireNonNull(params.remove("method"), "Method must be set to use raw request")); String method = Objects.requireNonNull(queryStringParams.remove("method"), "Method must be set to use raw request");
httpRequestBuilder.path("/"+ requireNonNull(params.remove("path"), "Path must be set to use raw request")); String path = "/"+ Objects.requireNonNull(queryStringParams.remove("path"), "Path must be set to use raw request");
httpRequestBuilder.body(body); HttpEntity entity = null;
if (body != null && body.length() > 0) {
// And everything else is a url parameter! entity = new StringEntity(body, RestClient.JSON_CONTENT_TYPE);
for (Map.Entry<String, String> entry : params.entrySet()) { }
httpRequestBuilder.addParam(entry.getKey(), entry.getValue()); // And everything else is a url parameter!
ElasticsearchResponse response = restClient.performRequest(method, path, queryStringParams, entity);
return new RestTestResponse(response);
}
List<Integer> ignores = new ArrayList<>();
Map<String, String> requestParams;
if (params == null) {
requestParams = Collections.emptyMap();
} else {
requestParams = new HashMap<>(params);
if (params.isEmpty() == false) {
//ignore is a special parameter supported by the clients, shouldn't be sent to es
String ignoreString = requestParams.remove("ignore");
if (ignoreString != null) {
try {
ignores.add(Integer.valueOf(ignoreString));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("ignore value should be a number, found [" + ignoreString + "] instead");
}
}
} }
return httpRequestBuilder;
} }
//create doesn't exist in the spec but is supported in the clients (index with op_type=create) //create doesn't exist in the spec but is supported in the clients (index with op_type=create)
@ -208,51 +172,91 @@ public class RestClient implements Closeable {
String api = indexCreateApi ? "index" : apiName; String api = indexCreateApi ? "index" : apiName;
RestApi restApi = restApi(api); RestApi restApi = restApi(api);
HttpRequestBuilder httpRequestBuilder = httpRequestBuilder();
//divide params between ones that go within query string and ones that go within path //divide params between ones that go within query string and ones that go within path
Map<String, String> pathParts = new HashMap<>(); Map<String, String> pathParts = new HashMap<>();
if (params != null) { Map<String, String> queryStringParams = new HashMap<>();
for (Map.Entry<String, String> entry : params.entrySet()) { for (Map.Entry<String, String> entry : requestParams.entrySet()) {
if (restApi.getPathParts().contains(entry.getKey())) { if (restApi.getPathParts().contains(entry.getKey())) {
pathParts.put(entry.getKey(), entry.getValue()); pathParts.put(entry.getKey(), entry.getValue());
} else {
if (restApi.getParams().contains(entry.getKey()) || ALWAYS_ACCEPTED_QUERY_STRING_PARAMS.contains(entry.getKey())) {
queryStringParams.put(entry.getKey(), entry.getValue());
} else { } else {
if (restApi.getParams().contains(entry.getKey()) || ALWAYS_ACCEPTED_QUERY_STRING_PARAMS.contains(entry.getKey())) { throw new IllegalArgumentException("param [" + entry.getKey() + "] not supported in ["
httpRequestBuilder.addParam(entry.getKey(), entry.getValue()); + restApi.getName() + "] " + "api");
} else {
throw new IllegalArgumentException("param [" + entry.getKey() +
"] not supported in [" + restApi.getName() + "] api");
}
} }
} }
} }
if (indexCreateApi) { if (indexCreateApi) {
httpRequestBuilder.addParam("op_type", "create"); queryStringParams.put("op_type", "create");
} }
List<String> supportedMethods = restApi.getSupportedMethods(pathParts.keySet()); List<String> supportedMethods = restApi.getSupportedMethods(pathParts.keySet());
String requestMethod;
StringEntity requestBody = null;
if (Strings.hasLength(body)) { if (Strings.hasLength(body)) {
if (!restApi.isBodySupported()) { if (!restApi.isBodySupported()) {
throw new IllegalArgumentException("body is not supported by [" + restApi.getName() + "] api"); throw new IllegalArgumentException("body is not supported by [" + restApi.getName() + "] api");
} }
//test the GET with source param instead of GET/POST with body //randomly test the GET with source param instead of GET/POST with body
if (supportedMethods.contains("GET") && RandomizedTest.rarely()) { if (supportedMethods.contains("GET") && RandomizedTest.rarely()) {
logger.debug("sending the request body as source param with GET method"); logger.debug("sending the request body as source param with GET method");
httpRequestBuilder.addParam("source", body).method("GET"); queryStringParams.put("source", body);
requestMethod = "GET";
} else { } else {
httpRequestBuilder.body(body).method(RandomizedTest.randomFrom(supportedMethods)); requestMethod = RandomizedTest.randomFrom(supportedMethods);
requestBody = new StringEntity(body, RestClient.JSON_CONTENT_TYPE);
} }
} else { } else {
if (restApi.isBodyRequired()) { if (restApi.isBodyRequired()) {
throw new IllegalArgumentException("body is required by [" + restApi.getName() + "] api"); throw new IllegalArgumentException("body is required by [" + restApi.getName() + "] api");
} }
httpRequestBuilder.method(RandomizedTest.randomFrom(supportedMethods)); requestMethod = RandomizedTest.randomFrom(supportedMethods);
} }
//the rest path to use is randomized out of the matching ones (if more than one) //the rest path to use is randomized out of the matching ones (if more than one)
RestPath restPath = RandomizedTest.randomFrom(restApi.getFinalPaths(pathParts)); RestPath restPath = RandomizedTest.randomFrom(restApi.getFinalPaths(pathParts));
return httpRequestBuilder.pathParts(restPath.getPathParts()); //Encode rules for path and query string parameters are different. We use URI to encode the path.
//We need to encode each path part separately, as each one might contain slashes that need to be escaped, which needs to
//be done manually.
String requestPath;
if (restPath.getPathParts().length == 0) {
requestPath = "/";
} else {
StringBuilder finalPath = new StringBuilder();
for (String pathPart : restPath.getPathParts()) {
try {
finalPath.append('/');
// We append "/" to the path part to handle parts that start with - or other invalid characters
URI uri = new URI(null, null, null, -1, "/" + pathPart, null, null);
//manually escape any slash that each part may contain
finalPath.append(uri.getRawPath().substring(1).replaceAll("/", "%2F"));
} catch (URISyntaxException e) {
throw new RuntimeException("unable to build uri", e);
}
}
requestPath = finalPath.toString();
}
Header[] requestHeaders = new Header[headers.size()];
int index = 0;
for (Map.Entry<String, String> header : headers.entrySet()) {
logger.info("Adding header {}\n with value {}", header.getKey(), header.getValue());
requestHeaders[index++] = new BasicHeader(header.getKey(), header.getValue());
}
logger.debug("calling api [{}]", apiName);
try {
ElasticsearchResponse response = restClient.performRequest(requestMethod, requestPath,
queryStringParams, requestBody, requestHeaders);
return new RestTestResponse(response);
} catch(ElasticsearchResponseException e) {
if (ignores.contains(e.getElasticsearchResponse().getStatusLine().getStatusCode())) {
return new RestTestResponse(e);
}
throw e;
}
} }
private RestApi restApi(String apiName) { private RestApi restApi(String apiName) {
@ -263,21 +267,7 @@ public class RestClient implements Closeable {
return restApi; return restApi;
} }
protected HttpRequestBuilder httpRequestBuilder(URL url) { protected RestClient createRestClient(URL[] urls, Settings settings) throws IOException {
return new HttpRequestBuilder(httpClient)
.addHeaders(threadContext.getHeaders())
.protocol(protocol)
.host(url.getHost())
.port(url.getPort());
}
protected HttpRequestBuilder httpRequestBuilder() {
//the address used is randomized between the available ones
URL url = RandomizedTest.randomFrom(urls);
return httpRequestBuilder(url);
}
protected CloseableHttpClient createHttpClient(Settings settings) throws IOException {
SSLConnectionSocketFactory sslsf; SSLConnectionSocketFactory sslsf;
String keystorePath = settings.get(TRUSTSTORE_PATH); String keystorePath = settings.get(TRUSTSTORE_PATH);
if (keystorePath != null) { if (keystorePath != null) {
@ -307,8 +297,24 @@ public class RestClient implements Closeable {
.register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", sslsf) .register("https", sslsf)
.build(); .build();
return HttpClients.createMinimal(
new PoolingHttpClientConnectionManager(socketFactoryRegistry, null, null, null, 15, TimeUnit.SECONDS)); List<Header> headers = new ArrayList<>();
try (ThreadContext threadContext = new ThreadContext(settings)) {
for (Map.Entry<String, String> entry : threadContext.getHeaders().entrySet()) {
headers.add(new BasicHeader(entry.getKey(), entry.getValue()));
}
}
CloseableHttpClient httpClient = HttpClientBuilder.create().setDefaultHeaders(headers)
.setConnectionManager(new PoolingHttpClientConnectionManager(socketFactoryRegistry)).build();
String protocol = settings.get(PROTOCOL, "http");
HttpHost[] hosts = new HttpHost[urls.length];
for (int i = 0; i < hosts.length; i++) {
URL url = urls[i];
hosts[i] = new HttpHost(url.getHost(), url.getPort(), protocol);
}
return RestClient.builder().setHttpClient(httpClient).setHosts(hosts).build();
} }
/** /**
@ -316,6 +322,6 @@ public class RestClient implements Closeable {
*/ */
@Override @Override
public void close() { public void close() {
IOUtils.closeWhileHandlingException(httpClient); IOUtils.closeWhileHandlingException(restClient);
} }
} }

View File

@ -18,31 +18,54 @@
*/ */
package org.elasticsearch.test.rest.client; package org.elasticsearch.test.rest.client;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.util.EntityUtils;
import org.apache.lucene.util.IOUtils;
import org.elasticsearch.client.ElasticsearchResponse;
import org.elasticsearch.client.ElasticsearchResponseException;
import org.elasticsearch.test.rest.Stash; import org.elasticsearch.test.rest.Stash;
import org.elasticsearch.test.rest.client.http.HttpResponse;
import org.elasticsearch.test.rest.json.JsonPath; import org.elasticsearch.test.rest.json.JsonPath;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
/** /**
* Response obtained from a REST call * Response obtained from a REST call, eagerly reads the response body into a string for later optional parsing.
* Supports parsing the response body as json when needed and returning specific values extracted from it * Supports parsing the response body as json when needed and returning specific values extracted from it.
*/ */
public class RestResponse { public class RestTestResponse {
private final HttpResponse response; private final ElasticsearchResponse response;
private final String body;
private JsonPath parsedResponse; private JsonPath parsedResponse;
public RestResponse(HttpResponse response) { public RestTestResponse(ElasticsearchResponse response) {
this.response = response; this.response = response;
if (response.getEntity() != null) {
try {
this.body = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
} catch (IOException e) {
EntityUtils.consumeQuietly(response.getEntity());
throw new RuntimeException(e);
} finally {
IOUtils.closeWhileHandlingException(response);
}
} else {
this.body = null;
}
}
public RestTestResponse(ElasticsearchResponseException responseException) {
this.response = responseException.getElasticsearchResponse();
this.body = responseException.getResponseBody();
} }
public int getStatusCode() { public int getStatusCode() {
return response.getStatusCode(); return response.getStatusLine().getStatusCode();
} }
public String getReasonPhrase() { public String getReasonPhrase() {
return response.getReasonPhrase(); return response.getStatusLine().getReasonPhrase();
} }
/** /**
@ -57,18 +80,18 @@ public class RestResponse {
} }
return parsedResponse.evaluate(""); return parsedResponse.evaluate("");
} }
return response.getBody(); return body;
} }
/** /**
* Returns the body as a string * Returns the body as a string
*/ */
public String getBodyAsString() { public String getBodyAsString() {
return response.getBody(); return body;
} }
public boolean isError() { public boolean isError() {
return response.isError(); return response.getStatusLine().getStatusCode() >= 400;
} }
/** /**
@ -82,7 +105,6 @@ public class RestResponse {
* Parses the response body as json and extracts a specific value from it (identified by the provided path) * Parses the response body as json and extracts a specific value from it (identified by the provided path)
*/ */
public Object evaluate(String path, Stash stash) throws IOException { public Object evaluate(String path, Stash stash) throws IOException {
if (response == null) { if (response == null) {
return null; return null;
} }
@ -93,8 +115,8 @@ public class RestResponse {
//special case: api that don't support body (e.g. exists) return true if 200, false if 404, even if no body //special case: api that don't support body (e.g. exists) return true if 200, false if 404, even if no body
//is_true: '' means the response had no body but the client returned true (caused by 200) //is_true: '' means the response had no body but the client returned true (caused by 200)
//is_false: '' means the response had no body but the client returned false (caused by 404) //is_false: '' means the response had no body but the client returned false (caused by 404)
if ("".equals(path) && !response.supportsBody()) { if ("".equals(path) && HttpHead.METHOD_NAME.equals(response.getRequestLine().getMethod())) {
return !response.isError(); return isError() == false;
} }
return null; return null;
} }
@ -103,7 +125,7 @@ public class RestResponse {
} }
private boolean isJson() { private boolean isJson() {
String contentType = response.getHeaders().get("Content-Type"); String contentType = response.getFirstHeader("Content-Type");
return contentType != null && contentType.contains("application/json"); return contentType != null && contentType.contains("application/json");
} }
@ -111,9 +133,9 @@ public class RestResponse {
if (parsedResponse != null) { if (parsedResponse != null) {
return parsedResponse; return parsedResponse;
} }
if (response == null || !response.hasBody()) { if (response == null || body == null) {
return null; return null;
} }
return parsedResponse = new JsonPath(response.getBody()); return parsedResponse = new JsonPath(body);
} }
} }

View File

@ -1,40 +0,0 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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.elasticsearch.test.rest.client.http;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import java.net.URI;
/**
* Allows to send DELETE requests providing a body (not supported out of the box)
*/
public class HttpDeleteWithEntity extends HttpEntityEnclosingRequestBase {
public final static String METHOD_NAME = "DELETE";
public HttpDeleteWithEntity(final URI uri) {
setURI(uri);
}
@Override
public String getMethod() {
return METHOD_NAME;
}
}

View File

@ -1,40 +0,0 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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.elasticsearch.test.rest.client.http;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import java.net.URI;
/**
* Allows to send GET requests providing a body (not supported out of the box)
*/
public class HttpGetWithEntity extends HttpEntityEnclosingRequestBase {
public final static String METHOD_NAME = "GET";
public HttpGetWithEntity(final URI uri) {
setURI(uri);
}
@Override
public String getMethod() {
return METHOD_NAME;
}
}

View File

@ -1,250 +0,0 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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.elasticsearch.test.rest.client.http;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.network.NetworkAddress;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.http.HttpServerTransport;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Executable builder for an http request
* Holds an {@link org.apache.http.client.HttpClient} that is used to send the built http request
*/
public class HttpRequestBuilder {
private static final ESLogger logger = Loggers.getLogger(HttpRequestBuilder.class);
static final Charset DEFAULT_CHARSET = Charset.forName("utf-8");
private final CloseableHttpClient httpClient;
private String protocol = "http";
private String host;
private int port;
private String path = "";
private final Map<String, String> params = new HashMap<>();
private final Map<String, String> headers = new HashMap<>();
private String method = HttpGetWithEntity.METHOD_NAME;
private String body;
public HttpRequestBuilder(CloseableHttpClient httpClient) {
this.httpClient = httpClient;
}
public HttpRequestBuilder host(String host) {
this.host = host;
return this;
}
public HttpRequestBuilder httpTransport(HttpServerTransport httpServerTransport) {
InetSocketTransportAddress transportAddress = (InetSocketTransportAddress) httpServerTransport.boundAddress().publishAddress();
return host(NetworkAddress.format(transportAddress.address().getAddress())).port(transportAddress.address().getPort());
}
public HttpRequestBuilder port(int port) {
this.port = port;
return this;
}
/**
* Sets the path to send the request to. Url encoding needs to be applied by the caller.
* Use {@link #pathParts(String...)} instead if the path needs to be encoded, part by part.
*/
public HttpRequestBuilder path(String path) {
this.path = path;
return this;
}
/**
* Sets the path by providing the different parts (without slashes), which will be properly encoded.
*/
public HttpRequestBuilder pathParts(String... path) {
//encode rules for path and query string parameters are different. We use URI to encode the path, and URLEncoder for each query string parameter (see addParam).
//We need to encode each path part separately though, as each one might contain slashes that need to be escaped, which needs to be done manually.
if (path.length == 0) {
this.path = "/";
return this;
}
StringBuilder finalPath = new StringBuilder();
for (String pathPart : path) {
try {
finalPath.append('/');
// We append "/" to the path part to handle parts that start with - or other invalid characters
URI uri = new URI(null, null, null, -1, "/" + pathPart, null, null);
//manually escape any slash that each part may contain
finalPath.append(uri.getRawPath().substring(1).replaceAll("/", "%2F"));
} catch(URISyntaxException e) {
throw new RuntimeException("unable to build uri", e);
}
}
this.path = finalPath.toString();
return this;
}
public HttpRequestBuilder addParam(String name, String value) {
try {
this.params.put(name, URLEncoder.encode(value, "utf-8"));
return this;
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public HttpRequestBuilder addHeaders(Map<String, String> headers) {
this.headers.putAll(headers);
return this;
}
public HttpRequestBuilder addHeader(String name, String value) {
this.headers.put(name, value);
return this;
}
public HttpRequestBuilder protocol(String protocol) {
this.protocol = protocol;
return this;
}
public HttpRequestBuilder method(String method) {
this.method = method;
return this;
}
public HttpRequestBuilder body(String body) {
if (Strings.hasLength(body)) {
this.body = body;
}
return this;
}
public HttpResponse execute() throws IOException {
HttpUriRequest httpUriRequest = buildRequest();
if (logger.isTraceEnabled()) {
StringBuilder stringBuilder = new StringBuilder(httpUriRequest.getMethod()).append(" ").append(httpUriRequest.getURI());
if (Strings.hasLength(body)) {
stringBuilder.append("\n").append(body);
}
logger.trace("sending request \n{}", stringBuilder.toString());
}
for (Map.Entry<String, String> entry : this.headers.entrySet()) {
logger.trace("adding header [{} => {}]", entry.getKey(), entry.getValue());
httpUriRequest.addHeader(entry.getKey(), entry.getValue());
}
try (CloseableHttpResponse closeableHttpResponse = httpClient.execute(httpUriRequest)) {
HttpResponse httpResponse = new HttpResponse(httpUriRequest, closeableHttpResponse);
logger.trace("got response \n{}\n{}", closeableHttpResponse, httpResponse.hasBody() ? httpResponse.getBody() : "");
return httpResponse;
}
}
private HttpUriRequest buildRequest() {
if (HttpGetWithEntity.METHOD_NAME.equalsIgnoreCase(method)) {
return addOptionalBody(new HttpGetWithEntity(buildUri()));
}
if (HttpHead.METHOD_NAME.equalsIgnoreCase(method)) {
checkBodyNotSupported();
return new HttpHead(buildUri());
}
if (HttpOptions.METHOD_NAME.equalsIgnoreCase(method)) {
checkBodyNotSupported();
return new HttpOptions(buildUri());
}
if (HttpDeleteWithEntity.METHOD_NAME.equalsIgnoreCase(method)) {
return addOptionalBody(new HttpDeleteWithEntity(buildUri()));
}
if (HttpPut.METHOD_NAME.equalsIgnoreCase(method)) {
return addOptionalBody(new HttpPut(buildUri()));
}
if (HttpPost.METHOD_NAME.equalsIgnoreCase(method)) {
return addOptionalBody(new HttpPost(buildUri()));
}
throw new UnsupportedOperationException("method [" + method + "] not supported");
}
private URI buildUri() {
StringBuilder uriBuilder = new StringBuilder(protocol).append("://").append(host).append(":").append(port).append(path);
if (params.size() > 0) {
uriBuilder.append("?").append(params.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&")));
}
//using this constructor no url encoding happens, as we did everything upfront in addParam and pathPart methods
return URI.create(uriBuilder.toString());
}
private HttpEntityEnclosingRequestBase addOptionalBody(HttpEntityEnclosingRequestBase requestBase) {
if (Strings.hasText(body)) {
requestBase.setEntity(new StringEntity(body, DEFAULT_CHARSET));
}
return requestBase;
}
private void checkBodyNotSupported() {
if (Strings.hasText(body)) {
throw new IllegalArgumentException("request body not supported with head request");
}
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder(method).append(" '")
.append(host).append(":").append(port).append(path).append("'");
if (!params.isEmpty()) {
stringBuilder.append(", params=").append(params);
}
if (Strings.hasLength(body)) {
stringBuilder.append(", body=\n").append(body);
}
return stringBuilder.toString();
}
}

View File

@ -1,108 +0,0 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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.elasticsearch.test.rest.client.http;
import org.apache.http.Header;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.util.EntityUtils;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.Loggers;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* Response obtained from an http request
* Always consumes the whole response body loading it entirely into a string
*/
public class HttpResponse {
private static final ESLogger logger = Loggers.getLogger(HttpResponse.class);
private final HttpUriRequest httpRequest;
private final int statusCode;
private final String reasonPhrase;
private final String body;
private final Map<String, String> headers = new HashMap<>();
HttpResponse(HttpUriRequest httpRequest, CloseableHttpResponse httpResponse) {
this.httpRequest = httpRequest;
this.statusCode = httpResponse.getStatusLine().getStatusCode();
this.reasonPhrase = httpResponse.getStatusLine().getReasonPhrase();
for (Header header : httpResponse.getAllHeaders()) {
this.headers.put(header.getName(), header.getValue());
}
if (httpResponse.getEntity() != null) {
try {
this.body = EntityUtils.toString(httpResponse.getEntity(), HttpRequestBuilder.DEFAULT_CHARSET);
} catch (IOException e) {
EntityUtils.consumeQuietly(httpResponse.getEntity());
throw new RuntimeException(e);
} finally {
try {
httpResponse.close();
} catch (IOException e) {
logger.error("Failed closing response", e);
}
}
} else {
this.body = null;
}
}
public boolean isError() {
return statusCode >= 400;
}
public int getStatusCode() {
return statusCode;
}
public String getReasonPhrase() {
return reasonPhrase;
}
public String getBody() {
return body;
}
public boolean hasBody() {
return body != null;
}
public boolean supportsBody() {
return !HttpHead.METHOD_NAME.equals(httpRequest.getMethod());
}
public Map<String, String> getHeaders() {
return headers;
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder(statusCode).append(" ").append(reasonPhrase);
if (hasBody()) {
stringBuilder.append("\n").append(body);
}
return stringBuilder.toString();
}
}

View File

@ -18,13 +18,13 @@
*/ */
package org.elasticsearch.test.rest.section; package org.elasticsearch.test.rest.section;
import org.elasticsearch.client.ElasticsearchResponseException;
import org.elasticsearch.common.Strings; import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.test.rest.RestTestExecutionContext; import org.elasticsearch.test.rest.RestTestExecutionContext;
import org.elasticsearch.test.rest.client.RestException; import org.elasticsearch.test.rest.client.RestTestResponse;
import org.elasticsearch.test.rest.client.RestResponse;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
@ -89,7 +89,7 @@ public class DoSection implements ExecutableSection {
} }
try { try {
RestResponse restResponse = executionContext.callApi(apiCallSection.getApi(), apiCallSection.getParams(), RestTestResponse restTestResponse = executionContext.callApi(apiCallSection.getApi(), apiCallSection.getParams(),
apiCallSection.getBodies(), apiCallSection.getHeaders()); apiCallSection.getBodies(), apiCallSection.getHeaders());
if (Strings.hasLength(catchParam)) { if (Strings.hasLength(catchParam)) {
String catchStatusCode; String catchStatusCode;
@ -100,16 +100,18 @@ public class DoSection implements ExecutableSection {
} else { } else {
throw new UnsupportedOperationException("catch value [" + catchParam + "] not supported"); throw new UnsupportedOperationException("catch value [" + catchParam + "] not supported");
} }
fail(formatStatusCodeMessage(restResponse, catchStatusCode)); fail(formatStatusCodeMessage(restTestResponse, catchStatusCode));
} }
} catch(RestException e) { } catch(ElasticsearchResponseException e) {
RestTestResponse restTestResponse = new RestTestResponse(e);
if (!Strings.hasLength(catchParam)) { if (!Strings.hasLength(catchParam)) {
fail(formatStatusCodeMessage(e.restResponse(), "2xx")); fail(formatStatusCodeMessage(restTestResponse, "2xx"));
} else if (catches.containsKey(catchParam)) { } else if (catches.containsKey(catchParam)) {
assertStatusCode(e.restResponse()); assertStatusCode(restTestResponse);
} else if (catchParam.length() > 2 && catchParam.startsWith("/") && catchParam.endsWith("/")) { } else if (catchParam.length() > 2 && catchParam.startsWith("/") && catchParam.endsWith("/")) {
//the text of the error message matches regular expression //the text of the error message matches regular expression
assertThat(formatStatusCodeMessage(e.restResponse(), "4xx|5xx"), e.statusCode(), greaterThanOrEqualTo(400)); assertThat(formatStatusCodeMessage(restTestResponse, "4xx|5xx"),
e.getElasticsearchResponse().getStatusLine().getStatusCode(), greaterThanOrEqualTo(400));
Object error = executionContext.response("error"); Object error = executionContext.response("error");
assertThat("error was expected in the response", error, notNullValue()); assertThat("error was expected in the response", error, notNullValue());
//remove delimiters from regex //remove delimiters from regex
@ -122,19 +124,19 @@ public class DoSection implements ExecutableSection {
} }
} }
private void assertStatusCode(RestResponse restResponse) { private void assertStatusCode(RestTestResponse restTestResponse) {
Tuple<String, org.hamcrest.Matcher<Integer>> stringMatcherTuple = catches.get(catchParam); Tuple<String, org.hamcrest.Matcher<Integer>> stringMatcherTuple = catches.get(catchParam);
assertThat(formatStatusCodeMessage(restResponse, stringMatcherTuple.v1()), assertThat(formatStatusCodeMessage(restTestResponse, stringMatcherTuple.v1()),
restResponse.getStatusCode(), stringMatcherTuple.v2()); restTestResponse.getStatusCode(), stringMatcherTuple.v2());
} }
private String formatStatusCodeMessage(RestResponse restResponse, String expected) { private String formatStatusCodeMessage(RestTestResponse restTestResponse, String expected) {
String api = apiCallSection.getApi(); String api = apiCallSection.getApi();
if ("raw".equals(api)) { if ("raw".equals(api)) {
api += "[method=" + apiCallSection.getParams().get("method") + " path=" + apiCallSection.getParams().get("path") + "]"; api += "[method=" + apiCallSection.getParams().get("method") + " path=" + apiCallSection.getParams().get("path") + "]";
} }
return "expected [" + expected + "] status code but api [" + api + "] returned [" return "expected [" + expected + "] status code but api [" + api + "] returned [" + restTestResponse.getStatusCode() +
+ restResponse.getStatusCode() + " " + restResponse.getReasonPhrase() + "] [" + restResponse.getBodyAsString() + "]"; " " + restTestResponse.getReasonPhrase() + "] [" + restTestResponse.getBodyAsString() + "]";
} }
private static Map<String, Tuple<String, org.hamcrest.Matcher<Integer>>> catches = new HashMap<>(); private static Map<String, Tuple<String, org.hamcrest.Matcher<Integer>>> catches = new HashMap<>();
@ -145,6 +147,7 @@ public class DoSection implements ExecutableSection {
catches.put("forbidden", tuple("403", equalTo(403))); catches.put("forbidden", tuple("403", equalTo(403)));
catches.put("request_timeout", tuple("408", equalTo(408))); catches.put("request_timeout", tuple("408", equalTo(408)));
catches.put("unavailable", tuple("503", equalTo(503))); catches.put("unavailable", tuple("503", equalTo(503)));
catches.put("request", tuple("4xx|5xx", allOf(greaterThanOrEqualTo(400), not(equalTo(404)), not(equalTo(408)), not(equalTo(409)), not(equalTo(403))))); catches.put("request", tuple("4xx|5xx",
allOf(greaterThanOrEqualTo(400), not(equalTo(404)), not(equalTo(408)), not(equalTo(409)), not(equalTo(403)))));
} }
} }