Skip to content
This repository was archived by the owner on Nov 8, 2024. It is now read-only.

Verify whether Dredd handles binary data #1094

Merged
merged 15 commits into from
Aug 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
lib

docs/_build
6 changes: 6 additions & 0 deletions docs/data-structures.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ Transaction object is passed as a first argument to [hook functions](hooks.md) a
- fullPath: `/message` (string) - expanded [URI Template][] with parameters (if any) used for the HTTP request Dredd performs to the tested server
- request (object) - the HTTP request Dredd performs to the tested server, taken from the API description
- body: `Hello world!\n` (string)
- bodyEncoding (enum) - can be manually set in [hooks](hooks.md)
- `utf-8` (string) - indicates `body` contains a textual content encoded in UTF-8
- `base64` (string) - indicates `body` contains a binary content encoded in Base64
- headers (object) - keys are HTTP header names, values are HTTP header contents
- uri: `/message` (string) - request URI as it was written in API description
- method: `POST` (string)
Expand All @@ -36,6 +39,9 @@ Transaction object is passed as a first argument to [hook functions](hooks.md) a
- statusCode: `200` (string)
- headers (object) - keys are HTTP header names, values are HTTP header contents
- body (string)
- bodyEncoding (enum)
- `utf-8` (string) - indicates `body` contains a textual content encoded in UTF-8
- `base64` (string) - indicates `body` contains a binary content encoded in Base64
- skip: `false` (boolean) - can be set to `true` and the transaction will be skipped
- fail: `false` (enum) - can be set to `true` or string and the transaction will fail
- (string) - failure message with details why the transaction failed
Expand Down
2 changes: 1 addition & 1 deletion docs/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ If there is no body example or schema specified for the response in your API des

If you want to enforce the incoming body is empty, you can use [hooks](hooks.md):

```js
```javascript
:[hooks example](../test/fixtures/response/empty-body-hooks.js)
```

Expand Down
56 changes: 56 additions & 0 deletions docs/how-to-guides.md
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,62 @@ Most of the authentication schemes use HTTP header for carrying the authenticati
:[Swagger example](../test/fixtures/request/application-x-www-form-urlencoded.yaml)
```

## Working with Images and other Binary Bodies

