2 var after = require('after');
3 var Buffer = require('safe-buffer').Buffer
4 var express = require('../')
5 , request = require('supertest')
6 , assert = require('assert');
7 var onFinished = require('on-finished');
8 var path = require('path');
9 var should = require('should');
10 var fixtures = path.join(__dirname, 'fixtures');
11 var utils = require('./support/utils');
13 describe('res', function(){
14 describe('.sendFile(path)', function () {
15 it('should error missing path', function (done) {
16 var app = createApp();
20 .expect(500, /path.*required/, done);
23 it('should error for non-string path', function (done) {
24 var app = createApp(42)
28 .expect(500, /TypeError: path must be a string to res.sendFile/, done)
31 it('should transfer a file', function (done) {
32 var app = createApp(path.resolve(fixtures, 'name.txt'));
36 .expect(200, 'tobi', done);
39 it('should transfer a file with special characters in string', function (done) {
40 var app = createApp(path.resolve(fixtures, '% of dogs.txt'));
44 .expect(200, '20%', done);
47 it('should include ETag', function (done) {
48 var app = createApp(path.resolve(fixtures, 'name.txt'));
52 .expect('ETag', /^(?:W\/)?"[^"]+"$/)
53 .expect(200, 'tobi', done);
56 it('should 304 when ETag matches', function (done) {
57 var app = createApp(path.resolve(fixtures, 'name.txt'));
61 .expect('ETag', /^(?:W\/)?"[^"]+"$/)
62 .expect(200, 'tobi', function (err, res) {
63 if (err) return done(err);
64 var etag = res.headers.etag;
67 .set('If-None-Match', etag)
72 it('should 404 for directory', function (done) {
73 var app = createApp(path.resolve(fixtures, 'blog'));
80 it('should 404 when not found', function (done) {
81 var app = createApp(path.resolve(fixtures, 'does-no-exist'));
83 app.use(function (req, res) {
93 it('should not override manual content-types', function (done) {
96 app.use(function (req, res) {
97 res.contentType('application/x-bogus');
98 res.sendFile(path.resolve(fixtures, 'name.txt'));
103 .expect('Content-Type', 'application/x-bogus')
107 it('should not error if the client aborts', function (done) {
109 var cb = after(2, done)
112 app.use(function (req, res) {
113 setImmediate(function () {
114 res.sendFile(path.resolve(fixtures, 'name.txt'));
116 setTimeout(function () {
123 app.use(function (err, req, res, next) {
128 var server = app.listen()
129 var test = request(server).get('/')
133 describe('with "cacheControl" option', function () {
134 it('should enable cacheControl by default', function (done) {
135 var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'))
139 .expect('Cache-Control', 'public, max-age=0')
143 it('should accept cacheControl option', function (done) {
144 var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'), { cacheControl: false })
148 .expect(utils.shouldNotHaveHeader('Cache-Control'))
153 describe('with "dotfiles" option', function () {
154 it('should not serve dotfiles by default', function (done) {
155 var app = createApp(path.resolve(__dirname, 'fixtures/.name'));
162 it('should accept dotfiles option', function(done){
163 var app = createApp(path.resolve(__dirname, 'fixtures/.name'), { dotfiles: 'allow' });
168 .expect(shouldHaveBody(Buffer.from('tobi')))
173 describe('with "headers" option', function () {
174 it('should accept headers option', function (done) {
179 var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'), { headers: headers });
183 .expect('x-success', 'sent')
184 .expect('x-other', 'done')
188 it('should ignore headers option on 404', function (done) {
189 var headers = { 'x-success': 'sent' };
190 var app = createApp(path.resolve(__dirname, 'fixtures/does-not-exist'), { headers: headers });
194 .expect(utils.shouldNotHaveHeader('X-Success'))
199 describe('with "immutable" option', function () {
200 it('should add immutable cache-control directive', function (done) {
201 var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'), {
208 .expect('Cache-Control', 'public, max-age=14400, immutable')
213 describe('with "maxAge" option', function () {
214 it('should set cache-control max-age from number', function (done) {
215 var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'), {
221 .expect('Cache-Control', 'public, max-age=14400')
225 it('should set cache-control max-age from string', function (done) {
226 var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'), {
232 .expect('Cache-Control', 'public, max-age=14400')
237 describe('with "root" option', function () {
238 it('should not transfer relative with without', function (done) {
239 var app = createApp('test/fixtures/name.txt');
243 .expect(500, /must be absolute/, done);
246 it('should serve relative to "root"', function (done) {
247 var app = createApp('name.txt', {root: fixtures});
251 .expect(200, 'tobi', done);
254 it('should disallow requesting out of "root"', function (done) {
255 var app = createApp('foo/../../user.html', {root: fixtures});
264 describe('.sendFile(path, fn)', function () {
265 it('should invoke the callback when complete', function (done) {
266 var cb = after(2, done);
267 var app = createApp(path.resolve(fixtures, 'name.txt'), cb);
274 it('should invoke the callback when client aborts', function (done) {
275 var cb = after(1, done);
278 app.use(function (req, res) {
279 setImmediate(function () {
280 res.sendFile(path.resolve(fixtures, 'name.txt'), function (err) {
282 err.code.should.equal('ECONNABORTED');
289 var server = app.listen()
290 var test = request(server).get('/')
291 test.expect(200, cb);
294 it('should invoke the callback when client already aborted', function (done) {
295 var cb = after(1, done);
298 app.use(function (req, res) {
299 onFinished(res, function () {
300 res.sendFile(path.resolve(fixtures, 'name.txt'), function (err) {
302 err.code.should.equal('ECONNABORTED');
309 var server = app.listen()
310 var test = request(server).get('/')
311 test.expect(200, cb);
314 it('should invoke the callback without error when HEAD', function (done) {
316 var cb = after(2, done);
318 app.use(function (req, res) {
319 res.sendFile(path.resolve(fixtures, 'name.txt'), cb);
327 it('should invoke the callback without error when 304', function (done) {
329 var cb = after(3, done);
331 app.use(function (req, res) {
332 res.sendFile(path.resolve(fixtures, 'name.txt'), cb);
337 .expect('ETag', /^(?:W\/)?"[^"]+"$/)
338 .expect(200, 'tobi', function (err, res) {
339 if (err) return cb(err);
340 var etag = res.headers.etag;
343 .set('If-None-Match', etag)
348 it('should invoke the callback on 404', function(done){
351 app.use(function (req, res) {
352 res.sendFile(path.resolve(fixtures, 'does-not-exist'), function (err) {
354 err.status.should.equal(404);
361 .expect(200, 'got it', done);
365 describe('.sendFile(path, options)', function () {
366 it('should pass options to send module', function (done) {
367 request(createApp(path.resolve(fixtures, 'name.txt'), { start: 0, end: 1 }))
369 .expect(200, 'to', done)
373 describe('.sendfile(path, fn)', function(){
374 it('should invoke the callback when complete', function(done){
376 var cb = after(2, done);
378 app.use(function(req, res){
379 res.sendfile('test/fixtures/user.html', cb)
387 it('should utilize the same options as express.static()', function(done){
390 app.use(function(req, res){
391 res.sendfile('test/fixtures/user.html', { maxAge: 60000 });
396 .expect('Cache-Control', 'public, max-age=60')
400 it('should invoke the callback when client aborts', function (done) {
401 var cb = after(1, done);
404 app.use(function (req, res) {
405 setImmediate(function () {
406 res.sendfile('test/fixtures/name.txt', function (err) {
408 err.code.should.equal('ECONNABORTED');
415 var server = app.listen()
416 var test = request(server).get('/')
417 test.expect(200, cb);
420 it('should invoke the callback when client already aborted', function (done) {
421 var cb = after(1, done);
424 app.use(function (req, res) {
425 onFinished(res, function () {
426 res.sendfile('test/fixtures/name.txt', function (err) {
428 err.code.should.equal('ECONNABORTED');
435 var server = app.listen()
436 var test = request(server).get('/')
437 test.expect(200, cb);
440 it('should invoke the callback without error when HEAD', function (done) {
442 var cb = after(2, done);
444 app.use(function (req, res) {
445 res.sendfile('test/fixtures/name.txt', cb);
453 it('should invoke the callback without error when 304', function (done) {
455 var cb = after(3, done);
457 app.use(function (req, res) {
458 res.sendfile('test/fixtures/name.txt', cb);
463 .expect('ETag', /^(?:W\/)?"[^"]+"$/)
464 .expect(200, 'tobi', function (err, res) {
465 if (err) return cb(err);
466 var etag = res.headers.etag;
469 .set('If-None-Match', etag)
474 it('should invoke the callback on 404', function(done){
478 app.use(function(req, res){
479 res.sendfile('test/fixtures/nope.html', function(err){
480 assert.equal(calls++, 0);
481 assert(!res.headersSent);
482 res.send(err.message);
488 .expect(200, /^ENOENT.*?, stat/, done);
491 it('should not override manual content-types', function(done){
494 app.use(function(req, res){
495 res.contentType('txt');
496 res.sendfile('test/fixtures/user.html');
501 .expect('Content-Type', 'text/plain; charset=utf-8')
505 it('should invoke the callback on 403', function(done){
508 app.use(function(req, res){
509 res.sendfile('test/fixtures/foo/../user.html', function(err){
510 assert(!res.headersSent);
511 res.send(err.message);
521 it('should invoke the callback on socket error', function(done){
524 app.use(function(req, res){
525 res.sendfile('test/fixtures/user.html', function(err){
526 assert(!res.headersSent);
527 req.socket.listeners('error').should.have.length(1); // node's original handler
531 req.socket.emit('error', new Error('broken!'));
540 describe('.sendfile(path)', function(){
541 it('should not serve dotfiles', function(done){
544 app.use(function(req, res){
545 res.sendfile('test/fixtures/.name');
553 it('should accept dotfiles option', function(done){
556 app.use(function(req, res){
557 res.sendfile('test/fixtures/.name', { dotfiles: 'allow' });
563 .expect(shouldHaveBody(Buffer.from('tobi')))
567 it('should accept headers option', function(done){
574 app.use(function(req, res){
575 res.sendfile('test/fixtures/user.html', { headers: headers });
580 .expect('x-success', 'sent')
581 .expect('x-other', 'done')
585 it('should ignore headers option on 404', function(done){
587 var headers = { 'x-success': 'sent' };
589 app.use(function(req, res){
590 res.sendfile('test/fixtures/user.nothing', { headers: headers });
595 .expect(utils.shouldNotHaveHeader('X-Success'))
599 it('should transfer a file', function (done) {
602 app.use(function (req, res) {
603 res.sendfile('test/fixtures/name.txt');
608 .expect(200, 'tobi', done);
611 it('should transfer a directory index file', function (done) {
614 app.use(function (req, res) {
615 res.sendfile('test/fixtures/blog/');
620 .expect(200, '<b>index</b>', done);
623 it('should 404 for directory without trailing slash', function (done) {
626 app.use(function (req, res) {
627 res.sendfile('test/fixtures/blog');
635 it('should transfer a file with urlencoded name', function (done) {
638 app.use(function (req, res) {
639 res.sendfile('test/fixtures/%25%20of%20dogs.txt');
644 .expect(200, '20%', done);
647 it('should not error if the client aborts', function (done) {
649 var cb = after(2, done)
652 app.use(function (req, res) {
653 setImmediate(function () {
654 res.sendfile(path.resolve(fixtures, 'name.txt'));
656 setTimeout(function () {
663 app.use(function (err, req, res, next) {
668 var server = app.listen()
669 var test = request(server).get('/')
673 describe('with an absolute path', function(){
674 it('should transfer the file', function(done){
677 app.use(function(req, res){
678 res.sendfile(path.join(__dirname, '/fixtures/user.html'))
683 .expect('Content-Type', 'text/html; charset=UTF-8')
684 .expect(200, '<p>{{user.name}}</p>', done);
688 describe('with a relative path', function(){
689 it('should transfer the file', function(done){
692 app.use(function(req, res){
693 res.sendfile('test/fixtures/user.html');
698 .expect('Content-Type', 'text/html; charset=UTF-8')
699 .expect(200, '<p>{{user.name}}</p>', done);
702 it('should serve relative to "root"', function(done){
705 app.use(function(req, res){
706 res.sendfile('user.html', { root: 'test/fixtures/' });
711 .expect('Content-Type', 'text/html; charset=UTF-8')
712 .expect(200, '<p>{{user.name}}</p>', done);
715 it('should consider ../ malicious when "root" is not set', function(done){
718 app.use(function(req, res){
719 res.sendfile('test/fixtures/foo/../user.html');
727 it('should allow ../ when "root" is set', function(done){
730 app.use(function(req, res){
731 res.sendfile('foo/../user.html', { root: 'test/fixtures' });
739 it('should disallow requesting out of "root"', function(done){
742 app.use(function(req, res){
743 res.sendfile('foo/../../user.html', { root: 'test/fixtures' });
751 it('should next(404) when not found', function(done){
755 app.use(function(req, res){
756 res.sendfile('user.html');
759 app.use(function(req, res){
760 assert(0, 'this should not be called');
763 app.use(function(err, req, res, next){
770 .end(function(err, res){
771 res.statusCode.should.equal(404);
772 calls.should.equal(1);
777 describe('with non-GET', function(){
778 it('should still serve', function(done){
781 app.use(function(req, res){
782 res.sendfile(path.join(__dirname, '/fixtures/name.txt'))
787 .expect('tobi', done);
794 describe('.sendfile(path, options)', function () {
795 it('should pass options to send module', function (done) {
798 app.use(function (req, res) {
799 res.sendfile(path.resolve(fixtures, 'name.txt'), { start: 0, end: 1 })
804 .expect(200, 'to', done)
808 function createApp(path, options, fn) {
811 app.use(function (req, res) {
812 res.sendFile(path, options, fn);
818 function shouldHaveBody (buf) {
819 return function (res) {
820 var body = !Buffer.isBuffer(res.body)
821 ? Buffer.from(res.text)
823 assert.ok(body, 'response has body')
824 assert.strictEqual(body.toString('hex'), buf.toString('hex'))