~mmcm/akiban-server/tree-schema-map-wildcards

« back to all changes in this revision

Viewing changes to src/test/java/com/akiban/rest/RestServiceScriptsIT.java

  • Committer: Mike McMahon
  • Date: 2013-05-01 19:28:19 UTC
  • mfrom: (2627.1.29 stage)
  • Revision ID: mmcm@comcast.net-20130501192819-wszn9wtik2cs1m5g
MergeĀ fromĀ trunk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/**
 
2
 * Copyright (C) 2009-2013 Akiban Technologies, Inc.
 
3
 *
 
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.
 
8
 *
 
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.
 
13
 *
 
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/>.
 
16
 */
 
17
 
 
18
package com.akiban.rest;
 
19
 
 
20
import static com.akiban.util.JsonUtils.readTree;
 
21
import static org.junit.Assert.fail;
 
22
 
 
23
import java.io.ByteArrayInputStream;
 
24
import java.io.File;
 
25
import java.io.IOException;
 
26
import java.io.UnsupportedEncodingException;
 
27
import java.net.MalformedURLException;
 
28
import java.net.URISyntaxException;
 
29
import java.net.URL;
 
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;
 
36
import java.util.Map;
 
37
 
 
38
import junit.framework.ComparisonFailure;
 
39
 
 
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;
 
48
 
 
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;
 
60
 
 
61
/**
 
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
 
65
 * are:
 
66
 * 
 
67
 * <pre>
 
68
 * GET address
 
69
 * DELETE address
 
70
 * QUERY query
 
71
 * EXPLAIN query
 
72
 * POST address content
 
73
 * PUT address content
 
74
 * PATCH address content
 
75
 * EQUALS expected
 
76
 * CONTAINS expected
 
77
 * JSONEQ expected
 
78
 * HEADERS expected
 
79
 * EMPTY
 
80
 * NOTEMPTY
 
81
 * SHOW
 
82
 * DEBUG
 
83
 * </pre>
 
84
 * 
 
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.:
 
94
 * 
 
95
 * <pre>
 
96
 * POST    /builder/implode/test.customers @
 
97
 * </pre>
 
98
 * 
 
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
 
102
 * that method.
 
103
 * 
 
104
 * @author peter
 
105
 */
 
106
@RunWith(NamedParameterizedRunner.class)
 
107
public class RestServiceScriptsIT extends ITBase {
 
108
 
 
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);
 
112
    }
 
113
 
 
114
    private static final Logger LOG = LoggerFactory.getLogger(RestServiceScriptsIT.class.getName());
 
115
 
 
116
    private static final File RESOURCE_DIR = new File("src/test/resources/"
 
117
            + RestServiceScriptsIT.class.getPackage().getName().replace('.', '/'));
 
118
 
 
119
    public static final String SCHEMA_NAME = "test";
 
120
 
 
121
    private static class CaseParams {
 
122
        public final String subDir;
 
123
        public final String caseName;
 
124
        public final String script;
 
125
 
 
126
        private CaseParams(String subDir, String caseName, String script) {
 
127
            this.subDir = subDir;
 
128
            this.caseName = caseName;
 
129
            this.script = script;
 
130
        }
 
131
    }
 
132
 
 
133
    static class Result {
 
134
        HttpExchange conn;
 
135
        String output = "<not executed>";
 
136
    }
 
137
 
 
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;
 
143
 
 
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);
 
149
        httpClient.start();
 
150
    }
 
151
 
 
152
    @Override
 
153
    protected GuicedServiceManager.BindingsConfigurationProvider serviceBindingsProvider() {
 
154
        return super.serviceBindingsProvider().bindAndRequire(RestService.class, RestServiceImpl.class)
 
155
                .bindAndRequire(BasicInfoSchemaTablesService.class, BasicInfoSchemaTablesServiceImpl.class);
 
156
    }
 
157
 
 
158
    @Override
 
159
    protected Map<String, String> startupConfigProperties() {
 
160
        return uniqueStartupConfigProperties(RestServiceScriptsIT.class);
 
161
    }
 
162
 
 
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());
 
168
            }
 
169
        });
 
170
        return result;
 
171
    }
 
172
 
 
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);
 
180
                continue;
 
181
            }
 
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);
 
187
 
 
188
                result.add(Parameterization.create(subDirName + File.separator + caseName, new CaseParams(subDirName,
 
189
                        caseName, script)));
 
