UNPKG

combohandler

Version:

Simple Yahoo!-style combo handler.

921 lines (760 loc) 37 kB
/*global describe, before, after, it, sinon */ require("should-http"); var fs = require('fs'); var path = require('path'); var combo = require('../'), express = require('express'), server = require('../lib/server'), assert = require('assert'), request = require('request'), PORT = 8942, BASE_URL = 'http://localhost:' + PORT; process.env.NODE_ENV = 'test'; var FIXTURES_DIR = __dirname + '/fixtures'; describe('combohandler', function () { /*jshint expr:true */ var app, httpServer; before(function (done) { app = server({ roots: { '/css': FIXTURES_DIR + '/root/css', '/js' : FIXTURES_DIR + '/root/js' } }); httpServer = app.listen(PORT, done); }); after(function (done) { httpServer.close(done); }); it("should accept baseApp in server export", function () { var baseApp = express(); var spyUse = sinon.spy(baseApp, "use"); server({}, baseApp); spyUse.called.should.not.be.ok; spyUse.restore(); }); it("should setup logger and errorHandler in development", function () { process.env.NODE_ENV = '';// default = 'development' var devApp = server({}), routerStack = devApp._router.stack; routerStack.should.be.an.Array.and.have.lengthOf(5); routerStack[4].should.have.property("handle"); routerStack[4].handle.should.be.a.Function; routerStack[4].handle.name.should.equal("comboErrorHandler"); }); it("should only use combo.errorHandler in baseless production env", function () { process.env.NODE_ENV = 'production'; var prodApp = server({}), routerStack = prodApp._router.stack; routerStack.should.be.an.Array.and.have.lengthOf(3); routerStack[2].should.have.property("handle"); routerStack[2].handle.should.be.a.Function; routerStack[2].handle.name.should.equal("comboErrorHandler"); }); it("should return an array of middleware callbacks when invoked", function () { var callbacks = combo.combine(); callbacks.should.be.an.instanceOf(Array); callbacks.should.have.lengthOf(1); callbacks[0].name.should.equal('combineMiddleware'); }); it('should combine JavaScript', function (done) { request(BASE_URL + '/js?a.js&b.js', function (err, res, body) { assert.ifError(err); res.should.have.status(200); res.should.have.header('content-type', 'application/javascript; charset=utf-8'); res.should.have.header('last-modified'); body.should.equal('a();\n\nb();\n'); done(); }); }); it('should combine CSS', function (done) { request(BASE_URL + '/css?a.css&b.css', function (err, res, body) { assert.ifError(err); res.should.have.status(200); res.should.have.header('content-type', 'text/css; charset=utf-8'); res.should.have.header('last-modified'); body.should.equal('.a { color: green; }\n\n.b { color: green; }\n'); done(); }); }); it('should support symlinks pointing outside the root', function (done) { request(BASE_URL + '/js?a.js&b.js&outside.js', function (err, res, body) { assert.ifError(err); res.should.have.status(200); res.should.have.header('content-type', 'application/javascript; charset=utf-8'); res.should.have.header('last-modified'); body.should.equal('a();\n\nb();\n\noutside();\n'); done(); }); }); // -- Config Options ------------------------------------------------------- describe('config: maxAge', function () { before(function () { app.get('/max-age-null', combo.combine({ rootPath: FIXTURES_DIR + '/root/js', maxAge : null }), combo.respond); app.get('/max-age-0', combo.combine({ rootPath: FIXTURES_DIR + '/root/js', maxAge : 0 }), combo.respond); }); it('should default to one year', function (done) { request(BASE_URL + '/js?a.js', function (err, res, body) { assert.ifError(err); res.should.have.status(200); res.should.have.header('cache-control', 'public,max-age=31536000'); res.headers.should.have.property('expires'); var expires = new Date(res.headers.expires); ((expires - Date.now()) / 1000).should.be.within(31535990, 31536000); done(); }); }); it('should not set Cache-Control and Expires headers when maxAge is null', function (done) { request(BASE_URL + '/max-age-null?a.js', function (err, res, body) { assert.ifError(err); res.should.have.status(200); res.headers.should.not.have.property('cache-control'); res.headers.should.not.have.property('expires'); done(); }); }); it('should support a maxAge of 0', function (done) { request(BASE_URL + '/max-age-0?a.js', function (err, res, body) { assert.ifError(err); res.should.have.status(200); res.should.have.header('cache-control', 'public,max-age=0'); res.headers.should.have.property('expires'); var expires = new Date(res.headers.expires); ((expires - Date.now()) / 1000).should.be.within(-5, 5); done(); }); }); }); describe('config: errorMaxAge', function () { before(function () { function throwBadRequest(req, res, next) { next(new combo.BadRequest('errorMaxAge')); } app.use('/error-max-age-null', throwBadRequest); app.use('/error-max-age-null', combo.errorHandler({ errorMaxAge: null })); app.use('/error-max-age-0', throwBadRequest); app.use('/error-max-age-0', combo.errorHandler({ errorMaxAge: 0 })); }); it('should default to five minutes', function (done) { request(BASE_URL + '/js?err.js', function (err, res, body) { res.should.have.status(400); res.should.have.header('cache-control', 'public,max-age=300'); res.headers.should.have.property('expires'); var expires = new Date(res.headers.expires); ((expires - Date.now()) / 1000).should.be.within(290, 300); done(); }); }); it('should set private Cache-Control and no-cache headers when errorMaxAge is null', function (done) { request(BASE_URL + '/error-max-age-null?a.js', function (err, res, body) { assert.ifError(err); res.should.have.status(400); res.should.have.header('cache-control', 'private,no-store'); res.should.have.header('pragma', 'no-cache'); res.headers.should.have.property('expires'); var expires = new Date(res.headers.expires).getTime(); expires.should.equal(new Date(0).getTime()); done(); }); }); it('should support an errorMaxAge of 0', function (done) { request(BASE_URL + '/error-max-age-0?a.js', function (err, res, body) { assert.ifError(err); res.should.have.status(400); res.should.have.header('cache-control', 'public,max-age=0'); res.headers.should.have.property('expires'); var expires = new Date(res.headers.expires); ((expires - Date.now()) / 1000).should.be.within(-5, 5); done(); }); }); }); describe('config: basePath', function () { describe('when absent', function () { it("should NOT append cssUrls middleware to callbacks", function () { var callbacks = combo.combine(); callbacks.should.have.lengthOf(1); callbacks[0].name.should.not.equal('cssUrlsMiddleware'); }); }); describe('when present', function () { it("should append cssUrls middleware to callbacks", function () { var callbacks = combo.combine({ basePath: 'foo' }); callbacks.should.have.lengthOf(2); callbacks[1].name.should.equal('cssUrlsMiddleware'); }); }); }); describe('config: webRoot', function () { describe('when absent', function () { it("should NOT append cssUrls middleware to callbacks", function () { var callbacks = combo.combine(); callbacks.should.have.lengthOf(1); callbacks[0].name.should.not.equal('cssUrlsMiddleware'); }); }); describe('when present', function () { it("should append cssUrls middleware to callbacks", function () { var callbacks = combo.combine({ webRoot: 'foo' }); callbacks.should.have.lengthOf(2); callbacks[1].name.should.equal('cssUrlsMiddleware'); }); }); }); describe('config: rootPath', function () { it("should error when value does not exist", function () { /*jshint immed: false */ (function () { combo.combine({ rootPath: '/foo' }); }).should.throwError(); }); describe('with route parameters', function () { it("should prepend dynamicPath middleware to callbacks", function () { var callbacks = combo.combine({ rootPath: FIXTURES_DIR + '/:root/js' }); callbacks.should.have.lengthOf(2); callbacks[0].name.should.equal('dynamicPathMiddleware'); }); }); }); // -- Errors --------------------------------------------------------------- describe('errors', function () { before(function () { app.get('/error-next?', function (req, res, next) { var poo = new Error('poo'); poo.stack = null; // silence irrelevant output next(poo); }, combo.combine({ rootPath: FIXTURES_DIR + '/root/js' }), combo.respond); app.get('/error-throw?', combo.combine({ rootPath: FIXTURES_DIR + '/root/js' }), function (req, res, next) { throw 'poo'; }, combo.respond); }); it('should inherit from Error', function () { var err = new combo.BadRequest('test'); err.should.be.an.instanceOf(Error); err.name.should.equal('BadRequest'); err.message.should.equal('test'); }); it('should set content-type text/plain when responding 400 Bad Request', function (done) { request(BASE_URL + '/js?bogus.js', function (err, res, body) { assert.ifError(err); res.should.have.status(400); res.should.have.header('content-type', 'text/plain; charset=utf-8'); done(); }); }); it('should return a 500 when error before middleware', assertResponds({ path: '/error-next?a.js', status: 500 })); it('should return a 500 when error after middleware', assertResponds({ path: '/error-throw?a.js', status: 500 })); it('should return a 400 Bad Request error when no files are specified', assertResponds({ path: '/js', body: 'Bad request. No files requested.', status: 400 })); it('should throw a 400 Bad Request error when a file is not found', assertResponds({ path: '/js?bogus.js', body: 'Bad request. File not found: bogus.js', status: 400 })); it('should throw a 400 Bad Request error when a white-listed MIME type is not found', assertResponds({ path: '/js?foo.bar', body: 'Bad request. Illegal MIME type present.', status: 400 })); it('should throw a 400 Bad Request error when an unmapped MIME type is found with other valid types', assertResponds({ path: '/js?a.js&foo.bar', body: 'Bad request. Only one MIME type allowed per request.', status: 400 })); it('should throw a 400 Bad Request error when more than one valid MIME type is found', assertResponds({ path: '/js?a.js&b.css', body: 'Bad request. Only one MIME type allowed per request.', status: 400 })); it('should throw a 400 Bad Request error when a querystring is truncated', assertResponds({ path: '/js?a.js&b', body: 'Bad request. Truncated query parameters.', status: 400 })); it('should throw a 400 Bad Request error when a querystring is dramatically truncated', assertResponds({ path: '/js?a', body: 'Bad request. Truncated query parameters.', status: 400 })); describe('path traversal', function () { var paths = [ '../../../../package.json', '..%2f..%2f..%2f..%2fpackage.json', '%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fpackage.json', '%2e%2e/%2e%2e/%2e%2e/%2e%2e/package.json', '..\\..\\..\\..\\package.json', '..%5c..%5c..%5c..%5cpackage.json', '%2e%2e%5c%2e%2e%5c%2e%2e%5c%2e%2e%5cpackage.json', '%2e%2e\\%2e%2e\\%2e%2e\\%2e%2e\\package.json', '....//....//....//....//package.json', '....%2f%2f....%2f%2f....%2f%2f....%2f%2fpackage.json', '....\\\\....\\\\....\\\\....\\\\package.json', '....%5c%5c....%5c%5c....%5c%5c....%5c%5cpackage.json' ]; paths.forEach(function (path) { it('should throw a 400 Bad Request error when path traversal is attempted: ' + path, function (done) { request(BASE_URL + '/js?' + path, function (err, res, body) { assert.ifError(err); res.should.have.status(400); body.should.match(/^Bad request. File not found: /); done(); }); }); }); }); }); // -- Optional Middleware -------------------------------------------------- describe("url rewrites", function () { // NOTE: we do not currently support the space terminator for CSS escapes. // ".unicode-escaped { background: url(__PATH__d\\E9 cha\\EEn\\E9.png); }", var TEMPLATE_URLS = fs.readFileSync(path.join(FIXTURES_DIR, 'rewrite/urls.tmpl'), 'utf-8'); var TEMPLATE_MORE = fs.readFileSync(path.join(FIXTURES_DIR, 'rewrite/deeper/more.tmpl'), 'utf-8'); // TODO: are "../" paths being rewritten correctly? var TEMPLATE_IMPORTS = fs.readFileSync(path.join(FIXTURES_DIR, 'rewrite/imports.tmpl'), 'utf-8'); var URLS_UNMODIFIED = fs.readFileSync(path.join(FIXTURES_DIR, 'rewrite/urls.css'), 'utf-8'); var IMPORTS_UNMODIFIED = fs.readFileSync(path.join(FIXTURES_DIR, 'rewrite/imports.css'), 'utf-8'); before(function () { app.get('/norewrite', combo.combine({ rootPath: FIXTURES_DIR + '/rewrite' }), combo.respond); app.get('/rewrite', combo.combine({ rootPath: FIXTURES_DIR + '/rewrite', basePath: "/rewritten/" }), combo.respond); app.get('/rewrite-noslash', combo.combine({ rootPath: FIXTURES_DIR + '/rewrite', basePath: "/rewritten" }), combo.respond); app.get('/rewrite-imports', combo.combine({ rootPath: FIXTURES_DIR + '/rewrite', basePath: "/rewritten/", rewriteImports: true }), combo.respond); app.get('/rewrite-middleware-before-combine', combo.cssUrls({ basePath: "/rewritten/" }), combo.combine({ rootPath: FIXTURES_DIR + '/rewrite' }), combo.respond); app.get('/rewrite-middleware-noconfig', combo.combine({ rootPath: FIXTURES_DIR + '/rewrite' }), combo.cssUrls(), combo.respond); app.get('/rewrite-root', combo.combine({ rootPath: FIXTURES_DIR + '/rewrite', webRoot: FIXTURES_DIR + '/' }), combo.respond); app.get('/rewrite-root-noslash', combo.combine({ rootPath: FIXTURES_DIR + '/rewrite', webRoot: FIXTURES_DIR }), combo.respond); app.get('/rewrite-root-imports', combo.combine({ rootPath: FIXTURES_DIR + '/rewrite', webRoot: FIXTURES_DIR + '/', rewriteImports: true }), combo.respond); }); it("should not rewrite without a basePath or webRoot", assertResponds({ path: "/norewrite?urls.css", body: URLS_UNMODIFIED })); it("should not rewrite without a basePath or webRoot as middleware", assertResponds({ path: "/rewrite-middleware-noconfig?urls.css", body: URLS_UNMODIFIED })); it("should not rewrite when middleware before combine()", assertResponds({ path: "/rewrite-middleware-before-combine?urls.css", body: URLS_UNMODIFIED })); describe("with configured basePath", function () { var URLS_REWRITTEN = TEMPLATE_URLS.replace(/__PATH__/g, '/rewritten/'); var MORE_REWRITTEN = TEMPLATE_MORE.replace(/__PATH__/g, '/rewritten/'); var MORE_URLS_REWRITTEN = [URLS_REWRITTEN, MORE_REWRITTEN].join("\n"); var IMPORTS_REWRITTEN = TEMPLATE_IMPORTS.replace(/__PATH__/g, '/rewritten/') .replace(/__DOTS__/g, ''); it("should allow basePath without trailing slash", assertResponds({ path: "/rewrite-noslash?urls.css", body: URLS_REWRITTEN })); it("should rewrite valid urls", assertResponds({ path: "/rewrite?urls.css&deeper/more.css", body: MORE_URLS_REWRITTEN })); it("should NOT rewrite import paths when disabled", assertResponds({ path: "/rewrite?imports.css", body: IMPORTS_UNMODIFIED })); it("should rewrite import paths when enabled", assertResponds({ path: "/rewrite-imports?imports.css", body: IMPORTS_REWRITTEN })); }); describe("with configured webRoot", function () { var URLS_REBASED = TEMPLATE_URLS.replace(/__PATH__/g, '/rewrite/'); var MORE_REBASED = TEMPLATE_MORE.replace(/__PATH__/g, '/rewrite/'); var MORE_URLS_REBASED = [URLS_REBASED, MORE_REBASED].join("\n"); var IMPORTS_REBASED = TEMPLATE_IMPORTS.replace(/__PATH__/g, '/rewrite/') .replace(/__DOTS__/g, ''); it("should allow webRoot without trailing slash", assertResponds({ path: "/rewrite-root-noslash?urls.css", body: URLS_REBASED })); it("should rewrite valid urls", assertResponds({ path: "/rewrite-root?urls.css&deeper/more.css", body: MORE_URLS_REBASED })); it("should NOT rewrite import paths when disabled", assertResponds({ path: "/rewrite-root?imports.css", body: IMPORTS_UNMODIFIED })); it("should rewrite import paths when enabled", assertResponds({ path: "/rewrite-root-imports?imports.css", body: IMPORTS_REBASED })); }); }); describe("dynamic paths", function () { before(function () { app.get('/dynamic/:version', combo.combine({ rootPath: FIXTURES_DIR + '/dynamic/:version' }), combo.respond); app.get('/:version/param-first', combo.combine({ rootPath: FIXTURES_DIR + '/dynamic/:version' }), combo.respond); app.get('/dynamic/:version/empty-combo', combo.dynamicPath({ rootPath: FIXTURES_DIR + '/dynamic/:version/static' }), combo.combine(), combo.respond); app.get('/dynamic/:version/doubled', combo.dynamicPath({ rootPath: FIXTURES_DIR + '/dynamic/:version/static' }), combo.combine({ rootPath: FIXTURES_DIR + '/dynamic/:version/static' }), combo.respond); app.get('/non-dynamic', combo.dynamicPath({ rootPath: FIXTURES_DIR + '/dynamic/decafbad' }), combo.combine({ rootPath: FIXTURES_DIR + '/dynamic/decafbad' }), combo.respond); app.get('/dynamic-no-config', combo.dynamicPath(), combo.combine({ rootPath: FIXTURES_DIR + '/dynamic/decafbad' }), combo.respond); app.get('/route-only/:version/lib', combo.combine({ rootPath: FIXTURES_DIR + '/root' }), combo.respond); }); it("should work when param found at end of path", assertResponds({ path: "/dynamic/decafbad?a.js&b.js", body: "a();\n\nb();\n" })); it("should work when param found at beginning of path", assertResponds({ path: "/decafbad/param-first?a.js&static/c.js", body: "a();\n\nc();\n" })); it("should work when rootPath not passed to combine()", assertResponds({ path: "/dynamic/decafbad/empty-combo?c.js&d.js", body: "c();\n\nd();\n" })); it("should work when param found before end of path", assertResponds({ path: "/dynamic/decafbad/empty-combo?c.js&d.js", body: "c();\n\nd();\n" })); it("should work when middleware is run twice on same route", assertResponds({ path: "/dynamic/decafbad/doubled?c.js&d.js", body: "c();\n\nd();\n" })); it("should not fail when param not present", assertResponds({ path: "/non-dynamic?a.js&b.js", body: "a();\n\nb();\n" })); it("should not fail when config missing", assertResponds({ path: "/dynamic-no-config?a.js&b.js", body: "a();\n\nb();\n" })); it("should work when param only found in route, not rootPath", assertResponds({ path: "/route-only/deadbeef/lib?js/a.js&js/b.js", body: "a();\n\nb();\n" })); it("should error when param does not correspond to existing path", assertResponds({ path: "/dynamic/deadbeef?a.js", // body: "Bad request. Unable to resolve path: /dynamic/deadbeef", // status: 400 status: 500 // Express 4.x removes app.router, so the default error handler is used. // (these tests should be split into separate files, anyhow) })); describe("with multiple parameters", function () { before(function () { app.get("/dynamic/:major/:minor", combo.combine({ rootPath: FIXTURES_DIR + "/dynamic/:major/:minor" }), combo.respond); app.get("/:major/:minor/rootpath-omit", combo.combine({ rootPath: FIXTURES_DIR + "/dynamic/:major/static" }), combo.respond); app.get("/omit-route/:major", combo.combine({ rootPath: FIXTURES_DIR + "/dynamic/:major/:minor" }), combo.respond); app.get("/:major/separated/route/:minor", combo.combine({ rootPath: FIXTURES_DIR + "/:major/:minor/static" }), combo.respond); app.get("/:major/separated/path/:minor", combo.combine({ rootPath: FIXTURES_DIR + "/:major/decafbad/:minor" }), combo.respond); app.get('/doubled-in-rootpath/:major', combo.combine({ rootPath: FIXTURES_DIR + '/dynamic/:major/:major' }), combo.respond); app.get('/doubled-with-suffixes/:major', combo.combine({ rootPath: FIXTURES_DIR + '/dynamic/:major/static/:major' }), combo.respond); }); it("should resolve path", assertResponds({ path: "/dynamic/decafbad/static?c.js&d.js", body: "c();\n\nd();\n" })); it("should resolve root path that omits route parameter", assertResponds({ path: "/decafbad/latest/rootpath-omit?c.js&d.js", body: "c();\n\nd();\n" })); it("should resolve route that has more parameters than root path", assertResponds({ path: "/omit-route/decafbad?a.js&static/c.js", body: "a();\n\nc();\n" })); it("should resolve route that has separated parameters", assertResponds({ path: "/dynamic/separated/route/decafbad?c.js&d.js", body: "c();\n\nd();\n" })); it("should resolve root path that has separated parameters", assertResponds({ path: "/dynamic/separated/path/static?c.js&d.js", body: "c();\n\nd();\n" })); it("should resolve route that has identical parameters in root path", assertResponds({ path: "/doubled-in-rootpath/baddecaf?e.js&f.js", body: "e();\n\nf();\n" })); it("should resolve route that has separated identical parameters in root path", assertResponds({ path: "/doubled-with-suffixes/cafebabe?g.js&h.js", body: "g();\n\nh();\n" })); }); }); // -- Complex Integration -------------------------------------------------- describe("complex", function () { // Strange things may happen when you mix symlinks, parameters, and complex routes var COMPLEX_ROOT = FIXTURES_DIR + '/complex'; var TEMPLATE_IMPORTS_SIMPLE = [ '@import "__ROOT__css/parent.css";', '@import "__ROOT__css/urls/child/dir.css";', '@import "__ROOT__css/urls/sibling.css";', '@import "__ROOT__css/urls/also-sibling.css";', '' ].join('\n'); var TEMPLATE_URLS_SIMPLE = [ '.relatives { background: url(__ROOT__images/cousin.png); }', '.offspring { background: url(__ROOT__css/urls/images/grandchild.png); }', '' ].join('\n'); var TEMPLATE_SIMPLE = TEMPLATE_IMPORTS_SIMPLE + TEMPLATE_URLS_SIMPLE; var SIMPLE_IMPORTS_RAW = [ '@import "../parent.css";', '@import "child/dir.css";', '@import "./sibling.css";', '@import "../urls/also-sibling.css";', '' ].join('\n'); var SIMPLE_URLS_RAW = [ '.relatives { background: url(../../images/cousin.png); }', '.offspring { background: url(./images/grandchild.png); }', '' ].join('\n'); var SIMPLE_RAW = SIMPLE_IMPORTS_RAW + SIMPLE_URLS_RAW; function dynamicFiletree(opts) { var expectedRelativePath = opts.relativePath || "js/a.js"; var expectedResolvedPath = path.join(COMPLEX_ROOT, opts.realPath, expectedRelativePath); var expectedRootPath = path.join(COMPLEX_ROOT, opts.rootPath); return function (req, res, next) { var rootPath = res.locals.rootPath; rootPath.should.equal(expectedRootPath); var relativePath = res.locals.relativePaths[0]; relativePath.should.equal(expectedRelativePath); fs.realpath(path.join(rootPath, relativePath), function (err, resolved) { assert.ifError(err); resolved.should.equal(expectedResolvedPath); next(); }); }; } function dynamicSymlinks(opts) { var expectedTemplateFile = opts.template || TEMPLATE_SIMPLE; var expectedRelativePath = opts.relativePath || "css/urls/simple.css"; var expectedResolvedBody = expectedTemplateFile.replace(/__ROOT__/g, opts.rootPath); return function (req, res, next) { var relativePath = res.locals.relativePaths[0]; relativePath.should.equal(expectedRelativePath); // console.error(res.body); res.body.should.equal(expectedResolvedBody); next(); }; } describe("route with fully-qualified dynamic path", function () { before(function () { var combined = combo.combine({ webRoot : COMPLEX_ROOT, rootPath: COMPLEX_ROOT + '/versioned/:version/base/' }); app.get("/c/:version/fs-fq", combined, dynamicFiletree({ realPath: "/versioned/deeper/base/", rootPath: "/versioned/deeper/base/" }), combo.respond); app.get("/c/:version/ln-fq", combined, dynamicFiletree({ realPath: "/base/", rootPath: "/versioned/shallower/base/" }), combo.respond); app.get("/c/:version/fq-noimports", combined, dynamicSymlinks({ template: SIMPLE_IMPORTS_RAW + TEMPLATE_URLS_SIMPLE, realPath: "/versioned/shallower/base/", rootPath: "/versioned/shallower/base/" }), combo.respond); }); it("should read rootPath from filesystem directly", assertResponds({ path: "/c/deeper/fs-fq?js/a.js&js/b.js" })); it("should resolve rootPath through symlink", assertResponds({ path: "/c/shallower/ln-fq?js/a.js&js/b.js" })); it("should only rewrite url() through symlink, not imports", assertResponds({ path: "/c/shallower/fq-noimports?css/urls/simple.css" })); }); describe("route with one-sided dynamic path", function () { describe("and rootPath symlinked shallower", function () { describe("when resolveSymlinks is true", function () { before(function () { var resolved = combo.combine({ rewriteImports: true, webRoot : COMPLEX_ROOT, rootPath: COMPLEX_ROOT + '/versioned/shallower/base/' }); app.get("/r/:version/fs-shallow", resolved, dynamicFiletree({ realPath: "/base/", rootPath: "/base/" }), combo.respond); app.get("/r/:version/ln-shallow", resolved, dynamicSymlinks({ rootPath: "/base/" }), combo.respond); }); it("should resolve files from realpath in filesystem", assertResponds({ path: "/r/cafebabe/fs-shallow?js/a.js&js/b.js" })); it("should rewrite url() through symlink", assertResponds({ path: "/r/cafebabe/ln-shallow?css/urls/simple.css" })); }); describe("when resolveSymlinks is false", function () { before(function () { var symlinkd = combo.combine({ rewriteImports: true, resolveSymlinks: false, webRoot : COMPLEX_ROOT, rootPath: COMPLEX_ROOT + '/versioned/shallower/base/' }); app.get("/s/:version/fs-shallow", symlinkd, dynamicFiletree({ realPath: "/base/", rootPath: "/versioned/shallower/base/" }), combo.respond); app.get("/s/:version/ln-shallow", symlinkd, dynamicSymlinks({ rootPath: "/versioned/shallower/base/" }), combo.respond); }); it("should resolve files from symlink in filesystem", assertResponds({ path: "/s/cafebabe/fs-shallow?js/a.js&js/b.js" })); it("should rewrite url() using symlink", assertResponds({ path: "/s/cafebabe/ln-shallow?css/urls/simple.css" })); }); }); describe("and rootPath symlinked deeper", function () { describe("when resolveSymlinks is true", function () { before(function () { var resolved = combo.combine({ rewriteImports: true, webRoot : COMPLEX_ROOT, rootPath: COMPLEX_ROOT + '/deep-link/' }); app.get("/r/:version/fs-deeper", resolved, dynamicFiletree({ realPath: "/versioned/deeper/base/", rootPath: "/versioned/deeper/base/" }), combo.respond); app.get("/r/:version/ln-deeper", resolved, dynamicSymlinks({ rootPath: "/versioned/deeper/base/" }), combo.respond); }); it("should read rootPath from filesystem directly", assertResponds({ path: "/r/cafebabe/fs-deeper?js/a.js&js/b.js" })); it("should *still* rewrite url() through symlink", assertResponds({ path: "/r/cafebabe/ln-deeper?css/urls/simple.css" })); }); describe("when resolveSymlinks is false", function () { before(function () { var symlinkd = combo.combine({ rewriteImports: true, resolveSymlinks: false, webRoot : COMPLEX_ROOT, rootPath: COMPLEX_ROOT + '/deep-link/' }); app.get("/s/:version/fs-deeper", symlinkd, dynamicFiletree({ realPath: "/versioned/deeper/base/", rootPath: "/deep-link/" }), combo.respond); app.get("/s/:version/ln-deeper", symlinkd, dynamicSymlinks({ rootPath: "/deep-link/" }), combo.respond); }); it("should read rootPath from symlink in filesystem", assertResponds({ path: "/s/cafebabe/fs-deeper?js/a.js&js/b.js" })); it("should *still* rewrite url() using symlink", assertResponds({ path: "/s/cafebabe/ln-deeper?css/urls/simple.css" })); }); }); }); }); // -- Helpers -------------------------------------------------------------- function assertResponds(config) { var expectedPath = config.path; var expectedBody = config.body; var expectedStatus = config.status || 200; return function (done) { request(BASE_URL + config.path, function (err, res, body) { assert.ifError(err); if (expectedBody) { body.should.equal(expectedBody); } res.should.have.status(expectedStatus); done(); }); }; } });