From 40d62b96b4f51c6b1c38b5d32319e84b9b454ad7 Mon Sep 17 00:00:00 2001 From: offirgolan Date: Sun, 18 Nov 2018 22:24:10 -0800 Subject: [PATCH 1/4] feat(core): Support custom functions in matchRequestsBy config options --- docs/configuration.md | 180 ++++++++++++++---- .../core/src/utils/normalize-request.js | 32 +++- .../unit/utils/normalize-request-test.js | 54 +++++- packages/@pollyjs/utils/package.json | 2 +- yarn.lock | 7 +- 5 files changed, 212 insertions(+), 63 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index e6425bda..ea092611 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -30,7 +30,7 @@ _Default_: `false` Logs requests and their responses to the console grouped by the recording name. -__Example__ +**Example** ```js polly.configure({ @@ -46,7 +46,7 @@ _Default_: `true` If a request's recording is not found, pass-through to the server and record the response. -__Example__ +**Example** ```js polly.configure({ @@ -62,7 +62,7 @@ _Default_: `false` If a request's recording has expired, pass-through to the server and record a new response. -__Example__ +**Example** ```js polly.configure({ @@ -79,7 +79,7 @@ If `false`, Polly will throw when attempting to persist any failed requests. A request is considered to be a failed request when its response's status code is `< 200` or `≥ 300`. -__Example__ +**Example** ```js polly.configure({ @@ -95,7 +95,7 @@ _Default_: `null` After how long the recorded request will be considered expired from the time it was persisted. -__Example__ +**Example** ```js polly.configure({ @@ -118,7 +118,7 @@ The Polly mode. Can be one of the following: - `record`: Force Polly to record all requests. This will overwrite recordings that already exist. - `passthrough`: Passes all requests through directly to the server without recording or replaying. -__Example__ +**Example** ```js polly.configure({ @@ -133,7 +133,7 @@ _Default_: `[]` The adapter(s) polly will hook into. -__Example__ +**Example** ```js import XHRAdapter from '@pollyjs/adapter-xhr'; @@ -154,10 +154,10 @@ _Default_: `{}` Options to be passed into the adapters keyed by the adapter name. -?> __NOTE:__ Check out the appropriate documentation pages for each adapter +?> **NOTE:** Check out the appropriate documentation pages for each adapter for more details. -__Example__ +**Example** ```js polly.configure({ @@ -176,7 +176,7 @@ _Default_: `null` The persister to use for recording and replaying requests. -__Example__ +**Example** ```js import RESTPersister from '@pollyjs/persister-rest'; @@ -201,10 +201,10 @@ _Default_: `{}` Options to be passed into the persister keyed by the persister name. -?> __NOTE:__ Check out the appropriate documentation pages for each persister +?> **NOTE:** Check out the appropriate documentation pages for each persister for more details. -__Example__ +**Example** ```js polly.configure({ @@ -227,7 +227,7 @@ recording will only contain the requests that were made during the lifespan of the Polly instance. When enabled, new requests will be appended to the recording file. -__Example__ +**Example** ```js polly.configure({ @@ -244,7 +244,7 @@ _Default_: `Timing.fixed(0)` The timeout delay strategy used when replaying requests. -__Example__ +**Example** ```js import { Timing } from '@pollyjs/core'; @@ -291,12 +291,12 @@ a GUID for the request. - ### method - _Type_: `Boolean` + _Type_: `Boolean | Function` _Default_: `true` The request method (e.g. `GET`, `POST`) - __Example__ + **Example** ```js polly.configure({ @@ -304,16 +304,24 @@ a GUID for the request. method: false } }); + + polly.configure({ + matchRequestsBy: { + method(method) { + return method.toLowerCase(); + } + } + }); ``` - ### headers - _Type_: `Boolean | Object` + _Type_: `Boolean | Function | Object` _Default_: `true` The request headers. - __Example__ + **Example** ```js polly.configure({ @@ -321,11 +329,20 @@ a GUID for the request. headers: false } }); + + polly.configure({ + matchRequestsBy: { + headers(headers) { + delete headers['X-AUTH-TOKEN']; + return headers; + } + } + }); ``` - Specific headers can also be excluded: + Specific headers can also be excluded with the following: - __Example__ + **Example** ```js polly.configure({ @@ -339,12 +356,12 @@ a GUID for the request. - ### body - _Type_: `Boolean` + _Type_: `Boolean | Function` _Default_: `true` The request body. - __Example__ + **Example** ```js polly.configure({ @@ -352,6 +369,17 @@ a GUID for the request. body: false } }); + + polly.configure({ + matchRequestsBy: { + body(body) { + const json = JSON.parse(body); + + delete json.email; + return JSON.stringify(json); + } + } + }); ``` - ### order @@ -372,7 +400,7 @@ a GUID for the request. await fetch('/models/1', { method: 'POST', body: JSON.stringify(model) }); // Get our updated model - model = await fetch('/models/1').then(res => res.json()) + model = await fetch('/models/1').then(res => res.json()); // Assert that our change persisted expect(model.foo).to.equal('bar'); @@ -381,7 +409,7 @@ a GUID for the request. The order of the requests matter since the payload for the first and last fetch are different. - __Example__ + **Example** ```js polly.configure({ @@ -393,12 +421,12 @@ a GUID for the request. - ### url.protocol - _Type_: `Boolean` + _Type_: `Boolean | Function` _Default_: `true` The request url protocol (e.g. `http:`). - __Example__ + **Example** ```js polly.configure({ @@ -408,16 +436,26 @@ a GUID for the request. } } }); + + polly.configure({ + matchRequestsBy: { + url: { + protocol(protocol) { + return 'https:'; + } + } + } + }); ``` - ### url.username - _Type_: `Boolean` + _Type_: `Boolean | Function` _Default_: `true` Username of basic authentication. - __Example__ + **Example** ```js polly.configure({ @@ -427,16 +465,25 @@ a GUID for the request. } } }); + polly.configure({ + matchRequestsBy: { + url: { + username(username) { + return 'username'; + } + } + } + }); ``` - ### url.password - _Type_: `Boolean` + _Type_: `Boolean | Function` _Default_: `true` Password of basic authentication. - __Example__ + **Example** ```js polly.configure({ @@ -445,17 +492,24 @@ a GUID for the request. password: false } } + matchRequestsBy: { + url: { + password(password) { + return 'password'; + } + } + } }); ``` - ### url.hostname - _Type_: `Boolean` + _Type_: `Boolean | Function` _Default_: `true` - Host name with port number. + Host name without port number. - __Example__ + **Example** ```js polly.configure({ @@ -465,16 +519,26 @@ a GUID for the request. } } }); + + polly.configure({ + matchRequestsBy: { + url: { + hostname(hostname) { + return hostname.replace('.com', '.net'); + } + } + } + }); ``` - ### url.port - _Type_: `Boolean` + _Type_: `Boolean | Function` _Default_: `true` Port number. - __Example__ + **Example** ```js polly.configure({ @@ -484,22 +548,34 @@ a GUID for the request. } } }); + + polly.configure({ + matchRequestsBy: { + url: { + port(port) { + return 3000; + } + } + } + }); ``` - ### url.pathname - _Type_: `Boolean` + _Type_: `Boolean | Function` _Default_: `true` URL path. - __Example__ + **Example** ```js polly.configure({ matchRequestsBy: { url: { - pathname: false + pathname(pathname) { + return pathname.replace('/api/v1', '/api'); + } } } }); @@ -507,12 +583,12 @@ a GUID for the request. - ### url.query - _Type_: `Boolean` + _Type_: `Boolean | Function` _Default_: `true` Sorted query string. - __Example__ + **Example** ```js polly.configure({ @@ -522,16 +598,26 @@ a GUID for the request. } } }); + + polly.configure({ + matchRequestsBy: { + url: { + query(query) { + return { ...query, token: '' }; + } + } + } + }); ``` - ### url.hash - _Type_: `Boolean` + _Type_: `Boolean | Function` _Default_: `false` The "fragment" portion of the URL including the pound-sign (`#`). - __Example__ + **Example** ```js polly.configure({ @@ -541,4 +627,14 @@ a GUID for the request. } } }); + + polly.configure({ + matchRequestsBy: { + url: { + hash(hash) { + return hash.replace(/token=[0-9]+/, ''); + } + } + } + }); ``` diff --git a/packages/@pollyjs/core/src/utils/normalize-request.js b/packages/@pollyjs/core/src/utils/normalize-request.js index 0b71c88b..f62e7105 100644 --- a/packages/@pollyjs/core/src/utils/normalize-request.js +++ b/packages/@pollyjs/core/src/utils/normalize-request.js @@ -1,4 +1,5 @@ import isObjectLike from 'lodash-es/isObjectLike'; +import isFunction from 'lodash-es/isFunction'; import stringify from 'fast-json-stable-stringify'; import parseUrl from './parse-url'; @@ -8,15 +9,23 @@ const { keys } = Object; const { isArray } = Array; const { parse } = JSON; -export function method(method) { - return (method || 'GET').toUpperCase(); +export function method(method, config) { + return isFunction(config) + ? config(method || 'GET') + : (method || 'GET').toUpperCase(); } export function url(url, config = {}) { const parsedUrl = parseUrl(url, true); // Remove any url properties that have been disabled via the config - keys(config).forEach(key => !config[key] && parsedUrl.set(key, '')); + keys(config).forEach(key => { + if (isFunction(config[key])) { + parsedUrl.set(key, config[key](parsedUrl[key])); + } else if (!config[key]) { + parsedUrl.set(key, ''); + } + }); // Sort Query Params if (isObjectLike(parsedUrl.query)) { @@ -31,18 +40,23 @@ export function headers(headers, config) { if (isObjectLike(normalizedHeaders)) { normalizedHeaders = new HTTPHeaders(normalizedHeaders); + } - // Filter out excluded headers - if (isObjectLike(config) && isArray(config.exclude)) { - config.exclude.forEach(header => (normalizedHeaders[header] = null)); - } + if (isFunction(config)) { + normalizedHeaders = config(normalizedHeaders); + } else if ( + isObjectLike(normalizedHeaders) && + isObjectLike(config) && + isArray(config.exclude) + ) { + config.exclude.forEach(header => delete normalizedHeaders[header]); } return normalizedHeaders; } -export function body(body) { - return body; +export function body(body, config) { + return isFunction(config) ? config(body) : body; } export default { diff --git a/packages/@pollyjs/core/tests/unit/utils/normalize-request-test.js b/packages/@pollyjs/core/tests/unit/utils/normalize-request-test.js index 25751466..7b12e8dd 100644 --- a/packages/@pollyjs/core/tests/unit/utils/normalize-request-test.js +++ b/packages/@pollyjs/core/tests/unit/utils/normalize-request-test.js @@ -26,6 +26,10 @@ describe('Unit | Utils | Normalize Request', function() { expect(method('delete')).to.equal('DELETE'); expect(method('option')).to.equal('OPTION'); }); + + it('should support a custom fn', function() { + expect(method('GET', m => m.toLowerCase())).to.equal('get'); + }); }); describe('headers', function() { @@ -53,6 +57,23 @@ describe('Unit | Utils | Normalize Request', function() { ) ).to.deep.equal({ accept: 'foo' }); }); + + it('should support a custom fn', function() { + expect( + headers( + { + Accept: 'foo', + test: 'test', + 'Content-Type': 'Bar' + }, + headers => { + delete headers.test; + + return headers; + } + ) + ).to.deep.equal({ accept: 'foo', 'content-type': 'Bar' }); + }); }); describe('url', function() { @@ -62,7 +83,7 @@ describe('Unit | Utils | Normalize Request', function() { ); }); - it('should respect `hash` config', function() { + it('should respect `matchRequestsBy.url` config', function() { [ [ /* config options */ @@ -72,43 +93,54 @@ describe('Unit | Utils | Normalize Request', function() { /* expected when true */ [true, 'http://hash-test.com?a=1&b=1&c=1#hello=world'], /* expected when false */ - [false, 'http://hash-test.com?a=1&b=1&c=1'] + [false, 'http://hash-test.com?a=1&b=1&c=1'], + /* expected when custom fn */ + [ + h => h.replace('=', '!='), + 'http://hash-test.com?a=1&b=1&c=1#hello!=world' + ] ], [ 'protocol', 'http://protocol-test.com', [true, 'http://protocol-test.com'], - [false, '//protocol-test.com'] + [false, '//protocol-test.com'], + [p => p.replace('http', 'https'), 'https://protocol-test.com'] ], [ 'query', 'http://query-test.com?b=1&c=1&a=1', [true, 'http://query-test.com?a=1&b=1&c=1'], - [false, 'http://query-test.com'] + [false, 'http://query-test.com'], + [q => ({ ...q, c: 2 }), 'http://query-test.com?a=1&b=1&c=2'] ], [ 'username', 'https://username:password@username-test.com', [true, 'https://username:password@username-test.com'], - [false, 'https://username-test.com'] + [false, 'https://username-test.com'], + [u => `${u}123`, 'https://username123:password@username-test.com'] ], [ 'password', 'https://username:password@password-test.com', [true, 'https://username:password@password-test.com'], - [false, 'https://username@password-test.com'] + [false, 'https://username@password-test.com'], + [p => `${p}123`, 'https://username:password123@password-test.com'] ], [ 'port', 'https://port-test.com:8000', [true, 'https://port-test.com:8000'], - [false, 'https://port-test.com'] + [false, 'https://port-test.com'], + [p => Number(p) + 1, 'https://port-test.com:8001'] ], [ 'pathname', 'https://pathname-test.com/bar/baz', [true, 'https://pathname-test.com/bar/baz'], - [false, 'https://pathname-test.com'] + [false, 'https://pathname-test.com'], + [p => p.replace('bar', 'foo'), 'https://pathname-test.com/foo/baz'] ] ].forEach(([rule, input, ...options]) => { options.forEach(([optionValue, expectedOutput]) => { @@ -121,4 +153,10 @@ describe('Unit | Utils | Normalize Request', function() { expect(url('/some/path')).to.equal('/some/path'); }); }); + + describe('body', function() { + it('should support a custom fn', function() { + expect(body('foo', b => b.toUpperCase())).to.equal('FOO'); + }); + }); }); diff --git a/packages/@pollyjs/utils/package.json b/packages/@pollyjs/utils/package.json index 15c52814..8d557e1c 100644 --- a/packages/@pollyjs/utils/package.json +++ b/packages/@pollyjs/utils/package.json @@ -39,7 +39,7 @@ ], "license": "Apache-2.0", "dependencies": { - "url-parse": "^1.4.1" + "url-parse": "^1.4.4" }, "devDependencies": { "npm-run-all": "^4.1.3", diff --git a/yarn.lock b/yarn.lock index fd0b5f09..c07d60c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13408,9 +13408,10 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url-parse@^1.4.1: - version "1.4.3" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.3.tgz#bfaee455c889023219d757e045fa6a684ec36c15" +url-parse@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.4.tgz#cac1556e95faa0303691fec5cf9d5a1bc34648f8" + integrity sha512-/92DTTorg4JjktLNLe6GPS2/RvAd/RGr6LuktmWSMLEOa6rjnlrFXNgSbSmkNvCoL2T028A0a1JaJLzRMlFoHg== dependencies: querystringify "^2.0.0" requires-port "^1.0.0" From cc79bb649ceccecab78a4218f3f1db609885721c Mon Sep 17 00:00:00 2001 From: offirgolan Date: Sun, 18 Nov 2018 22:44:05 -0800 Subject: [PATCH 2/4] refactor: Move serializeRequestBody into the identify method --- packages/@pollyjs/core/src/-private/request.js | 17 +++++++---------- tests/integration/adapter-browser-tests.js | 6 ++---- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/@pollyjs/core/src/-private/request.js b/packages/@pollyjs/core/src/-private/request.js index d9c962c4..41811c05 100644 --- a/packages/@pollyjs/core/src/-private/request.js +++ b/packages/@pollyjs/core/src/-private/request.js @@ -129,11 +129,8 @@ export default class PollyRequest extends HTTPBase { this.response = new PollyResponse(); this.didRespond = false; - // Serialize the body which handles FormData + Blobs/Files - this.serializedBody = await this.serializeBody(); - // Setup this request's identifiers, id, and order - this._identify(); + await this._identify(); // Timestamp the request this.timestamp = timestamp(); @@ -172,10 +169,6 @@ export default class PollyRequest extends HTTPBase { await this._emit('response', this.response); } - async serializeBody() { - return serializeRequestBody(this.body); - } - _overrideRecordingName(recordingName) { validateRecordingName(recordingName); this.recordingName = recordingName; @@ -195,7 +188,7 @@ export default class PollyRequest extends HTTPBase { return this[ROUTE].emit(eventName, this, ...args); } - _identify() { + async _identify() { const polly = this[POLLY]; const { _requests: requests } = polly; const { matchRequestsBy } = this.config; @@ -205,12 +198,16 @@ export default class PollyRequest extends HTTPBase { keys(NormalizeRequest).forEach(key => { if (this[key] && matchRequestsBy[key]) { identifiers[key] = NormalizeRequest[key]( - key === 'body' ? this.serializedBody : this[key], + this[key], matchRequestsBy[key] ); } }); + if (identifiers.body) { + identifiers.body = await serializeRequestBody(identifiers.body); + } + // Store the identifiers for debugging and testing this.identifiers = freeze(identifiers); diff --git a/tests/integration/adapter-browser-tests.js b/tests/integration/adapter-browser-tests.js index 1507a3b4..246925b0 100644 --- a/tests/integration/adapter-browser-tests.js +++ b/tests/integration/adapter-browser-tests.js @@ -15,10 +15,10 @@ export default function adapterBrowserTests() { ); server.post('/submit').intercept((req, res) => { - const body = req.serializedBody; + const body = req.identifiers.body; // Make sure the form data exists in the identifiers - expect(req.identifiers.body).to.include(recordingName); + expect(body).to.include(recordingName); expect(body).to.include(`string=${recordingName}`); expect(body).to.include( @@ -49,8 +49,6 @@ export default function adapterBrowserTests() { // Make sure the form data exists in the identifiers expect(req.identifiers.body).to.equal(dataUrl); - expect(req.serializedBody).to.equal(dataUrl); - res.sendStatus(200); }); From 23f2e7f2ac40f0a4287e542c2ec6355b3dca19c4 Mon Sep 17 00:00:00 2001 From: offirgolan Date: Mon, 19 Nov 2018 23:38:10 -0800 Subject: [PATCH 3/4] fix: Fix PR issues and add test case --- .../core/src/utils/normalize-request.js | 23 ++++++++----------- .../unit/utils/normalize-request-test.js | 18 +++++++++++---- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/packages/@pollyjs/core/src/utils/normalize-request.js b/packages/@pollyjs/core/src/utils/normalize-request.js index f62e7105..879cb5f5 100644 --- a/packages/@pollyjs/core/src/utils/normalize-request.js +++ b/packages/@pollyjs/core/src/utils/normalize-request.js @@ -1,5 +1,4 @@ import isObjectLike from 'lodash-es/isObjectLike'; -import isFunction from 'lodash-es/isFunction'; import stringify from 'fast-json-stable-stringify'; import parseUrl from './parse-url'; @@ -9,10 +8,12 @@ const { keys } = Object; const { isArray } = Array; const { parse } = JSON; +function isFunction(fn) { + return typeof fn === 'function'; +} + export function method(method, config) { - return isFunction(config) - ? config(method || 'GET') - : (method || 'GET').toUpperCase(); + return isFunction(config) ? config(method) : method.toUpperCase(); } export function url(url, config = {}) { @@ -36,19 +37,13 @@ export function url(url, config = {}) { } export function headers(headers, config) { - let normalizedHeaders = headers; + const normalizedHeaders = new HTTPHeaders(headers); - if (isObjectLike(normalizedHeaders)) { - normalizedHeaders = new HTTPHeaders(normalizedHeaders); + if (isFunction(config)) { + return config(normalizedHeaders); } - if (isFunction(config)) { - normalizedHeaders = config(normalizedHeaders); - } else if ( - isObjectLike(normalizedHeaders) && - isObjectLike(config) && - isArray(config.exclude) - ) { + if (isObjectLike(config) && isArray(config.exclude)) { config.exclude.forEach(header => delete normalizedHeaders[header]); } diff --git a/packages/@pollyjs/core/tests/unit/utils/normalize-request-test.js b/packages/@pollyjs/core/tests/unit/utils/normalize-request-test.js index 7b12e8dd..e4786176 100644 --- a/packages/@pollyjs/core/tests/unit/utils/normalize-request-test.js +++ b/packages/@pollyjs/core/tests/unit/utils/normalize-request-test.js @@ -14,10 +14,6 @@ describe('Unit | Utils | Normalize Request', function() { }); describe('method', function() { - it('should default to GET', function() { - expect(method()).to.equal('GET'); - }); - it('should handle all verbs', function() { expect(method('get')).to.equal('GET'); expect(method('put')).to.equal('PUT'); @@ -74,6 +70,20 @@ describe('Unit | Utils | Normalize Request', function() { ) ).to.deep.equal({ accept: 'foo', 'content-type': 'Bar' }); }); + + it('should not mutate the original headers in the custom fn', function() { + const reqHeaders = { foo: 'bar' }; + + expect( + headers(reqHeaders, headers => { + delete headers.foo; + + return headers; + }) + ).to.deep.equal({}); + + expect(reqHeaders).to.deep.equal({ foo: 'bar' }); + }); }); describe('url', function() { From ef4916654949b446dac105f316d129523b62f6c8 Mon Sep 17 00:00:00 2001 From: offirgolan Date: Sat, 24 Nov 2018 12:57:43 -0800 Subject: [PATCH 4/4] docs: Add alert around modifying the request body --- docs/configuration.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index ea092611..66df37ec 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -361,6 +361,8 @@ a GUID for the request. The request body. + !> Please make sure you do not modify the passed in body. If you need to make changes, create a copy of it first. The body function receives the actual request body — any modifications to it will result with it being sent out with the request. + **Example** ```js