190
            }
 
191
        }
 
192
        return result;
 
193
    }
 
194
 
 
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);
 
199
    }
 
200
 
 
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);
 
206
        }
 
207
        for (File data : subDir.listFiles(new RegexFilenameFilter(".*\\.dat"))) {
 
208
            loadDataFile(SCHEMA_NAME, data);
 
209
        }
 
210
    }
 
211
 
 
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));
 
216
    }
 
217
 
 
218
    @After
 
219
    public void finish() throws Exception {
 
220
        httpClient.stop();
 
221
    }
 
222
 
 
223
    private void error(String message) {
 
224
        error(message, result.output);
 
225
    }
 
226
 
 
227
    private void error(String message, String s) {
 
228
        String error = String.format("%s in %s:%d <%s>", message, caseParams.caseName, lineNumber, s);
 
229
        errors.add(error);
 
230
    }
 
231
 
 
232
    @Test
 
233
    public void testRequest() throws Exception {
 
234
        loadDatabase(caseParams.subDir);
 
235
 
 
236
        // Execute lines of script
 
237
 
 
238
        result.conn = null;
 
239
        result.output = "<not executed>";
 
240
        lineNumber = 0;
 
241
 
 
242
        try {
 
243
            for (String line : caseParams.script.split("\n")) {
 
244
                lineNumber++;
 
245
                line = line.trim();
 
246
 
 
247
                while (line.contains("  ")) {
 
248
                    line = line.replace("  ", " ");
 
249
                }
 
250
                if (line.startsWith("#") || line.isEmpty()) {
 
251
                    continue;
 
252
                }
 
253
                String[] pieces = line.split(" ");
 
254
                String command = pieces[0].toUpperCase();
 
255
 
 
256
                switch (command) {
 
257
                case "DEBUG":
 
258
                    debug(lineNumber);
 
259
                    break;
 
260
                case "GET":
 
261
                case "DELETE": {
 
262
                    result.conn = null;
 
263
                    if (pieces.length < 2) {
 
264
                        error("Missing argument");
 
265
                        continue;
 
266
                    }
 
267
                    executeRestCall(command, pieces[1], null);
 
268
                    break;
 
269
                }
 
270
                case "QUERY":
 
271
                    result.conn = null;
 
272
                    if (pieces.length < 2) {
 
273
                        error("Missing argument");
 
274
                        continue;
 
275
                    }
 
276
                    executeRestCall("GET", "/sql/query?q=" + trimAndURLEncode(value(line, 1)), null);
 
277
                    break;
 
278
                case "EXPLAIN":
 
279
                    result.conn = null;
 
280
                    if (pieces.length < 2) {
 
281
                        error("Missing argument");
 
282
                        continue;
 
283
                    }
 
284
                    executeRestCall("GET", "/sql/explain?q=" + trimAndURLEncode(value(line, 1)), null);
 
285
                    break;
 
286
                case "POST":
 
287
                case "PUT":
 
288
                case "PATCH": {
 
289
                    result.conn = null;
 
290
                    pieces = line.split(" ", 3);
 
291
                    if (pieces.length < 3) {
 
292
                        error("Missing argument");
 
293
                        continue;
 
294
                    }
 
295
                    String contents = value(line, 2);
 
296
                    executeRestCall(command, pieces[1], contents);
 
297
                    break;
 
298
                }
 
299
                case "EQUALS":
 
300
                    if (pieces.length < 2) {
 
301
                        error("Missing argument");
 
302
                        continue;
 
303
                    }
 
304
                    compareStrings("Incorrect response", value(line, 1), result.output);
 
305
                    break;
 
306
                case "CONTAINS":
 
307
                    if (pieces.length < 2) {
 
308
                        error("Missing argument");
 
309
                        continue;
 
310
                    }
 
311
                    if (!result.output.contains(value(line, 1))) {
 
312
                        LOG.error("Incorrect value - actual returned value is:\n{}", result.output);
 
313
                        error("Incorrect response");
 
314
                    }
 
315
                    break;
 
316
                case "JSONEQ":
 
317
                    if (pieces.length < 2) {
 
318
                        error("Missing argument");
 
319
                        continue;
 
320
                    }
 
321
                    compareAsJSON("Unexpected response", value(line, 1), result.output);
 
322
                    break;
 
323
                case "HEADERS":
 
324
                    if (pieces.length < 2) {
 
325
                        error("Missing argument");
 
326
                        continue;
 
327
                    }
 
328
                    compareHeaders(result.conn, value(line, 1));
 
329
                    break;
 
330
                case "NOTEMPTY":
 
331
                    if (result.output.isEmpty() || result.conn == null) {
 
332
                        error("Expected non-empty response");
 
333
                        continue;
 
334
                    }
 
335
                    break;
 
336
                case "EMPTY":
 
337
                    if (!result.output.isEmpty()) {
 
338
                        error("Expected empty response");
 
339
                    }
 
340
                    break;
 
341
                case "SHOW":
 
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);
 
345
                    break;
 
346
                default:
 
347
                    result.conn = null;
 
348
                    error("Unknown script command '" + command + "'");
 
349
                }
 
350
            }
 
351
        } finally {
 
352
            result.conn = null;
 
353
        }
 
