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

Commit

Permalink
feat: support binary req/res bodies
Browse files Browse the repository at this point in the history
Uses Base64 encoding as the serialization, which allows also non-JS
hooks to set request/response bodies to a binary content.

Close #617
Close #87
Close #836
  • Loading branch information
honzajavorek committed Jul 26, 2018
1 parent 514d2ff commit d5414b3
Show file tree
Hide file tree
Showing 10 changed files with 68 additions and 94 deletions.
50 changes: 44 additions & 6 deletions src/transaction-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -560,9 +560,10 @@ Interface of the hooks functions will be unified soon across all hook functions:
options.uri = url.format(urlObject) + transaction.fullPath;
options.method = transaction.request.method;
options.headers = transaction.request.headers;
options.body = transaction.request.body;
options.body = Buffer.from(transaction.request.body, transaction.request.bodyEncoding);
options.proxy = false;
options.followRedirect = false;
options.encoding = null;
return options;
}

Expand Down Expand Up @@ -624,8 +625,8 @@ Not performing HTTP request for '${transaction.name}'.\
// Sets the Content-Length header. Overrides user-provided Content-Length
// header value in case it's out of sync with the real length of the body.
setContentLength(transaction) {
const { headers } = transaction.request;
const { body } = transaction.request;
const headers = transaction.request.headers;
const body = Buffer.from(transaction.request.body, transaction.request.bodyEncoding);

const contentLengthHeaderName = caseless(headers).has('Content-Length');
if (contentLengthHeaderName) {
Expand Down Expand Up @@ -656,14 +657,39 @@ the real body length is 0. Using 0 instead.\
// An actual HTTP request, before validation hooks triggering
// and the response validation is invoked here
performRequestAndValidate(test, transaction, hooks, callback) {
if (transaction.request.body instanceof Buffer) {
const bodyBytes = transaction.request.body;

// TODO case insensitive check to either base64 or utf8 or error
if (transaction.request.bodyEncoding === 'base64') {
transaction.request.body = bodyBytes.toString('base64');
} else if (transaction.request.bodyEncoding) {
transaction.request.body = bodyBytes.toString();
} else {
const bodyText = bodyBytes.toString('utf8');
if (bodyText.includes('\ufffd')) {
// 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. Transferring raw bytes
// over the Dredd hooks interface (JSON over TCP) is a mess, so let's
// encode it as Base64
transaction.request.body = bodyBytes.toString('base64');
transaction.request.bodyEncoding = 'base64';
} else {
transaction.request.body = bodyText;
transaction.request.bodyEncoding = 'utf8';
}
}
}

if (transaction.request.body && this.isMultipart(transaction.request.headers)) {
transaction.request.body = this.fixApiBlueprintMultipartBody(transaction.request.body);
}

this.setContentLength(transaction);
const requestOptions = this.getRequestOptionsFromTransaction(transaction);

const handleRequest = (err, res, body) => {
const handleRequest = (err, res, bodyBytes) => {
if (err) {
logger.debug('Requesting tested server errored:', `${err}` || err.code);
test.title = transaction.id;
Expand All @@ -681,8 +707,20 @@ the real body length is 0. Using 0 instead.\
headers: res.headers
};

if (body) {
transaction.real.body = body;
if (bodyBytes) {
const bodyText = bodyBytes.toString('utf8');
if (bodyText.includes('\ufffd')) {
// 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. Transferring raw bytes
// over the Dredd hooks interface (JSON over TCP) is a mess, so let's
// encode it as Base64
transaction.real.body = bodyBytes.toString('base64');
transaction.real.bodyEncoding = 'base64';
} else {
transaction.real.body = bodyText;
transaction.real.bodyEncoding = 'utf8';
}
} else if (transaction.expected.body) {
// Leaving body as undefined skips its validation completely. In case
// there is no real body, but there is one expected, the empty string
Expand Down
1 change: 0 additions & 1 deletion test/fixtures/image.png

This file was deleted.

Binary file added test/fixtures/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion test/fixtures/request/application-octet-stream-hooks.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const hooks = require('hooks');

hooks.beforeEach((transaction, done) => {
transaction.request.body = Buffer.from([0xFF, 0xEF, 0xBF, 0xBE]).toString();
transaction.request.body = Buffer.from([0xFF, 0xEF, 0xBF, 0xBE]).toString('base64');
transaction.request.bodyEncoding = 'base64';
done();
});
3 changes: 2 additions & 1 deletion test/fixtures/request/image-png-hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const path = require('path');

hooks.beforeEach((transaction, done) => {
const buffer = fs.readFileSync(path.join(__dirname, '../image.png'));
transaction.request.body = buffer.toString();
transaction.request.body = buffer.toString('base64');
transaction.request.bodyEncoding = 'base64';
done();
});
5 changes: 2 additions & 3 deletions test/fixtures/response/binary-assert-body-hooks.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
const hooks = require('hooks');
const fs = require('fs');
const path = require('path');
const { assert } = require('chai');

hooks.beforeEachValidation((transaction, done) => {
const buffer = fs.readFileSync(path.join(__dirname, '../image.png'));
assert.equal(transaction.real.body, buffer.toString());
const bytes = fs.readFileSync(path.join(__dirname, '../image.png'));
transaction.expected.body = bytes.toString('base64');
done();
});
8 changes: 0 additions & 8 deletions test/fixtures/response/binary-invalid-utf8-hooks.js

This file was deleted.

9 changes: 0 additions & 9 deletions test/fixtures/response/binary-invalid-utf8.apib

This file was deleted.

16 changes: 0 additions & 16 deletions test/fixtures/response/binary-invalid-utf8.yaml

This file was deleted.

31 changes: 18 additions & 13 deletions test/integration/request-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ describe('Sending \'application/json\' request', () => {
const app = createServer({ bodyParser: bodyParser.text({ type: contentType }) });
app.post('/data', (req, res) => res.json({ test: 'OK' }));

const path = './test/fixtures/request/application-json.apib';
const dredd = new Dredd({ options: { path } });
const dredd = new Dredd({ options: { path: './test/fixtures/request/application-json.apib' } });

runDreddWithServer(dredd, app, (err, info) => {
runtimeInfo = info;
Expand Down Expand Up @@ -134,11 +133,9 @@ describe('Sending \'text/plain\' request', () => {
const contentType = 'text/plain';

before((done) => {
const path = './test/fixtures/request/text-plain.apib';

const app = createServer({ bodyParser: bodyParser.text({ type: contentType }) });
app.post('/data', (req, res) => res.json({ test: 'OK' }));
const dredd = new Dredd({ options: { path } });
const dredd = new Dredd({ options: { path: './test/fixtures/request/text-plain.apib' } });

runDreddWithServer(dredd, app, (err, info) => {
runtimeInfo = info;
Expand Down Expand Up @@ -185,12 +182,16 @@ describe('Sending \'text/plain\' request', () => {
});
});

it('results in one request being delivered to the server', () => assert.isTrue(runtimeInfo.server.requestedOnce));
it('the request has the expected Content-Type', () => assert.equal(runtimeInfo.server.lastRequest.headers['content-type'], contentType));
it('results in one request being delivered to the server', () =>
assert.isTrue(runtimeInfo.server.requestedOnce)
);
it('the request has the expected Content-Type', () =>
assert.equal(runtimeInfo.server.lastRequest.headers['content-type'], contentType)
);
it('the request has the expected format', () =>
assert.equal(
runtimeInfo.server.lastRequest.body.toString(),
Buffer.from([0xFF, 0xEF, 0xBF, 0xBE]).toString()
runtimeInfo.server.lastRequest.body.toString('base64'),
Buffer.from([0xFF, 0xEF, 0xBF, 0xBE]).toString('base64')
)
);
it('results in one passing test', () => {
Expand Down Expand Up @@ -230,12 +231,16 @@ describe('Sending \'text/plain\' request', () => {
});
});

it('results in one request being delivered to the server', () => assert.isTrue(runtimeInfo.server.requestedOnce));
it('the request has the expected Content-Type', () => assert.equal(runtimeInfo.server.lastRequest.headers['content-type'], contentType));
it('results in one request being delivered to the server', () =>
assert.isTrue(runtimeInfo.server.requestedOnce)
);
it('the request has the expected Content-Type', () =>
assert.equal(runtimeInfo.server.lastRequest.headers['content-type'], contentType)
);
it('the request has the expected format', () =>
assert.equal(
runtimeInfo.server.lastRequest.body.toString(),
fs.readFileSync(path.join(__dirname, '../fixtures/image.png')).toString()
runtimeInfo.server.lastRequest.body.toString('base64'),
fs.readFileSync(path.join(__dirname, '../fixtures/image.png')).toString('base64')
)
);
it('results in one passing test', () => {
Expand Down
37 changes: 0 additions & 37 deletions test/integration/response-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,40 +350,3 @@ const Dredd = require('../../src/dredd');
});
})
);

[
{
name: 'API Blueprint',
path: './test/fixtures/response/binary-invalid-utf8.apib'
},
{
name: 'Swagger',
path: './test/fixtures/response/binary-invalid-utf8.yaml'
}
].forEach(apiDescription =>
describe(`Working with binary responses, which are not valid UTF-8, in the ${apiDescription.name}`, () => {
let runtimeInfo;

before((done) => {
const app = createServer();
app.get('/binary', (req, res) =>
res.type('application/octet-stream').send(Buffer.from([0xFF, 0xEF, 0xBF, 0xBE]))
);

const dredd = new Dredd({
options: {
path: apiDescription.path,
hookfiles: './test/fixtures/response/binary-invalid-utf8-hooks.js'
}
});
runDreddWithServer(dredd, app, (err, info) => {
runtimeInfo = info;
done(err);
});
});

it('evaluates the response as valid', () =>
assert.deepInclude(runtimeInfo.dredd.stats, { tests: 1, passes: 1 })
);
})
);

0 comments on commit d5414b3

Please sign in to comment.