From 7d76faebf24e75fe01ed817b110cb93900a966b7 Mon Sep 17 00:00:00 2001 From: Honza Javorek Date: Fri, 20 Jul 2018 19:02:06 +0200 Subject: [PATCH 01/15] test: test whether Dredd handles binary responses --- test/fixtures/image.png | 1 + .../response/binary-assert-body-hooks.js | 10 ++++ .../response/binary-ignore-body-hooks.js | 6 ++ test/fixtures/response/binary.apib | 9 +++ test/fixtures/response/binary.yaml | 19 ++++++ test/integration/response-test.js | 60 +++++++++++++++++++ 6 files changed, 105 insertions(+) create mode 120000 test/fixtures/image.png create mode 100644 test/fixtures/response/binary-assert-body-hooks.js create mode 100644 test/fixtures/response/binary-ignore-body-hooks.js create mode 100644 test/fixtures/response/binary.apib create mode 100644 test/fixtures/response/binary.yaml diff --git a/test/fixtures/image.png b/test/fixtures/image.png new file mode 120000 index 000000000..a72888b11 --- /dev/null +++ b/test/fixtures/image.png @@ -0,0 +1 @@ +../../docs/_images/dredd-logo.png \ No newline at end of file diff --git a/test/fixtures/response/binary-assert-body-hooks.js b/test/fixtures/response/binary-assert-body-hooks.js new file mode 100644 index 000000000..d14184ef3 --- /dev/null +++ b/test/fixtures/response/binary-assert-body-hooks.js @@ -0,0 +1,10 @@ +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()); + done(); +}); diff --git a/test/fixtures/response/binary-ignore-body-hooks.js b/test/fixtures/response/binary-ignore-body-hooks.js new file mode 100644 index 000000000..eb5c5a5b2 --- /dev/null +++ b/test/fixtures/response/binary-ignore-body-hooks.js @@ -0,0 +1,6 @@ +const hooks = require('hooks'); + +hooks.beforeEachValidation((transaction, done) => { + transaction.real.body = ''; + done(); +}); diff --git a/test/fixtures/response/binary.apib b/test/fixtures/response/binary.apib new file mode 100644 index 000000000..e0d03006c --- /dev/null +++ b/test/fixtures/response/binary.apib @@ -0,0 +1,9 @@ +FORMAT: 1A + +# Images API + +## Resource [/image.png] + +### Retrieve Representation [GET] + ++ Response 200 (image/png) diff --git a/test/fixtures/response/binary.yaml b/test/fixtures/response/binary.yaml new file mode 100644 index 000000000..fd85ec81d --- /dev/null +++ b/test/fixtures/response/binary.yaml @@ -0,0 +1,19 @@ +swagger: "2.0" +info: + version: "1.0" + title: Images API +schemes: + - http +produces: + - image/png +paths: + /image.png: + get: + responses: + 200: + description: Representation + examples: + "image/png": "" + schema: + type: string + format: binary diff --git a/test/integration/response-test.js b/test/integration/response-test.js index fd32509c0..c56cf9d28 100644 --- a/test/integration/response-test.js +++ b/test/integration/response-test.js @@ -1,4 +1,5 @@ const { assert } = require('chai'); +const path = require('path'); const { runDreddWithServer, createServer } = require('./helpers'); const Dredd = require('../../src/dredd'); @@ -288,3 +289,62 @@ const Dredd = require('../../src/dredd'); }); }) ); + +[ + { + name: 'API Blueprint', + path: './test/fixtures/response/binary.apib' + }, + { + name: 'Swagger', + path: './test/fixtures/response/binary.yaml' + } +].forEach(apiDescription => + describe(`Working with binary responses in the ${apiDescription.name}`, () => { + const imagePath = path.join(__dirname, '../fixtures/image.png'); + const app = createServer(); + app.get('/image.png', (req, res) => res.type('image/png').sendFile(imagePath)); + + describe('when the body is described as empty and there are hooks to remove the real body', () => { + let runtimeInfo; + + before((done) => { + const dredd = new Dredd({ + options: { + path: apiDescription.path, + hookfiles: './test/fixtures/response/binary-ignore-body-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 }) + ); + }); + + describe('when the body is described as empty and there are hooks to assert the real body', () => { + let runtimeInfo; + + before((done) => { + const dredd = new Dredd({ + options: { + path: apiDescription.path, + hookfiles: './test/fixtures/response/binary-assert-body-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 }) + ); + }); + }) +); From 3307b020fe6ca479bb35392496ecefdc8b3f9b9b Mon Sep 17 00:00:00 2001 From: Honza Javorek Date: Mon, 23 Jul 2018 12:45:09 +0200 Subject: [PATCH 02/15] test: test non-UTF-8 bytes Addressing https://github.com/apiaryio/dredd/pull/836? --- .../response/binary-invalid-utf8-hooks.js | 8 ++++ .../response/binary-invalid-utf8.apib | 9 ++++ .../response/binary-invalid-utf8.yaml | 19 +++++++++ test/integration/response-test.js | 41 ++++++++++++++++++- 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/response/binary-invalid-utf8-hooks.js create mode 100644 test/fixtures/response/binary-invalid-utf8.apib create mode 100644 test/fixtures/response/binary-invalid-utf8.yaml diff --git a/test/fixtures/response/binary-invalid-utf8-hooks.js b/test/fixtures/response/binary-invalid-utf8-hooks.js new file mode 100644 index 000000000..1fa8b4c14 --- /dev/null +++ b/test/fixtures/response/binary-invalid-utf8-hooks.js @@ -0,0 +1,8 @@ +const hooks = require('hooks'); +const { assert } = require('chai'); + +hooks.beforeEachValidation((transaction, done) => { + assert.equal(typeof transaction.real.body, 'string'); + assert.equal(transaction.real.body, Buffer.from([0xFF, 0xEF, 0xBF, 0xBE]).toString()); + done(); +}); diff --git a/test/fixtures/response/binary-invalid-utf8.apib b/test/fixtures/response/binary-invalid-utf8.apib new file mode 100644 index 000000000..4f4e9f362 --- /dev/null +++ b/test/fixtures/response/binary-invalid-utf8.apib @@ -0,0 +1,9 @@ +FORMAT: 1A + +# Images API + +## Resource [/binary] + +### Retrieve Representation [GET] + ++ Response 200 (application/octet-stream) diff --git a/test/fixtures/response/binary-invalid-utf8.yaml b/test/fixtures/response/binary-invalid-utf8.yaml new file mode 100644 index 000000000..4b57f1631 --- /dev/null +++ b/test/fixtures/response/binary-invalid-utf8.yaml @@ -0,0 +1,19 @@ +swagger: "2.0" +info: + version: "1.0" + title: Images API +schemes: + - http +produces: + - application/octet-stream +paths: + /binary: + get: + responses: + 200: + description: Representation + examples: + "application/octet-stream": "" + schema: + type: string + format: binary diff --git a/test/integration/response-test.js b/test/integration/response-test.js index c56cf9d28..a4656d0f1 100644 --- a/test/integration/response-test.js +++ b/test/integration/response-test.js @@ -303,7 +303,9 @@ const Dredd = require('../../src/dredd'); describe(`Working with binary responses in the ${apiDescription.name}`, () => { const imagePath = path.join(__dirname, '../fixtures/image.png'); const app = createServer(); - app.get('/image.png', (req, res) => res.type('image/png').sendFile(imagePath)); + app.get('/image.png', (req, res) => + res.type('image/png').sendFile(imagePath) + ); describe('when the body is described as empty and there are hooks to remove the real body', () => { let runtimeInfo; @@ -348,3 +350,40 @@ 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 }) + ); + }) +); From 5264ce6c9cf0edd20dd344cb4b7b306392fd57d0 Mon Sep 17 00:00:00 2001 From: Honza Javorek Date: Mon, 23 Jul 2018 15:12:27 +0200 Subject: [PATCH 03/15] test: test binary requests --- .../request/application-octet-stream-hooks.js | 6 ++ .../request/application-octet-stream.apib | 14 +++ .../request/application-octet-stream.yaml | 26 ++++++ test/fixtures/request/image-png-hooks.js | 9 ++ test/fixtures/request/image-png.apib | 15 +++ test/fixtures/request/image-png.yaml | 26 ++++++ .../response/binary-invalid-utf8.apib | 2 +- .../response/binary-invalid-utf8.yaml | 2 +- test/integration/request-test.js | 92 +++++++++++++++++++ 9 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/request/application-octet-stream-hooks.js create mode 100644 test/fixtures/request/application-octet-stream.apib create mode 100644 test/fixtures/request/application-octet-stream.yaml create mode 100644 test/fixtures/request/image-png-hooks.js create mode 100644 test/fixtures/request/image-png.apib create mode 100644 test/fixtures/request/image-png.yaml diff --git a/test/fixtures/request/application-octet-stream-hooks.js b/test/fixtures/request/application-octet-stream-hooks.js new file mode 100644 index 000000000..148abfc24 --- /dev/null +++ b/test/fixtures/request/application-octet-stream-hooks.js @@ -0,0 +1,6 @@ +const hooks = require('hooks'); + +hooks.beforeEach((transaction, done) => { + transaction.request.body = Buffer.from([0xFF, 0xEF, 0xBF, 0xBE]).toString(); + done(); +}); diff --git a/test/fixtures/request/application-octet-stream.apib b/test/fixtures/request/application-octet-stream.apib new file mode 100644 index 000000000..b4687f14a --- /dev/null +++ b/test/fixtures/request/application-octet-stream.apib @@ -0,0 +1,14 @@ +FORMAT: 1A + +# Binary API + +## Resource [/binary] + +### Send Binary Data [POST] + ++ Request (application/octet-stream) + ++ Response 200 (application/json; charset=utf-8) + + Body + + {"test": "OK"} diff --git a/test/fixtures/request/application-octet-stream.yaml b/test/fixtures/request/application-octet-stream.yaml new file mode 100644 index 000000000..cbea921c4 --- /dev/null +++ b/test/fixtures/request/application-octet-stream.yaml @@ -0,0 +1,26 @@ +swagger: "2.0" +info: + version: "1.0" + title: Binary API +schemes: + - http +consumes: + - application/octet-stream +produces: + - application/json +paths: + /binary: + post: + parameters: + - name: binary + in: body + required: true + schema: + type: string + format: binary + responses: + 200: + description: 'Test OK' + examples: + application/json; charset=utf-8: + test: 'OK' diff --git a/test/fixtures/request/image-png-hooks.js b/test/fixtures/request/image-png-hooks.js new file mode 100644 index 000000000..971db5e42 --- /dev/null +++ b/test/fixtures/request/image-png-hooks.js @@ -0,0 +1,9 @@ +const hooks = require('hooks'); +const fs = require('fs'); +const path = require('path'); + +hooks.beforeEach((transaction, done) => { + const buffer = fs.readFileSync(path.join(__dirname, '../image.png')); + transaction.request.body = buffer.toString(); + done(); +}); diff --git a/test/fixtures/request/image-png.apib b/test/fixtures/request/image-png.apib new file mode 100644 index 000000000..d4e0b0400 --- /dev/null +++ b/test/fixtures/request/image-png.apib @@ -0,0 +1,15 @@ +FORMAT: 1A + +# Images API + +## Resource [/image.png] + +### Send an Image [PUT] + ++ Request (image/png) + ++ Response 200 (application/json; charset=utf-8) + + Body + + {"test": "OK"} + diff --git a/test/fixtures/request/image-png.yaml b/test/fixtures/request/image-png.yaml new file mode 100644 index 000000000..8d6765a7c --- /dev/null +++ b/test/fixtures/request/image-png.yaml @@ -0,0 +1,26 @@ +swagger: "2.0" +info: + version: "1.0" + title: Images API +schemes: + - http +consumes: + - image/png +produces: + - application/json +paths: + /image.png: + put: + parameters: + - name: binary + in: body + required: true + schema: + type: string + format: binary + responses: + 200: + description: 'Test OK' + examples: + application/json; charset=utf-8: + test: 'OK' diff --git a/test/fixtures/response/binary-invalid-utf8.apib b/test/fixtures/response/binary-invalid-utf8.apib index 4f4e9f362..1b6c1d284 100644 --- a/test/fixtures/response/binary-invalid-utf8.apib +++ b/test/fixtures/response/binary-invalid-utf8.apib @@ -1,6 +1,6 @@ FORMAT: 1A -# Images API +# Binary API ## Resource [/binary] diff --git a/test/fixtures/response/binary-invalid-utf8.yaml b/test/fixtures/response/binary-invalid-utf8.yaml index 4b57f1631..65bebe9c7 100644 --- a/test/fixtures/response/binary-invalid-utf8.yaml +++ b/test/fixtures/response/binary-invalid-utf8.yaml @@ -1,7 +1,7 @@ swagger: "2.0" info: version: "1.0" - title: Images API + title: Binary API schemes: - http produces: diff --git a/test/integration/request-test.js b/test/integration/request-test.js index 92f1064cf..71392fdeb 100644 --- a/test/integration/request-test.js +++ b/test/integration/request-test.js @@ -1,5 +1,7 @@ const bodyParser = require('body-parser'); const { assert } = require('chai'); +const fs = require('fs'); +const path = require('path'); const { runDreddWithServer, createServer } = require('./helpers'); const Dredd = require('../../src/dredd'); @@ -152,3 +154,93 @@ describe('Sending \'text/plain\' request', () => { assert.equal(runtimeInfo.dredd.stats.passes, 1); }); }); + +[ + { + name: 'API Blueprint', + path: './test/fixtures/request/application-octet-stream.apib' + }, + { + name: 'Swagger', + path: './test/fixtures/request/application-octet-stream.yaml' + } +].forEach(apiDescription => + describe(`Sending 'application/octet-stream' request described in ${apiDescription.name}`, () => { + let runtimeInfo; + const contentType = 'application/octet-stream'; + + before((done) => { + const app = createServer({ bodyParser: bodyParser.raw({ type: contentType }) }); + app.post('/binary', (req, res) => res.json({ test: 'OK' })); + + const dredd = new Dredd({ + options: { + path: apiDescription.path, + hookfiles: './test/fixtures/request/application-octet-stream-hooks.js' + } + }); + runDreddWithServer(dredd, app, (err, info) => { + runtimeInfo = info; + done(err); + }); + }); + + 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() + ) + ); + it('results in one passing test', () => { + assert.equal(runtimeInfo.dredd.stats.tests, 1); + assert.equal(runtimeInfo.dredd.stats.passes, 1); + }); + }) +); + +[ + { + name: 'API Blueprint', + path: './test/fixtures/request/image-png.apib' + }, + { + name: 'Swagger', + path: './test/fixtures/request/image-png.yaml' + } +].forEach(apiDescription => + describe(`Sending 'image/png' request described in ${apiDescription.name}`, () => { + let runtimeInfo; + const contentType = 'image/png'; + + before((done) => { + const app = createServer({ bodyParser: bodyParser.raw({ type: contentType }) }); + app.put('/image.png', (req, res) => res.json({ test: 'OK' })); + + const dredd = new Dredd({ + options: { + path: apiDescription.path, + hookfiles: './test/fixtures/request/image-png-hooks.js' + } + }); + runDreddWithServer(dredd, app, (err, info) => { + runtimeInfo = info; + done(err); + }); + }); + + 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() + ) + ); + it('results in one passing test', () => { + assert.equal(runtimeInfo.dredd.stats.tests, 1); + assert.equal(runtimeInfo.dredd.stats.passes, 1); + }); + }) +); From b0106d11cec363a0728ce777711a2696238e0877 Mon Sep 17 00:00:00 2001 From: Honza Javorek Date: Tue, 24 Jul 2018 16:30:06 +0200 Subject: [PATCH 04/15] test: mitigate an issue with Swagger schemas of binary format https://github.com/apiaryio/fury-adapter-swagger/issues/193 --- test/fixtures/response/binary-invalid-utf8.yaml | 3 --- test/fixtures/response/binary.yaml | 3 --- 2 files changed, 6 deletions(-) diff --git a/test/fixtures/response/binary-invalid-utf8.yaml b/test/fixtures/response/binary-invalid-utf8.yaml index 65bebe9c7..a1e5c2cd5 100644 --- a/test/fixtures/response/binary-invalid-utf8.yaml +++ b/test/fixtures/response/binary-invalid-utf8.yaml @@ -14,6 +14,3 @@ paths: description: Representation examples: "application/octet-stream": "" - schema: - type: string - format: binary diff --git a/test/fixtures/response/binary.yaml b/test/fixtures/response/binary.yaml index fd85ec81d..4b69f8ad8 100644 --- a/test/fixtures/response/binary.yaml +++ b/test/fixtures/response/binary.yaml @@ -14,6 +14,3 @@ paths: description: Representation examples: "image/png": "" - schema: - type: string - format: binary From 514d2ffe53c97d75448ad2e95f79952706aa9b10 Mon Sep 17 00:00:00 2001 From: Honza Javorek Date: Tue, 24 Jul 2018 17:07:59 +0200 Subject: [PATCH 05/15] docs: add docs on binary bodies --- docs/how-it-works.md | 2 +- docs/how-to-guides.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/docs/how-it-works.md b/docs/how-it-works.md index 93e283a77..d342f5945 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -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) ``` diff --git a/docs/how-to-guides.md b/docs/how-to-guides.md index b5d5a2268..e15da8430 100644 --- a/docs/how-to-guides.md +++ b/docs/how-to-guides.md @@ -523,6 +523,38 @@ 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). + +### 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). From d5414b39d7d45ec074578f64b262eedd9cd8dcfc Mon Sep 17 00:00:00 2001 From: Honza Javorek Date: Wed, 25 Jul 2018 16:35:07 +0200 Subject: [PATCH 06/15] 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 https://github.com/apiaryio/dredd/issues/617 Close https://github.com/apiaryio/dredd/issues/87 Close https://github.com/apiaryio/dredd/pull/836 --- src/transaction-runner.js | 50 +++++++++++++++--- test/fixtures/image.png | Bin 33 -> 15666 bytes .../request/application-octet-stream-hooks.js | 3 +- test/fixtures/request/image-png-hooks.js | 3 +- .../response/binary-assert-body-hooks.js | 5 +- .../response/binary-invalid-utf8-hooks.js | 8 --- .../response/binary-invalid-utf8.apib | 9 ---- .../response/binary-invalid-utf8.yaml | 16 ------ test/integration/request-test.js | 31 ++++++----- test/integration/response-test.js | 37 ------------- 10 files changed, 68 insertions(+), 94 deletions(-) mode change 120000 => 100644 test/fixtures/image.png delete mode 100644 test/fixtures/response/binary-invalid-utf8-hooks.js delete mode 100644 test/fixtures/response/binary-invalid-utf8.apib delete mode 100644 test/fixtures/response/binary-invalid-utf8.yaml diff --git a/src/transaction-runner.js b/src/transaction-runner.js index cbdf1cc71..31cadb7d9 100644 --- a/src/transaction-runner.js +++ b/src/transaction-runner.js @@ -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; } @@ -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) { @@ -656,6 +657,31 @@ 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); } @@ -663,7 +689,7 @@ the real body length is 0. Using 0 instead.\ 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; @@ -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 diff --git a/test/fixtures/image.png b/test/fixtures/image.png deleted file mode 120000 index a72888b11..000000000 --- a/test/fixtures/image.png +++ /dev/null @@ -1 +0,0 @@ -../../docs/_images/dredd-logo.png \ No newline at end of file diff --git a/test/fixtures/image.png b/test/fixtures/image.png new file mode 100644 index 0000000000000000000000000000000000000000..4b8991ea786be009b848971dabdf0739ddf0934a GIT binary patch literal 15666 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 { - 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(); }); diff --git a/test/fixtures/request/image-png-hooks.js b/test/fixtures/request/image-png-hooks.js index 971db5e42..a58146b94 100644 --- a/test/fixtures/request/image-png-hooks.js +++ b/test/fixtures/request/image-png-hooks.js @@ -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(); }); diff --git a/test/fixtures/response/binary-assert-body-hooks.js b/test/fixtures/response/binary-assert-body-hooks.js index d14184ef3..5ecb43267 100644 --- a/test/fixtures/response/binary-assert-body-hooks.js +++ b/test/fixtures/response/binary-assert-body-hooks.js @@ -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(); }); diff --git a/test/fixtures/response/binary-invalid-utf8-hooks.js b/test/fixtures/response/binary-invalid-utf8-hooks.js deleted file mode 100644 index 1fa8b4c14..000000000 --- a/test/fixtures/response/binary-invalid-utf8-hooks.js +++ /dev/null @@ -1,8 +0,0 @@ -const hooks = require('hooks'); -const { assert } = require('chai'); - -hooks.beforeEachValidation((transaction, done) => { - assert.equal(typeof transaction.real.body, 'string'); - assert.equal(transaction.real.body, Buffer.from([0xFF, 0xEF, 0xBF, 0xBE]).toString()); - done(); -}); diff --git a/test/fixtures/response/binary-invalid-utf8.apib b/test/fixtures/response/binary-invalid-utf8.apib deleted file mode 100644 index 1b6c1d284..000000000 --- a/test/fixtures/response/binary-invalid-utf8.apib +++ /dev/null @@ -1,9 +0,0 @@ -FORMAT: 1A - -# Binary API - -## Resource [/binary] - -### Retrieve Representation [GET] - -+ Response 200 (application/octet-stream) diff --git a/test/fixtures/response/binary-invalid-utf8.yaml b/test/fixtures/response/binary-invalid-utf8.yaml deleted file mode 100644 index a1e5c2cd5..000000000 --- a/test/fixtures/response/binary-invalid-utf8.yaml +++ /dev/null @@ -1,16 +0,0 @@ -swagger: "2.0" -info: - version: "1.0" - title: Binary API -schemes: - - http -produces: - - application/octet-stream -paths: - /binary: - get: - responses: - 200: - description: Representation - examples: - "application/octet-stream": "" diff --git a/test/integration/request-test.js b/test/integration/request-test.js index 71392fdeb..a58c4de0b 100644 --- a/test/integration/request-test.js +++ b/test/integration/request-test.js @@ -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; @@ -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; @@ -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', () => { @@ -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', () => { diff --git a/test/integration/response-test.js b/test/integration/response-test.js index a4656d0f1..4aea1cfb7 100644 --- a/test/integration/response-test.js +++ b/test/integration/response-test.js @@ -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 }) - ); - }) -); From 1965d9fc83795669cff3cbba86fef24aabfeb037 Mon Sep 17 00:00:00 2001 From: Honza Javorek Date: Thu, 26 Jul 2018 17:19:16 +0200 Subject: [PATCH 07/15] refactor: modularize and unit test the code --- src/performRequest.js | 161 ++++++++++++++ src/transaction-runner.js | 202 +++++------------- .../createTransactionRes-test.js | 48 +++++ .../performRequest/detectBodyEncoding-test.js | 25 +++ .../performRequest/getBodyAsBuffer-test.js | 51 +++++ .../normalizeBodyEncoding-test.js | 27 +++ .../performRequest/setContentLength-test.js | 123 +++++++++++ test/unit/transaction-runner-test.js | 146 ------------- 8 files changed, 491 insertions(+), 292 deletions(-) create mode 100644 src/performRequest.js create mode 100644 test/unit/performRequest/createTransactionRes-test.js create mode 100644 test/unit/performRequest/detectBodyEncoding-test.js create mode 100644 test/unit/performRequest/getBodyAsBuffer-test.js create mode 100644 test/unit/performRequest/normalizeBodyEncoding-test.js create mode 100644 test/unit/performRequest/setContentLength-test.js diff --git a/src/performRequest.js b/src/performRequest.js new file mode 100644 index 000000000..3c19d95a3 --- /dev/null +++ b/src/performRequest.js @@ -0,0 +1,161 @@ +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 = setContentLength(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'; + } + const lcEncoding = encoding.toLowerCase(); + if (lcEncoding === 'utf-8' || lcEncoding === 'utf8') { + return 'utf-8'; + } + if (lcEncoding === 'base64') { + return 'base64'; + } + 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 setContentLength(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'; +} + + +performRequest.normalizeBodyEncoding = normalizeBodyEncoding; +performRequest.getBodyAsBuffer = getBodyAsBuffer; +performRequest.setContentLength = setContentLength; +performRequest.createTransactionRes = createTransactionRes; +performRequest.detectBodyEncoding = detectBodyEncoding; + + +module.exports = performRequest; diff --git a/src/transaction-runner.js b/src/transaction-runner.js index 31cadb7d9..d9fff2bc8 100644 --- a/src/transaction-runner.js +++ b/src/transaction-runner.js @@ -1,10 +1,8 @@ const async = require('async'); -const caseless = require('caseless'); const chai = require('chai'); const clone = require('clone'); const gavel = require('gavel'); const os = require('os'); -const requestLib = require('request'); const url = require('url'); const { Pitboss } = require('pitboss-ng'); @@ -12,6 +10,8 @@ const addHooks = require('./add-hooks'); const logger = require('./logger'); const packageData = require('./../package.json'); const sortTransactions = require('./sort-transactions'); +const performRequest = require('./performRequest'); + function headersArrayToObject(arr) { return Array.from(arr).reduce((result, currentItem) => { @@ -549,24 +549,6 @@ Interface of the hooks functions will be unified soon across all hook functions: this.error = this.error || error; } - getRequestOptionsFromTransaction(transaction) { - const urlObject = { - protocol: transaction.protocol, - hostname: transaction.host, - port: transaction.port - }; - - const options = clone(this.configuration.http || {}); - options.uri = url.format(urlObject) + transaction.fullPath; - options.method = transaction.request.method; - options.headers = transaction.request.headers; - options.body = Buffer.from(transaction.request.body, transaction.request.bodyEncoding); - options.proxy = false; - options.followRedirect = false; - options.encoding = null; - return options; - } - // This is actually doing more some pre-flight and conditional skipping of // the transcation based on the configuration or hooks. TODO rename executeTransaction(transaction, hooks, callback) { @@ -622,106 +604,70 @@ Not performing HTTP request for '${transaction.name}'.\ this.performRequestAndValidate(test, transaction, hooks, callback); } - // 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.headers; - const body = Buffer.from(transaction.request.body, transaction.request.bodyEncoding); - - const contentLengthHeaderName = caseless(headers).has('Content-Length'); - if (contentLengthHeaderName) { - const contentLengthValue = parseInt(headers[contentLengthHeaderName], 10); - - if (body) { - const calculatedContentLengthValue = Buffer.byteLength(body); - if (contentLengthValue !== calculatedContentLengthValue) { - logger.warn(`\ -Specified Content-Length header is ${contentLengthValue}, but \ -the real body length is ${calculatedContentLengthValue}. Using \ -${calculatedContentLengthValue} instead.\ -`); - headers[contentLengthHeaderName] = calculatedContentLengthValue; - } - } else if (contentLengthValue !== 0) { - logger.warn(`\ -Specified Content-Length header is ${contentLengthValue}, but \ -the real body length is 0. Using 0 instead.\ -`); - headers[contentLengthHeaderName] = 0; - } - } else { - headers['Content-Length'] = body ? Buffer.byteLength(body) : 0; - } - } - // 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); - } + // TODO + // 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); + // Fixing API Blueprint 'multipart/form-data' bodies: + // https://github.com/apiaryio/api-blueprint/issues/401 + // + // Only bodies coming from the API Blueprint parser need fixing (not bodies + // set by Dredd users in hooks) and those can only be of the UTF-8 encoding. + // + // This is a workaround of a parser bug and as such it belongs to + // dredd-transactions, which should take care of the differences between + // formats. Having the fix here means 'before*' hooks are provided with + // incorrect request bodies. + // if (isMultipart(headers)) { + // body = Buffer.from(fixApiBlueprintMultipartBody(body.toString('utf-8'))); + // } + + // /** + // * Detects whether given request headers indicate the request body + // * is of the 'multipart/form-data' media type. + // * + // * @param {Object} headers + // */ + // function isMultipart(headers) { + // const contentType = caseless(headers).get('Content-Type'); + // return contentType ? contentType.includes('multipart') : false; + // } + + // /** + // * Finds newlines not preceeded by carriage returns and replaces them by + // * newlines preceeded by carriage returns. + // * + // * See https://github.com/apiaryio/api-blueprint/issues/401 + // * + // * @param {String} body + // */ + // function fixApiBlueprintMultipartBody(body) { + // return body.replace(/\r?\n/g, '\r\n'); + // } + + const uri = url.format({ + protocol: transaction.protocol, + hostname: transaction.host, + port: transaction.port + }) + transaction.fullPath; + const options = { http: this.configuration.http }; - const handleRequest = (err, res, bodyBytes) => { - if (err) { - logger.debug('Requesting tested server errored:', `${err}` || err.code); + performRequest(uri, transaction.request, options, (error, real) => { + if (error) { + logger.debug('Requesting tested server errored:', error); test.title = transaction.id; test.expected = transaction.expected; test.request = transaction.request; - this.emitError(err, test); + this.emitError(error, test); return callback(); } + transaction.real = real; - logger.verbose('Handling HTTP response from tested server'); - - // The data models as used here must conform to Gavel.js as defined in 'http-response.coffee' - transaction.real = { - statusCode: res.statusCode, - headers: res.headers - }; - - 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) { + if (!transaction.real.body && 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 // ensures Gavel does the validation. @@ -739,27 +685,7 @@ the real body length is 0. Using 0 instead.\ this.validateTransaction(test, transaction, callback); }); }); - }; - - try { - this.performRequest(requestOptions, handleRequest); - } catch (error) { - logger.debug('Requesting tested server errored:', error); - test.title = transaction.id; - test.expected = transaction.expected; - test.request = transaction.request; - this.emitError(error, test); - callback(); - } - } - - performRequest(options, callback) { - const protocol = options.uri.split(':')[0].toUpperCase(); - logger.verbose(`\ -About to perform an ${protocol} request to the server \ -under test: ${options.method} ${options.uri}\ -`); - requestLib(options, callback); + }); } validateTransaction(test, transaction, callback) { @@ -859,22 +785,6 @@ include a message body: https://tools.ietf.org/html/rfc7231#section-6.3\ }); }); } - - isMultipart(headers) { - const contentType = caseless(headers).get('Content-Type'); - if (contentType) { - return contentType.indexOf('multipart') > -1; - } - return false; - } - - // Finds newlines not preceeded by carriage returns and replaces them by - // newlines preceeded by carriage returns. - // - // See https://github.com/apiaryio/api-blueprint/issues/401 - fixApiBlueprintMultipartBody(body) { - return body.replace(/\r?\n/g, '\r\n'); - } } module.exports = TransactionRunner; diff --git a/test/unit/performRequest/createTransactionRes-test.js b/test/unit/performRequest/createTransactionRes-test.js new file mode 100644 index 000000000..580252d3d --- /dev/null +++ b/test/unit/performRequest/createTransactionRes-test.js @@ -0,0 +1,48 @@ +const { assert } = require('chai'); + +const { createTransactionRes } = require('../../../src/perform-request'); + + +describe('performRequest.createTransactionRes()', () => { + const res = { statusCode: 200, headers: {} }; + + it('sets the status code', () => + assert.deepEqual( + createTransactionRes(res), + { statusCode: 200, headers: {} } + ) + ); + it('copies the headers', () => { + const headers = { 'Content-Type': 'application/json' }; + const transactionRes = createTransactionRes({ statusCode: 200, headers }); + headers['X-Header'] = 'abcd'; + + assert.deepEqual( + transactionRes, + { statusCode: 200, headers: { 'Content-Type': 'application/json' } } + ); + }); + it('does not set empty body', () => + assert.deepEqual( + createTransactionRes(res, Buffer.from([])), + { statusCode: 200, headers: {} } + ) + ); + it('sets textual body as a string with UTF-8 encoding', () => + assert.deepEqual( + createTransactionRes(res, Buffer.from('řeřicha')), + { statusCode: 200, headers: {}, body: 'řeřicha', bodyEncoding: 'utf-8' } + ) + ); + it('sets binary body as a string with Base64 encoding', () => + assert.deepEqual( + createTransactionRes(res, Buffer.from([0xFF, 0xBE])), + { + statusCode: 200, + headers: {}, + body: Buffer.from([0xFF, 0xBE]).toString('base64'), + bodyEncoding: 'base64' + } + ) + ); +}); diff --git a/test/unit/performRequest/detectBodyEncoding-test.js b/test/unit/performRequest/detectBodyEncoding-test.js new file mode 100644 index 000000000..d5afb6d8a --- /dev/null +++ b/test/unit/performRequest/detectBodyEncoding-test.js @@ -0,0 +1,25 @@ +const { assert } = require('chai'); + +const { detectBodyEncoding } = require('../../../src/perform-request'); + + +describe('performRequest.detectBodyEncoding()', () => { + it('detects binary content as Base64', () => + assert.equal( + detectBodyEncoding(Buffer.from([0xFF, 0xEF, 0xBF, 0xBE])), + 'base64' + ) + ); + it('detects textual content as UTF-8', () => + assert.equal( + detectBodyEncoding(Buffer.from('řeřicha')), + 'utf-8' + ) + ); + it('detects no content as UTF-8', () => + assert.equal( + detectBodyEncoding(Buffer.from([])), + 'utf-8' + ) + ); +}); diff --git a/test/unit/performRequest/getBodyAsBuffer-test.js b/test/unit/performRequest/getBodyAsBuffer-test.js new file mode 100644 index 000000000..25992b1a0 --- /dev/null +++ b/test/unit/performRequest/getBodyAsBuffer-test.js @@ -0,0 +1,51 @@ +const { assert } = require('chai'); + +const { getBodyAsBuffer } = require('../../../src/perform-request'); + + +describe('performRequest.getBodyAsBuffer()', () => { + describe('when the body is a Buffer', () => { + it('returns the body unmodified', () => { + const body = Buffer.from([0xFF, 0xEF, 0xBF, 0xBE]); + assert.equal(getBodyAsBuffer(body), body); + }); + it('ignores encoding', () => { + const body = Buffer.from([0xFF, 0xEF, 0xBF, 0xBE]); + assert.equal(getBodyAsBuffer(body, 'utf-8'), body); + }); + }); + + [undefined, null, ''].forEach((body) => { + describe(`when the body is ${JSON.stringify(body)}`, () => { + it('returns empty Buffer without encoding', () => + assert.deepEqual(getBodyAsBuffer(body), Buffer.from([])) + ); + it('returns empty Buffer with encoding set to UTF-8', () => + assert.deepEqual(getBodyAsBuffer(body, 'utf-8'), Buffer.from([])) + ); + it('returns empty Buffer with encoding set to Base64', () => + assert.deepEqual(getBodyAsBuffer(body, 'base64'), Buffer.from([])) + ); + }); + }); + + describe('when the body is neither Buffer or string', () => { + it('gracefully stringifies the input', () => { + const body = new Error('Ouch!'); + assert.deepEqual(getBodyAsBuffer(body), Buffer.from('Error: Ouch!')); + }); + }); + + describe('when the body is a string', () => { + it('assumes UTF-8 without encoding', () => + assert.deepEqual(getBodyAsBuffer('abc'), Buffer.from('abc')) + ); + it('respects encoding set to UTF-8', () => + assert.deepEqual(getBodyAsBuffer('abc', 'utf-8'), Buffer.from('abc')) + ); + it('respects encoding set to Base64', () => { + const body = Buffer.from('abc').toString('base64'); + assert.deepEqual(getBodyAsBuffer(body, 'base64'), Buffer.from('abc')); + }); + }); +}); diff --git a/test/unit/performRequest/normalizeBodyEncoding-test.js b/test/unit/performRequest/normalizeBodyEncoding-test.js new file mode 100644 index 000000000..e58562021 --- /dev/null +++ b/test/unit/performRequest/normalizeBodyEncoding-test.js @@ -0,0 +1,27 @@ +const { assert } = require('chai'); + +const { normalizeBodyEncoding } = require('../../../src/perform-request'); + + +describe('performRequest.normalizeBodyEncoding()', () => { + ['utf-8', 'utf8', 'UTF-8', 'UTF8'].forEach(value => + it(`normalizes ${JSON.stringify(value)} to utf-8`, () => + assert.equal(normalizeBodyEncoding(value), 'utf-8') + ) + ); + ['base64', 'Base64'].forEach(value => + it(`normalizes ${JSON.stringify(value)} to base64`, () => + assert.equal(normalizeBodyEncoding(value), 'base64') + ) + ); + [undefined, null, '', false].forEach(value => + it(`defaults ${JSON.stringify(value)} to utf-8`, () => + assert.equal(normalizeBodyEncoding(value), 'utf-8') + ) + ); + it('throws an error on "latin2"', () => + assert.throws(() => { + normalizeBodyEncoding('latin2'); + }, /^unsupported encoding/i) + ); +}); diff --git a/test/unit/performRequest/setContentLength-test.js b/test/unit/performRequest/setContentLength-test.js new file mode 100644 index 000000000..f6c5525b3 --- /dev/null +++ b/test/unit/performRequest/setContentLength-test.js @@ -0,0 +1,123 @@ +const sinon = require('sinon'); +const { assert } = require('chai'); + +const { setContentLength } = require('../../../src/perform-request'); + + +describe('performRequest.setContentLength()', () => { + let headers; + + const logger = { warn: sinon.spy() }; + beforeEach(() => logger.warn.reset()); + + describe('when there is no body and no Content-Length', () => { + beforeEach(() => { + headers = setContentLength({}, Buffer.from(''), { logger }); + }); + + it('does not warn', () => + assert.isFalse(logger.warn.called) + ); + it('has the Content-Length header set to 0', () => + assert.deepPropertyVal(headers, 'Content-Length', '0') + ); + }); + + describe('when there is no body and the Content-Length is set to 0', () => { + beforeEach(() => { + headers = setContentLength({ + 'Content-Length': '0' + }, Buffer.from(''), { logger }); + }); + + it('does not warn', () => + assert.isFalse(logger.warn.called) + ); + it('has the Content-Length header set to 0', () => + assert.deepPropertyVal(headers, 'Content-Length', '0') + ); + }); + + describe('when there is body and the Content-Length is not set', () => { + beforeEach(() => { + headers = setContentLength({}, Buffer.from('abcd'), { logger }); + }); + + it('does not warn', () => + assert.isFalse(logger.warn.called) + ); + it('has the Content-Length header set to 4', () => + assert.deepPropertyVal(headers, 'Content-Length', '4') + ); + }); + + describe('when there is body and the Content-Length is correct', () => { + beforeEach(() => { + headers = setContentLength({ + 'Content-Length': '4' + }, Buffer.from('abcd'), { logger }); + }); + + it('does not warn', () => + assert.isFalse(logger.warn.called) + ); + it('has the Content-Length header set to 4', () => + assert.deepPropertyVal(headers, 'Content-Length', '4') + ); + }); + + describe('when there is no body and the Content-Length is wrong', () => { + beforeEach(() => { + headers = setContentLength({ + 'Content-Length': '42' + }, Buffer.from(''), { logger }); + }); + + it('warns about the discrepancy', () => + assert.match(logger.warn.lastCall.args[0], /but the real body length is/) + ); + it('has the Content-Length header set to 0', () => + assert.deepPropertyVal(headers, 'Content-Length', '0') + ); + }); + + describe('when there is body and the Content-Length is wrong', () => { + beforeEach(() => { + headers = setContentLength({ + 'Content-Length': '42' + }, Buffer.from('abcd'), { logger }); + }); + + it('warns about the discrepancy', () => + assert.match(logger.warn.lastCall.args[0], /but the real body length is/) + ); + it('has the Content-Length header set to 4', () => + assert.deepPropertyVal(headers, 'Content-Length', '4') + ); + }); + + describe('when the existing header name has unusual casing', () => { + beforeEach(() => { + headers = setContentLength({ + 'CoNtEnT-lEnGtH': '4' + }, Buffer.from('abcd'), { logger }); + }); + + it('has the CoNtEnT-lEnGtH header set to 4', () => + assert.deepEqual(headers, { 'CoNtEnT-lEnGtH': '4' }) + ); + }); + + describe('when there are modifications to the headers', () => { + const originalHeaders = {}; + + beforeEach(() => { + headers = setContentLength(originalHeaders, Buffer.from('abcd'), { logger }); + }); + + it('does not modify the original headers object', () => { + assert.deepEqual(originalHeaders, {}); + assert.deepEqual(headers, { 'Content-Length': '4' }); + }); + }); +}); diff --git a/test/unit/transaction-runner-test.js b/test/unit/transaction-runner-test.js index b4c100a14..4c084e3b4 100644 --- a/test/unit/transaction-runner-test.js +++ b/test/unit/transaction-runner-test.js @@ -1,5 +1,4 @@ const bodyParser = require('body-parser'); -const caseless = require('caseless'); const clone = require('clone'); const express = require('express'); const htmlStub = require('html'); @@ -731,151 +730,6 @@ describe('TransactionRunner', () => { }); }); - describe('setContentLength(transaction)', () => { - const bodyFixture = JSON.stringify({ - type: 'bulldozer', - name: 'willy', - id: '5229c6e8e4b0bd7dbb07e29c' - }, null, 2); - - const transactionFixture = { - name: 'Group Machine > Machine > Delete Message > Bogus example name', - id: 'POST /machines', - host: '127.0.0.1', - port: '3000', - request: { - headers: { - 'Content-Type': 'application/json', - 'User-Agent': 'Dredd/0.2.1 (Darwin 13.0.0; x64)' - }, - uri: '/machines', - method: 'POST' - }, - expected: { - headers: { 'content-type': 'application/json' - }, - status: '202', - body: bodyFixture - }, - origin: { - resourceGroupName: 'Group Machine', - resourceName: 'Machine', - actionName: 'Delete Message', - exampleName: 'Bogus example name' - }, - fullPath: '/machines', - protocol: 'http:' - }; - - const scenarios = [{ - name: 'Content-Length is not set, body is not set', - headers: {}, - body: '', - warning: false - }, - { - name: 'Content-Length is set, body is not set', - headers: { 'Content-Length': 0 }, - body: '', - warning: false - }, - { - name: 'Content-Length is not set, body is set', - headers: {}, - body: bodyFixture, - warning: false - }, - { - name: 'Content-Length is set, body is set', - headers: { 'Content-Length': bodyFixture.length }, - body: bodyFixture, - warning: false - }, - { - name: 'Content-Length has wrong value, body is not set', - headers: { 'Content-Length': bodyFixture.length }, - body: '', - warning: true - }, - { - name: 'Content-Length has wrong value, body is set', - headers: { 'Content-Length': 0 }, - body: bodyFixture, - warning: true - }, - { - name: 'case of the header name does not matter', - headers: { 'CoNtEnT-lEnGtH': bodyFixture.length }, - body: bodyFixture, - warning: false - } - ]; - - scenarios.forEach(scenario => - describe(scenario.name, () => { - const expectedContentLength = scenario.body.length; - let realRequest; - let loggerSpy; - - beforeEach((done) => { - loggerSpy = sinon.spy(loggerStub, 'warn'); - - transaction = clone(transactionFixture); - transaction.request.body = scenario.body; - - Object.keys(scenario.headers).forEach((name) => { - const value = scenario.headers[name]; - transaction.request.headers[name] = value; - }); - - nock('http://127.0.0.1:3000') - .post('/machines') - .reply(transaction.expected.status, function () { - realRequest = this.req; - return scenario.body; - }); - - runner.executeTransaction(transaction, done); - }); - afterEach(() => { - nock.cleanAll(); - loggerSpy.restore(); - }); - - if (scenario.warning) { - it('warns about discrepancy between provided Content-Length and real body length', () => { - assert.isTrue(loggerSpy.calledOnce); - - const message = loggerSpy.getCall(0).args[0].toLowerCase(); - assert.include(message, `the real body length is ${expectedContentLength}`); - assert.include(message, `using ${expectedContentLength} instead`); - }); - } else { - it('does not warn', () => assert.isFalse(loggerSpy.called)); - } - - context('the real request', () => { - it('has the Content-Length header', () => assert.isOk(caseless(realRequest.headers).has('Content-Length'))); - it(`has the Content-Length header set to ${expectedContentLength}`, () => - assert.equal( - caseless(realRequest.headers).get('Content-Length'), - expectedContentLength - ) - ); - }); - context('the transaction object', () => { - it('has the Content-Length header', () => assert.isOk(caseless(transaction.request.headers).has('Content-Length'))); - it(`has the Content-Length header set to ${expectedContentLength}`, () => - assert.equal( - caseless(transaction.request.headers).get('Content-Length'), - expectedContentLength - ) - ); - }); - }) - ); - }); - describe('exceuteAllTransactions(transactions, hooks, callback)', () => { runner = null; let hooks; From a281db9abc76ceb202dae70b78f2188def4430c8 Mon Sep 17 00:00:00 2001 From: Honza Javorek Date: Mon, 30 Jul 2018 16:50:23 +0200 Subject: [PATCH 08/15] test: unit test the performRequest itself --- src/transaction-runner.js | 42 ------ .../createTransactionRes-test.js | 2 +- .../performRequest/detectBodyEncoding-test.js | 2 +- .../performRequest/getBodyAsBuffer-test.js | 2 +- .../normalizeBodyEncoding-test.js | 2 +- .../performRequest/performRequest-test.js | 137 ++++++++++++++++++ .../performRequest/setContentLength-test.js | 2 +- 7 files changed, 142 insertions(+), 47 deletions(-) create mode 100644 test/unit/performRequest/performRequest-test.js diff --git a/src/transaction-runner.js b/src/transaction-runner.js index d9fff2bc8..8db8036c1 100644 --- a/src/transaction-runner.js +++ b/src/transaction-runner.js @@ -607,48 +607,6 @@ Not performing HTTP request for '${transaction.name}'.\ // An actual HTTP request, before validation hooks triggering // and the response validation is invoked here performRequestAndValidate(test, transaction, hooks, callback) { - // TODO - // if (transaction.request.body && this.isMultipart(transaction.request.headers)) { - // transaction.request.body = this.fixApiBlueprintMultipartBody(transaction.request.body); - // } - - // Fixing API Blueprint 'multipart/form-data' bodies: - // https://github.com/apiaryio/api-blueprint/issues/401 - // - // Only bodies coming from the API Blueprint parser need fixing (not bodies - // set by Dredd users in hooks) and those can only be of the UTF-8 encoding. - // - // This is a workaround of a parser bug and as such it belongs to - // dredd-transactions, which should take care of the differences between - // formats. Having the fix here means 'before*' hooks are provided with - // incorrect request bodies. - // if (isMultipart(headers)) { - // body = Buffer.from(fixApiBlueprintMultipartBody(body.toString('utf-8'))); - // } - - // /** - // * Detects whether given request headers indicate the request body - // * is of the 'multipart/form-data' media type. - // * - // * @param {Object} headers - // */ - // function isMultipart(headers) { - // const contentType = caseless(headers).get('Content-Type'); - // return contentType ? contentType.includes('multipart') : false; - // } - - // /** - // * Finds newlines not preceeded by carriage returns and replaces them by - // * newlines preceeded by carriage returns. - // * - // * See https://github.com/apiaryio/api-blueprint/issues/401 - // * - // * @param {String} body - // */ - // function fixApiBlueprintMultipartBody(body) { - // return body.replace(/\r?\n/g, '\r\n'); - // } - const uri = url.format({ protocol: transaction.protocol, hostname: transaction.host, diff --git a/test/unit/performRequest/createTransactionRes-test.js b/test/unit/performRequest/createTransactionRes-test.js index 580252d3d..6b110b930 100644 --- a/test/unit/performRequest/createTransactionRes-test.js +++ b/test/unit/performRequest/createTransactionRes-test.js @@ -1,6 +1,6 @@ const { assert } = require('chai'); -const { createTransactionRes } = require('../../../src/perform-request'); +const { createTransactionRes } = require('../../../src/performRequest'); describe('performRequest.createTransactionRes()', () => { diff --git a/test/unit/performRequest/detectBodyEncoding-test.js b/test/unit/performRequest/detectBodyEncoding-test.js index d5afb6d8a..88deb5052 100644 --- a/test/unit/performRequest/detectBodyEncoding-test.js +++ b/test/unit/performRequest/detectBodyEncoding-test.js @@ -1,6 +1,6 @@ const { assert } = require('chai'); -const { detectBodyEncoding } = require('../../../src/perform-request'); +const { detectBodyEncoding } = require('../../../src/performRequest'); describe('performRequest.detectBodyEncoding()', () => { diff --git a/test/unit/performRequest/getBodyAsBuffer-test.js b/test/unit/performRequest/getBodyAsBuffer-test.js index 25992b1a0..1b1ca742e 100644 --- a/test/unit/performRequest/getBodyAsBuffer-test.js +++ b/test/unit/performRequest/getBodyAsBuffer-test.js @@ -1,6 +1,6 @@ const { assert } = require('chai'); -const { getBodyAsBuffer } = require('../../../src/perform-request'); +const { getBodyAsBuffer } = require('../../../src/performRequest'); describe('performRequest.getBodyAsBuffer()', () => { diff --git a/test/unit/performRequest/normalizeBodyEncoding-test.js b/test/unit/performRequest/normalizeBodyEncoding-test.js index e58562021..0df0eee5b 100644 --- a/test/unit/performRequest/normalizeBodyEncoding-test.js +++ b/test/unit/performRequest/normalizeBodyEncoding-test.js @@ -1,6 +1,6 @@ const { assert } = require('chai'); -const { normalizeBodyEncoding } = require('../../../src/perform-request'); +const { normalizeBodyEncoding } = require('../../../src/performRequest'); describe('performRequest.normalizeBodyEncoding()', () => { diff --git a/test/unit/performRequest/performRequest-test.js b/test/unit/performRequest/performRequest-test.js new file mode 100644 index 000000000..a73bf372b --- /dev/null +++ b/test/unit/performRequest/performRequest-test.js @@ -0,0 +1,137 @@ +const sinon = require('sinon'); +const { assert } = require('chai'); + +const performRequest = require('../../../src/performRequest'); + + +describe('performRequest()', () => { + const uri = 'http://example.com/42'; + const uriS = 'https://example.com/42'; + const transactionReq = { + method: 'POST', + headers: { 'Content-Type': 'text/plain' }, + body: 'Hello' + }; + const res = { statusCode: 200, headers: { 'Content-Type': 'text/plain' } }; + const request = sinon.stub().callsArgWithAsync(1, null, res, Buffer.from('Bye')); + const logger = { debug: sinon.spy() }; + + beforeEach(() => { logger.debug.reset(); }); + + it('does not modify the original HTTP options object', (done) => { + const httpOptions = { json: true }; + performRequest(uri, transactionReq, { http: httpOptions, request }, () => { + assert.deepEqual(httpOptions, { json: true }); + done(); + }); + }); + it('does not allow to override the hardcoded HTTP options', (done) => { + performRequest(uri, transactionReq, { http: { proxy: true }, request }, () => { + assert.isFalse(request.firstCall.args[0].proxy); + done(); + }); + }); + it('forbids the HTTP client library to respect proxy settings', (done) => { + performRequest(uri, transactionReq, { request }, () => { + assert.isFalse(request.firstCall.args[0].proxy); + done(); + }); + }); + it('forbids the HTTP client library to follow redirects', (done) => { + performRequest(uri, transactionReq, { request }, () => { + assert.isFalse(request.firstCall.args[0].followRedirect); + done(); + }); + }); + it('propagates the HTTP method to the HTTP client library', (done) => { + performRequest(uri, transactionReq, { request }, () => { + assert.equal(request.firstCall.args[0].method, transactionReq.method); + done(); + }); + }); + it('propagates the URI to the HTTP client library', (done) => { + performRequest(uri, transactionReq, { request }, () => { + assert.equal(request.firstCall.args[0].uri, uri); + done(); + }); + }); + it('propagates the HTTP request body as a Buffer', (done) => { + performRequest(uri, transactionReq, { request }, () => { + assert.deepEqual(request.firstCall.args[0].body, Buffer.from('Hello')); + done(); + }); + }); + it('handles exceptions when preparing the HTTP request body', (done) => { + const invalidTransactionReq = Object.assign( + { bodyEncoding: 'latin2' }, + transactionReq + ); + performRequest(uri, invalidTransactionReq, { request }, (err) => { + assert.instanceOf(err, Error); + done(); + }); + }); + it('logs before performing the HTTP request', (done) => { + performRequest(uri, transactionReq, { request, logger }, () => { + assert.equal( + logger.debug.firstCall.args[0], + `Performing HTTP request to the server under test: POST ${uri}` + ); + done(); + }); + }); + it('logs before performing the HTTPS request', (done) => { + performRequest(uriS, transactionReq, { request, logger }, () => { + assert.equal( + logger.debug.firstCall.args[0], + `Performing HTTPS request to the server under test: POST ${uriS}` + ); + done(); + }); + }); + it('logs on receiving the HTTP response', (done) => { + performRequest(uri, transactionReq, { request, logger }, () => { + assert.equal( + logger.debug.lastCall.args[0], + 'Handling HTTP response from the server under test' + ); + done(); + }); + }); + it('logs on receiving the HTTPS response', (done) => { + performRequest(uriS, transactionReq, { request, logger }, () => { + assert.equal( + logger.debug.lastCall.args[0], + 'Handling HTTPS response from the server under test' + ); + done(); + }); + }); + it('handles exceptions when requesting the server under test', (done) => { + const error = new Error('Ouch!'); + const invalidRequest = sinon.stub().throws(error); + performRequest(uri, transactionReq, { request: invalidRequest }, (err) => { + assert.deepEqual(err, error); + done(); + }); + }); + it('handles errors when requesting the server under test', (done) => { + const error = new Error('Ouch!'); + const invalidRequest = sinon.stub().callsArgWithAsync(1, error); + performRequest(uri, transactionReq, { request: invalidRequest }, (err) => { + assert.deepEqual(err, error); + done(); + }); + }); + it('provides the real HTTP response object', (done) => { + performRequest(uri, transactionReq, { request }, (err, real) => { + assert.deepEqual(real, { + statusCode: 200, + headers: { 'Content-Type': 'text/plain' }, + body: 'Bye', + bodyEncoding: 'utf-8' + }); + done(); + }); + }); +}); diff --git a/test/unit/performRequest/setContentLength-test.js b/test/unit/performRequest/setContentLength-test.js index f6c5525b3..0227e029f 100644 --- a/test/unit/performRequest/setContentLength-test.js +++ b/test/unit/performRequest/setContentLength-test.js @@ -1,7 +1,7 @@ const sinon = require('sinon'); const { assert } = require('chai'); -const { setContentLength } = require('../../../src/perform-request'); +const { setContentLength } = require('../../../src/performRequest'); describe('performRequest.setContentLength()', () => { From c2ca0e074c0120515e376f228cd9cad3e975e227 Mon Sep 17 00:00:00 2001 From: Honza Javorek Date: Mon, 30 Jul 2018 18:11:26 +0200 Subject: [PATCH 09/15] docs: update docs with bodyEncoding and binary request bodies --- docs/data-structures.md | 6 ++++++ docs/how-to-guides.md | 28 ++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/data-structures.md b/docs/data-structures.md index d2f0d899c..524d38aa2 100644 --- a/docs/data-structures.md +++ b/docs/data-structures.md @@ -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) @@ -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 diff --git a/docs/how-to-guides.md b/docs/how-to-guides.md index e15da8430..ade55a791 100644 --- a/docs/how-to-guides.md +++ b/docs/how-to-guides.md @@ -527,13 +527,37 @@ Most of the authentication schemes use HTTP header for carrying the authenticati 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). -### API Blueprint +### 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 +#### Swagger ```yaml :[Swagger example](../test/fixtures/response/binary.yaml) From 32aa8154206d34c6f66538e1afd5edb72c2dc2d3 Mon Sep 17 00:00:00 2001 From: Honza Javorek Date: Tue, 31 Jul 2018 11:27:40 +0200 Subject: [PATCH 10/15] fix: upgrade dredd-transactions Normalize 'multipart/form-data' bodies for both API Blueprint and Swagger, avoid ignoring falsy values at multiple places (URI parameters, x-example) --- package-lock.json | 12 ++++++------ package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 781ab2138..f016f2bfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2624,9 +2624,9 @@ "integrity": "sha512-bve7maXvfKW+vcsRpP8gzEDzkTg8O6AoCGvi/52pnllzhl/nmex8XLrHOUEQ42Z8GshcyftvG+E4s5vcd/qo0Q==" }, "dredd-transactions": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/dredd-transactions/-/dredd-transactions-6.1.3.tgz", - "integrity": "sha512-cNuoU83aYpFd47dwcUaqqaZo8cEU7C/sDlJaLERxU27XsI1YoqDrh+5p9EmzsPIzRu836GCdhESm0RBVbfHMxA==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/dredd-transactions/-/dredd-transactions-6.1.5.tgz", + "integrity": "sha512-DWiqzXx5nAqBYSf/tv4ha0LQoFOTknhgkI53tgSV/xIUnpz+S3ZFu7rgkTVXBrHpcfsg87B0aieIfE95mCki0w==", "requires": { "clone": "2.1.1", "fury": "3.0.0-beta.7", @@ -7629,9 +7629,9 @@ }, "dependencies": { "escodegen": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.10.0.tgz", - "integrity": "sha512-fjUOf8johsv23WuIKdNQU4P9t9jhQ4Qzx6pC2uW890OloK3Zs1ZAoCNpg/2larNF501jLl3UNy0kIRcF6VI22g==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.0.tgz", + "integrity": "sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw==", "requires": { "esprima": "^3.1.3", "estraverse": "^4.2.0", diff --git a/package.json b/package.json index b42ef84c1..16c113f53 100644 --- a/package.json +++ b/package.json @@ -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", From 3d396f6240bccb11ecb80ae4e27d315a30ec057e Mon Sep 17 00:00:00 2001 From: Honza Javorek Date: Tue, 31 Jul 2018 15:09:02 +0200 Subject: [PATCH 11/15] test: update unit tests to new architecture --- test/unit/transaction-runner-test.js | 162 +-------------------------- 1 file changed, 3 insertions(+), 159 deletions(-) diff --git a/test/unit/transaction-runner-test.js b/test/unit/transaction-runner-test.js index 4c084e3b4..ee3041a4a 100644 --- a/test/unit/transaction-runner-test.js +++ b/test/unit/transaction-runner-test.js @@ -427,18 +427,18 @@ describe('TransactionRunner', () => { beforeEach(() => { configuration.options['dry-run'] = true; runner = new Runner(configuration); - sinon.spy(runner, 'performRequest'); + sinon.spy(runner, 'performRequestAndValidate'); }); afterEach(() => { configuration.options['dry-run'] = false; - runner.performRequest.restore(); + runner.performRequestAndValidate.restore(); }); it('should skip the tests', done => runner.executeTransaction(transaction, () => { - assert.isOk(runner.performRequest.notCalled); + assert.isOk(runner.performRequestAndValidate.notCalled); done(); }) ); @@ -1240,162 +1240,6 @@ describe('TransactionRunner', () => { }); }); - describe('executeTransaction(transaction, callback) multipart', () => { - let multiPartTransaction; - let notMultiPartTransaction; - runner = null; - beforeEach(() => { - runner = new Runner(configuration); - multiPartTransaction = { - name: 'Group Machine > Machine > Post Message> Bogus example name', - id: 'POST /machines/message', - host: '127.0.0.1', - port: '3000', - request: { - body: '\n--BOUNDARY \ncontent-disposition: form-data; name="mess12"\n\n{"message":"mess1"}\n--BOUNDARY\n\nContent-Disposition: form-data; name="mess2"\n\n{"message":"mess1"}\n--BOUNDARY--', - headers: { - 'Content-Type': 'multipart/form-data; boundary=BOUNDARY', - 'User-Agent': 'Dredd/0.2.1 (Darwin 13.0.0; x64)', - 'Content-Length': 180 - }, - uri: '/machines/message', - method: 'POST' - }, - expected: { - headers: { - 'content-type': 'text/htm' - } - }, - body: '', - status: '204', - origin: { - resourceGroupName: 'Group Machine', - resourceName: 'Machine', - actionName: 'Post Message', - exampleName: 'Bogus example name' - }, - fullPath: '/machines/message', - protocol: 'http:' - }; - - notMultiPartTransaction = { - name: 'Group Machine > Machine > Post Message> Bogus example name', - id: 'POST /machines/message', - host: '127.0.0.1', - port: '3000', - request: { - body: '\n--BOUNDARY \ncontent-disposition: form-data; name="mess12"\n\n{"message":"mess1"}\n--BOUNDARY\n\nContent-Disposition: form-data; name="mess2"\n\n{"message":"mess1"}\n--BOUNDARY--', - headers: { - 'Content-Type': 'text/plain', - 'User-Agent': 'Dredd/0.2.1 (Darwin 13.0.0; x64)', - 'Content-Length': 180 - }, - uri: '/machines/message', - method: 'POST' - }, - expected: { - headers: { - 'content-type': 'text/htm' - } - }, - body: '', - status: '204', - origin: { - resourceGroupName: 'Group Machine', - resourceName: 'Machine', - actionName: 'Post Message', - exampleName: 'Bogus example name' - }, - fullPath: '/machines/message', - protocol: 'http:' - }; - }); - - describe('when multipart header in request', () => { - const parsedBody = '\r\n--BOUNDARY \r\ncontent-disposition: form-data; name="mess12"\r\n\r\n{"message":"mess1"}\r\n--BOUNDARY\r\n\r\nContent-Disposition: form-data; name="mess2"\r\n\r\n{"message":"mess1"}\r\n--BOUNDARY--'; - beforeEach(() => { - server = nock('http://127.0.0.1:3000') - .post('/machines/message') - .reply(204); - configuration.server = 'http://127.0.0.1:3000'; - }); - - afterEach(() => nock.cleanAll()); - - it('should replace line feed in body', done => - runner.executeTransaction(multiPartTransaction, () => { - assert.isOk(server.isDone()); - assert.equal(multiPartTransaction.request.body, parsedBody, 'Body'); - assert.include(multiPartTransaction.request.body, '\r\n'); - done(); - }) - ); - }); - - describe('when multipart header in request is with lowercase key', () => { - const parsedBody = '\r\n--BOUNDARY \r\ncontent-disposition: form-data; name="mess12"\r\n\r\n{"message":"mess1"}\r\n--BOUNDARY\r\n\r\nContent-Disposition: form-data; name="mess2"\r\n\r\n{"message":"mess1"}\r\n--BOUNDARY--'; - beforeEach(() => { - server = nock('http://127.0.0.1:3000') - .post('/machines/message') - .reply(204); - configuration.server = 'http://127.0.0.1:3000'; - - delete multiPartTransaction.request.headers['Content-Type']; - multiPartTransaction.request.headers['content-type'] = 'multipart/form-data; boundary=BOUNDARY'; - }); - - afterEach(() => nock.cleanAll()); - - it('should replace line feed in body', done => - runner.executeTransaction(multiPartTransaction, () => { - assert.isOk(server.isDone()); - assert.equal(multiPartTransaction.request.body, parsedBody, 'Body'); - assert.include(multiPartTransaction.request.body, '\r\n'); - done(); - }) - ); - }); - - describe('when multipart header in request, but body already has some CR (added in hooks e.g.s)', () => { - beforeEach(() => { - server = nock('http://127.0.0.1:3000') - .post('/machines/message') - .reply(204); - configuration.server = 'http://127.0.0.1:3000'; - multiPartTransaction.request.body = '\r\n--BOUNDARY \r\ncontent-disposition: form-data; name="mess12"\r\n\r\n{"message":"mess1"}\r\n--BOUNDARY\r\n\r\nContent-Disposition: form-data; name="mess2"\r\n\r\n{"message":"mess1"}\r\n--BOUNDARY--'; - }); - - afterEach(() => nock.cleanAll()); - - it('should not add CR again', done => - runner.executeTransaction(multiPartTransaction, () => { - assert.isOk(server.isDone()); - assert.notInclude(multiPartTransaction.request.body, '\r\r'); - done(); - }) - ); - }); - - describe('when multipart header is not in request', () => { - beforeEach(() => { - server = nock('http://127.0.0.1:3000') - .post('/machines/message') - .reply(204); - configuration.server = 'http://127.0.0.1:3000'; - }); - - afterEach(() => nock.cleanAll()); - - it('should not include any line-feed in body', done => - runner.executeTransaction(notMultiPartTransaction, () => { - assert.isOk(server.isDone()); - assert.notInclude(multiPartTransaction.request.body, '\r\n'); - done(); - }) - ); - }); - }); - describe('#executeAllTransactions', () => { configuration = { server: 'http://127.0.0.1:3000', From be53b7dae2f8b15b1b078245651f56bc8d61b70c Mon Sep 17 00:00:00 2001 From: Honza Javorek Date: Thu, 2 Aug 2018 14:57:01 +0200 Subject: [PATCH 12/15] chore: ignore docs build when linting JS code --- .eslintignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintignore b/.eslintignore index 86e9501ee..5675bac71 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,2 @@ lib - +docs/_build From 1f6c251ef051da1e044608469fcc89d11b944bbd Mon Sep 17 00:00:00 2001 From: Honza Javorek Date: Thu, 2 Aug 2018 14:57:26 +0200 Subject: [PATCH 13/15] refactor: make the function name clearer --- src/performRequest.js | 6 +++--- ...s => normalizeContentLengthHeader-test.js} | 20 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) rename test/unit/performRequest/{setContentLength-test.js => normalizeContentLengthHeader-test.js} (82%) diff --git a/src/performRequest.js b/src/performRequest.js index 3c19d95a3..5533195b1 100644 --- a/src/performRequest.js +++ b/src/performRequest.js @@ -34,7 +34,7 @@ function performRequest(uri, transactionReq, options, callback) { try { httpOptions.body = getBodyAsBuffer(transactionReq.body, transactionReq.bodyEncoding); - httpOptions.headers = setContentLength(transactionReq.headers, httpOptions.body); + httpOptions.headers = normalizeContentLengthHeader(transactionReq.headers, httpOptions.body); const protocol = httpOptions.uri.split(':')[0].toUpperCase(); logger.debug(`Performing ${protocol} request to the server under test: ` @@ -98,7 +98,7 @@ function normalizeBodyEncoding(encoding) { * @param {Object} [options] * @param {Object} [options.logger] Custom logger */ -function setContentLength(headers, body, options = {}) { +function normalizeContentLengthHeader(headers, body, options = {}) { const logger = options.logger || defaultLogger; const modifiedHeaders = Object.assign({}, headers); @@ -153,7 +153,7 @@ function detectBodyEncoding(body) { performRequest.normalizeBodyEncoding = normalizeBodyEncoding; performRequest.getBodyAsBuffer = getBodyAsBuffer; -performRequest.setContentLength = setContentLength; +performRequest.normalizeContentLengthHeader = normalizeContentLengthHeader; performRequest.createTransactionRes = createTransactionRes; performRequest.detectBodyEncoding = detectBodyEncoding; diff --git a/test/unit/performRequest/setContentLength-test.js b/test/unit/performRequest/normalizeContentLengthHeader-test.js similarity index 82% rename from test/unit/performRequest/setContentLength-test.js rename to test/unit/performRequest/normalizeContentLengthHeader-test.js index 0227e029f..ba74adfc2 100644 --- a/test/unit/performRequest/setContentLength-test.js +++ b/test/unit/performRequest/normalizeContentLengthHeader-test.js @@ -1,10 +1,10 @@ const sinon = require('sinon'); const { assert } = require('chai'); -const { setContentLength } = require('../../../src/performRequest'); +const { normalizeContentLengthHeader } = require('../../../src/performRequest'); -describe('performRequest.setContentLength()', () => { +describe('performRequest.normalizeContentLengthHeader()', () => { let headers; const logger = { warn: sinon.spy() }; @@ -12,7 +12,7 @@ describe('performRequest.setContentLength()', () => { describe('when there is no body and no Content-Length', () => { beforeEach(() => { - headers = setContentLength({}, Buffer.from(''), { logger }); + headers = normalizeContentLengthHeader({}, Buffer.from(''), { logger }); }); it('does not warn', () => @@ -25,7 +25,7 @@ describe('performRequest.setContentLength()', () => { describe('when there is no body and the Content-Length is set to 0', () => { beforeEach(() => { - headers = setContentLength({ + headers = normalizeContentLengthHeader({ 'Content-Length': '0' }, Buffer.from(''), { logger }); }); @@ -40,7 +40,7 @@ describe('performRequest.setContentLength()', () => { describe('when there is body and the Content-Length is not set', () => { beforeEach(() => { - headers = setContentLength({}, Buffer.from('abcd'), { logger }); + headers = normalizeContentLengthHeader({}, Buffer.from('abcd'), { logger }); }); it('does not warn', () => @@ -53,7 +53,7 @@ describe('performRequest.setContentLength()', () => { describe('when there is body and the Content-Length is correct', () => { beforeEach(() => { - headers = setContentLength({ + headers = normalizeContentLengthHeader({ 'Content-Length': '4' }, Buffer.from('abcd'), { logger }); }); @@ -68,7 +68,7 @@ describe('performRequest.setContentLength()', () => { describe('when there is no body and the Content-Length is wrong', () => { beforeEach(() => { - headers = setContentLength({ + headers = normalizeContentLengthHeader({ 'Content-Length': '42' }, Buffer.from(''), { logger }); }); @@ -83,7 +83,7 @@ describe('performRequest.setContentLength()', () => { describe('when there is body and the Content-Length is wrong', () => { beforeEach(() => { - headers = setContentLength({ + headers = normalizeContentLengthHeader({ 'Content-Length': '42' }, Buffer.from('abcd'), { logger }); }); @@ -98,7 +98,7 @@ describe('performRequest.setContentLength()', () => { describe('when the existing header name has unusual casing', () => { beforeEach(() => { - headers = setContentLength({ + headers = normalizeContentLengthHeader({ 'CoNtEnT-lEnGtH': '4' }, Buffer.from('abcd'), { logger }); }); @@ -112,7 +112,7 @@ describe('performRequest.setContentLength()', () => { const originalHeaders = {}; beforeEach(() => { - headers = setContentLength(originalHeaders, Buffer.from('abcd'), { logger }); + headers = normalizeContentLengthHeader(originalHeaders, Buffer.from('abcd'), { logger }); }); it('does not modify the original headers object', () => { From ae586827b84c73fc9984b2d1041dfc97ab9370e1 Mon Sep 17 00:00:00 2001 From: Honza Javorek Date: Thu, 2 Aug 2018 15:34:41 +0200 Subject: [PATCH 14/15] style: use switch --- src/performRequest.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/performRequest.js b/src/performRequest.js index 5533195b1..eada4bdb5 100644 --- a/src/performRequest.js +++ b/src/performRequest.js @@ -74,18 +74,18 @@ function getBodyAsBuffer(body, encoding) { * @param {string} encoding */ function normalizeBodyEncoding(encoding) { - if (!encoding) { - return 'utf-8'; + 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)'); } - const lcEncoding = encoding.toLowerCase(); - if (lcEncoding === 'utf-8' || lcEncoding === 'utf8') { - return 'utf-8'; - } - if (lcEncoding === 'base64') { - return 'base64'; - } - throw new Error(`Unsupported encoding: '${encoding}' (only UTF-8 and ` - + 'Base64 are supported)'); } From 59a383b78003f87c9c407bf2607cf3fe7e9c29f0 Mon Sep 17 00:00:00 2001 From: Honza Javorek Date: Thu, 2 Aug 2018 15:38:50 +0200 Subject: [PATCH 15/15] style: use underscores to declare private stuff in public API --- src/performRequest.js | 11 ++++++----- test/unit/performRequest/createTransactionRes-test.js | 4 +++- test/unit/performRequest/detectBodyEncoding-test.js | 4 +++- test/unit/performRequest/getBodyAsBuffer-test.js | 4 +++- .../unit/performRequest/normalizeBodyEncoding-test.js | 4 +++- .../normalizeContentLengthHeader-test.js | 4 +++- 6 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/performRequest.js b/src/performRequest.js index eada4bdb5..a4f7af8b6 100644 --- a/src/performRequest.js +++ b/src/performRequest.js @@ -151,11 +151,12 @@ function detectBodyEncoding(body) { } -performRequest.normalizeBodyEncoding = normalizeBodyEncoding; -performRequest.getBodyAsBuffer = getBodyAsBuffer; -performRequest.normalizeContentLengthHeader = normalizeContentLengthHeader; -performRequest.createTransactionRes = createTransactionRes; -performRequest.detectBodyEncoding = detectBodyEncoding; +// 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; diff --git a/test/unit/performRequest/createTransactionRes-test.js b/test/unit/performRequest/createTransactionRes-test.js index 6b110b930..8c03c7d9c 100644 --- a/test/unit/performRequest/createTransactionRes-test.js +++ b/test/unit/performRequest/createTransactionRes-test.js @@ -1,6 +1,8 @@ const { assert } = require('chai'); -const { createTransactionRes } = require('../../../src/performRequest'); +const { + _createTransactionRes: createTransactionRes +} = require('../../../src/performRequest'); describe('performRequest.createTransactionRes()', () => { diff --git a/test/unit/performRequest/detectBodyEncoding-test.js b/test/unit/performRequest/detectBodyEncoding-test.js index 88deb5052..06bd10517 100644 --- a/test/unit/performRequest/detectBodyEncoding-test.js +++ b/test/unit/performRequest/detectBodyEncoding-test.js @@ -1,6 +1,8 @@ const { assert } = require('chai'); -const { detectBodyEncoding } = require('../../../src/performRequest'); +const { + _detectBodyEncoding: detectBodyEncoding +} = require('../../../src/performRequest'); describe('performRequest.detectBodyEncoding()', () => { diff --git a/test/unit/performRequest/getBodyAsBuffer-test.js b/test/unit/performRequest/getBodyAsBuffer-test.js index 1b1ca742e..c4341d2f1 100644 --- a/test/unit/performRequest/getBodyAsBuffer-test.js +++ b/test/unit/performRequest/getBodyAsBuffer-test.js @@ -1,6 +1,8 @@ const { assert } = require('chai'); -const { getBodyAsBuffer } = require('../../../src/performRequest'); +const { + _getBodyAsBuffer: getBodyAsBuffer +} = require('../../../src/performRequest'); describe('performRequest.getBodyAsBuffer()', () => { diff --git a/test/unit/performRequest/normalizeBodyEncoding-test.js b/test/unit/performRequest/normalizeBodyEncoding-test.js index 0df0eee5b..89f0f2257 100644 --- a/test/unit/performRequest/normalizeBodyEncoding-test.js +++ b/test/unit/performRequest/normalizeBodyEncoding-test.js @@ -1,6 +1,8 @@ const { assert } = require('chai'); -const { normalizeBodyEncoding } = require('../../../src/performRequest'); +const { + _normalizeBodyEncoding: normalizeBodyEncoding +} = require('../../../src/performRequest'); describe('performRequest.normalizeBodyEncoding()', () => { diff --git a/test/unit/performRequest/normalizeContentLengthHeader-test.js b/test/unit/performRequest/normalizeContentLengthHeader-test.js index ba74adfc2..a5fb7e910 100644 --- a/test/unit/performRequest/normalizeContentLengthHeader-test.js +++ b/test/unit/performRequest/normalizeContentLengthHeader-test.js @@ -1,7 +1,9 @@ const sinon = require('sinon'); const { assert } = require('chai'); -const { normalizeContentLengthHeader } = require('../../../src/performRequest'); +const { + _normalizeContentLengthHeader: normalizeContentLengthHeader +} = require('../../../src/performRequest'); describe('performRequest.normalizeContentLengthHeader()', () => {