Skip to content

Commit

Permalink
make tests faster
Browse files Browse the repository at this point in the history
  • Loading branch information
James McKinney committed Oct 7, 2014
1 parent 8403967 commit 4683ffd
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 38 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ install:
- npm install
after_script:
# @see https://github.com/cainus/node-coveralls#istanbul
- DELAY=1000 WHITELIST=httpbin.org,dummyimage.com,localhost istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage
- DELAY=100 WHITELIST=localhost istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ The code is just over 100 lines, making it easy to tailor to your needs.
npm start
curl -I http://localhost:5000/http%3A%2F%2Fwww.opennorth.ca%2Fimg%2Fheader_logo.png/352/72

The URL structure is `/:url/:width/:height`. The `:url` parameter must be escaped/encoded. If the remote image's width or height is greater than the given `:width` or `:height`, it will be resized, maintaining aspect ratio, and cropped. If smaller, it will be padded with white pixels. The equivalent ImageMagick command is:
The URL structure is `/:url/:width/:height`. The `:url` parameter must be escaped/encoded. If the remote image's width or height is greater than the given `:width` or `:height`, it will be resized, maintaining aspect ratio, and cropped. If smaller, it will be padded with white pixels. The equivalent ImageMagick command for the example URL above is:

convert in.jpg -thumbnail 100x100^> -gravity center -extent 100x100 out.jpg
convert input.jpg -thumbnail 352x72^> -gravity center -extent 352x72 output.jpg

The `Cache-Control` header sets a `max-age` of one year.

Expand All @@ -27,13 +27,13 @@ Image proxy:

* Supports HTTP and HTTPS
* Follows 301 and 302 redirects
* Sets a maximum timeout for the remote server, through a `DELAY` environment variable (default `5000`)
* Sets a maximum timeout for the remote server
* Handles complex MIME types like `image/jpeg; charset=utf-8`
* Optional whitelisting using regular expressions, through a `WHITELIST` environment variable
* Optional whitelisting using regular expressions

Image manipulation:

* Accepts a custom width and height, up to a maximum extent
* Accepts a custom width and height, up to 1000x1000
* Resizes, centers and crops the image

HTTP server:
Expand All @@ -43,6 +43,12 @@ HTTP server:

