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

Commit d5414b3

Browse files
committed
feat: support binary req/res bodies
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
1 parent 514d2ff commit d5414b3

File tree

10 files changed

+68
-94
lines changed

10 files changed

+68
-94
lines changed

src/transaction-runner.js

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -560,9 +560,10 @@ Interface of the hooks functions will be unified soon across all hook functions:
560560
options.uri = url.format(urlObject) + transaction.fullPath;
561561
options.method = transaction.request.method;
562562
options.headers = transaction.request.headers;
563-
options.body = transaction.request.body;
563+
options.body = Buffer.from(transaction.request.body, transaction.request.bodyEncoding);
564564
options.proxy = false;
565565
options.followRedirect = false;
566+
options.encoding = null;
566567
return options;
567568
}
568569

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

630631
const contentLengthHeaderName = caseless(headers).has('Content-Length');
631632
if (contentLengthHeaderName) {
@@ -656,14 +657,39 @@ the real body length is 0. Using 0 instead.\
656657
// An actual HTTP request, before validation hooks triggering
657658
// and the response validation is invoked here
658659
performRequestAndValidate(test, transaction, hooks, callback) {
660+
if (transaction.request.body instanceof Buffer) {
661+
const bodyBytes = transaction.request.body;
662+
663+
// TODO case insensitive check to either base64 or utf8 or error
664+
if (transaction.request.bodyEncoding === 'base64') {
665+
transaction.request.body = bodyBytes.toString('base64');
666+
} else if (transaction.request.bodyEncoding) {
667+
transaction.request.body = bodyBytes.toString();
668+
} else {
669+
const bodyText = bodyBytes.toString('utf8');
670+
if (bodyText.includes('\ufffd')) {
671+
// U+FFFD is a replacement character in UTF-8 and indicates there
672+
// are some bytes which could not been translated as UTF-8. Therefore
673+
// let's assume the body is in binary format. Transferring raw bytes
674+
// over the Dredd hooks interface (JSON over TCP) is a mess, so let's
675+
// encode it as Base64
676+
transaction.request.body = bodyBytes.toString('base64');
677+
transaction.request.bodyEncoding = 'base64';
678+
} else {
679+
transaction.request.body = bodyText;
680+
transaction.request.bodyEncoding = 'utf8';
681+
}
682+
}
683+
}
684+
659685
if (transaction.request.body && this.isMultipart(transaction.request.headers)) {
660686
transaction.request.body = this.fixApiBlueprintMultipartBody(transaction.request.body);
661687
}
662688

663689
this.setContentLength(transaction);
664690
const requestOptions = this.getRequestOptionsFromTransaction(transaction);
665691

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

684-
if (body) {
685-
transaction.real.body = body;
710+
if (bodyBytes) {
711+
const bodyText = bodyBytes.toString('utf8');
712+
if (bodyText.includes('\ufffd')) {
713+
// U+FFFD is a replacement character in UTF-8 and indicates there
714+
// are some bytes which could not been translated as UTF-8. Therefore
715+
// let's assume the body is in binary format. Transferring raw bytes
716+
// over the Dredd hooks interface (JSON over TCP) is a mess, so let's
717+
// encode it as Base64
718+
transaction.real.body = bodyBytes.toString('base64');
719+
transaction.real.bodyEncoding = 'base64';
720+
} else {
721+
transaction.real.body = bodyText;
722+
transaction.real.bodyEncoding = 'utf8';
723+
}
686724
} else if (transaction.expected.body) {
687725
// Leaving body as undefined skips its validation completely. In case
688726
// there is no real body, but there is one expected, the empty string

test/fixtures/image.png

Lines changed: 0 additions & 1 deletion
This file was deleted.

test/fixtures/image.png

15.3 KB
Loading
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const hooks = require('hooks');
22

33
hooks.beforeEach((transaction, done) => {
4-
transaction.request.body = Buffer.from([0xFF, 0xEF, 0xBF, 0xBE]).toString();
4+
transaction.request.body = Buffer.from([0xFF, 0xEF, 0xBF, 0xBE]).toString('base64');
5+
transaction.request.bodyEncoding = 'base64';
56
done();
67
});

test/fixtures/request/image-png-hooks.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const path = require('path');
44

55
hooks.beforeEach((transaction, done) => {
66
const buffer = fs.readFileSync(path.join(__dirname, '../image.png'));
7-
transaction.request.body = buffer.toString();
7+
transaction.request.body = buffer.toString('base64');
8+
transaction.request.bodyEncoding = 'base64';
89
done();
910
});
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
const hooks = require('hooks');
22
const fs = require('fs');
33
const path = require('path');
4-
const { assert } = require('chai');
54

65
hooks.beforeEachValidation((transaction, done) => {
7-
const buffer = fs.readFileSync(path.join(__dirname, '../image.png'));
8-
assert.equal(transaction.real.body, buffer.toString());
6+
const bytes = fs.readFileSync(path.join(__dirname, '../image.png'));
7+
transaction.expected.body = bytes.toString('base64');
98
done();
109
});

test/fixtures/response/binary-invalid-utf8-hooks.js

Lines changed: 0 additions & 8 deletions
This file was deleted.

test/fixtures/response/binary-invalid-utf8.apib

Lines changed: 0 additions & 9 deletions
This file was deleted.

test/fixtures/response/binary-invalid-utf8.yaml

Lines changed: 0 additions & 16 deletions
This file was deleted.

test/integration/request-test.js

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ describe('Sending \'application/json\' request', () => {
1414
const app = createServer({ bodyParser: bodyParser.text({ type: contentType }) });
1515
app.post('/data', (req, res) => res.json({ test: 'OK' }));
1616

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

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

136135
before((done) => {
137-
const path = './test/fixtures/request/text-plain.apib';
138-
139136
const app = createServer({ bodyParser: bodyParser.text({ type: contentType }) });
140137
app.post('/data', (req, res) => res.json({ test: 'OK' }));
141-
const dredd = new Dredd({ options: { path } });
138+
const dredd = new Dredd({ options: { path: './test/fixtures/request/text-plain.apib' } });
142139

143140
runDreddWithServer(dredd, app, (err, info) => {
144141
runtimeInfo = info;
@@ -185,12 +182,16 @@ describe('Sending \'text/plain\' request', () => {
185182
});
186183
});
187184

188-
it('results in one request being delivered to the server', () => assert.isTrue(runtimeInfo.server.requestedOnce));
189-
it('the request has the expected Content-Type', () => assert.equal(runtimeInfo.server.lastRequest.headers['content-type'], contentType));
185+
it('results in one request being delivered to the server', () =>
186+
assert.isTrue(runtimeInfo.server.requestedOnce)
187+
);
188+
it('the request has the expected Content-Type', () =>
189+
assert.equal(runtimeInfo.server.lastRequest.headers['content-type'], contentType)
190+
);
190191
it('the request has the expected format', () =>
191192
assert.equal(
192-
runtimeInfo.server.lastRequest.body.toString(),
193-
Buffer.from([0xFF, 0xEF, 0xBF, 0xBE]).toString()
193+
runtimeInfo.server.lastRequest.body.toString('base64'),
194+
Buffer.from([0xFF, 0xEF, 0xBF, 0xBE]).toString('base64')
194195
)
195196
);
196197
it('results in one passing test', () => {
@@ -230,12 +231,16 @@ describe('Sending \'text/plain\' request', () => {
230231
});
231232
});
232233

233-
it('results in one request being delivered to the server', () => assert.isTrue(runtimeInfo.server.requestedOnce));
234-
it('the request has the expected Content-Type', () => assert.equal(runtimeInfo.server.lastRequest.headers['content-type'], contentType));
234+
it('results in one request being delivered to the server', () =>
235+
assert.isTrue(runtimeInfo.server.requestedOnce)
236+
);
237+
it('the request has the expected Content-Type', () =>
238+
assert.equal(runtimeInfo.server.lastRequest.headers['content-type'], contentType)
239+
);
235240
it('the request has the expected format', () =>
236241
assert.equal(
237-
runtimeInfo.server.lastRequest.body.toString(),
238-
fs.readFileSync(path.join(__dirname, '../fixtures/image.png')).toString()
242+
runtimeInfo.server.lastRequest.body.toString('base64'),
243+
fs.readFileSync(path.join(__dirname, '../fixtures/image.png')).toString('base64')
239244
)
240245
);
241246
it('results in one passing test', () => {

0 commit comments

Comments
 (0)