Skip to content

Commit

Permalink
feat(adapter-xhr): Add support for handling binary data (#333)
Browse files Browse the repository at this point in the history
  • Loading branch information
offirgolan authored May 18, 2020
1 parent 111bebf commit 48ea1d7
Show file tree
Hide file tree
Showing 13 changed files with 164 additions and 122 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
"lint-staged": "^8.2.0",
"mocha": "^6.1.4",
"npm-run-all": "^4.1.5",
"prettier": "^1.18.2",
"prettier": "~1.18.2",
"rimraf": "^2.6.3",
"rollup": "^1.14.6",
"rollup-plugin-alias": "^1.5.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/@pollyjs/adapter-fetch/src/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import Adapter from '@pollyjs/adapter';
import { isBufferUtf8Representable } from '@pollyjs/utils';
import isNode from 'detect-node';
import { Buffer } from 'buffer/';
import bufferToArrayBuffer from 'to-arraybuffer';

import serializeHeaders from './utils/serializer-headers';
import isBufferUtf8Representable from './utils/is-buffer-utf8-representable';

const { defineProperty } = Object;
const IS_STUBBED = Symbol();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,6 @@ describe('Integration | Fetch Adapter', function() {
});

it('should be able to download binary content', async function() {
this.timeout(10000);

const fetch = async () =>
Buffer.from(
await this.fetch('/assets/32x32.png').then(res => res.arrayBuffer())
Expand Down
2 changes: 1 addition & 1 deletion packages/@pollyjs/adapter-node-http/rollup.config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import createJestTestConfig from '../../../scripts/rollup/jest.test.config';

import { external } from './rollup.config.shared';

const testExternal = [...external, 'crypto', 'fs', 'path'];
const testExternal = [...external, 'fs', 'path'];

export default [
createNodeTestConfig({ external: testExternal }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import NodeHTTPAdapter from '../../src';
import nativeRequest from '../utils/native-request';
import pollyConfig from '../utils/polly-config';
import getResponseFromRequest from '../utils/get-response-from-request';
import calculateHashFromStream from '../utils/calculate-hash-from-stream';

describe('Integration | Node Http Adapter', function() {
describe('Concurrency', function() {
Expand Down Expand Up @@ -62,6 +61,41 @@ describe('Integration | Node Http Adapter', function() {
adapterTests();
adapterIdentifierTests();
commonTests(http);

it('should be able to download binary content', async function() {
const fetch = async () =>
Buffer.from(
await this.relativeFetch('/assets/32x32.png').then(res =>
res.arrayBuffer()
)
);

this.polly.disconnectFrom(NodeHTTPAdapter);

const nativeResponseBuffer = await fetch();

this.polly.connectTo(NodeHTTPAdapter);

const recordedResponseBuffer = await fetch();

const { recordingName, config } = this.polly;

await this.polly.stop();
this.polly = new Polly(recordingName, config);
this.polly.replay();

const replayedResponseBuffer = await fetch();

expect(nativeResponseBuffer.equals(recordedResponseBuffer)).to.equal(
true
);
expect(recordedResponseBuffer.equals(replayedResponseBuffer)).to.equal(
true
);
expect(nativeResponseBuffer.equals(replayedResponseBuffer)).to.equal(
true
);
});
});

describe('https', function() {
Expand Down Expand Up @@ -123,42 +157,6 @@ function commonTests(transport) {
expect(request.body).to.include('@pollyjs/adapter-node-http');
});

it('should be able to download binary content', async function() {
const url = `${protocol}//via.placeholder.com/150/92c952`;

this.polly.disconnectFrom(NodeHTTPAdapter);

const nativeResponseStream = await getResponseFromRequest(
transport.request(url)
);

this.polly.connectTo(NodeHTTPAdapter);

const recordedResponseStream = await getResponseFromRequest(
transport.request(url)
);

const { recordingName, config } = this.polly;

await this.polly.stop();
this.polly = new Polly(recordingName, config);
this.polly.replay();

const replayedResponseStream = await getResponseFromRequest(
transport.request(url)
);

const [nativeHash, recordedHash, replayedHash] = await Promise.all([
calculateHashFromStream(nativeResponseStream),
calculateHashFromStream(recordedResponseStream),
calculateHashFromStream(replayedResponseStream)
]);

expect(nativeHash).to.equal(recordedHash);
expect(recordedHash).to.equal(replayedHash);
expect(nativeHash).to.equal(replayedHash);
});

it('should handle aborting a request', async function() {
const { server } = this.polly;
const url = `${protocol}//example.com`;
Expand Down

This file was deleted.

3 changes: 2 additions & 1 deletion packages/@pollyjs/adapter-xhr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@
"watch-all": "npm-run-all --parallel build:watch test:build:watch"
},
"dependencies": {
"@offirgolan/nise": "^4.1.0",
"@pollyjs/adapter": "^4.2.1",
"@pollyjs/utils": "^4.1.0",
"nise": "^1.5.0"
"to-arraybuffer": "^1.0.1"
},
"devDependencies": {
"@pollyjs/core": "^4.2.1",
Expand Down
79 changes: 55 additions & 24 deletions packages/@pollyjs/adapter-xhr/src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import fakeXhr from 'nise/lib/fake-xhr';
import fakeXhr from '@offirgolan/nise/lib/fake-xhr';
import Adapter from '@pollyjs/adapter';
import { isBufferUtf8Representable } from '@pollyjs/utils';
import { Buffer } from 'buffer/';
import bufferToArrayBuffer from 'to-arraybuffer';

import resolveXhr from './utils/resolve-xhr';
import serializeResponseHeaders from './utils/serialize-response-headers';
Expand All @@ -8,6 +11,8 @@ const SEND = Symbol();
const ABORT_HANDLER = Symbol();
const stubbedXhrs = new WeakSet();

const BINARY_RESPONSE_TYPES = ['arraybuffer', 'blob'];

export default class XHRAdapter extends Adapter {
static get id() {
return 'xhr';
Expand Down Expand Up @@ -73,28 +78,6 @@ export default class XHRAdapter extends Adapter {
}
}

respondToRequest(pollyRequest, error) {
const { xhr } = pollyRequest.requestArguments;

if (pollyRequest[ABORT_HANDLER]) {
xhr.removeEventListener('abort', pollyRequest[ABORT_HANDLER]);
}

if (pollyRequest.aborted) {
return;
} else if (error) {
// If an error was received then call the `error` method on the fake XHR
// request provided by nise which will simulate a network error on the request.
// The onerror handler will be called and the status will be 0.
// https://github.com/sinonjs/nise/blob/v1.4.10/lib/fake-xhr/index.js#L614-L621
xhr.error();
} else {
const { response } = pollyRequest;

xhr.respond(response.statusCode, response.headers, response.body);
}
}

async passthroughRequest(pollyRequest) {
const { xhr: fakeXhr } = pollyRequest.requestArguments;
const xhr = new this.NativeXMLHttpRequest();
Expand All @@ -108,6 +91,9 @@ export default class XHRAdapter extends Adapter {
);

xhr.async = fakeXhr.async;
xhr.responseType = BINARY_RESPONSE_TYPES.includes(fakeXhr.responseType)
? 'arraybuffer'
: 'text';

if (fakeXhr.async) {
xhr.timeout = fakeXhr.timeout;
Expand All @@ -120,10 +106,55 @@ export default class XHRAdapter extends Adapter {

await resolveXhr(xhr, pollyRequest.body);

let body = xhr.response;
let isBinary = false;

// responseType will either be `arraybuffer` or `text`
if (xhr.responseType === 'arraybuffer') {
const buffer = Buffer.from(xhr.response);

isBinary = !isBufferUtf8Representable(buffer);
body = buffer.toString(isBinary ? 'hex' : 'utf8');
}

return {
statusCode: xhr.status,
headers: serializeResponseHeaders(xhr.getAllResponseHeaders()),
body: xhr.responseText
body,
isBinary
};
}

respondToRequest(pollyRequest, error) {
const { xhr } = pollyRequest.requestArguments;

if (pollyRequest[ABORT_HANDLER]) {
xhr.removeEventListener('abort', pollyRequest[ABORT_HANDLER]);
}

if (pollyRequest.aborted) {
return;
} else if (error) {
// If an error was received then call the `error` method on the fake XHR
// request provided by nise which will simulate a network error on the request.
// The onerror handler will be called and the status will be 0.
// https://github.com/sinonjs/nise/blob/v1.4.10/lib/fake-xhr/index.js#L614-L621
xhr.error();
} else {
const { statusCode, headers, body, isBinary } = pollyRequest.response;
let responseBody = body;

if (isBinary) {
const buffer = Buffer.from(body, 'hex');

if (BINARY_RESPONSE_TYPES.includes(xhr.responseType)) {
responseBody = bufferToArrayBuffer(buffer);
} else {
responseBody = buffer.toString('utf8');
}
}

xhr.respond(statusCode, headers, responseBody);
}
}
}
38 changes: 38 additions & 0 deletions packages/@pollyjs/adapter-xhr/tests/integration/adapter-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import adapterTests from '@pollyjs-tests/integration/adapter-tests';
import adapterBrowserTests from '@pollyjs-tests/integration/adapter-browser-tests';
import adapterIdentifierTests from '@pollyjs-tests/integration/adapter-identifier-tests';
import InMemoryPersister from '@pollyjs/persister-in-memory';
import { Buffer } from 'buffer/';

import xhrRequest from '../utils/xhr-request';
import XHRAdapter from '../../src';
Expand Down Expand Up @@ -63,6 +64,43 @@ describe('Integration | XHR Adapter', function() {

expect(abortEventCalled).to.equal(true);
});

['arraybuffer', 'blob', 'text'].forEach(responseType =>
it(`should be able to download binary content (${responseType})`, async function() {
const fetch = async () =>
Buffer.from(
await this.fetch('/assets/32x32.png', {
responseType
}).then(res => res.arrayBuffer())
);

this.polly.disconnectFrom(XHRAdapter);

const nativeResponseBuffer = await fetch();

this.polly.connectTo(XHRAdapter);

const recordedResponseBuffer = await fetch();

const { recordingName, config } = this.polly;

await this.polly.stop();
this.polly = new Polly(recordingName, config);
this.polly.replay();

const replayedResponseBuffer = await fetch();

expect(nativeResponseBuffer.equals(recordedResponseBuffer)).to.equal(
true
);
expect(recordedResponseBuffer.equals(replayedResponseBuffer)).to.equal(
true
);
expect(nativeResponseBuffer.equals(replayedResponseBuffer)).to.equal(
true
);
})
);
});

describe('Integration | XHR Adapter | Init', function() {
Expand Down
6 changes: 5 additions & 1 deletion packages/@pollyjs/adapter-xhr/tests/utils/xhr-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,18 @@ export default function request(url, obj = {}) {
}
}

if (obj.responseType) {
xhr.responseType = obj.responseType;
}

xhr.onreadystatechange = () =>
xhr.readyState === XMLHttpRequest.DONE && resolve(xhr);
xhr.onerror = () => resolve(xhr);

xhr.send(obj.body);
}).then(xhr => {
const responseBody =
xhr.status === 204 && xhr.responseText === '' ? null : xhr.responseText;
xhr.status === 204 && xhr.response === '' ? null : xhr.response;

return new Response(responseBody, {
status: xhr.status || 500,
Expand Down
4 changes: 4 additions & 0 deletions packages/@pollyjs/utils/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ export { default as Serializers } from './utils/serializers';
export { default as URL } from './utils/url';

export { default as getFactoryId } from './utils/get-factory-id';

export {
default as isBufferUtf8Representable
} from './utils/is-buffer-utf8-representable';
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Buffer } from 'buffer/';
import { Buffer } from 'buffer';

/**
* Determine if the given buffer is utf8.
Expand Down
Loading

0 comments on commit 48ea1d7

Please sign in to comment.