From 1990dd0bcf681de6a233a39280ce898c120d96fa Mon Sep 17 00:00:00 2001 From: Tobias Mansfield-Williams Date: Mon, 23 Oct 2017 11:09:41 +0200 Subject: [PATCH] Fix binary response (#15) Fixes sinonjs/sinon#1570 Supports raw binary responses Binary strings will be encoded using utf-8, instead of being returned unchanged in the response body. This commit allows binary response data to be passed as an array buffer directly to nise, while keeping the existing behavior of encoding normal strings in utf-8 as discussed in https://github.com/sinonjs/sinon/issues/875#issuecomment-152981521. --- README.md | 2 +- lib/fake-server/index.js | 12 +++++++-- lib/fake-server/index.test.js | 50 ++++++++++++++++++++++++++++++----- lib/fake-xhr/index.js | 28 ++++++++++++++++---- lib/fake-xhr/index.test.js | 37 ++++++++++++++++++++------ package.json | 3 ++- 6 files changed, 109 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index ef8bd95..e26f071 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,7 @@ Causes the server to respond to any request not matched by another response with `response` can be one of three things: -1. A `String` representing the response body +1. A `String` or `ArrayBuffer` representing the response body 2. An `Array` with status, headers and response body, e.g. `[200, { "Content-Type": "text/html", "Content-Length": 2 }, "OK"]` 3. A `Function`. diff --git a/lib/fake-server/index.js b/lib/fake-server/index.js index 219bbe9..6aa7957 100644 --- a/lib/fake-server/index.js +++ b/lib/fake-server/index.js @@ -6,6 +6,8 @@ var format = require("./format"); var configureLogError = require("../configure-logger"); var pathToRegexp = require("path-to-regexp"); +var supportsArrayBuffer = typeof ArrayBuffer !== "undefined"; + function responseArray(handler) { var response = handler; @@ -14,8 +16,14 @@ function responseArray(handler) { } if (typeof response[2] !== "string") { - throw new TypeError("Fake server response body should be string, but was " + - typeof response[2]); + if (!supportsArrayBuffer) { + throw new TypeError("Fake server response body should be a string, but was " + + typeof response[2]); + } + else if (!(response[2] instanceof ArrayBuffer)) { + throw new TypeError("Fake server response body should be a string or ArrayBuffer, but was " + + typeof response[2]); + } } return response; diff --git a/lib/fake-server/index.test.js b/lib/fake-server/index.test.js index 45f6ef3..eb15858 100644 --- a/lib/fake-server/index.test.js +++ b/lib/fake-server/index.test.js @@ -11,6 +11,8 @@ var FakeXMLHttpRequest = fakeXhr.FakeXMLHttpRequest; var assert = referee.assert; var refute = referee.refute; +var supportsArrayBuffer = typeof ArrayBuffer !== "undefined"; + describe("sinonFakeServer", function () { beforeEach(function () { global.DOMParser = DOMParser; @@ -200,6 +202,12 @@ describe("sinonFakeServer", function () { this.getRootAsync.send(); sinon.spy(this.getRootAsync, "respond"); + this.getRootAsyncArrayBuffer = new FakeXMLHttpRequest(); + this.getRootAsyncArrayBuffer.responseType = "arraybuffer"; + this.getRootAsyncArrayBuffer.open("GET", "/", true); + this.getRootAsyncArrayBuffer.send(); + sinon.spy(this.getRootAsyncArrayBuffer, "respond"); + this.postRootAsync = new FakeXMLHttpRequest(); this.postRootAsync.open("POST", "/", true); this.postRootAsync.send(); @@ -224,13 +232,14 @@ describe("sinonFakeServer", function () { this.sandbox.restore(); }); - it("responds to queued async requests", function () { + it("responds to queued async text requests", function () { this.server.respondWith("Oh yeah! Duffman!"); this.server.respond(); assert(this.getRootAsync.respond.called); assert.equals(this.getRootAsync.respond.args[0], [200, {}, "Oh yeah! Duffman!"]); + assert.equals(this.getRootAsync.readyState, FakeXMLHttpRequest.DONE); }); it("responds to all queued async requests", function () { @@ -264,13 +273,15 @@ describe("sinonFakeServer", function () { assert(xhr.respond.called); }); - it("responds with status, headers, and body", function () { + it("responds with status, headers, and text body", function () { var headers = { "Content-Type": "X-test" }; this.server.respondWith([201, headers, "Oh yeah!"]); this.server.respond(); + assert(this.getRootAsync.respond.called); assert.equals(this.getRootAsync.respond.args[0], [201, headers, "Oh yeah!"]); + assert.equals(this.getRootAsync.readyState, FakeXMLHttpRequest.DONE); }); it("handles responding with empty queue", function () { @@ -490,7 +501,7 @@ describe("sinonFakeServer", function () { assert.equals(handler.args[0], [xhr, "1337"]); }); - it("throws understandable error if response is not a string", function () { + it("throws understandable error if response is not a string or ArrayBuffer", function () { var server = this.server; assert.exception( @@ -498,12 +509,12 @@ describe("sinonFakeServer", function () { server.respondWith("/", {}); }, { - message: "Fake server response body should be string, but was object" + message: "Fake server response body should be a string or ArrayBuffer, but was object" } ); }); - it("throws understandable error if response in array is not a string", function () { + it("throws understandable error if response in array is not a string or ArrayBuffer", function () { var server = this.server; assert.exception( @@ -511,7 +522,7 @@ describe("sinonFakeServer", function () { server.respondWith("/", [200, {}]); }, { - message: "Fake server response body should be string, but was undefined" + message: "Fake server response body should be a string or ArrayBuffer, but was undefined" } ); }); @@ -534,6 +545,33 @@ describe("sinonFakeServer", function () { assert.equals(this.postRootAsync.respond.args[0], [200, {}, "All POSTs"]); assert.equals(this.postPathAsync.respond.args[0], [200, {}, "Particular POST"]); }); + + if (supportsArrayBuffer) { + it("responds to queued async arraybuffer requests", function () { + var buffer = new Uint8Array([160, 64, 0, 0, 32, 193]).buffer; + + this.server.respondWith(buffer); + + this.server.respond(); + + assert(this.getRootAsyncArrayBuffer.respond.called); + assert.equals(this.getRootAsyncArrayBuffer.respond.args[0], [200, {}, buffer]); + assert.equals(this.getRootAsyncArrayBuffer.readyState, FakeXMLHttpRequest.DONE); + }); + + it("responds with status, headers, and arraybuffer body", function () { + var buffer = new Uint8Array([160, 64, 0, 0, 32, 193]).buffer; + + var headers = { "Content-Type": "X-test" }; + this.server.respondWith([201, headers, buffer]); + + this.server.respond(); + + assert(this.getRootAsyncArrayBuffer.respond.called); + assert.equals(this.getRootAsyncArrayBuffer.respond.args[0], [201, headers, buffer]); + assert.equals(this.getRootAsyncArrayBuffer.readyState, FakeXMLHttpRequest.DONE); + }); + } }); describe(".respondWith (FunctionHandler)", function () { diff --git a/lib/fake-xhr/index.js b/lib/fake-xhr/index.js index 793dc7a..76954f4 100644 --- a/lib/fake-xhr/index.js +++ b/lib/fake-xhr/index.js @@ -224,16 +224,34 @@ function verifyHeadersReceived(xhr) { } } -function verifyResponseBodyType(body) { - if (typeof body !== "string") { - var error = new Error("Attempted to respond to fake XMLHttpRequest with " + - body + ", which is not a string."); +function verifyResponseBodyType(body, responseType) { + var error = null; + var isString = typeof body === "string"; + + if (responseType === "arraybuffer") { + + if (!isString && !(body instanceof ArrayBuffer)) { + error = new Error("Attempted to respond to fake XMLHttpRequest with " + + body + ", which is not a string or ArrayBuffer."); + error.name = "InvalidBodyException"; + } + } + else if (!isString) { + error = new Error("Attempted to respond to fake XMLHttpRequest with " + + body + ", which is not a string."); error.name = "InvalidBodyException"; + } + + if (error) { throw error; } } function convertToArrayBuffer(body, encoding) { + if (body instanceof ArrayBuffer) { + return body; + } + return new TextEncoder(encoding || "utf-8").encode(body).buffer; } @@ -576,7 +594,7 @@ extend(FakeXMLHttpRequest.prototype, sinonEvent.EventTarget, { setResponseBody: function setResponseBody(body) { verifyRequestSent(this); verifyHeadersReceived(this); - verifyResponseBodyType(body); + verifyResponseBodyType(body, this.responseType); var contentType = this.overriddenMimeType || this.getResponseHeader("Content-Type"); var isTextResponse = this.responseType === "" || this.responseType === "text"; diff --git a/lib/fake-xhr/index.test.js b/lib/fake-xhr/index.test.js index 727d7f4..9a9d827 100644 --- a/lib/fake-xhr/index.test.js +++ b/lib/fake-xhr/index.test.js @@ -9,7 +9,7 @@ var sinonSandbox = require("sinon").sandbox; var sinon = require("sinon"); var extend = require("just-extend"); -var TextDecoder = global.TextDecoder || require("text-encoding").TextDecoder; +var TextEncoder = global.TextEncoder || require("text-encoding").TextEncoder; var DOMParser = require("xmldom").DOMParser; var assert = referee.assert; var refute = referee.refute; @@ -46,10 +46,16 @@ function runWithWorkingXHROveride(workingXHR, test) { } } -function assertArrayBufferMatches(actual, expected, encoding) { +function toBinaryString(buffer) { + return String.fromCharCode.apply(null, new Uint8Array(buffer)); +} + +function assertArrayBufferMatches(actual, expected) { assert(actual instanceof ArrayBuffer, "${0} expected to be an ArrayBuffer"); - var actualString = new TextDecoder(encoding || "utf-8").decode(actual); - assert.same(actualString, expected, "ArrayBuffer [${0}] expected to match ArrayBuffer [${1}]"); + assert(expected instanceof ArrayBuffer, "${0} expected to be an ArrayBuffer"); + var actualString = toBinaryString(actual); + var expectedString = toBinaryString(expected); + assert.same(actualString, expectedString, "ArrayBuffer [${0}] expected to match ArrayBuffer [${1}]"); } function assertBlobMatches(actual, expected, done) { @@ -1613,7 +1619,7 @@ describe("FakeXMLHttpRequest", function () { this.xhr.send(); this.xhr.respond(); - assertArrayBufferMatches(this.xhr.response, ""); + assertArrayBufferMatches(this.xhr.response, new Uint8Array([]).buffer); }); it("returns ArrayBuffer when responseType='arraybuffer'", function () { @@ -1623,17 +1629,32 @@ describe("FakeXMLHttpRequest", function () { this.xhr.respond(200, { "Content-Type": "application/octet-stream" }, "a test buffer"); - assertArrayBufferMatches(this.xhr.response, "a test buffer"); + var expected = new TextEncoder("utf-8").encode("a test buffer").buffer; + assertArrayBufferMatches(this.xhr.response, expected); }); - it("returns binary data correctly when responseType='arraybuffer'", function () { + it("returns utf-8 strings correctly when responseType='arraybuffer'", function () { this.xhr.responseType = "arraybuffer"; this.xhr.open("GET", "/"); this.xhr.send(); this.xhr.respond(200, { "Content-Type": "application/octet-stream" }, "\xFF"); - assertArrayBufferMatches(this.xhr.response, "\xFF"); + var expectedBuffer = new TextEncoder("utf-8").encode("\xFF").buffer; + + assertArrayBufferMatches(this.xhr.response, expectedBuffer); + }); + + it("returns binary data correctly when responseType='arraybuffer'", function () { + this.xhr.responseType = "arraybuffer"; + this.xhr.open("GET", "/"); + this.xhr.send(); + + var buffer = new Uint8Array([160, 64, 0, 0, 32, 193]).buffer; + + this.xhr.respond(200, { "Content-Type": "application/octet-stream" }, buffer); + + assertArrayBufferMatches(this.xhr.response, buffer); }); }); } diff --git a/package.json b/package.json index 6a6e3c6..9f823f8 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "eslintConfig": { "extends": "eslint-config-sinon", "globals": { - "ArrayBuffer": false + "ArrayBuffer": false, + "Uint8Array": false }, "env": { "mocha": true