Skip to content

Commit

Permalink
Request assertion improvements
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Aaron Quamme committed Oct 16, 2016
1 parent 35c5b0a commit 6a1e8f9
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 85 deletions.
114 changes: 61 additions & 53 deletions 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 = [ <Two GET requests, both made to '/posts'> ];
*
* 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) };
}
});
});
Expand All @@ -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);
}
}

91 changes: 59 additions & 32 deletions tests/integration/server-request-assertion-test.js
Expand Up @@ -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);
Expand All @@ -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();
});
});
Expand All @@ -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();

Expand All @@ -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();
});
});
});

0 comments on commit 6a1e8f9

Please sign in to comment.