From 6a1e8f9709ea071564ac7f0de84743a73c35366c Mon Sep 17 00:00:00 2001 From: Aaron Quamme Date: Sat, 15 Oct 2016 19:52:14 -0500 Subject: [PATCH] Request assertion improvements * server.received... returns the number of matching requests server.didNotReceive returns a boolean * Arbitrary FakeXHR attributes can be passed * Removed 'with' alias for 'to' * Make server.requests a public attribute --- addon/request-assertion.js | 114 ++++++++++-------- .../server-request-assertion-test.js | 91 +++++++++----- 2 files changed, 120 insertions(+), 85 deletions(-) diff --git a/addon/request-assertion.js b/addon/request-assertion.js index db264beaf..994590d5a 100644 --- a/addon/request-assertion.js +++ b/addon/request-assertion.js @@ -1,83 +1,91 @@ /* * RequestAssertion implements a simple DSL for asserting that specific requests were made. * ``` - * let it = new RequestAssertion(requests); + * let requests = [ ]; * - * it.received.delete.to('/posts'); - * it.didNotReceive.request.to('/contacts'); - * it.received.get.to(/\/contacts\/\d+\/); - * it.received.put.to('/posts/5', { title: 'The Title' }); - * it.received.post.with('/listing', { price: 5000 }); - * it.didNotReceive.post.to('any'); + * let it = new RequestAssertion(requests); + * it.received.delete.to('/posts'); // => 0 + * it.didNotReceive.request.to('/contacts'); // => true + * it.received.get.to(/\/contacts\/\d+\/); // => 0 + * it.received.put.to('/posts/5', { requestBody: { title: 'The Title' } }); + * it.received.get.to({}); // => 2 * ``` * */ +import Ember from 'ember'; import _isPlainObject from 'lodash/lang/isPlainObject'; import _isMatch from 'lodash/lang/isMatch'; import _isNumber from 'lodash/lang/isNumber'; -function requestMatches(request, verb, url, params) { - return verbMatches(request, verb) && - urlMatches(request, url) && - paramsMatch(request, params); - - function verbMatches(request, expectedVerb) { - return expectedVerb === 'request' || // 'request' matches all HTTP verbs - request.method.toUpperCase() === expectedVerb.toUpperCase(); - } - - function urlMatches(request, expectedUrl) { - let url = request.url.replace(/\?.*$/, ''); // remove query params - return !expectedUrl || // if no url is passed, then we don't care -- so consider it a match - expectedUrl === 'any' || // 'any' matches all urls - expectedUrl instanceof RegExp && expectedUrl.test(url) || - expectedUrl === url; - } - - function paramsMatch(request, expectedParams) { - // if no params are passed, consider it a match - if (!_isPlainObject(expectedParams)) { - return true; - } +const { merge } = Ember; - let comparingQueryParams = false; - let actualParams; - try { - actualParams = JSON.parse(request.requestBody || undefined); // throw if requestBody is null - } catch (e) { - comparingQueryParams = true; - actualParams = request.queryParams; - } +// Functions keyed by FakeXHR attribute keys which are used to compare actual vs expected values +// If comparing a key which does not having a corresponding comparator, then `_isMatch` is used (see `requestsMatch`) +const comparators = { + method(actual, expected) { + return expected === 'request' || // 'request' matches all HTTP verbs + actual.toUpperCase() === expected.toUpperCase(); + }, - return _isMatch(actualParams, expectedParams, looseCompareIfQueryParams); + queryParams(actual, expected) { + return _isMatch(actual, expected, castExpectedNumbersToStrings); - function looseCompareIfQueryParams(actualValue, expectedValue) { - if (comparingQueryParams && _isNumber(expectedValue)) { + function castExpectedNumbersToStrings(actualValue, expectedValue) { + if (_isNumber(expectedValue)) { return String(expectedValue) === actualValue; } } + }, + + requestBody(actual, expected) { + let actualParsed; + try { + actualParsed = JSON.parse(actual || undefined); // throw if requestBody is null + } catch (e) {} + return _isMatch(actualParsed, expected); + }, + + url(actual, expected) { + let actualSansQPs = actual.replace(/\?.*$/, ''); // remove query params + return !expected || // if no url is passed, then we don't care -- so consider it a match + expected instanceof RegExp && expected.test(actualSansQPs) || + expected === actualSansQPs; } +}; + +function requestMatches(actualRequest, expectedRequest) { + return Object.keys(expectedRequest).every(key => { + if (comparators[key]) { + return comparators[key](actualRequest[key], expectedRequest[key]); + } else { + return _isMatch(actualRequest[key], expectedRequest[key]); + } + }); } -function to(requests, expectMatch, verb, ...toParams) { +function to(requests, expectToFindMatch, method, ...toParams) { if (requests.length === 0) { - return expectMatch === false; + return !expectToFindMatch; } let url = (typeof toParams[0] === 'string' || toParams[0] instanceof RegExp) && toParams[0] || null; let requestParams = _isPlainObject(toParams[0]) && toParams[0] || _isPlainObject(toParams[1]) && toParams[1]; - let foundMatch = requests.some(request => requestMatches(request, verb, url, requestParams)); + let expectedRequest = merge({ method, url }, requestParams || {}); + let foundMatches = requests.filter(actualRequest => requestMatches(actualRequest, expectedRequest)); - return foundMatch === expectMatch; + if (expectToFindMatch) { + return foundMatches.length; + } else { + return !foundMatches.length; + } } -class Verb { - constructor(requests, received) { - ['get', 'post', 'put', 'delete', 'head', 'request'].forEach(verb => { - Object.defineProperty(this, verb, { +class Method { + constructor(requests, expectToFindMatch) { + ['get', 'post', 'put', 'delete', 'head', 'request'].forEach(method => { + Object.defineProperty(this, method, { get() { - let f = to.bind(null, requests, received, verb); - return { to: f, with: f }; + return { to: to.bind(this, requests, expectToFindMatch, method) }; } }); }); @@ -90,11 +98,11 @@ export default class RequestAssertion { } get received() { - return new Verb(this._requests, true); + return new Method(this._requests, true); } get didNotReceive() { - return new Verb(this._requests, false); + return new Method(this._requests, false); } } diff --git a/tests/integration/server-request-assertion-test.js b/tests/integration/server-request-assertion-test.js index 2036a6b33..a0a1e5486 100644 --- a/tests/integration/server-request-assertion-test.js +++ b/tests/integration/server-request-assertion-test.js @@ -14,34 +14,25 @@ module('Integration | Server Request Assertion', { } }); -test('server reports no requests', function(assert) { - assert.expect(1); - - assert.ok(this.server.didNotReceive.get.to('any')); -}); - -['get', 'post', 'put', 'delete', 'head'].forEach(verb => { - test(`it reports ${verb} to '/contacts'`, function(assert) { - assert.expect(4); - let done = assert.async(); +test('it returns the number of matching requests when `received` is used', function(assert) { + let done = assert.async(); - this.server[verb]('/contacts', () => true); + this.server.get('/contacts', () => true); - $.ajax({ - method: verb, - url: '/contacts' - }).done(() => { - assert.ok(this.server.received[verb].to('/contacts')); - assert.ok(this.server.received.request.to('/contacts')); - assert.notOk(this.server.didNotReceive[verb].to('/contacts')); - assert.notOk(this.server.didNotReceive.request.to('/contacts')); + $.ajax('/contacts').done(() => { + $.ajax('/contacts').done(() => { + assert.equal(this.server.received.get.to('/contacts'), 2); done(); }); }); }); +test('it returns a boolean when `didNotReceive` is used', function(assert) { + assert.equal(this.server.didNotReceive.request.to('/contacts'), true); +}); + test('it matches query params', function(assert) { - assert.expect(6); + assert.expect(3); let done = assert.async(); this.server.get('/contacts', () => true); @@ -51,12 +42,9 @@ test('it matches query params', function(assert) { url: '/contacts', data: { page: 4, sort: 'name' } }).done(() => { - assert.ok(this.server.received.get.to('/contacts', { page: 4 })); - assert.ok(this.server.received.get.to('/contacts', { sort: 'name' })); - assert.ok(this.server.received.get.to('/contacts', { page: 4, sort: 'name' })); - assert.ok(this.server.received.get.with({ page: 4 })); - assert.ok(this.server.received.get.with({ sort: 'name' })); - assert.ok(this.server.received.get.with({ page: 4, sort: 'name' })); + assert.ok(this.server.received.get.to('/contacts', { queryParams: { page: 4 } })); + assert.ok(this.server.received.get.to('/contacts', { queryParams: { sort: 'name' } })); + assert.ok(this.server.received.get.to('/contacts', { queryParams: { page: 4, sort: 'name' } })); done(); }); }); @@ -79,15 +67,33 @@ test('it matches post params', function(assert) { } }) }).done(() => { - assert.ok(this.server.received.post.to('/contacts', { name: 'Bilbo', age: 111 })); - assert.ok(this.server.received.post.with({ name: 'Bilbo', address: { zip: 'none' } })); - assert.notOk(this.server.received.post.to('/contacts', { name: 'Frodo' })); - assert.notOk(this.server.received.post.with({ name: 'Frodo' })); + assert.ok(this.server.received.post.to('/contacts', { requestBody: { name: 'Bilbo', age: 111 } })); + assert.ok(this.server.received.post.to({ requestBody: { name: 'Bilbo', address: { zip: 'none' } } })); + assert.notOk(this.server.received.post.to('/contacts', { requestBody: { name: 'Frodo' } })); + assert.notOk(this.server.received.post.to({ requestBody: { name: 'Frodo' } })); + done(); + }); +}); + +test('it matches requestHeaders', function(assert) { + let done = assert.async(); + + this.server.get('/contacts', () => true); + + $.ajax({ + method: 'GET', + url: '/contacts', + headers: { + 'X-Header': 'hello' + } + }).done(() => { + assert.ok(this.server.received.get.to('/contacts', { requestHeaders: { 'X-Header': 'hello' } })); + assert.ok(this.server.didNotReceive.get.to('/contacts', { requestHeaders: { 'X-Header': 'hi' } })); done(); }); }); -test('"to" accepts RegExps', function(assert) { +test('it matches URLs specified as RegExps', function(assert) { assert.expect(2); let done = assert.async(); @@ -110,10 +116,31 @@ test('server.requests.last returns the last request that was made', function(ass this.server.get('/contacts', () => true); this.server.get('/posts', () => true); - $.ajax({ url: '/contacts' }).done(() => { + $.ajax('/contacts').done(() => { $.ajax('/posts').done(() => { assert.equal(this.server.requests.last.url, '/posts'); done(); }); }); }); + +['get', 'post', 'put', 'delete', 'head'].forEach(verb => { + test(`it reports ${verb} to '/contacts'`, function(assert) { + assert.expect(4); + let done = assert.async(); + + this.server[verb]('/contacts', () => true); + + $.ajax({ + method: verb, + url: '/contacts' + }).done(() => { + assert.ok(this.server.received[verb].to('/contacts')); + assert.ok(this.server.received.request.to('/contacts')); + assert.notOk(this.server.didNotReceive[verb].to('/contacts')); + assert.notOk(this.server.didNotReceive.request.to('/contacts')); + done(); + }); + }); +}); +