If you need more features, see [node-imageable](https://github.com/sdepold/node-imageable) and [node-imageable-server](https://github.com/dawanda/node-imageable-server).

### Environment variables

* `DELAY`: The timeout delay in milliseconds, after which the proxy will respond with a HTTP 504 Gateway Timeout server error. Default: `5000`
* `WHITELIST`: A comma-separated list of domains to whitelist, e.g. `.gov,facebook.com`, which will be transformed into the regular expressions `/\.gov$/` and `/facebook\.com$/`.
* `PORT`: If running the server, changes the port on which it listens. Default: `5000`

## Deployment

### Heroku
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"scripts": {
"start": "./bin/image-proxy",
"test": "DELAY=1000 WHITELIST=httpbin.org,dummyimage.com,localhost mocha --timeout 2500"
"test": "DELAY=100 WHITELIST=localhost mocha"
},
"dependencies": {
"express": ">=3",
Expand Down
16 changes: 16 additions & 0 deletions test/fixtures/server-cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-----BEGIN CERTIFICATE-----
MIICcTCCAdoCCQDTgzSLdDTF0TANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJV
UzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQKEwZKb3llbnQxEDAO
BgNVBAsTB05vZGUuanMxDzANBgNVBAMTBmFnZW50MjEgMB4GCSqGSIb3DQEJARYR
cnlAdGlueWNsb3Vkcy5vcmcwHhcNMTMwODAxMTExOTAwWhcNNDAxMjE2MTExOTAw
WjB9MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYD
VQQKEwZKb3llbnQxEDAOBgNVBAsTB05vZGUuanMxDzANBgNVBAMTBmFnZW50MjEg
MB4GCSqGSIb3DQEJARYRcnlAdGlueWNsb3Vkcy5vcmcwgZ8wDQYJKoZIhvcNAQEB
BQADgY0AMIGJAoGBAKGYRnu2BdY2R8flqKPLICWO/7NoRVGH4KZBY1uBF/VYXyA2
VT5O7461mt6oA372BItGyNxdbMEvQBRcLiXTueKF5D+KYu30bWem6A/AxxYvnqU4
tP+uhsXNuGNQTp8i0vBDM/nUx7QGeP1Kda6C936PCNt7wbGPKPNyACNMbnptAgMB
AAEwDQYJKoZIhvcNAQEFBQADgYEATzjDAPocPA2Jm8wrLBW+fOC478wMo9gT3Y3N
ZU6fnF2dEPFLNETCMtDxnKhi4hnBpaiZ0fu0oaR1cSDRIVtlyW4azNjny4495C0F
JLuP5P5pz+rJe+ImKw+mO1ARA9fUAL3VN6/kVXY/EspwWJcLbJ5jdsDmkRbV52hX
Th4jkAI=
-----END CERTIFICATE-----
15 changes: 15 additions & 0 deletions test/fixtures/server-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQChmEZ7tgXWNkfH5aijyyAljv+zaEVRh+CmQWNbgRf1WF8gNlU+
Tu+OtZreqAN+9gSLRsjcXWzBL0AUXC4l07niheQ/imLt9G1npugPwMcWL56lOLT/
robFzbhjUE6fItLwQzP51Me0Bnj9SnWugvd+jwjbe8GxjyjzcgAjTG56bQIDAQAB
AoGAd19C6g5731N30T5hRqY+GCC72a90TZc/p/Fz0Vva8/4VP3mDnSS4qMaVIlgh
RP++OZjPtqI5PbiG8MNrv7vZe0UXlV7oZE0IA+jomUXsplbwMFf6pkrqdyHi+cbm
rBudhmKeLUgNA6peMGVA83C5g2SMqU5kB+tWzZT7Rs9rsyECQQDWpXxZgULqbFZv
wjpIDGWjOpQZrv123bJ9TQ+VoskCu4vlyDJqDJPwnscl8NnzpFJriDARn0WrB2sd
8GCX1yEpAkEAwLo/MYG5elkNRsE5/vINSIo04Gu6tP/Sd7EBtHYAPHUPjs/MhhVX
tMIGtACheHMwjGRPyr8pboEp2LEap4GjpQJBALNsy+CJ0+TfwPVU96EIc+GZcvlx
NMErGyvwwclEtSDKo2vmCHZrozLtlu1ZQueOgbMPuZbRe8w2vEzfhe8HTtkCQAYy
NrPlwsvPLyEWN0IeEBVD9D0+2WrWSrL0auSdYpaPAOgLgDzTVNWH42VIG+jeczIg
S3xuNuvJlUnVL9Ew1s0CQQCly+gduXtvOYip1/Stm/65kT7d8ICQgjh0XSPw/kUC
llVMQY3z1iFCaj/z0Csr0t0kJ534bH7GP3LOoNruV0p9
-----END RSA PRIVATE KEY-----
171 changes: 140 additions & 31 deletions test/test.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,98 @@
var request = require('supertest')
, fs = require('fs')
, http = require('http')
, https = require('https')
, png = fs.readFileSync('test/fixtures/test.png')
, app = require('../lib/image-proxy')();

var mock = require('http').createServer(function (req, res) {
res.writeHead(200, {
'Content-Type': 'image/png; charset=utf-8'
});
res.end(require('fs').readFileSync('./test/fixtures/test.png'), 'binary');
});
mock.listen(8080);
function server(req, res) {
var path = require('url').parse(req.url).pathname;
if (path === '/301') {
res.writeHead(301, {
'Location': 'http://localhost:8080/test.png'
});
res.end();
}
else if (path === '/302') {
res.writeHead(302, {
'Location': 'http://localhost:8080/test.png'
});
res.end();
}
else if (path === '/location-relative') {
res.writeHead(302, {
'Location': '/test.png'
});
res.end();
}
else if (path === '/location-empty') {
res.writeHead(302, {
'Location': ''
});
res.end();
}
else if (path === '/location-missing') {
res.writeHead(302);
res.end();
}
else if (path === '/404') {
res.writeHead(404);
res.end();
}
else if (path === '/content-type-invalid') {
res.writeHead(200, {
'Content-Type': 'text/plain'
});
res.end();
}
else if (path === '/content-type-empty') {
res.writeHead(200, {
'Content-Type': ''
});
res.end();
}
else if (path === '/content-type-missing') {
res.writeHead(200);
res.end();
}
else if (path === '/timeout') {
res.writeHead(200, {
'Content-Type': 'image/png'
});
setTimeout(function () {
res.end(png, 'binary');
}, 1000);
}
else if (path === '/complex.png') {
res.writeHead(200, {
'Content-Type': 'image/png; charset=utf-8'
});
res.end(png, 'binary');
}
else if (path === '/test.png') {
res.writeHead(200, {
'Content-Type': 'image/png'
});
res.end(png, 'binary');
}
else {
res.writeHead(500);
res.end(path);
}
}

var options = {
// https://raw.githubusercontent.com/joyent/node/master/test/fixtures/keys/agent2-cert.pem
cert: fs.readFileSync('test/fixtures/server-cert.pem')
// https://raw.githubusercontent.com/joyent/node/master/test/fixtures/keys/agent2-key.pem
, key: fs.readFileSync('test/fixtures/server-key.pem')
};

// @see https://github.com/mikeal/request/issues/418
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
http.globalAgent.maxSockets = 100;
http.createServer(server).listen(8080);
https.createServer(options, server).listen(8081);

describe('GET /:url/:width/:height', function () {
it('fails if a host is not in the whitelist', function (done) {
Expand All @@ -19,32 +104,32 @@ describe('GET /:url/:width/:height', function () {

it('fails if width is a non-integer', function (done) {
request(app)
.get('/http%3A%2F%2Fdummyimage.com%2F500%2F500/noninteger/100')
.get('/http%3A%2F%2Flocalhost:8080%2Ftest.png/noninteger/100')
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(404, 'Expected width to be an integer', done);
});
it('fails if width is too large', function (done) {
request(app)
.get('/http%3A%2F%2Fdummyimage.com%2F500%2F500/1001/100')
.get('/http%3A%2F%2Flocalhost:8080%2Ftest.png/1001/100')
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(404, 'Expected width to be less than or equal to 1000', done);
});
it('fails if height is a non-integer', function (done) {
request(app)
.get('/http%3A%2F%2Fdummyimage.com%2F500%2F500/100/noninteger')
.get('/http%3A%2F%2Flocalhost:8080%2Ftest.png/100/noninteger')
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(404, 'Expected height to be an integer', done);
});
it('fails if height is too large', function (done) {
request(app)
.get('/http%3A%2F%2Fdummyimage.com%2F500%2F500/100/1001')
.get('/http%3A%2F%2Flocalhost:8080%2Ftest.png/100/1001')
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(404, 'Expected height to be less than or equal to 1000', done);
});

it('fails if the protocol is unsupported', function (done) {
request(app)
.get('/ftp%3A%2F%2Fhttpbin.org/100/100')
.get('/ftp%3A%2F%2Flocalhost:8080/100/100')
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(404, 'Expected URI scheme to be HTTP or HTTPS', done);
});
Expand All @@ -55,64 +140,88 @@ describe('GET /:url/:width/:height', function () {
.expect(404, 'Expected URI host to be non-empty', done);
});

it('follows 301 and 302 redirects', function (done) {
it('follows 301 redirects', function (done) {
request(app)
.get('/http%3A%2F%2Fhttpbin.org%2Fredirect-to%3Furl%3Dhttp%3A%2F%2Fdummyimage.com%2F500%2F500/100/100')
.get('/http%3A%2F%2Flocalhost:8080%2F301/100/100')
.expect('Content-Type', 'image/png')
.expect('Cache-Control', 'max-age=31536000, public')
.expect(200, done);
});
it('fails if a redirect has no Location header', function (done) {
it('follows 302 redirects', function (done) {
request(app)
.get('/http%3A%2F%2Flocalhost:8080%2F302/100/100')
.expect('Content-Type', 'image/png')
.expect('Cache-Control', 'max-age=31536000, public')
.expect(200, done);
});
// it('follows local redirects', function (done) {
// request(app)
// .get('/http%3A%2F%2Flocalhost:8080%2Flocation-relative/100/100')
// .expect('Content-Type', 'image/png')
// .expect('Cache-Control', 'max-age=31536000, public')
// .expect(200, done);
// });
it('fails if the Location header is empty', function (done) {
request(app)
.get('/http%3A%2F%2Flocalhost:8080%2Flocation-empty/100/100')
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(404, 'Expected response code 200, got 302', done);
});
it('fails if the Location header is missing', function (done) {
request(app)
.get('/http%3A%2F%2Fhttpbin.org%2Fredirect-to%3Furl/100/100')
.get('/http%3A%2F%2Flocalhost:8080%2Flocation-missing/100/100')
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(404, 'Expected response code 200, got 302', done);
});

it('fails if the status code is not 200', function (done) {
request(app)
.get('/http%3A%2F%2Fhttpbin.org%2Fstatus%2F404/100/100')
.get('/http%3A%2F%2Flocalhost:8080%2F404/100/100')
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(404, 'Expected response code 200, got 404', done);
});

it('fails if the content type is invalid', function (done) {
request(app)
.get('/http%3A%2F%2Flocalhost:8080%2Fcontent-type-invalid/100/100')
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(404, 'Expected content type image/gif, image/jpeg, image/png, image/jpg, got text/plain', done);
});
it('fails if the content type is empty', function (done) {
request(app)
.get('/http%3A%2F%2Fhttpbin.org%2Fresponse-headers%3FContent-Type/100/100')
.get('/http%3A%2F%2Flocalhost:8080%2Fcontent-type-empty/100/100')
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(404, 'Expected content type image/gif, image/jpeg, image/png, image/jpg, got ', done);
});
it('fails if the content type is invalid', function (done) {
it('fails if the content type is missing', function (done) {
request(app)
.get('/http%3A%2F%2Fhttpbin.org%2Fresponse-headers%3FContent-Type%3Dtext%2Fplain/100/100')
.get('/http%3A%2F%2Flocalhost:8080%2Fcontent-type-missing/100/100')
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(404, 'Expected content type image/gif, image/jpeg, image/png, image/jpg, got text/plain', done);
.expect(404, 'Expected content type image/gif, image/jpeg, image/png, image/jpg, got ', done);
});
it('parses a complex content type', function (done) {
request(app)
.get('/http%3A%2F%2Flocalhost:8080%2Ftest.png/100/100')
.get('/http%3A%2F%2Flocalhost:8080%2Fcomplex.png/100/100')
.expect('Content-Type', 'image/png')
.expect(200, done);
});

it('timesout if the request takes longer than 5 seconds');
// @todo This test doesn't pass...
// it('timesout if the request takes longer than 5 seconds', function (done) {
// request(app)
// .get('/http%3A%2F%2Fhttpbin.org%2Fdelay%2F2/100/100')
// .expect(504, done);
// });
it('timesout if the request takes too long', function (done) {
request(app)
.get('/http%3A%2F%2Flocalhost:8080%2Ftimeout/100/100')
.expect(504, done);
});

it('supports HTTP', function (done) {
request(app)
.get('/http%3A%2F%2Fdummyimage.com%2F500%2F500/100/100')
.get('/http%3A%2F%2Flocalhost:8080%2Ftest.png/100/100')
.expect('Content-Type', 'image/png')
.expect('Cache-Control', 'max-age=31536000, public')
.expect(200, done);
});
it('supports HTTPS', function (done) {
request(app)
.get('/https%3A%2F%2Fdummyimage.com%2F500%2F500/100/100')
.get('/https%3A%2F%2Flocalhost:8081%2Ftest.png/100/100')
.expect('Content-Type', 'image/png')
.expect('Cache-Control', 'max-age=31536000, public')
.expect(200, done);
Expand Down

0 comments on commit 4683ffd

Please sign in to comment.