354
        if (!errors.isEmpty()) {
 
355
            String failMessage = "Failed with " + errors.size() + " errors:";
 
356
            for (String s : errors) {
 
357
                failMessage += "\n  " + s;
 
358
            }
 
359
            fail(failMessage);
 
360
        }
 
361
    }
 
362
 
 
363
    private void executeRestCall(final String command, final String address, final String contents) throws Exception {
 
364
        String[] pieces = address.split("\\|");
 
365
        try {
 
366
            result.conn = openConnection(pieces[0], command);
 
367
            if (contents != null) {
 
368
                postContents(result.conn, contents.getBytes());
 
369
            }
 
370
            // After postContents to override default
 
371
            if (pieces.length > 1) {
 
372
                result.conn.setRequestContentType(pieces[1]);
 
373
            }
 
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);
 
380
        }
 
381
    }
 
382
 
 
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);
 
388
        return exchange;
 
389
    }
 
390
 
 
391
    private String getOutput(HttpExchange httpConn) throws IOException {
 
392
        return ((ContentExchange) httpConn).getResponseContent();
 
393
    }
 
394
 
 
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) {
 
399
                s = "";
 
400
            } else {
 
401
                s = Strings.dumpFileToString(new File(new File(RESOURCE_DIR, caseParams.subDir), s.substring(1)));
 
402
            }
 
403
        } else {
 
404
            s = s.replace("\\n", "\n").replace("\\n", "\t");
 
405
        }
 
406
        return s;
 
407
    }
 
408
 
 
409
    private static String trimAndURLEncode(String s) throws UnsupportedEncodingException {
 
410
        return URLEncoder.encode(s.trim().replaceAll("\\s+", " "), "UTF-8");
 
411
    }
 
412
 
 
413
    private String diff(String a, String b) {
 
414
        return new ComparisonFailure("", a, b).getMessage();
 
415
    }
 
416
 
 
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));
 
421
        }
 
422
    }
 
423
 
 
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() : "";
 
429
        try {
 
430
            if (!expectedTrimmed.isEmpty()) {
 
431
                expectedNode = readTree(expected);
 
432
            }
 
433
            if (!actualTrimmed.isEmpty()) {
 
434
                actualNode = readTree(actual);
 
435
            }
 
436
        } catch (JsonParseException e) {
 
437
            // Note: This case handles the jsonp tests. Somewhat fragile, but
 
438
            // not horrible yet.
 
439
        }
 
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()));
 
444
            }
 
445
        } else {
 
446
            compareStrings(assertMsg, expected, actual);
 
447
        }
 
448
    }
 
449
 
 
450
    private void compareHeaders(HttpExchange httpConn, String checkHeaders) throws Exception {
 
451
        ContentExchange exch = (ContentExchange) httpConn;
 
452
 
 
453
        String[] headerList = checkHeaders.split(Strings.NL);
 
454
        for (String header : headerList) {
 
455
            String[] nameValue = header.split(":", 2);
 
456
 
 
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]));
 
461
                }
 
462
            } else {
 
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()));
 
466
                }
 
467
            }
 
468
        }
 
469
    }
 
470
 
 
471
    private void fullyDisconnect(HttpExchange httpConn) throws InterruptedException {
 
472
        // If there is a failure, leaving junk in any of the streams can cause
 
473
        // cascading issues.
 
474
        // Get rid of anything left and disconnect.
 
475
        httpConn.waitForDone();
 
476
        httpConn.reset();
 
477
    }
 
478
}