Skip to content

Commit

Permalink
Add request assertion feature to server
Browse files Browse the repository at this point in the history
The server tracks all handled requests and provides an API to allow
users to check whether a request matching the given parameters has
been made since the server was started.

```javascript
  server.received.get.to('/contacts', { name: 'Darth Vader' })
  server.didNotReceive.post.to('any');
  server.received.get.to(/\/secrets.*/);
  server.didNotReceive.request.to('/robot/5');
```

There is also a getter for the last request made to the server:
```javascript
  server.lastRequest
```
  • Loading branch information
Aaron Quamme committed Sep 12, 2016
1 parent c07911a commit 4779275
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 1 deletion.
90 changes: 90 additions & 0 deletions addon/request-assertion.js
@@ -0,0 +1,90 @@
/* * RequestAssertion implements a simple DSL for asserting that specific requests were made.
* ```
* let it = new RequestAssertion(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');
* ```
*
*/
import _isPlainObject from 'lodash/lang/isPlainObject';

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;
}

let actualParams;
try {
actualParams = JSON.parse(request.requestBody || NaN); // || NaN makes parse throw if requestBody is null
} catch (e) {
actualParams = request.queryParams;
}

return Object.keys(expectedParams).every(k => String(actualParams[k]) === String(expectedParams[k]));
}
}

function to(requests, expectMatch, verb, ...toParams) {
if (requests.length === 0) {
return expectMatch === false;
}

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));

return foundMatch === expectMatch;
}

class Verb {
constructor(requests, received) {
['get', 'post', 'put', 'delete', 'head', 'request'].forEach(verb => {
Object.defineProperty(this, verb, {
get() {
let f = to.bind(null, requests, received, verb);
return { to: f, with: f };
}
});
});
}
}

export default class RequestAssertion {
constructor(requests = []) {
this._requests = requests;
}

get received() {
return new Verb(this._requests, true);
}

get didNotReceive() {
return new Verb(this._requests, false);
}
}

18 changes: 17 additions & 1 deletion addon/server.js
Expand Up @@ -8,6 +8,7 @@ import Schema from './orm/schema';
import assert from './assert';
import SerializerRegistry from './serializer-registry';
import RouteHandler from './route-handler';
import RequestAssertion from './request-assertion';
import Ember from 'ember';

import _pick from 'lodash/object/pick';
Expand Down Expand Up @@ -105,7 +106,6 @@ function extractRouteArguments(args) {
}

export default class Server {

constructor(options = {}) {
this.config(options);
}
Expand All @@ -117,6 +117,8 @@ export default class Server {

this.options = config;

this._requests = [];

this.timing = this.timing || config.timing || 400;
this.namespace = this.namespace || config.namespace || '';
this.urlPrefix = this.urlPrefix || config.urlPrefix || '';
Expand Down Expand Up @@ -362,6 +364,19 @@ export default class Server {
});
}

get received() {
return new RequestAssertion(this._requests).received;
}

get didNotReceive() {
return new RequestAssertion(this._requests).didNotReceive;
}

get lastRequest() {
let requests = this._requests;
return requests[requests.length - 1];
}

_defineRouteHandlerHelpers() {
[['get'], ['post'], ['put'], ['delete', 'del'], ['patch'], ['head']].forEach(([verb, alias]) => {
this[verb] = (path, ...args) => {
Expand Down Expand Up @@ -397,6 +412,7 @@ export default class Server {
this.pretender[verb](
fullPath,
(request) => {
this._requests.push(request);
let [ code, headers, response ] = routeHandler.handle(request);
return [ code, headers, this._serialize(response) ];
},
Expand Down
112 changes: 112 additions & 0 deletions tests/integration/server-request-assertion-test.js
@@ -0,0 +1,112 @@
import {module, test} from 'qunit';
import Server from 'ember-cli-mirage/server';

module('Integration | Server Request Assertion', {
beforeEach() {
this.server = new Server({
environment: 'development'
});
this.server.timing = 0;
this.server.logging = false;
},
afterEach() {
this.server.shutdown();
}
});

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(`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();
});
});
});

test('matches query params', function(assert) {
assert.expect(6);
let done = assert.async();

this.server.get('/contacts', () => true);

$.ajax({
method: 'GET',
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' }));
done();
});
});

test('matches post params', function(assert) {
assert.expect(4);
let done = assert.async();

this.server.post('/contacts', () => true);

$.ajax({
method: 'POST',
url: '/contacts',
data: JSON.stringify({ name: 'Bilbo', age: 111 })
}).done(() => {
assert.ok(this.server.received.post.to('/contacts', { name: 'Bilbo', age: 111 }));
assert.ok(this.server.received.post.with({ name: 'Bilbo' }));
assert.notOk(this.server.received.post.to('/contacts', { name: 'Frodo' }));
assert.notOk(this.server.received.post.with({ name: 'Frodo' }));
done();
});
});

test('to accepts RegExps', function(assert) {
assert.expect(2);
let done = assert.async();

this.server.get('/contacts/:id', () => true);

$.ajax({
method: 'GET',
url: '/contacts/5'
}).done(() => {
assert.ok(this.server.received.get.to(/\/contacts\/\d+$/));
assert.notOk(this.server.received.get.to(/\/shizzles\/\d+$/));
done();
});
});

test('lastRequest returns the last request that was made', function(assert) {
assert.expect(1);
let done = assert.async();

this.server.get('/contacts', () => true);
this.server.get('/posts', () => true);

$.ajax({ url: '/contacts' }).done(() => {
$.ajax('/posts').done(() => {
assert.equal(this.server.lastRequest.url, '/posts');
done();
});
});
});

0 comments on commit 4779275

Please sign in to comment.