Skip to content

Commit

Permalink
Fix binary response (#15)
Browse files Browse the repository at this point in the history
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
sinonjs/sinon#875 (comment).
  • Loading branch information
tzrh authored and fatso83 committed Oct 23, 2017
1 parent e2e8648 commit 1990dd0
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 23 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
12 changes: 10 additions & 2 deletions lib/fake-server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down
50 changes: 44 additions & 6 deletions lib/fake-server/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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 () {
Expand Down Expand Up @@ -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 () {
Expand Down Expand Up @@ -490,28 +501,28 @@ 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(
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(
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"
}
);
});
Expand All @@ -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 () {
Expand Down
28 changes: 23 additions & 5 deletions lib/fake-xhr/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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";
Expand Down
37 changes: 29 additions & 8 deletions lib/fake-xhr/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 () {
Expand All @@ -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);
});
});
}
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"eslintConfig": {
"extends": "eslint-config-sinon",
"globals": {
"ArrayBuffer": false
"ArrayBuffer": false,
"Uint8Array": false
},
"env": {
"mocha": true
Expand Down

0 comments on commit 1990dd0

Please sign in to comment.