Skip to content

Commit

Permalink
Adds .format to the response object. Closes eugef#94
Browse files Browse the repository at this point in the history
Signed-off-by: jmnsf <jmnsferreira@gmail.com>
  • Loading branch information
jmnsf committed Aug 1, 2016
1 parent cdefb3c commit 3da2831
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 6 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ option | description | default value
------ | ----------- | -------------
`eventEmitter` | event emitter used by `response` object | `mockEventEmitter`
`writableStream` | writable stream used by `response` object | `mockWritableStream`
`req` | Request object being responded to | null

> NOTE: The out-of-the-box mock event emitter included with `node-mocks-http` is
not a functional event emitter and as such does not actually emit events. If you
Expand All @@ -133,6 +134,15 @@ var res = httpMocks.createResponse({
// ...
```

### .createMocks()

```js
httpMocks.createMocks(reqOptions, resOptions)
```

Merges `createRequest` and `createResponse`. Passes given options object to each
constructor.

## Design Decisions

We wanted some simple mocks without a large framework.
Expand Down
18 changes: 18 additions & 0 deletions lib/http-mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,23 @@
var request = require('./mockRequest');
var response = require('./mockResponse');

/**
* Creates linked req and res objects. Enables using methods that require both
* objects to interact, for example res.format.
*
* @param {Object} reqOpts Options for req creation, see
* @mockRequest.createRequest
* @param {Object} resOpts Options for res creation, see
* @mockResponse.createResponse
* @return {Object} Object with both mocks: { req, res }
*/
var createRequestResponse = function (reqOpts, resOpts) {
var req = request.createRequest(reqOpts);
var res = response.createResponse(Object.assign({}, resOpts, { req: req }));

return { req: req, res: res };
};

exports.createRequest = request.createRequest;
exports.createResponse = response.createResponse;
exports.createMocks = createRequestResponse;
39 changes: 38 additions & 1 deletion lib/mockRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

var url = require('url');
var typeis = require('type-is');
var accepts = require('accepts');
var events = require('events');

var standardRequestOptions = [
Expand Down Expand Up @@ -76,6 +77,17 @@ function createRequest(options) {
//parse query string from url to object
if (Object.keys(mockRequest.query).length === 0) {
mockRequest.query = require('querystring').parse(mockRequest.url.split('?')[1]);

if (mockRequest.query.hasOwnProperty) return;

Object.defineProperty(
mockRequest.query,
'hasOwnProperty',
{
enumerable: false,
value: Object.hasOwnProperty.bind(mockRequest.query)
}
);
}

// attach any other provided objects into the request for more advanced testing
Expand Down Expand Up @@ -153,6 +165,32 @@ function createRequest(options) {
return typeis(mockRequest, types);
};

/**
* Function: accepts
*
* Checks for matching content types in the Accept header.
*
* Examples:
*
* mockRequest.headers['accept'] = 'application/json'
*
* mockRequest.accepts('json');
* // => 'json'
*
* mockRequest.accepts('html');
* // => false
*
* mockRequest.accepts(['html', 'json']);
* // => 'json'
*
* @param {String|String[]} types Mime type(s) to check against
* @return {String|false} Matching type or false if no match.
*/
mockRequest.accepts = function(types) {
var Accepts = accepts(mockRequest);
return Accepts.type(types);
};

/**
* Function: param
*
Expand Down Expand Up @@ -336,7 +374,6 @@ function createRequest(options) {
};

return mockRequest;

}

module.exports.createRequest = createRequest;
30 changes: 29 additions & 1 deletion lib/mockResponse.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ function createResponse(options) {
var writableStream = new WritableStream();
var eventEmitter = new EventEmitter();

var mockRequest = options.req;
// create mockResponse

var mockResponse = {};
Expand Down Expand Up @@ -328,7 +329,7 @@ function createResponse(options) {
* @return {ServerResponse} for chaining
* @api public
*/
mockResponse.contentType = mockResponse.type = function(type){
mockResponse.contentType = mockResponse.type = function(type) {
return mockResponse.set('Content-Type', type.indexOf('/') >= 0 ? type : mime.lookup(type));
};

Expand Down Expand Up @@ -565,6 +566,33 @@ function createResponse(options) {

};

/**
* Chooses the correct response function from the given supported types.
* This method requires that the mockResponse object be initialized with a
* mockRequest object reference, otherwise it will throw. @see createMocks.
*
* @param {Object} supported Object with formats to handler functions.
* @return {Object} Whatever handler function returns.
*/
mockResponse.format = function(supported) {
supported = supported || {};
var types = Object.keys(supported);

if (types.length === 0) return mockResponse.sendStatus(406);
if (!mockRequest) {
throw new Error(
'Request object unavailable. Use createMocks or pass in a ' +
'request object in createResponse to use format.'
);
}

var accepted = mockRequest.accepts(types);

if (accepted) return supported[accepted]();
if (supported.default) return supported.default();
return mockResponse.sendStatus(406);
};

// WritableStream.writable is not a function
// mockResponse.writable = function() {
// return writableStream.writable.apply(this, arguments);
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"node": ">=0.6"
},
"dependencies": {
"accepts": "^1.3.3",
"lodash.assign": "^4.0.6",
"mime": "^1.3.4",
"type-is": "^1.6.4"
Expand Down
22 changes: 21 additions & 1 deletion test/lib/mockRequest.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,27 @@ describe('mockRequest', function() {

});

describe('.accepts()', function() {
var request;

beforeEach(function() {
request = mockRequest.createRequest({ headers: { accept: 'text/html' }});
});

it('returns type if the same as Accept header', function() {
expect(request.accepts('text/html')).to.equal('text/html');
expect(request.accepts('html')).to.equal('html');
});

it('returns the first matching type of an array of types', function() {
expect(request.accepts(['json', 'html'])).to.equal('html');
});

it('returns false when types dont match', function() {
expect(request.accepts(['json', 'xml'])).to.equal(false);
});
});

describe('.param()', function() {
var request;

Expand Down Expand Up @@ -442,7 +463,6 @@ describe('mockRequest', function() {
expect(request.get('key')).to.not.exist;
expect(request.param('key')).to.be.undefined;
});

});

describe('helper functions', function() {
Expand Down
55 changes: 52 additions & 3 deletions test/lib/mockResponse.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var sinonChai = require('sinon-chai');
chai.use(sinonChai);

var mockResponse = require('../../lib/mockResponse');
var mockRequest = require('../../lib/mockRequest');

describe('mockResponse', function() {

Expand Down Expand Up @@ -562,16 +563,64 @@ describe('mockResponse', function() {
expect(response.emit).to.have.been.calledWith('send');
expect(response.emit).to.have.been.calledWith('end');
});

});

// TODO: fix in 2.0; method should mimic Express Response.redirect()
describe('.redirect()', function() {

it('method should mimic Express Response.redirect()');

});

describe('.format()', function() {
var response, request;

beforeEach(function () {
request = mockRequest.createRequest();
response = mockResponse.createResponse({ req: request });
});

it('sends 406 when given no supported formats', function() {
response.format({});
expect(response.statusCode).to.equal(406);
expect(response._getData()).to.equal('Not Acceptable');
});

it('throws when no request object is available', function() {
response = mockResponse.createResponse();

expect(function() {
response.format({ html: function() {} });
}).to.throw(Error, /Request object unavailable/);
});

it('calls the handler for the closest accepted type', function() {
var htmlSpy = sinon.spy();
var jsonSpy = sinon.spy();

request.headers.accept = 'application/json';
response.format({ html: htmlSpy, json: jsonSpy });

expect(htmlSpy).to.not.have.been.called;
expect(jsonSpy).to.have.been.called;
});

it('sends 406 when no match is found', function() {
var htmlSpy = sinon.spy();
var jsonSpy = sinon.spy();

request.headers.accept = 'text/xml';
response.format({ html: htmlSpy, json: jsonSpy });

expect(htmlSpy).to.not.have.been.called;
expect(jsonSpy).to.not.have.been.called;
expect(response.statusCode).to.equal(406);
});

it('runs default function if it exists and no match is found', function() {
var defaultSpy = sinon.spy();
response.format({ default: defaultSpy });
expect(defaultSpy).to.have.been.called;
});
});
});

// TODO: fix in 2.0; methods should be inherited from Node ServerResponse
Expand Down

0 comments on commit 3da2831

Please sign in to comment.