The API description formats generally do not provide a way to describe binary content. The easiest solution is to describe only the media type, to [leave out the body](how-it-works.md#empty-response-body), and to handle the rest using [hooks](hooks.md).

### Binary Request Body

#### API Blueprint

```apiblueprint
:[API Blueprint example](../test/fixtures/request/image-png.apib)
```

#### Swagger

```yaml
:[Swagger example](../test/fixtures/request/image-png.yaml)
```

#### Hooks

In hooks, you can populate the request body with real binary data. The data must be in a form of a [Base64-encoded](https://en.wikipedia.org/wiki/Base64) string.

```javascript
:[Hooks example](../test/fixtures/request/image-png-hooks.js)
```

### Binary Response Body

#### API Blueprint

```apiblueprint
:[API Blueprint example](../test/fixtures/response/binary.apib)
```

#### Swagger

```yaml
:[Swagger example](../test/fixtures/response/binary.yaml)
```

> **Note:** Do not use the explicit `binary` or `bytes` formats with response bodies, as Dredd is not able to properly work with those ([fury-adapter-swagger#193](https://github.com/apiaryio/fury-adapter-swagger/issues/193)).

### Hooks

In hooks, you can either assert the body:

```javascript
:[Hooks example](../test/fixtures/response/binary-assert-body-hooks.js)
```

Or you can ignore it:

```javascript
:[Hooks example](../test/fixtures/response/binary-ignore-body-hooks.js)
```

## Multiple Requests and Responses

> **Note:** For details on this topic see also [How Dredd Works With HTTP Transactions](how-it-works.md#choosing-http-transactions).
Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"clone": "2.1.1",
"coffeescript": "1.12.7",
"cross-spawn": "6.0.5",
"dredd-transactions": "6.1.3",
"dredd-transactions": "6.1.5",
"file": "0.2.2",
"fs-extra": "6.0.1",
"gavel": "2.1.2",
Expand Down
162 changes: 162 additions & 0 deletions src/performRequest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
const defaultRequest = require('request');
const caseless = require('caseless');

const defaultLogger = require('./logger');


/**
* Performs the HTTP request as described in the 'transaction.request' object.
*
* In future we should introduce a 'real' request object as well so user has
* access to the modifications made on the way.
*
* @param {string} uri
* @param {Object} transactionReq
* @param {Object} [options]
* @param {Object} [options.logger] Custom logger
* @param {Object} [options.request] Custom 'request' library implementation
* @param {Object} [options.http] Custom default 'request' library options
* @param {Function} callback
*/
function performRequest(uri, transactionReq, options, callback) {
if (typeof options === 'function') {
[options, callback] = [{}, options];
}
const logger = options.logger || defaultLogger;
const request = options.request || defaultRequest;

const httpOptions = Object.assign({}, options.http || {});
httpOptions.proxy = false;
httpOptions.followRedirect = false;
httpOptions.encoding = null;
httpOptions.method = transactionReq.method;
httpOptions.uri = uri;

try {
httpOptions.body = getBodyAsBuffer(transactionReq.body, transactionReq.bodyEncoding);
httpOptions.headers = normalizeContentLengthHeader(transactionReq.headers, httpOptions.body);

const protocol = httpOptions.uri.split(':')[0].toUpperCase();
logger.debug(`Performing ${protocol} request to the server under test: `
+ `${httpOptions.method} ${httpOptions.uri}`);

request(httpOptions, (error, res, resBody) => {
logger.debug(`Handling ${protocol} response from the server under test`);
if (error) {
callback(error);
} else {
callback(null, createTransactionRes(res, resBody));
}
});
} catch (error) {
process.nextTick(() => callback(error));
}
}


/**
* Coerces the HTTP request body to a Buffer
*
* @param {string|Buffer} body
* @param {*} encoding
*/
function getBodyAsBuffer(body, encoding) {
return body instanceof Buffer
? body
: Buffer.from(`${body || ''}`, normalizeBodyEncoding(encoding));
}


/**
* Returns the encoding as either 'utf-8' or 'base64'. Throws
* an error in case any other encoding is provided.
*
* @param {string} encoding
*/
function normalizeBodyEncoding(encoding) {
if (!encoding) { return 'utf-8'; }

switch (encoding.toLowerCase()) {
case 'utf-8':
case 'utf8':
return 'utf-8';
case 'base64':
return 'base64';
default:
throw new Error(`Unsupported encoding: '${encoding}' (only UTF-8 and `
+ 'Base64 are supported)');
}
}


/**
* Detects an existing Content-Length header and overrides the user-provided
* header value in case it's out of sync with the real length of the body.
*
* @param {Object} headers HTTP request headers
* @param {Buffer} body HTTP request body
* @param {Object} [options]
* @param {Object} [options.logger] Custom logger
*/
function normalizeContentLengthHeader(headers, body, options = {}) {
const logger = options.logger || defaultLogger;

const modifiedHeaders = Object.assign({}, headers);
const calculatedValue = Buffer.byteLength(body);
const name = caseless(modifiedHeaders).has('Content-Length');
if (name) {
const value = parseInt(modifiedHeaders[name], 10);
if (value !== calculatedValue) {
modifiedHeaders[name] = `${calculatedValue}`;
logger.warn(`Specified Content-Length header is ${value}, but the real `
+ `body length is ${calculatedValue}. Using ${calculatedValue} instead.`);
}
} else {
modifiedHeaders['Content-Length'] = `${calculatedValue}`;
}
return modifiedHeaders;
}


/**
* Real transaction response object factory. Serializes binary responses
* to string using Base64 encoding.
*
* @param {Object} res Node.js HTTP response
* @param {Buffer} body HTTP response body as Buffer
*/
function createTransactionRes(res, body) {
const transactionRes = {
statusCode: res.statusCode,
headers: Object.assign({}, res.headers)
};
if (Buffer.byteLength(body || '')) {
transactionRes.bodyEncoding = detectBodyEncoding(body);
transactionRes.body = body.toString(transactionRes.bodyEncoding);
}
return transactionRes;
}


/**
* @param {Buffer} body
*/
function detectBodyEncoding(body) {
// U+FFFD is a replacement character in UTF-8 and indicates there
// are some bytes which could not been translated as UTF-8. Therefore
// let's assume the body is in binary format. Dredd encodes binary as
// Base64 to be able to transfer it wrapped in JSON over the TCP to non-JS
// hooks implementations.
return body.toString().includes('\ufffd') ? 'base64' : 'utf-8';
}


// only for the purpose of unit tests
performRequest._normalizeBodyEncoding = normalizeBodyEncoding;
performRequest._getBodyAsBuffer = getBodyAsBuffer;
performRequest._normalizeContentLengthHeader = normalizeContentLengthHeader;
performRequest._createTransactionRes = createTransactionRes;
performRequest._detectBodyEncoding = detectBodyEncoding;


module.exports = performRequest;
Loading