Skip to content

Commit 964e3a4

Browse files
committed
added download method to server responder
1 parent c9faeea commit 964e3a4

File tree

6 files changed

+222
-5
lines changed

6 files changed

+222
-5
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules
22
coverage
33
log/*.log
4+
tmp/*.*

.jshintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
"predef" : [ // Extra globals.
1212
"__dirname",
13+
"__filename",
1314
"Buffer",
1415
"event",
1516
"exports",

lib/server/responder.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,30 @@ var responder = {
107107
res.header('Location', url);
108108
res.send(status);
109109
return next(false);
110+
},
111+
112+
download: function (res, args, next) {
113+
args = args || {};
114+
args.contentType = args.contentType || 'application/octet-stream';
115+
116+
if (!args.filename) {
117+
return responder.error(res, new restify.InternalError('Download requires filename arg'), next);
118+
}
119+
120+
if (!args.stream) {
121+
return responder.error(res, new restify.InternalError('Download requires stream arg'), next);
122+
}
123+
124+
res.setHeader('Content-Type', args.contentType);
125+
res.setHeader('Content-Disposition', 'attachment; filename=' + args.filename);
126+
127+
if (args.contentLength) {
128+
res.setHeader('Content-Length', args.contentLength);
129+
}
130+
131+
res.status(200);
132+
args.stream.pipe(res);
133+
next();
110134
}
111135
};
112136

test/server/responder.functional.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
var assert = require('assert');
2+
var fs = require('fs');
3+
var path = require('path');
24
var restify = require('restify');
35
var support = require('../support');
46
var http = support.http;
@@ -33,6 +35,19 @@ var methods = {
3335
redirect_to_relative_path: function (req, res, next) {
3436
var args = {url: './foo/bar'};
3537
return responder.redirect(req, res, args, next);
38+
},
39+
40+
download_content: function (req, res, next) {
41+
var stats = fs.statSync(__filename);
42+
43+
var args = {
44+
filename: 'responder.functional.js',
45+
contentType: 'application/javascript',
46+
stream: fs.createReadStream(__filename),
47+
contentLength: stats.size
48+
};
49+
50+
return responder.download(res, args, next);
3651
}
3752
};
3853

@@ -72,19 +87,27 @@ var routes = [
7287
url: "/test/redirects/relative_path",
7388
func: methods.redirect_to_relative_path,
7489
middleware: []
90+
},
91+
{
92+
method: "get",
93+
url: "/test/downloads/content",
94+
func: methods.download_content,
95+
middleware: []
7596
}
7697
];
7798

7899
describe("functional - server/responder.js", function () {
79100
var server;
80101
var http_client;
81102
var http_string_client;
103+
var http_raw_client;
82104

83105
before(function () {
84106
server = http.server.create(routes);
85107
server.start();
86108
http_client = http.client();
87109
http_string_client = http.string_client();
110+
http_raw_client = http.raw_client();
88111
});
89112

90113
after(function () {
@@ -164,4 +187,39 @@ describe("functional - server/responder.js", function () {
164187
});
165188
});
166189
});
190+
191+
describe("download()", function () {
192+
var filename, stream, stats;
193+
194+
it("returns correct headers and content", function (done) {
195+
http_raw_client.get('/test/downloads/content', function (err, data, res, req) {
196+
assert.ifError(err); // connection error;
197+
198+
req.on('result', function (err, res) {
199+
assert.ifError(err); // HTTP status code >= 400;
200+
201+
filename = path.join(__dirname, '../../tmp', 'test.js');
202+
stream = fs.createWriteStream(filename);
203+
204+
res.on('data', function (chunk) {
205+
stream.write(chunk);
206+
});
207+
208+
res.on('end', function () {
209+
stream.end(function () {
210+
assert.equal(res.headers['content-type'], 'application/javascript');
211+
assert.equal(res.headers['content-length'], 7948);
212+
assert.equal(res.headers['content-disposition'], 'attachment; filename=responder.functional.js');
213+
assert.equal(res.statusCode, 200);
214+
215+
stats = fs.statSync(filename);
216+
assert.equal(stats.size, 7948);
217+
218+
done();
219+
});
220+
});
221+
});
222+
});
223+
});
224+
});
167225
});

test/server/responder.unit.js

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
var assert = require('assert');
2+
var fs = require('fs');
23
var sinon = require('sinon');
34
var support = require('../support');
45
var responder = main.server.responder;
@@ -7,7 +8,8 @@ var fake_res = {
78
header: function () {},
89
status: function () {},
910
json: function () {},
10-
send: function () {}
11+
send: function () {},
12+
setHeader: function () {}
1113
};
1214

1315
var fake_req = {
@@ -125,4 +127,117 @@ describe("responder.js", function () {
125127
});
126128
});
127129
});
130+
131+
describe("download()", function () {
132+
var args;
133+
134+
beforeEach(function () {
135+
args = {
136+
filename: 'foo.js',
137+
contentType: 'application/javascript',
138+
stream: fs.createReadStream(__filename),
139+
contentLength: 2000
140+
141+
};
142+
});
143+
144+
context("when no args passed in", function () {
145+
it("responds with an InternalError", function (done) {
146+
sinon.stub(responder, 'error', function (res, err, next) {
147+
next();
148+
});
149+
150+
responder.download(fake_res, null, fake_next);
151+
152+
assert.ok(responder.error.called);
153+
154+
responder.error.restore();
155+
done();
156+
});
157+
});
158+
159+
context("when args.filename not passed in", function () {
160+
it("responds with an InternalError", function (done) {
161+
sinon.stub(responder, 'error', function (res, err, next) {
162+
next();
163+
});
164+
165+
delete args.filename;
166+
responder.download(fake_res, args, fake_next);
167+
168+
assert.ok(responder.error.called);
169+
170+
responder.error.restore();
171+
done();
172+
});
173+
});
174+
175+
context("when args.stream not passed in", function () {
176+
it("responds with an InternalError", function (done) {
177+
sinon.stub(responder, 'error', function (res, err, next) {
178+
next();
179+
});
180+
181+
delete args.stream;
182+
responder.download(fake_res, args, fake_next);
183+
184+
assert.ok(responder.error.called);
185+
186+
responder.error.restore();
187+
done();
188+
});
189+
});
190+
191+
context("when args.contentType not passed in", function () {
192+
it("responds with Content-Type header set to application/octet-stream", function (done) {
193+
sinon.spy(fake_res, 'setHeader');
194+
sinon.stub(args.stream, 'pipe', function () { });
195+
196+
delete args.contentType;
197+
responder.download(fake_res, args, fake_next);
198+
199+
assert.ok(fake_res.setHeader.calledWith('Content-Type', 'application/octet-stream'));
200+
201+
fake_res.setHeader.restore();
202+
args.stream.pipe.restore();
203+
done();
204+
});
205+
});
206+
207+
context("when args.contentLength not passed in", function () {
208+
it("responds without Content-Length header set", function (done) {
209+
sinon.spy(fake_res, 'setHeader');
210+
sinon.stub(args.stream, 'pipe', function () { });
211+
212+
delete args.contentLength;
213+
responder.download(fake_res, args, fake_next);
214+
215+
assert.ok(!fake_res.setHeader.calledWith('Content-Length'));
216+
217+
fake_res.setHeader.restore();
218+
args.stream.pipe.restore();
219+
done();
220+
});
221+
});
222+
223+
context("when args passed in correctly", function () {
224+
it("responds without 200 status and content", function (done) {
225+
sinon.spy(fake_res, 'setHeader');
226+
sinon.spy(fake_res, 'status');
227+
sinon.stub(args.stream, 'pipe', function () { });
228+
229+
responder.download(fake_res, args, fake_next);
230+
231+
assert.ok(fake_res.setHeader.calledWith('Content-Type', 'application/javascript'));
232+
assert.ok(fake_res.setHeader.calledWith('Content-Length', 2000));
233+
assert.ok(fake_res.setHeader.calledWith('Content-Disposition', 'attachment; filename=foo.js'));
234+
assert.ok(fake_res.status.calledWith(200));
235+
assert.ok(args.stream.pipe.calledWith(fake_res));
236+
237+
fake_res.setHeader.restore();
238+
args.stream.pipe.restore();
239+
done();
240+
});
241+
});
242+
});
128243
});

test/support/http.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ TestServer.prototype.stop = function () {
3131
};
3232

3333
function HttpClient(args) {
34+
var method;
35+
3436
args = args || {};
3537
if (!args.host) {
3638
throw new Error('HTTP Client requires a host param');
@@ -41,11 +43,23 @@ function HttpClient(args) {
4143
this.port = args.port;
4244
this.url = 'http://' + this.host + ':' + this.port;
4345

44-
if (args.type === 'string') {
45-
this.client = restify.createStringClient({url: this.url});
46-
} else {
47-
this.client = restify.createJsonClient({url: this.url});
46+
method = 'createJsonClient';
47+
args.type = args.type || 'json';
48+
49+
switch (args.type) {
50+
case 'http':
51+
method = 'createClient';
52+
break;
53+
54+
case 'string':
55+
method = 'createStringClient';
56+
break;
57+
58+
default:
59+
break;
4860
}
61+
62+
this.client = restify[method]({url: this.url});
4963
}
5064

5165
HttpClient.prototype.get = function (path_or_options, cb) {
@@ -87,6 +101,10 @@ var http = {
87101
return new HttpClient({host: host, port: port});
88102
},
89103

104+
raw_client: function () {
105+
return new HttpClient({host: host, port: port, type: 'http'});
106+
},
107+
90108
server: {
91109
create: function (routes) {
92110
return new TestServer({routes: routes});

0 commit comments

Comments
 (0)