Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Adds tests for send and redirect.

  • Loading branch information...
commit 3ea0d0ebcdaa7cacaf48e3db706093f2b1c1d37a 1 parent aa925d3
@cpsubrian authored
View
57 expres.js
@@ -1,5 +1,6 @@
-var url = require('url');
-var expres = module.exports = {};
+var url = require('url')
+ , utils = require('./utils')
+ , expres = module.exports = {};
expres.middleware = function (req, res, next) {
var ctx = res;
@@ -275,19 +276,19 @@ expres.methods = {
if (fn) delete obj.default;
var keys = Object.keys(obj);
- var key = req.accepts(keys);
+ var key = utils.accepts(keys, req.headers['Accept'] || req.headers['accept']);
this.set('Vary', 'Accept');
if (key) {
- this.set('Content-Type', normalizeType(key));
+ this.set('Content-Type', utils.normalizeType(key));
obj[key](req, this, next);
} else if (fn) {
fn();
} else {
var err = new Error('Not Acceptable');
err.status = 406;
- err.types = normalizeTypes(keys);
+ err.types = utils.normalizeTypes(keys);
next(err);
}
@@ -359,57 +360,57 @@ expres.methods = {
*
* res.redirect('/login');
*
- * @param {String} url
+ * @param {String} toUrl
* @param {Number} code
*/
- redirect: function(url){
+ redirect: function(toUrl){
var req = this.req
, head = 'HEAD' == req.method
, status = 302
, body;
- // allow status / url
+ // allow status / toUrl
if (2 == arguments.length) {
- if ('number' == typeof url) {
- status = url;
- url = arguments[1];
+ if ('number' == typeof toUrl) {
+ status = toUrl;
+ toUrl = arguments[1];
} else {
status = arguments[1];
}
}
// setup redirect map
- var map = { back: req.get('Referrer') || '/' };
+ var map = { back: req.headers['Referrer'] || '/' };
// perform redirect
- url = map[url] || url;
+ toUrl = map[toUrl] || toUrl;
// relative
- if (!~url.indexOf('://') && 0 != url.indexOf('//')) {
+ if (!~toUrl.indexOf('://') && 0 != toUrl.indexOf('//')) {
var path = '';
// relative to path
- if ('.' == url[0]) {
- url = req.path + '/' + url;
- // relative to mount-point
- } else if ('/' != url[0]) {
- url = path + '/' + url;
+ if ('.' == toUrl[0]) {
+ toUrl = url.parse(req.url).path + '/' + toUrl;
+ // relative to root
+ } else if ('/' != toUrl[0]) {
+ toUrl = '/' + toUrl;
}
// Absolute
- var host = req.get('Host');
- url = '//' + host + url;
+ var host = req.headers['host'] || req.headers['Host'];
+ toUrl = '//' + host + toUrl;
}
// Support text/{plain,html} by default
this.format({
- text: function(){
- body = statusCodes[status] + '. Redirecting to ' + url;
+ 'text/plain': function(){
+ body = http.STATUS_CODES[status] + '. Redirecting to ' + toUrl;
},
- html: function(){
- var u = utils.escape(url);
- body = '<p>' + statusCodes[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>';
+ 'text/html': function(){
+ var u = utils.escape(toUrl);
+ body = '<p>' + http.STATUS_CODES[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>';
},
default: function(){
@@ -418,8 +419,8 @@ expres.methods = {
});
// Respond
- this.statusCode = status;
- this.set('Location', url);
+ this.status(status);
+ this.set('Location', toUrl);
this.set('Content-Length', Buffer.byteLength(body));
this.end(head ? null : body);
}
View
2  package.json
@@ -6,7 +6,7 @@
"dependencies": {},
"devDependencies": {
"mocha": "*",
- "superagent": "~0.9.5",
+ "supertest": "~0.3.1",
"clone": "~0.1.1"
},
"scripts": {
View
10 test/common.js
@@ -3,7 +3,7 @@ util = require('util');
http = require('http');
expres = require('../');
clone = require('clone');
-superagent = require('superagent');
+supertest = require('supertest');
var server;
@@ -13,8 +13,12 @@ response = function () {
return res;
};
-get = function (path, cb, done) {
- superagent.get('http://localhost:9000' + path, cb);
+get = function (path, cb) {
+ request().get(path).set('Host', 'example.com').end(cb);
+};
+
+request = function () {
+ return supertest(server);
};
respond = function (cb) {
View
14 test/json.js
@@ -9,7 +9,7 @@ describe('json', function () {
res.json({ foo: 'bar' });
});
- get('/?callback=foo', function (res) {
+ get('/?callback=foo', function (err, res) {
assert.equal(res.text, '{\n "foo": "bar"\n}');
done();
});
@@ -21,7 +21,7 @@ describe('json', function () {
res.json({ hello: 'world' });
});
- get('/', function (res) {
+ get('/', function (err, res) {
assert.equal(res.statusCode, 200);
assert.equal(res.headers['content-type'], 'application/vnd.example+json');
done();
@@ -34,7 +34,7 @@ describe('json', function () {
res.json(null);
});
- get('/', function (res){
+ get('/', function (err, res){
assert.equal(res.headers['content-type'], 'application/json; charset=utf-8');
assert.equal(res.text, 'null');
done();
@@ -48,7 +48,7 @@ describe('json', function () {
res.json(['foo', 'bar', 'baz']);
});
- get('/', function (res) {
+ get('/', function (err, res) {
assert.equal(res.headers['content-type'], 'application/json; charset=utf-8');
assert.equal(res.text, '[\n "foo",\n "bar",\n "baz"\n]');
done();
@@ -62,7 +62,7 @@ describe('json', function () {
res.json({ name: 'tobi' });
});
- get('/', function (res) {
+ get('/', function (err, res) {
assert.equal(res.headers['content-type'], 'application/json; charset=utf-8');
assert.equal(res.text, '{\n "name": "tobi"\n}');
done();
@@ -77,7 +77,7 @@ describe('json', function () {
res.json(201, { id: 1 });
});
- get('/', function (res) {
+ get('/', function (err, res) {
assert.equal(res.statusCode, 201);
assert.equal(res.headers['content-type'], 'application/json; charset=utf-8');
assert.equal(res.text, '{\n "id": 1\n}');
@@ -92,7 +92,7 @@ describe('json', function () {
res.json({ id: 1 }, 201);
});
- get('/', function (res) {
+ get('/', function (err, res) {
assert.equal(res.statusCode, 201);
assert.equal(res.headers['content-type'], 'application/json; charset=utf-8');
assert.equal(res.text, '{\n "id": 1\n}');
View
16 test/jsonp.js
@@ -9,7 +9,7 @@ describe('jsonp', function(){
res.jsonp({ count: 1 });
});
- get('/?callback=something', function (res) {
+ get('/?callback=something', function (err, res) {
assert.equal(res.headers['content-type'], 'text/javascript; charset=utf-8');
assert.equal(res.text, 'something({\n "count": 1\n});');
done();
@@ -21,7 +21,7 @@ describe('jsonp', function(){
res.jsonp({ count: 1 });
});
- get('/?callback=callbacks[123]', function (res) {
+ get('/?callback=callbacks[123]', function (err, res) {
assert.equal(res.headers['content-type'], 'text/javascript; charset=utf-8');
assert.equal(res.text, 'callbacks[123]({\n "count": 1\n});');
done();
@@ -33,7 +33,7 @@ describe('jsonp', function(){
res.jsonp({});
});
- get('/?callback=foo;bar()', function (res) {
+ get('/?callback=foo;bar()', function (err, res) {
assert.equal(res.headers['content-type'], 'text/javascript; charset=utf-8');
assert.equal(res.text, 'foobar({});');
done();
@@ -46,7 +46,7 @@ describe('jsonp', function(){
res.jsonp(null);
});
- get('/', function (res) {
+ get('/', function (err, res) {
assert.equal(res.headers['content-type'], 'application/json; charset=utf-8');
assert.equal(res.text, 'null');
done();
@@ -60,7 +60,7 @@ describe('jsonp', function(){
res.jsonp(['foo', 'bar', 'baz']);
});
- get('/', function (res) {
+ get('/', function (err, res) {
assert.equal(res.headers['content-type'], 'application/json; charset=utf-8');
assert.equal(res.text, '[\n "foo",\n "bar",\n "baz"\n]');
done();
@@ -74,7 +74,7 @@ describe('jsonp', function(){
res.jsonp({ name: 'tobi' });
});
- get('/', function (res) {
+ get('/', function (err, res) {
assert.equal(res.headers['content-type'], 'application/json; charset=utf-8');
assert.equal(res.text, '{\n "name": "tobi"\n}');
done();
@@ -89,7 +89,7 @@ describe('jsonp', function(){
res.jsonp(201, { id: 1 });
});
- get('/', function (res) {
+ get('/', function (err, res) {
assert.equal(res.statusCode, 201);
assert.equal(res.headers['content-type'], 'application/json; charset=utf-8');
assert.equal(res.text, '{\n "id": 1\n}');
@@ -104,7 +104,7 @@ describe('jsonp', function(){
res.jsonp({ id: 1 }, 201);
});
- get('/', function (res) {
+ get('/', function (err, res) {
assert.equal(res.statusCode, 201);
assert.equal(res.headers['content-type'], 'application/json; charset=utf-8');
assert.equal(res.text, '{\n "id": 1\n}');
View
200 test/redirect.js
@@ -0,0 +1,200 @@
+describe('redirect', function(){
+
+ beforeEach(createServer);
+ afterEach(closeServer);
+
+ describe('.redirect(url)', function(){
+ it('should default to a 302 redirect', function(done){
+ respond(function(req, res){
+ res.redirect('http://google.com');
+ });
+
+ get('/', function (err, res) {
+ assert.equal(res.statusCode, 302);
+ assert.equal(res.headers['location'], 'http://google.com');
+ done();
+ });
+ });
+
+ describe('with leading //', function(){
+ it('should pass through scheme-relative urls', function(done){
+ respond(function(req, res){
+ res.redirect('//cuteoverload.com');
+ });
+
+ get('/', function (err, res) {
+ assert.equal(res.headers['location'], '//cuteoverload.com');
+ done();
+ });
+ });
+ });
+
+
+ describe('with leading /', function(){
+ it('should construct scheme-relative urls', function(done){
+ respond(function(req, res){
+ res.redirect('/login');
+ });
+
+ get('/', function (err, res) {
+ assert.equal(res.headers['location'], '//example.com/login');
+ done();
+ });
+ });
+ });
+
+ describe('with leading ./', function(){
+ it('should construct path-relative urls', function(done){
+ respond(function(req, res){
+ res.redirect('./edit');
+ });
+
+ get('/post/1', function (err, res) {
+ assert.equal(res.headers['location'], '//example.com/post/1/./edit');
+ done();
+ });
+ });
+ });
+
+ describe('with leading ../', function(){
+ it('should construct path-relative urls', function(done){
+ respond(function(req, res){
+ res.redirect('../new');
+ });
+
+ get('/post/1', function (err, res) {
+ assert.equal(res.headers['location'], '//example.com/post/1/../new');
+ done();
+ });
+ });
+ });
+
+ describe('without leading /', function(){
+ it('should construct mount-point relative urls', function(done){
+ respond(function(req, res){
+ res.redirect('login');
+ });
+
+ get('/', function (err, res) {
+ assert.equal(res.headers['location'], '//example.com/login');
+ done();
+ });
+ });
+ });
+ });
+
+ describe('.redirect(status, url)', function(){
+ it('should set the response status', function(done){
+ respond(function(req, res){
+ res.redirect(303, 'http://google.com');
+ });
+
+ get('/', function (err, res) {
+ assert.equal(res.statusCode, 303);
+ assert.equal(res.headers['location'], 'http://google.com');
+ done();
+ });
+ });
+ });
+
+ describe('.redirect(url, status)', function(){
+ it('should set the response status', function(done){
+ respond(function(req, res){
+ res.redirect('http://google.com', 303);
+ });
+
+ get('/', function (err, res) {
+ assert.equal(res.statusCode, 303);
+ assert.equal(res.headers['location'], 'http://google.com');
+ done();
+ });
+ });
+ });
+
+ describe('when the request method is HEAD', function(){
+ it('should ignore the body', function(done){
+ respond(function(req, res){
+ res.redirect('http://google.com');
+ });
+
+ request().head('/').end(function (err, res) {
+ assert.equal(res.headers['location'], 'http://google.com');
+ assert.equal(res.text, '');
+ done();
+ });
+ });
+ });
+
+ describe('when accepting html', function(){
+ it('should respond with html', function(done){
+ respond(function(req, res){
+ res.redirect('http://google.com');
+ });
+
+ request()
+ .get('/')
+ .set('Accept', 'text/html')
+ .set('Host', 'example.com')
+ .end(function (err, res) {
+ assert.equal(res.headers['location'], 'http://google.com');
+ assert.equal(res.text, '<p>Moved Temporarily. Redirecting to <a href="http://google.com">http://google.com</a></p>');
+ done();
+ });
+ });
+
+ it('should escape the url', function(done){
+ respond(function(req, res){
+ res.redirect('<lame>');
+ });
+
+ request()
+ .get('/')
+ .set('Accept', 'text/html')
+ .set('Host', 'example.com')
+ .end(function (err, res) {
+ assert.equal(res.text, '<p>Moved Temporarily. Redirecting to <a href="//example.com/&lt;lame&gt;">//example.com/&lt;lame&gt;</a></p>');
+ done();
+ });
+ });
+ });
+
+ describe('when accepting text', function(){
+ it('should respond with text', function(done){
+ respond(function(req, res){
+ res.redirect('http://google.com');
+ });
+
+ request()
+ .get('/')
+ .set('Accept', 'text/plain, */*')
+ .set('Host', 'example.com')
+ .end(function (err, res) {
+ assert.equal(res.headers['location'], 'http://google.com');
+ assert.equal(res.headers['content-length'], '51');
+ assert.equal(res.text, 'Moved Temporarily. Redirecting to http://google.com');
+ done();
+ });
+ });
+ });
+
+ describe('when accepting neither text or html', function(){
+ it('should respond with an empty body', function(done){
+ respond(function(req, res){
+ res.redirect('http://google.com');
+ });
+
+ request()
+ .get('/')
+ .set('Accept', 'application/octet-stream')
+ .set('Host', 'example.com')
+ .end(function (err, res) {
+ assert.equal(res.status, 302);
+ assert.equal(res.headers['location'], 'http://google.com');
+ assert.equal(res.headers['content-type'], undefined);
+ assert.equal(res.headers['content-length'], '0');
+ assert.equal(res.text, undefined);
+ done();
+ });
+ });
+ });
+});
View
206 test/send.js
@@ -0,0 +1,206 @@
+describe('send', function(){
+
+ beforeEach(createServer);
+ afterEach(closeServer);
+
+ describe('.send(null)', function(){
+ it('should set body to ""', function(done){
+ respond(function(req, res){
+ res.send(null);
+ });
+
+ request()
+ .get('/')
+ .end(function (err, res) {
+ assert.equal(res.text, undefined);
+ done();
+ });
+ });
+ });
+
+ describe('.send(undefined)', function(){
+ it('should set body to ""', function(done){
+ respond(function(req, res){
+ res.send(undefined);
+ });
+
+ request()
+ .get('/')
+ .end(function (err, res) {
+ assert.equal(res.text, undefined);
+ done();
+ });
+ });
+ });
+
+ describe('.send(code)', function(){
+ it('should set .statusCode', function(done){
+ respond(function(req, res){
+ assert.equal(res.send(201), res);
+ });
+
+ request()
+ .get('/')
+ .expect('Created')
+ .expect(201, done);
+ });
+ });
+
+ describe('.send(code, body)', function(){
+ it('should set .statusCode and body', function(done){
+ respond(function(req, res){
+ res.send(201, 'Created :)');
+ });
+
+ request()
+ .get('/')
+ .expect('Created :)')
+ .expect(201, done);
+ });
+ });
+
+ describe('.send(body, code)', function(){
+ it('should be supported for backwards compat', function(done){
+ respond(function(req, res){
+ res.send('Bad!', 400);
+ });
+
+ request()
+ .get('/')
+ .expect('Bad!')
+ .expect(400, done);
+ });
+ });
+
+ describe('.send(String)', function(){
+ it('should send as html', function(done){
+ respond(function(req, res){
+ res.send('<p>hey</p>');
+ });
+
+ request()
+ .get('/')
+ .end(function(err, res){
+ assert.equal(res.headers['content-type'], 'text/html; charset=utf-8');
+ assert.equal(res.text, '<p>hey</p>');
+ assert.equal(res.statusCode, 200);
+ done();
+ });
+ });
+
+ it('should not override Content-Type', function(done){
+ respond(function(req, res){
+ res.set('Content-Type', 'text/plain').send('hey');
+ });
+
+ request()
+ .get('/')
+ .expect('Content-Type', 'text/plain')
+ .expect('hey')
+ .expect(200, done);
+ });
+ });
+
+ describe('.send(Buffer)', function(){
+ it('should send as octet-stream', function(done){
+ respond(function(req, res){
+ res.send(new Buffer('hello'));
+ });
+
+ request()
+ .get('/')
+ .end(function(err, res){
+ assert.equal(res.headers['content-type'], 'application/octet-stream');
+ assert.equal(res.statusCode, 200);
+ done();
+ });
+ });
+
+ it('should not override Content-Type', function(done){
+ respond(function(req, res){
+ res.set('Content-Type', 'text/plain').send(new Buffer('hey'));
+ });
+
+ request()
+ .get('/')
+ .end(function(err, res){
+ assert.equal(res.headers['content-type'], 'text/plain');
+ assert.equal(res.text, 'hey');
+ assert.equal(res.statusCode, 200);
+ done();
+ });
+ });
+ });
+
+ describe('.send(Object)', function(){
+ it('should send as application/json', function(done){
+ respond(function(req, res){
+ res.send({ name: 'tobi' });
+ });
+
+ request()
+ .get('/')
+ .end(function(err, res){
+ assert.equal(res.headers['content-type'], 'application/json; charset=utf-8');
+ assert.equal(res.text, '{\n "name": "tobi"\n}');
+ done();
+ });
+ });
+ });
+
+ describe('when the request method is HEAD', function(){
+ it('should ignore the body', function(done){
+ respond(function(req, res){
+ res.send('yay');
+ });
+
+ request()
+ .head('/')
+ .expect('', done);
+ });
+ });
+
+ describe('when .statusCode is 204', function(){
+ it('should strip Content-* fields & body', function(done){
+ respond(function(req, res){
+ res.status(204).send('foo');
+ });
+
+ request()
+ .get('/')
+ .end(function(err, res){
+ assert.equal(res.headers['content-type'], undefined);
+ assert.equal(res.headers['content-length'], undefined);
+ assert.equal(res.text, undefined);
+ done();
+ });
+ });
+ });
+
+ describe('when .statusCode is 304', function(){
+ it('should strip Content-* fields & body', function(done){
+ respond(function(req, res){
+ res.status(304).send('foo');
+ });
+
+ request()
+ .get('/')
+ .end(function(err, res){
+ assert.equal(res.headers['content-type'], undefined);
+ assert.equal(res.headers['content-length'], undefined);
+ assert.equal(res.text, undefined);
+ done();
+ });
+ });
+ });
+
+ it('should not support jsonp callbacks', function(done){
+ respond(function(req, res){
+ res.send({ foo: 'bar' });
+ });
+
+ request()
+ .get('/?callback=foo')
+ .expect('{\n "foo": "bar"\n}', done);
+ });
+});
View
152 utils.js
@@ -0,0 +1,152 @@
+/**
+ * Utilities (primarily copied from express).
+ */
+
+/**
+ * Normalize the given `type`, for example "html" becomes "text/html".
+ *
+ * @param {String} type
+ * @return {String}
+ */
+exports.normalizeType = function(type){
+ return type;
+};
+
+/**
+ * Normalize `types`, for example "html" becomes "text/html".
+ *
+ * @param {Array} types
+ * @return {Array}
+ */
+exports.normalizeTypes = function(types){
+ var ret = [];
+
+ for (var i = 0; i < types.length; ++i) {
+ ret.push(types[i]);
+ }
+
+ return ret;
+};
+
+/**
+ * Return the acceptable type in `types`, if any.
+ *
+ * @param {Array} types
+ * @param {String} str
+ * @return {String}
+ */
+exports.acceptsArray = function(types, str){
+ // accept anything when Accept is not present
+ if (!str) return types[0];
+
+ // parse
+ var accepted = exports.parseAccept(str)
+ , normalized = exports.normalizeTypes(types)
+ , len = accepted.length;
+
+ for (var i = 0; i < len; ++i) {
+ for (var j = 0, jlen = types.length; j < jlen; ++j) {
+ if (exports.accept(normalized[j].split('/'), accepted[i])) {
+ return types[j];
+ }
+ }
+ }
+};
+
+/**
+ * Check if `type(s)` are acceptable based on
+ * the given `str`.
+ *
+ * @param {String|Array} type(s)
+ * @param {String} str
+ * @return {Boolean|String}
+ */
+exports.accepts = function(type, str){
+ if ('string' == typeof type) type = type.split(/ *, */);
+ return exports.acceptsArray(type, str);
+};
+
+/**
+ * Check if `type` array is acceptable for `other`.
+ *
+ * @param {Array} type
+ * @param {Object} other
+ * @return {Boolean}
+ */
+exports.accept = function(type, other){
+ return (type[0] == other.type || '*' == other.type)
+ && (type[1] == other.subtype || '*' == other.subtype);
+};
+
+/**
+ * Parse accept `str`, returning
+ * an array objects containing
+ * `.type` and `.subtype` along
+ * with the values provided by
+ * `parseQuality()`.
+ *
+ * @param {Type} name
+ * @return {Type}
+ */
+exports.parseAccept = function(str){
+ return exports
+ .parseQuality(str)
+ .map(function(obj){
+ var parts = obj.value.split('/');
+ obj.type = parts[0];
+ obj.subtype = parts[1];
+ return obj;
+ });
+};
+
+/**
+ * Parse quality `str`, returning an
+ * array of objects with `.value` and
+ * `.quality`.
+ *
+ * @param {Type} name
+ * @return {Type}
+ */
+exports.parseQuality = function(str){
+ return str
+ .split(/ *, */)
+ .map(quality)
+ .filter(function(obj){
+ return obj.quality;
+ })
+ .sort(function(a, b){
+ return b.quality - a.quality;
+ });
+};
+
+/**
+ * Parse quality `str` returning an
+ * object with `.value` and `.quality`.
+ *
+ * @param {String} str
+ * @return {Object}
+ */
+function quality(str) {
+ var parts = str.split(/ *; */)
+ , val = parts[0];
+
+ var q = parts[1]
+ ? parseFloat(parts[1].split(/ *= */)[1])
+ : 1;
+
+ return { value: val, quality: q };
+}
+
+/**
+ * Escape special characters in the given string of html.
+ *
+ * @param {String} html
+ * @return {String}
+ */
+exports.escape = function(html) {
+ return String(html)
+ .replace(/&/g, '&amp;')
+ .replace(/"/g, '&quot;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+};

0 comments on commit 3ea0d0e

Please sign in to comment.
Something went wrong with that request. Please try again.