var _context = require('../lib/context');
var request = require('supertest');
var assert = require('assert');
var koa = require('..');
var fs = require('fs');
function Context(app, req, res) {
this.app = app;
this.req = req;
this.res = res;
}
Context.prototype = _context;
function context(req, res) {
req = req || { headers: {} };
res = res || { _headers: {} };
res.setHeader = function(k, v){ res._headers[k.toLowerCase()] = v };
var ctx = new Context({}, req, res);
return ctx;
}
describe('ctx.body=', function(){
describe('when Content-Type is set', function(){
it('should not override', function(){
var ctx = context();
ctx.type = 'png';
ctx.body = new Buffer('something');
assert('image/png' == ctx.responseHeader['content-type']);
})
describe('when body is an object', function(){
it('should override as json', function(){
var ctx = context();
ctx.body = 'hey';
assert('text/html; charset=utf-8' == ctx.responseHeader['content-type']);
ctx.body = { foo: 'bar' };
assert('application/json' == ctx.responseHeader['content-type']);
})
})
it('should override length', function(){
var ctx = context();
ctx.type = 'html';
ctx.body = 'something';
ctx.responseLength.should.equal(9);
})
})
describe('when a string is given', function(){
it('should default to text', function(){
var ctx = context();
ctx.body = 'Tobi';
assert('text/plain; charset=utf-8' == ctx.responseHeader['content-type']);
})
it('should set length', function(){
var ctx = context();
ctx.body = 'Tobi';
assert('4' == ctx.responseHeader['content-length']);
})
})
describe('when an html string is given', function(){
it('should default to html', function(){
var ctx = context();
ctx.body = '
Tobi
';
assert('text/html; charset=utf-8' == ctx.responseHeader['content-type']);
})
it('should set length', function(){
var string = 'Tobi
';
var ctx = context();
ctx.body = string;
assert.equal(ctx.responseLength, Buffer.byteLength(string));
})
it('should set length when body is overriden', function(){
var string = 'Tobi
';
var ctx = context();
ctx.body = string;
ctx.body = string + string;
assert.equal(ctx.responseLength, 2 * Buffer.byteLength(string));
})
})
describe('when a stream is given', function(){
it('should default to an octet stream', function(){
var ctx = context();
ctx.body = fs.createReadStream('LICENSE');
assert('application/octet-stream' == ctx.responseHeader['content-type']);
})
})
describe('when a buffer is given', function(){
it('should default to an octet stream', function(){
var ctx = context();
ctx.body = new Buffer('hey');
assert('application/octet-stream' == ctx.responseHeader['content-type']);
})
it('should set length', function(){
var ctx = context();
ctx.body = new Buffer('Tobi');
assert('4' == ctx.responseHeader['content-length']);
})
})
describe('when an object is given', function(){
it('should default to json', function(){
var ctx = context();
ctx.body = { foo: 'bar' };
assert('application/json' == ctx.responseHeader['content-type']);
})
})
})
describe('ctx.error(msg)', function(){
it('should set .status to 500', function(done){
var ctx = context();
try {
ctx.error('boom');
} catch (err) {
assert(500 == err.status);
done();
}
})
})
describe('ctx.error(msg, status)', function(){
it('should throw an error', function(done){
var ctx = context();
try {
ctx.error('name required', 400);
} catch (err) {
assert('name required' == err.message);
assert(400 == err.status);
done();
}
})
})
describe('ctx.error(status)', function(){
it('should throw an error', function(done){
var ctx = context();
try {
ctx.error(400);
} catch (err) {
assert('Bad Request' == err.message);
assert(400 == err.status);
done();
}
})
})
describe('ctx.length', function(){
describe('when Content-Length is defined', function(){
it('should return a number', function(){
var ctx = context();
ctx.header['content-length'] = '120';
ctx.length.should.equal(120);
})
})
})
describe('ctx.responseLength', function(){
describe('when Content-Length is defined', function(){
it('should return a number', function(){
var ctx = context();
ctx.set('Content-Length', '1024');
ctx.responseLength.should.equal(1024);
})
})
describe('when Content-Length is not defined', function(){
describe('and a .body is set', function(){
it('should return a number', function(){
var ctx = context();
ctx.body = 'foo';
ctx.responseLength.should.equal(3);
ctx.body = new Buffer('foo');
ctx.responseLength.should.equal(3);
})
})
describe('and .body is not', function(){
it('should return undefined', function(){
var ctx = context();
assert(null == ctx.responseLength);
})
})
})
})
describe('ctx.header', function(){
it('should return the request header object', function(){
var ctx = context();
ctx.header.should.equal(ctx.req.headers);
})
})
describe('ctx.protocol', function(){
describe('when encrypted', function(){
it('should return "https"', function(){
var ctx = context();
ctx.req.socket = { encrypted: true };
ctx.protocol.should.equal('https');
})
})
describe('when unencrypted', function(){
it('should return "http"', function(){
var ctx = context();
ctx.req.socket = {};
ctx.protocol.should.equal('http');
})
})
describe('when X-Forwarded-Proto is set', function(){
describe('and proxy is trusted', function(){
it('should be used', function(){
var ctx = context();
ctx.app.proxy = true;
ctx.req.socket = {};
ctx.header['x-forwarded-proto'] = 'https, http';
ctx.protocol.should.equal('https');
})
})
describe('and proxy is not trusted', function(){
it('should not be used', function(){
var ctx = context();
ctx.req.socket = {};
ctx.header['x-forwarded-proto'] = 'https, http';
ctx.protocol.should.equal('http');
})
})
})
})
describe('ctx.secure', function(){
it('should return true when encrypted', function(){
var ctx = context();
ctx.req.socket = { encrypted: true };
ctx.secure.should.be.true;
})
})
describe('ctx.host', function(){
it('should return host void of port', function(){
var ctx = context();
ctx.header.host = 'foo.com:3000';
ctx.host.should.equal('foo.com');
})
describe('when X-Forwarded-Host is present', function(){
describe('and proxy is not trusted', function(){
it('should be ignored', function(){
var ctx = context();
ctx.header['x-forwarded-host'] = 'bar.com';
ctx.header['host'] = 'foo.com';
ctx.host.should.equal('foo.com')
})
})
describe('and proxy is trusted', function(){
it('should be used', function(){
var ctx = context();
ctx.app.proxy = true;
ctx.header['x-forwarded-host'] = 'bar.com, baz.com';
ctx.header['host'] = 'foo.com';
ctx.host.should.equal('bar.com')
})
})
})
})
describe('ctx.status=', function(){
describe('when a status string', function(){
describe('and valid', function(){
it('should set the status', function(){
var ctx = context();
ctx.status = 'forbidden';
ctx.status.should.equal(403);
})
it('should be case-insensitive', function(){
var ctx = context();
ctx.status = 'ForBidden';
ctx.status.should.equal(403);
})
})
describe('and invalid', function(){
it('should throw', function(){
var ctx = context();
var err;
try {
ctx.status = 'maru';
} catch (e) {
err = e;
}
assert(err);
})
})
})
function strip(status) {
it('should strip content related header fields', function(done){
var app = koa();
app.use(function(next){
return function *(){
this.body = { foo: 'bar' };
this.set('Content-Type', 'application/json');
this.set('Content-Length', '15');
this.set('Transfer-Encoding', 'chunked');
this.status = status;
assert(null == this.responseHeader['content-type']);
assert(null == this.responseHeader['content-length']);
assert(null == this.responseHeader['transfer-encoding']);
}
});
request(app.listen())
.get('/')
.expect(status)
.end(done);
})
}
describe('when 204', function(){
strip(204);
})
describe('when 304', function(){
strip(304);
})
})
describe('ctx.stale', function(){
it('should be the inverse of ctx.fresh', function(){
var ctx = context();
ctx.status = 200;
ctx.req.method = 'GET';
ctx.req.headers['if-none-match'] = '123';
ctx.res._headers['etag'] = '123';
ctx.fresh.should.be.true;
ctx.stale.should.be.false;
})
})
describe('ctx.fresh', function(){
describe('the response is non-2xx', function(){
it('should return false', function(){
var ctx = context();
ctx.status = 404;
ctx.req.method = 'GET';
ctx.req.headers['if-none-match'] = '123';
ctx.res._headers['etag'] = '123';
ctx.fresh.should.be.false;
})
});
describe('the response is 2xx', function(){
describe('and etag matches', function(){
it('should return true', function(){
var ctx = context();
ctx.status = 200;
ctx.req.method = 'GET';
ctx.req.headers['if-none-match'] = '123';
ctx.res._headers['etag'] = '123';
ctx.fresh.should.be.true;
})
})
describe('and etag do not match', function(){
it('should return false', function(){
var ctx = context();
ctx.status = 200;
ctx.req.method = 'GET';
ctx.req.headers['if-none-match'] = '123';
ctx.res._headers['etag'] = 'hey';
ctx.fresh.should.be.false;
})
})
})
})
describe('ctx.accepted', function(){
describe('when Accept is populated', function(){
it('should return accepted types', function(){
var ctx = context();
ctx.req.headers.accept = 'application/*;q=0.2, image/jpeg;q=0.8, text/html, text/plain';
ctx.accepted.should.eql(['text/plain', 'text/html', 'image/jpeg', 'application/*']);
})
})
describe('when Accept is not populated', function(){
it('should return an empty array', function(){
var ctx = context();
ctx.accepted.should.eql([]);
})
})
})
describe('ctx.vary(field)', function(){
describe('when Vary is not set', function(){
it('should set it', function(){
var ctx = context();
ctx.vary('Accept');
ctx.responseHeader.vary.should.equal('Accept');
})
})
describe('when Vary is set', function(){
it('should append', function(){
var ctx = context();
ctx.vary('Accept');
ctx.vary('Accept-Encoding');
ctx.responseHeader.vary.should.equal('Accept, Accept-Encoding');
})
})
describe('when Vary already contains the value', function(){
it('should not append', function(){
var ctx = context();
ctx.vary('Accept');
ctx.vary('Accept-Encoding');
ctx.vary('Accept');
ctx.vary('Accept-Encoding');
ctx.responseHeader.vary.should.equal('Accept, Accept-Encoding');
})
})
})
describe('ctx.accepts(types)', function(){
describe('with no types', function(){
it('should return false', function(){
var ctx = context();
ctx.accepts('').should.be.false;
})
})
describe('when extensions are given', function(){
it('should convert to mime types', function(){
var ctx = context();
ctx.req.headers.accept = 'text/plain, text/html';
ctx.accepts('html').should.equal('html');
ctx.accepts('.html').should.equal('.html');
ctx.accepts('txt').should.equal('txt');
ctx.accepts('.txt').should.equal('.txt');
ctx.accepts('png').should.be.false;
})
})
describe('when an array is given', function(){
it('should return the first match', function(){
var ctx = context();
ctx.req.headers.accept = 'text/plain, text/html';
ctx.accepts(['png', 'text', 'html']).should.equal('text');
ctx.accepts(['png', 'html']).should.equal('html');
})
})
describe('when multiple arguments are given', function(){
it('should return the first match', function(){
var ctx = context();
ctx.req.headers.accept = 'text/plain, text/html';
ctx.accepts('png', 'text', 'html').should.equal('text');
ctx.accepts('png', 'html').should.equal('html');
})
})
describe('when present in Accept as an exact match', function(){
it('should return the type', function(){
var ctx = context();
ctx.req.headers.accept = 'text/plain, text/html';
ctx.accepts('text/html').should.equal('text/html');
ctx.accepts('text/plain').should.equal('text/plain');
})
})
describe('when present in Accept as a type match', function(){
it('should return the type', function(){
var ctx = context();
ctx.req.headers.accept = 'application/json, */*';
ctx.accepts('text/html').should.equal('text/html');
ctx.accepts('text/plain').should.equal('text/plain');
ctx.accepts('image/png').should.equal('image/png');
})
})
describe('when present in Accept as a subtype match', function(){
it('should return the type', function(){
var ctx = context();
ctx.req.headers.accept = 'application/json, text/*';
ctx.accepts('text/html').should.equal('text/html');
ctx.accepts('text/plain').should.equal('text/plain');
ctx.accepts('image/png').should.be.false;
})
})
})
describe('ctx.acceptedLanguages', function(){
describe('when Accept-Language is populated', function(){
it('should return accepted types', function(){
var ctx = context();
ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt';
ctx.acceptedLanguages.should.eql(['pt', 'es', 'en']);
})
})
describe('when Accept-Language is not populated', function(){
it('should return an empty array', function(){
var ctx = context();
ctx.acceptedLanguages.should.eql([]);
})
})
})
describe('ctx.acceptedCharsets', function(){
describe('when Accept-Charset is populated', function(){
it('should return accepted types', function(){
var ctx = context();
ctx.req.headers['accept-charset'] = 'utf-8, iso-8859-1;q=0.2, utf-7;q=0.5';
ctx.acceptedCharsets.should.eql(['utf-8', 'utf-7', 'iso-8859-1']);
})
})
describe('when Accept-Charset is not populated', function(){
it('should return an empty array', function(){
var ctx = context();
ctx.acceptedCharsets.should.eql([]);
})
})
})
describe('ctx.acceptedEncodings', function(){
describe('when Accept-Encoding is populated', function(){
it('should return accepted types', function(){
var ctx = context();
ctx.req.headers['accept-encoding'] = 'gzip, compress;q=0.2';
ctx.acceptedEncodings.should.eql(['gzip', 'compress', 'identity']);
})
})
describe('when Accept-Encoding is not populated', function(){
it('should return identity', function(){
var ctx = context();
ctx.acceptedEncodings.should.eql(['identity']);
})
})
})
describe('ctx.path', function(){
it('should return the pathname', function(){
var ctx = new Context(null, {
url: '/login?next=/dashboard'
});
ctx.path.should.equal('/login');
})
})
describe('ctx.path=', function(){
it('should set the pathname', function(){
var ctx = new Context(null, {
url: '/login?next=/dashboard'
});
ctx.path = '/logout';
ctx.path.should.equal('/logout');
ctx.url.should.equal('/logout?next=/dashboard');
})
})
describe('ctx.get(name)', function(){
it('should return the field value', function(){
var req = { headers: { 'host': 'http://google.com' } };
var ctx = new Context(null, req);
ctx.get('HOST').should.equal('http://google.com');
ctx.get('Host').should.equal('http://google.com');
ctx.get('host').should.equal('http://google.com');
})
})
describe('ctx.set(name, val)', function(){
it('should set a field value', function(){
var ctx = context();
ctx.set('x-foo', 'bar');
ctx.responseHeader['x-foo'].should.equal('bar');
})
it('should coerce to a string', function(){
var ctx = context();
ctx.set('x-foo', 5);
ctx.responseHeader['x-foo'].should.equal('5');
})
})
describe('ctx.set(object)', function(){
it('should set multiple fields', function(){
var ctx = context();
ctx.set({
foo: '1',
bar: '2'
});
ctx.responseHeader.foo.should.equal('1');
ctx.responseHeader.bar.should.equal('2');
})
})
describe('ctx.query', function(){
describe('when missing', function(){
it('should return an empty object', function(){
var ctx = context({ url: '/' });
ctx.query.should.eql({});
})
})
it('should return a parsed query-string', function(){
var ctx = context({ url: '/?page=2' });
ctx.query.page.should.equal('2');
})
})
describe('ctx.query=', function(){
it('should stringify and replace the querystring', function(){
var ctx = context({ url: '/store/shoes' });
ctx.query = { page: 2, color: 'blue' };
ctx.url.should.equal('/store/shoes?page=2&color=blue');
ctx.querystring.should.equal('page=2&color=blue');
})
})
describe('ctx.querystring=', function(){
it('should replace the querystring', function(){
var ctx = context({ url: '/store/shoes' });
ctx.querystring = 'page=2&color=blue';
ctx.url.should.equal('/store/shoes?page=2&color=blue');
ctx.querystring.should.equal('page=2&color=blue');
})
})
describe('ctx.type=', function(){
describe('with a mime', function(){
it('should set the Content-Type', function(){
var ctx = context();
ctx.type = 'text/plain';
ctx.responseHeader['content-type'].should.equal('text/plain');
})
})
describe('with an extension', function(){
it('should lookup the mime', function(){
var ctx = context();
ctx.type = 'json';
ctx.responseHeader['content-type'].should.equal('application/json');
})
})
})
describe('ctx.type', function(){
describe('with no Content-Type', function(){
it('should return null', function(){
var ctx = context();
assert(null == ctx.type);
})
})
describe('with a Content-Type', function(){
it('should return the mime', function(){
var ctx = context();
ctx.req.headers['content-type'] = 'text/html; charset=utf8';
ctx.type.should.equal('text/html');
})
})
})
describe('ctx.is(type)', function(){
it('should ignore params', function(){
var ctx = context();
ctx.header['content-type'] = 'text/html; charset=utf-8';
ctx.is('text/*').should.be.true;
})
describe('given a mime', function(){
it('should check the type', function(){
var ctx = context();
ctx.header['content-type'] = 'image/png';
ctx.is('image/png').should.be.true;
ctx.is('image/*').should.be.true;
ctx.is('*/png').should.be.true;
ctx.is('image/jpeg').should.be.false;
ctx.is('text/*').should.be.false;
ctx.is('*/jpeg').should.be.false;
})
})
describe('given an extension', function(){
it('should check the type', function(){
var ctx = context();
ctx.header['content-type'] = 'image/png';
ctx.is('png').should.be.true;
ctx.is('.png').should.be.true;
ctx.is('jpeg').should.be.false;
ctx.is('.jpeg').should.be.false;
})
})
})
describe('ctx.attachment([filename])', function(){
describe('when given a filename', function(){
it('should set the filename param', function(){
var ctx = context();
ctx.attachment('path/to/tobi.png');
var str = 'attachment; filename="tobi.png"';
ctx.responseHeader['content-disposition'].should.equal(str);
})
})
describe('when omitting filename', function(){
it('should not set filename param', function(){
var ctx = context();
ctx.attachment();
ctx.responseHeader['content-disposition'].should.equal('attachment');
})
})
})
describe('ctx.redirect(url)', function(){
it('should redirect to the given url', function(){
var ctx = context();
ctx.redirect('http://google.com');
ctx.responseHeader.location.should.equal('http://google.com');
ctx.status.should.equal(302);
})
describe('with "back"', function(){
it('should redirect to Referrer', function(){
var ctx = context();
ctx.req.headers.referrer = '/login';
ctx.redirect('back');
ctx.responseHeader.location.should.equal('/login');
})
it('should redirect to Referer', function(){
var ctx = context();
ctx.req.headers.referer = '/login';
ctx.redirect('back');
ctx.responseHeader.location.should.equal('/login');
})
it('should default to alt', function(){
var ctx = context();
ctx.redirect('back', '/index.html');
ctx.responseHeader.location.should.equal('/index.html');
})
it('should default redirect to /', function(){
var ctx = context();
ctx.redirect('back');
ctx.responseHeader.location.should.equal('/');
})
})
describe('when html is accepted', function(){
it('should respond with html', function(){
var ctx = context();
var url = 'http://google.com';
ctx.header.accept = 'text/html';
ctx.redirect(url);
ctx.responseHeader['content-type'].should.equal('text/html; charset=utf-8');
ctx.body.should.equal('Redirecting to ' + url + '.');
})
it('should escape the url', function(){
var ctx = context();
var url = '