2
* Copyright (C) 2009-2013 Akiban Technologies, Inc.
4
* This program is free software: you can redistribute it and/or modify
5
* it under the terms of the GNU Affero General Public License as published by
6
* the Free Software Foundation, either version 3 of the License, or
7
* (at your option) any later version.
9
* This program is distributed in the hope that it will be useful,
10
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
* GNU Affero General Public License for more details.
14
* You should have received a copy of the GNU Affero General Public License
15
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18
package com.akiban.rest;
20
import static com.akiban.util.JsonUtils.readTree;
21
import static org.junit.Assert.fail;
23
import java.io.ByteArrayInputStream;
25
import java.io.IOException;
26
import java.io.UnsupportedEncodingException;
27
import java.net.MalformedURLException;
28
import java.net.URISyntaxException;
30
import java.net.URLEncoder;
31
import java.util.ArrayList;
32
import java.util.Arrays;
33
import java.util.Collection;
34
import java.util.Comparator;
35
import java.util.List;
38
import junit.framework.ComparisonFailure;
40
import org.eclipse.jetty.client.ContentExchange;
41
import org.eclipse.jetty.client.HttpClient;
42
import org.eclipse.jetty.client.HttpExchange;
43
import org.junit.After;
44
import org.junit.Test;
45
import org.junit.runner.RunWith;
46
import org.slf4j.Logger;
47
import org.slf4j.LoggerFactory;
49
import com.akiban.http.HttpConductor;
50
import com.akiban.junit.NamedParameterizedRunner;
51
import com.akiban.junit.Parameterization;
52
import com.akiban.server.service.is.BasicInfoSchemaTablesService;
53
import com.akiban.server.service.is.BasicInfoSchemaTablesServiceImpl;
54
import com.akiban.server.service.servicemanager.GuicedServiceManager;
55
import com.akiban.server.test.it.ITBase;
56
import com.akiban.sql.RegexFilenameFilter;
57
import com.akiban.util.Strings;
58
import com.fasterxml.jackson.core.JsonParseException;
59
import com.fasterxml.jackson.databind.JsonNode;
62
* Scripted tests for REST end-points. Code was largely copied from
63
* RestServiceFilesIT. Difference is that this version finds files with the
64
* suffix ".script" and executes the command stream located in them. Commands
72
* POST address content
74
* PATCH address content
85
* where address is a path relative the resource end-point, content is a string
86
* value that is converted to bytes and sent with POST, PUT and PATCH
87
* operations, and expected is a value used in comparison with the most recently
88
* returned content. The values of the query, content and expected fields may be
89
* specified in-line, or as a reference to another file as in @filename. For
90
* in-line values, the character sequences "\n", "\t" and "\r" are converted to
91
* the corresponding new-line, tab and return characters. This transformation is
92
* not done if the value is supplied as a file reference. An empty string can be
93
* specified as simply @, e.g.:
96
* POST /builder/implode/test.customers @
99
* The SHOW and DEBUG commands are useful for debugging. SHOW simply prints out
100
* the actual content of the last REST response. The DEBUG command calls the
101
* static method {@link #debug(int)}. You can set a debugger breakpoint inside
106
@RunWith(NamedParameterizedRunner.class)
107
public class RestServiceScriptsIT extends ITBase {
109
private static void debug(int lineNumber) {
110
// Set a breakpoint here to debug on DEBUG statements
111
System.out.println("DEBUG executed on line " + lineNumber);
114
private static final Logger LOG = LoggerFactory.getLogger(RestServiceScriptsIT.class.getName());
116
private static final File RESOURCE_DIR = new File("src/test/resources/"
117
+ RestServiceScriptsIT.class.getPackage().getName().replace('.', '/'));
119
public static final String SCHEMA_NAME = "test";
121
private static class CaseParams {
122
public final String subDir;
123
public final String caseName;
124
public final String script;
126
private CaseParams(String subDir, String caseName, String script) {
127
this.subDir = subDir;
128
this.caseName = caseName;
129
this.script = script;
133
static class Result {
135
String output = "<not executed>";
138
protected final CaseParams caseParams;
139
protected final HttpClient httpClient;
140
private final List<String> errors = new ArrayList<>();
141
private final Result result = new Result();
142
private int lineNumber = 0;
144
public RestServiceScriptsIT(CaseParams caseParams) throws Exception {
145
this.caseParams = caseParams;
146
this.httpClient = new HttpClient();
147
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
148
httpClient.setMaxConnectionsPerAddress(10);
153
protected GuicedServiceManager.BindingsConfigurationProvider serviceBindingsProvider() {
154
return super.serviceBindingsProvider().bindAndRequire(RestService.class, RestServiceImpl.class)
155
.bindAndRequire(BasicInfoSchemaTablesService.class, BasicInfoSchemaTablesServiceImpl.class);
159
protected Map<String, String> startupConfigProperties() {
160
return uniqueStartupConfigProperties(RestServiceScriptsIT.class);
163
public static File[] gatherRequestFiles(File dir) {
164
File[] result = dir.listFiles(new RegexFilenameFilter(".*\\.(script)"));
165
Arrays.sort(result, new Comparator<File>() {
166
public int compare(File f1, File f2) {
167
return f1.getName().compareTo(f2.getName());
173
@NamedParameterizedRunner.TestParameters
174
public static Collection<Parameterization> gatherCases() throws Exception {
175
Collection<Parameterization> result = new ArrayList<>();
176
for (String subDirName : RESOURCE_DIR.list()) {
177
File subDir = new File(RESOURCE_DIR, subDirName);
178
if (!subDir.isDirectory()) {
179
LOG.warn("Skipping unexpected file: {}", subDir);
182
for (File requestFile : gatherRequestFiles(subDir)) {
183
String inputName = requestFile.getName();
184
int dotIndex = inputName.lastIndexOf('.');
185
String caseName = inputName.substring(0, dotIndex);
186
String script = Strings.dumpFileToString(requestFile);
188
result.add(Parameterization.create(subDirName + File.separator + caseName, new CaseParams(subDirName,
195
private URL getRestURL(String request) throws MalformedURLException {
196
int port = serviceManager().getServiceByClass(HttpConductor.class).getPort();
197
String context = serviceManager().getServiceByClass(RestService.class).getContextPath();
198
return new URL("http", "localhost", port, context + request);
201
private void loadDatabase(String subDirName) throws Exception {
202
File subDir = new File(RESOURCE_DIR, subDirName);
203
File schemaFile = new File(subDir, "schema.ddl");
204
if (schemaFile.exists()) {
205
loadSchemaFile(SCHEMA_NAME, schemaFile);
207
for (File data : subDir.listFiles(new RegexFilenameFilter(".*\\.dat"))) {
208
loadDataFile(SCHEMA_NAME, data);
212
private static void postContents(HttpExchange httpConn, byte[] request) throws IOException {
213
httpConn.setRequestContentType("application/json");
214
httpConn.setRequestHeader("Accept", "application/json");
215
httpConn.setRequestContentSource(new ByteArrayInputStream(request));
219
public void finish() throws Exception {
223
private void error(String message) {
224
error(message, result.output);
227
private void error(String message, String s) {
228
String error = String.format("%s in %s:%d <%s>", message, caseParams.caseName, lineNumber, s);
233
public void testRequest() throws Exception {
234
loadDatabase(caseParams.subDir);
236
// Execute lines of script
239
result.output = "<not executed>";
243
for (String line : caseParams.script.split("\n")) {
247
while (line.contains(" ")) {
248
line = line.replace(" ", " ");
250
if (line.startsWith("#") || line.isEmpty()) {
253
String[] pieces = line.split(" ");
254
String command = pieces[0].toUpperCase();
263
if (pieces.length < 2) {
264
error("Missing argument");
267
executeRestCall(command, pieces[1], null);
272
if (pieces.length < 2) {
273
error("Missing argument");
276
executeRestCall("GET", "/sql/query?q=" + trimAndURLEncode(value(line, 1)), null);
280
if (pieces.length < 2) {
281
error("Missing argument");
284
executeRestCall("GET", "/sql/explain?q=" + trimAndURLEncode(value(line, 1)), null);
290
pieces = line.split(" ", 3);
291
if (pieces.length < 3) {
292
error("Missing argument");
295
String contents = value(line, 2);
296
executeRestCall(command, pieces[1], contents);
300
if (pieces.length < 2) {
301
error("Missing argument");
304
compareStrings("Incorrect response", value(line, 1), result.output);
307
if (pieces.length < 2) {
308
error("Missing argument");
311
if (!result.output.contains(value(line, 1))) {
312
LOG.error("Incorrect value - actual returned value is:\n{}", result.output);
313
error("Incorrect response");
317
if (pieces.length < 2) {
318
error("Missing argument");
321
compareAsJSON("Unexpected response", value(line, 1), result.output);
324
if (pieces.length < 2) {
325
error("Missing argument");
328
compareHeaders(result.conn, value(line, 1));
331
if (result.output.isEmpty() || result.conn == null) {
332
error("Expected non-empty response");
337
if (!result.output.isEmpty()) {
338
error("Expected empty response");
342
int status = result.conn == null ? -1 : ((ContentExchange)result.conn).getResponseStatus();
343
System.out.printf("At line %d the most recent response status is %d. " + "The value is:\n%s\n",
344
lineNumber, status, result.output);
348
error("Unknown script command '" + command + "'");
354
if (!errors.isEmpty()) {
355
String failMessage = "Failed with " + errors.size() + " errors:";
356
for (String s : errors) {
357
failMessage += "\n " + s;
363
private void executeRestCall(final String command, final String address, final String contents) throws Exception {
364
String[] pieces = address.split("\\|");
366
result.conn = openConnection(pieces[0], command);
367
if (contents != null) {
368
postContents(result.conn, contents.getBytes());
370
// After postContents to override default
371
if (pieces.length > 1) {
372
result.conn.setRequestContentType(pieces[1]);
374
httpClient.send(result.conn);
375
result.conn.waitForDone();
376
result.output = getOutput(result.conn);
377
} catch (Exception e) {
378
result.output = e.toString();
379
fullyDisconnect(result.conn);
383
private HttpExchange openConnection(String address, String requestMethod) throws IOException, URISyntaxException {
384
URL url = getRestURL(address);
385
HttpExchange exchange = new ContentExchange(true);
386
exchange.setURI(url.toURI());
387
exchange.setMethod(requestMethod);
391
private String getOutput(HttpExchange httpConn) throws IOException {
392
return ((ContentExchange) httpConn).getResponseContent();
395
private String value(String line, int index) throws IOException {
396
String s = line.split(" ", index + 1)[index];
397
if (s.startsWith("@")) {
398
if (s.length() == 1) {
401
s = Strings.dumpFileToString(new File(new File(RESOURCE_DIR, caseParams.subDir), s.substring(1)));
404
s = s.replace("\\n", "\n").replace("\\n", "\t");
409
private static String trimAndURLEncode(String s) throws UnsupportedEncodingException {
410
return URLEncoder.encode(s.trim().replaceAll("\\s+", " "), "UTF-8");
413
private String diff(String a, String b) {
414
return new ComparisonFailure("", a, b).getMessage();
417
private void compareStrings(String assertMsg, String expected, String actual) {
418
if (!expected.equals(actual)) {
419
LOG.error("Incorrect value - actual returned value is:\n{}", actual);
420
error(assertMsg, diff(expected, actual));
424
private void compareAsJSON(String assertMsg, String expected, String actual) throws IOException {
425
JsonNode expectedNode = null;
426
JsonNode actualNode = null;
427
String expectedTrimmed = (expected != null) ? expected.trim() : "";
428
String actualTrimmed = (actual != null) ? actual.trim() : "";
430
if (!expectedTrimmed.isEmpty()) {
431
expectedNode = readTree(expected);
433
if (!actualTrimmed.isEmpty()) {
434
actualNode = readTree(actual);
436
} catch (JsonParseException e) {
437
// Note: This case handles the jsonp tests. Somewhat fragile, but
440
// Try manual equals and then assert strings for pretty print
441
if (expectedNode != null && actualNode != null) {
442
if (!expectedNode.equals(actualNode)) {
443
error(assertMsg, diff(expectedNode.toString(), actualNode.toString()));
446
compareStrings(assertMsg, expected, actual);
450
private void compareHeaders(HttpExchange httpConn, String checkHeaders) throws Exception {
451
ContentExchange exch = (ContentExchange) httpConn;
453
String[] headerList = checkHeaders.split(Strings.NL);
454
for (String header : headerList) {
455
String[] nameValue = header.split(":", 2);
457
if (nameValue[0].equals("responseCode")) {
458
if (Integer.parseInt(nameValue[1].trim()) != exch.getResponseStatus()) {
459
error("Incorrect Response Status",
460
String.format("%d expected %s", exch.getResponseStatus(), nameValue[1]));
463
if (!nameValue[1].trim().equals(exch.getResponseFields().getStringField(nameValue[0]))) {
464
error("Incorrect Response Header", String.format("%s expected %s", exch.getResponseFields()
465
.getStringField(nameValue[0]), nameValue[1].trim()));
471
private void fullyDisconnect(HttpExchange httpConn) throws InterruptedException {
472
// If there is a failure, leaving junk in any of the streams can cause
474
// Get rid of anything left and disconnect.
475
httpConn.waitForDone();