diff --git a/core/server/errorHandling.js b/core/server/errorHandling.js
index 6c5a52c8df..dc9bf4e8d8 100644
--- a/core/server/errorHandling.js
+++ b/core/server/errorHandling.js
@@ -9,7 +9,9 @@ var _ = require('underscore'),
// Paths for views
defaultErrorTemplatePath = path.resolve(configPaths().adminViews, 'user-error.hbs'),
userErrorTemplatePath = path.resolve(configPaths().themePath, 'error.hbs'),
- userErrorTemplateExists;
+ userErrorTemplateExists,
+
+ ONE_HOUR_S = 60 * 60;
/**
* Basic error handling helpers
@@ -182,6 +184,8 @@ errors = {
error404: function (req, res, next) {
var message = res.isAdmin && req.session.user ? "No Ghost Found" : "Page Not Found";
+ // 404 errors should be briefly cached
+ res.set({'Cache-Control': 'public, max-age=' + ONE_HOUR_S});
if (req.method === 'GET') {
this.renderErrorPage(404, message, req, res, next);
} else {
@@ -190,6 +194,13 @@ errors = {
},
error500: function (err, req, res, next) {
+ // 500 errors should never be cached
+ res.set({'Cache-Control': 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'});
+
+ if (err.status === 404) {
+ return this.error404(req, res, next);
+ }
+
if (req.method === 'GET') {
if (!err || !(err instanceof Error)) {
next();
diff --git a/core/server/helpers/index.js b/core/server/helpers/index.js
index 47ffdd3694..6587dc00b1 100644
--- a/core/server/helpers/index.js
+++ b/core/server/helpers/index.js
@@ -1,20 +1,22 @@
-var _ = require('underscore'),
- moment = require('moment'),
- downsize = require('downsize'),
- path = require('path'),
- when = require('when'),
+var downsize = require('downsize'),
hbs = require('express-hbs'),
+ moment = require('moment'),
+ path = require('path'),
polyglot = require('node-polyglot').instance,
- template = require('./template'),
- errors = require('../errorHandling'),
- models = require('../models'),
- filters = require('../filters'),
- packageInfo = require('../../../package.json'),
- version = packageInfo.version,
- scriptTemplate = _.template(''),
- isProduction = process.env.NODE_ENV === 'production',
+ _ = require('underscore'),
+ when = require('when'),
+
api = require('../api'),
config = require('../config'),
+ errors = require('../errorHandling'),
+ filters = require('../filters'),
+ models = require('../models'),
+ template = require('./template'),
+
+ assetTemplate = _.template('<%= source %>?v=<%= version %>'),
+ scriptTemplate = _.template(''),
+ isProduction = process.env.NODE_ENV === 'production',
+
coreHelpers = {},
registerHelpers;
@@ -128,7 +130,6 @@ coreHelpers.url = function (options) {
// *Usage example:*
// `{{asset "css/screen.css"}}`
// `{{asset "css/screen.css" ghost="true"}}`
-//
// Returns the path to the specified asset. The ghost
// flag outputs the asset path for the Ghost admin
coreHelpers.asset = function (context, options) {
@@ -137,7 +138,7 @@ coreHelpers.asset = function (context, options) {
output += config.paths().subdir + '/';
- if (!context.match(/^shared/)) {
+ if (!context.match(/^favicon\.ico$/) && !context.match(/^shared/)) {
if (isAdmin) {
output += 'ghost/';
} else {
@@ -146,6 +147,14 @@ coreHelpers.asset = function (context, options) {
}
output += context;
+
+ if (!context.match(/^favicon\.ico$/)) {
+ output = assetTemplate({
+ source: output,
+ version: coreHelpers.assetHash
+ });
+ }
+
return new hbs.handlebars.SafeString(output);
};
@@ -284,8 +293,8 @@ coreHelpers.ghostScriptTags = function () {
scriptFiles = _.map(scriptFiles, function (fileName) {
return scriptTemplate({
- source: config.paths().subdir + '/built/scripts/' + fileName,
- version: version
+ source: config.paths().subdir + '/ghost/scripts/' + fileName,
+ version: coreHelpers.assetHash
});
});
@@ -379,7 +388,7 @@ coreHelpers.ghost_foot = function (options) {
foot.push(scriptTemplate({
source: config.paths().subdir + '/shared/vendor/jquery/jquery.js',
- version: this.version
+ version: coreHelpers.assetHash
}));
return filters.doFilter('ghost_foot', foot).then(function (foot) {
@@ -593,11 +602,14 @@ function registerAsyncAdminHelper(name, fn) {
-registerHelpers = function (adminHbs) {
+registerHelpers = function (adminHbs, assetHash) {
// Expose hbs instance for admin
coreHelpers.adminHbs = adminHbs;
+ // Store hash for assets
+ coreHelpers.assetHash = assetHash;
+
// Register theme helpers
registerThemeHelper('asset', coreHelpers.asset);
diff --git a/core/server/index.js b/core/server/index.js
index ab8a9bb8c9..a215c3dd54 100644
--- a/core/server/index.js
+++ b/core/server/index.js
@@ -3,30 +3,31 @@
// modules to ensure config gets right setting.
// Module dependencies
-var config = require('./config'),
- express = require('express'),
- when = require('when'),
- _ = require('underscore'),
- semver = require('semver'),
- fs = require('fs'),
- errors = require('./errorHandling'),
- plugins = require('./plugins'),
- path = require('path'),
- Polyglot = require('node-polyglot'),
- mailer = require('./mail'),
- helpers = require('./helpers'),
- middleware = require('./middleware'),
- routes = require('./routes'),
- packageInfo = require('../../package.json'),
- models = require('./models'),
- permissions = require('./permissions'),
- uuid = require('node-uuid'),
- api = require('./api'),
- hbs = require('express-hbs'),
+var crypto = require('crypto'),
+ express = require('express'),
+ hbs = require('express-hbs'),
+ fs = require('fs'),
+ uuid = require('node-uuid'),
+ path = require('path'),
+ Polyglot = require('node-polyglot'),
+ semver = require('semver'),
+ _ = require('underscore'),
+ when = require('when'),
+
+ api = require('./api'),
+ config = require('./config'),
+ errors = require('./errorHandling'),
+ helpers = require('./helpers'),
+ mailer = require('./mail'),
+ middleware = require('./middleware'),
+ models = require('./models'),
+ permissions = require('./permissions'),
+ plugins = require('./plugins'),
+ routes = require('./routes'),
+ packageInfo = require('../../package.json'),
+
// Variables
- setup,
- init,
dbHash;
// If we're in development mode, require "when/console/monitor"
@@ -79,6 +80,9 @@ function initDbHashAndFirstRun() {
// Finally it starts the http server.
function setup(server) {
+ // create a hash for cache busting assets
+ var assetHash = (crypto.createHash('md5').update(packageInfo.version + Date().now).digest('hex')).substring(0, 10);
+
// Set up Polygot instance on the require module
Polyglot.instance = new Polyglot();
@@ -112,6 +116,7 @@ function setup(server) {
var adminHbs = hbs.create();
// ##Configuration
+ server.set('version hash', assetHash);
// return the correct mime type for woff filess
express['static'].mime.define({'application/font-woff': ['woff']});
@@ -124,7 +129,7 @@ function setup(server) {
server.set('admin view engine', adminHbs.express3({partialsDir: config.paths().adminViews + 'partials'}));
// Load helpers
- helpers.loadCoreHelpers(adminHbs);
+ helpers.loadCoreHelpers(adminHbs, assetHash);
// ## Middleware
middleware(server, dbHash);
diff --git a/core/server/middleware/index.js b/core/server/middleware/index.js
index 6ee8860e7d..920c4d2e85 100644
--- a/core/server/middleware/index.js
+++ b/core/server/middleware/index.js
@@ -18,7 +18,11 @@ var middleware = require('./middleware'),
BSStore = require('../bookshelf-session'),
models = require('../models'),
- expressServer;
+ expressServer,
+ ONE_HOUR_S = 60 * 60,
+ ONE_YEAR_S = 365 * 24 * ONE_HOUR_S,
+ ONE_HOUR_MS = ONE_HOUR_S * 1000,
+ ONE_YEAR_MS = 365 * 24 * ONE_HOUR_MS;
// ##Custom Middleware
@@ -186,9 +190,7 @@ function checkSSL(req, res, next) {
}
module.exports = function (server, dbHash) {
- var oneHour = 60 * 60 * 1000,
- oneYear = 365 * 24 * oneHour,
- subdir = config.paths().subdir,
+ var subdir = config.paths().subdir,
corePath = config.paths().corePath,
cookie;
@@ -206,16 +208,11 @@ module.exports = function (server, dbHash) {
// Favicon
expressServer.use(subdir, express.favicon(corePath + '/shared/favicon.ico'));
- // Shared static config
- expressServer.use(subdir + '/shared', express['static'](path.join(corePath, '/shared')));
-
+ // Static assets
+ // For some reason send divides the max age number by 1000
+ expressServer.use(subdir + '/shared', express['static'](path.join(corePath, '/shared'), {maxAge: ONE_HOUR_MS}));
expressServer.use(subdir + '/content/images', storage.get_storage().serve());
-
- // Serve our built scripts; can't use /scripts here because themes already are
- expressServer.use(subdir + '/built/scripts', express['static'](path.join(corePath, '/built/scripts'), {
- // Put a maxAge of one year on built scripts
- maxAge: oneYear
- }));
+ expressServer.use(subdir + '/ghost/scripts', express['static'](path.join(corePath, '/built/scripts'), {maxAge: ONE_YEAR_MS}));
// First determine whether we're serving admin or theme content
expressServer.use(manageAdminAndTheme);
@@ -223,25 +220,27 @@ module.exports = function (server, dbHash) {
// Force SSL
expressServer.use(checkSSL);
+
// Admin only config
- expressServer.use(subdir + '/ghost', middleware.whenEnabled('admin', express['static'](path.join(corePath, '/client/assets'))));
+ expressServer.use(subdir + '/ghost', middleware.whenEnabled('admin', express['static'](path.join(corePath, '/client/assets'), {maxAge: ONE_YEAR_MS})));
// Theme only config
expressServer.use(subdir, middleware.whenEnabled(expressServer.get('activeTheme'), middleware.staticTheme()));
// Add in all trailing slashes
- expressServer.use(slashes());
+ expressServer.use(slashes(true, {headers: {'Cache-Control': 'public, max-age=' + ONE_YEAR_S}}));
+ // Body parsing
expressServer.use(express.json());
expressServer.use(express.urlencoded());
expressServer.use(subdir + '/ghost/upload/', middleware.busboy);
expressServer.use(subdir + '/ghost/api/v0.1/db/', middleware.busboy);
- // Session handling
+ // ### Sessions
cookie = {
path: subdir + '/ghost',
- maxAge: 12 * oneHour
+ maxAge: 12 * ONE_HOUR_MS
};
// if SSL is forced, add secure flag to cookie
@@ -258,17 +257,25 @@ module.exports = function (server, dbHash) {
cookie: cookie
}));
+
//enable express csrf protection
expressServer.use(middleware.conditionalCSRF);
+
+
// local data
expressServer.use(ghostLocals);
- // So on every request we actually clean out reduntant passive notifications from the server side
+ // So on every request we actually clean out redundant passive notifications from the server side
expressServer.use(middleware.cleanNotifications);
-
// Initialise the views
expressServer.use(initViews);
- // process the application routes
+
+ // ### Caching
+ expressServer.use(middleware.cacheControl('public'));
+ expressServer.use('/api/', middleware.cacheControl('private'));
+ expressServer.use('/ghost/', middleware.cacheControl('private'));
+
+ // ### Routing
expressServer.use(subdir, expressServer.router);
// ### Error handling
diff --git a/core/server/middleware/middleware.js b/core/server/middleware/middleware.js
index 69f5c0f007..aad635102b 100644
--- a/core/server/middleware/middleware.js
+++ b/core/server/middleware/middleware.js
@@ -8,7 +8,9 @@ var _ = require('underscore'),
config = require('../config'),
path = require('path'),
api = require('../api'),
- expressServer;
+
+ expressServer,
+ ONE_HOUR_MS = 60 * 60 * 1000;
function isBlackListedFileType(file) {
var blackListedFileTypes = ['.hbs', '.md', '.json'],
@@ -89,16 +91,26 @@ var middleware = {
});
},
- // ### DisableCachedResult Middleware
- // Disable any caching until it can be done properly
- disableCachedResult: function (req, res, next) {
+ // ### CacheControl Middleware
+ // provide sensible cache control headers
+ cacheControl: function (options) {
/*jslint unparam:true*/
- res.set({
- 'Cache-Control': 'no-cache, must-revalidate',
- 'Expires': 'Sat, 26 Jul 1997 05:00:00 GMT'
- });
+ var profiles = {
+ 'public': 'public, max-age=0',
+ 'private': 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'
+ },
+ output;
- next();
+ if (_.isString(options) && profiles.hasOwnProperty(options)) {
+ output = profiles[options];
+ }
+
+ return function cacheControlHeaders(req, res, next) {
+ if (output) {
+ res.set({'Cache-Control': output});
+ }
+ next();
+ };
},
// ### whenEnabled Middleware
@@ -128,7 +140,8 @@ var middleware = {
// to allow unit testing
forwardToExpressStatic: function (req, res, next) {
api.settings.read('activeTheme').then(function (activeTheme) {
- express['static'](path.join(config.paths().themePath, activeTheme.value))(req, res, next);
+ // For some reason send divides the max age number by 1000
+ express['static'](path.join(config.paths().themePath, activeTheme.value), {maxAge: ONE_HOUR_MS})(req, res, next);
});
},
diff --git a/core/server/routes/api.js b/core/server/routes/api.js
index 6b39b002e6..39c2c6fa52 100644
--- a/core/server/routes/api.js
+++ b/core/server/routes/api.js
@@ -5,26 +5,26 @@ module.exports = function (server) {
// ### API routes
/* TODO: auth should be public auth not user auth */
// #### Posts
- server.get('/ghost/api/v0.1/posts', middleware.authAPI, middleware.disableCachedResult, api.requestHandler(api.posts.browse));
- server.post('/ghost/api/v0.1/posts', middleware.authAPI, middleware.disableCachedResult, api.requestHandler(api.posts.add));
- server.get('/ghost/api/v0.1/posts/:id', middleware.authAPI, middleware.disableCachedResult, api.requestHandler(api.posts.read));
- server.put('/ghost/api/v0.1/posts/:id', middleware.authAPI, middleware.disableCachedResult, api.requestHandler(api.posts.edit));
- server.del('/ghost/api/v0.1/posts/:id', middleware.authAPI, middleware.disableCachedResult, api.requestHandler(api.posts.destroy));
+ server.get('/ghost/api/v0.1/posts', middleware.authAPI, api.requestHandler(api.posts.browse));
+ server.post('/ghost/api/v0.1/posts', middleware.authAPI, api.requestHandler(api.posts.add));
+ server.get('/ghost/api/v0.1/posts/:id', middleware.authAPI, api.requestHandler(api.posts.read));
+ server.put('/ghost/api/v0.1/posts/:id', middleware.authAPI, api.requestHandler(api.posts.edit));
+ server.del('/ghost/api/v0.1/posts/:id', middleware.authAPI, api.requestHandler(api.posts.destroy));
// #### Settings
- server.get('/ghost/api/v0.1/settings/', middleware.authAPI, middleware.disableCachedResult, api.requestHandler(api.settings.browse));
- server.get('/ghost/api/v0.1/settings/:key/', middleware.authAPI, middleware.disableCachedResult, api.requestHandler(api.settings.read));
- server.put('/ghost/api/v0.1/settings/', middleware.authAPI, middleware.disableCachedResult, api.requestHandler(api.settings.edit));
+ server.get('/ghost/api/v0.1/settings/', middleware.authAPI, api.requestHandler(api.settings.browse));
+ server.get('/ghost/api/v0.1/settings/:key/', middleware.authAPI, api.requestHandler(api.settings.read));
+ server.put('/ghost/api/v0.1/settings/', middleware.authAPI, api.requestHandler(api.settings.edit));
// #### Users
- server.get('/ghost/api/v0.1/users/', middleware.authAPI, middleware.disableCachedResult, api.requestHandler(api.users.browse));
- server.get('/ghost/api/v0.1/users/:id/', middleware.authAPI, middleware.disableCachedResult, api.requestHandler(api.users.read));
- server.put('/ghost/api/v0.1/users/:id/', middleware.authAPI, middleware.disableCachedResult, api.requestHandler(api.users.edit));
+ server.get('/ghost/api/v0.1/users/', middleware.authAPI, api.requestHandler(api.users.browse));
+ server.get('/ghost/api/v0.1/users/:id/', middleware.authAPI, api.requestHandler(api.users.read));
+ server.put('/ghost/api/v0.1/users/:id/', middleware.authAPI, api.requestHandler(api.users.edit));
// #### Tags
- server.get('/ghost/api/v0.1/tags/', middleware.authAPI, middleware.disableCachedResult, api.requestHandler(api.tags.all));
+ server.get('/ghost/api/v0.1/tags/', middleware.authAPI, api.requestHandler(api.tags.all));
// #### Notifications
- server.del('/ghost/api/v0.1/notifications/:id', middleware.authAPI, middleware.disableCachedResult, api.requestHandler(api.notifications.destroy));
- server.post('/ghost/api/v0.1/notifications/', middleware.authAPI, middleware.disableCachedResult, api.requestHandler(api.notifications.add));
+ server.del('/ghost/api/v0.1/notifications/:id', middleware.authAPI, api.requestHandler(api.notifications.destroy));
+ server.post('/ghost/api/v0.1/notifications/', middleware.authAPI, api.requestHandler(api.notifications.add));
// #### Import/Export
server.get('/ghost/api/v0.1/db/', middleware.auth, api.db['export']);
server.post('/ghost/api/v0.1/db/', middleware.auth, api.db['import']);
- server.del('/ghost/api/v0.1/db/', middleware.authAPI, middleware.disableCachedResult, api.requestHandler(api.db.deleteAllContent));
+ server.del('/ghost/api/v0.1/db/', middleware.authAPI, api.requestHandler(api.db.deleteAllContent));
};
\ No newline at end of file
diff --git a/core/server/storage/localfilesystem.js b/core/server/storage/localfilesystem.js
index 53b30810d5..92d0892d41 100644
--- a/core/server/storage/localfilesystem.js
+++ b/core/server/storage/localfilesystem.js
@@ -56,7 +56,11 @@ localFileStore = _.extend(baseStore, {
// middleware for serving the files
'serve': function () {
- return express['static'](configPaths().imagesPath);
+ var ONE_HOUR_MS = 60 * 60 * 1000,
+ ONE_YEAR_MS = 365 * 24 * ONE_HOUR_MS;
+
+ // For some reason send divides the max age number by 1000
+ return express['static'](configPaths().imagesPath, {maxAge: ONE_YEAR_MS});
}
});
diff --git a/core/test/functional/admin/content_test.js b/core/test/functional/admin/content_test.js
index f1f5b535bd..194db5e522 100644
--- a/core/test/functional/admin/content_test.js
+++ b/core/test/functional/admin/content_test.js
@@ -1,6 +1,6 @@
/*globals casper, __utils__, url, testPost */
-CasperTest.begin("Content screen is correct", 21, function suite(test) {
+CasperTest.begin("Content screen is correct", 20, function suite(test) {
// Create a sample post
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
@@ -37,10 +37,6 @@ CasperTest.begin("Content screen is correct", 21, function suite(test) {
test.assertSelectorHasText("#usermenu .usermenu-profile a", "Your Profile");
test.assertSelectorHasText("#usermenu .usermenu-help a", "Help / Support");
test.assertSelectorHasText("#usermenu .usermenu-signout a", "Sign Out");
-
- test.assertResourceExists(function (resource) {
- return resource.url.match(/user-image\.png$/) && (resource.status === 200 || resource.status === 304);
- }, 'Default user image');
});
casper.then(function testViews() {
diff --git a/core/test/functional/admin/settings_test.js b/core/test/functional/admin/settings_test.js
index 4b2625b956..0882aef54a 100644
--- a/core/test/functional/admin/settings_test.js
+++ b/core/test/functional/admin/settings_test.js
@@ -1,6 +1,6 @@
/*globals casper, __utils__, url */
-CasperTest.begin("Settings screen is correct", 17, function suite(test) {
+CasperTest.begin("Settings screen is correct", 15, function suite(test) {
casper.thenOpen(url + "ghost/settings/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/settings\/general\/$/, "Ghost doesn't require login this time");
@@ -32,16 +32,6 @@ CasperTest.begin("Settings screen is correct", 17, function suite(test) {
test.assertEval(function testContentIsUser() {
return document.querySelector('.settings-content').id === 'user';
}, "loaded content is user screen");
-
- test.assertResourceExists(function (resource) {
- return resource.url.match(/user-image\.png$/) && (resource.status === 200 || resource.status === 304);
- }, 'Default user image');
-
- test.assertResourceExists(function (resource) {
- return resource.url.match(/user-cover\.png$/) && (resource.status === 200 || resource.status === 304);
- }, 'Default cover image');
-
-
}, function onTimeOut() {
test.fail('User screen failed to load');
});
diff --git a/core/test/functional/routes/admin_test.js b/core/test/functional/routes/admin_test.js
new file mode 100644
index 0000000000..06f9516ea8
--- /dev/null
+++ b/core/test/functional/routes/admin_test.js
@@ -0,0 +1,121 @@
+/*global describe, it, before, after */
+
+// # Frontend Route tests
+// As it stands, these tests depend on the database, and as such are integration tests.
+// Mocking out the models to not touch the DB would turn these into unit tests, and should probably be done in future,
+// But then again testing real code, rather than mock code, might be more useful...
+
+var request = require('supertest'),
+ should = require('should'),
+ moment = require('moment'),
+
+ testUtils = require('../../utils'),
+ config = require('../../../server/config'),
+
+ ONE_HOUR_S = 60 * 60,
+ ONE_YEAR_S = 365 * 24 * ONE_HOUR_S,
+ cacheRules = {
+ 'public': 'public, max-age=0',
+ 'hour': 'public, max-age=' + ONE_HOUR_S,
+ 'year': 'public, max-age=' + ONE_YEAR_S,
+ 'private': 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'
+ };
+
+describe('Admin Routing', function () {
+ function doEnd(done) {
+ return function (err, res) {
+ if (err) {
+ return done(err);
+ }
+
+ should.not.exist(res.headers['x-cache-invalidate']);
+ should.not.exist(res.headers['X-CSRF-Token']);
+ should.exist(res.headers['set-cookie']);
+ should.exist(res.headers.date);
+
+ done();
+ };
+ }
+
+ before(function (done) {
+ testUtils.clearData().then(function () {
+ // we initialise data, but not a user. No user should be required for navigating the frontend
+ return testUtils.initData();
+ }).then(function () {
+ done();
+ }, done);
+
+ // Setup the request object with the correct URL
+ request = request(config().url);
+ });
+
+ describe('Ghost Admin Signup', function () {
+ it('should have a session cookie which expires in 12 hours', function (done) {
+ request.get('/ghost/signup/')
+ .end(function firstRequest(err, res) {
+ if (err) {
+ return done(err);
+ }
+
+ should.not.exist(res.headers['x-cache-invalidate']);
+ should.not.exist(res.headers['X-CSRF-Token']);
+ should.exist(res.headers['set-cookie']);
+ should.exist(res.headers.date);
+
+ var expires;
+ // Session should expire 12 hours after the time in the date header
+ expires = moment(res.headers.date).add('Hours', 12).format("ddd, DD MMM YYYY HH:mm");
+ expires = new RegExp("Expires=" + expires);
+
+ res.headers['set-cookie'].should.match(expires);
+
+ done();
+ });
+ });
+
+ it('should redirect from /ghost to /ghost/signup when no user', function (done) {
+ request.get('/ghost/')
+ .expect('Location', /ghost\/signup/)
+ .expect('Cache-Control', cacheRules['private'])
+ .expect(302)
+ .end(doEnd(done));
+ });
+
+ it('should redirect from /ghost/signin to /ghost/signup when no user', function (done) {
+ request.get('/ghost/signin/')
+ .expect('Location', /ghost\/signup/)
+ .expect('Cache-Control', cacheRules['private'])
+ .expect(302)
+ .end(doEnd(done));
+ });
+
+ it('should respond with html for /ghost/signup', function (done) {
+ request.get('/ghost/signup/')
+ .expect('Content-Type', /html/)
+ .expect('Cache-Control', cacheRules['private'])
+ .expect(200)
+ .end(doEnd(done));
+ });
+
+ // Add user
+
+// it('should redirect from /ghost/signup to /ghost/signin with user', function (done) {
+// done();
+// });
+
+// it('should respond with html for /ghost/signin', function (done) {
+// done();
+// });
+
+ // Do Login
+
+// it('should redirect from /ghost/signup to /ghost/ when logged in', function (done) {
+// done();
+// });
+
+// it('should redirect from /ghost/signup to /ghost/ when logged in', function (done) {
+// done();
+// });
+
+ });
+});
\ No newline at end of file
diff --git a/core/test/functional/routes/frontend_test.js b/core/test/functional/routes/frontend_test.js
index 6323bbb7ae..8a5f15af2d 100644
--- a/core/test/functional/routes/frontend_test.js
+++ b/core/test/functional/routes/frontend_test.js
@@ -5,12 +5,21 @@
// Mocking out the models to not touch the DB would turn these into unit tests, and should probably be done in future,
// But then again testing real code, rather than mock code, might be more useful...
-var request = require('supertest'),
- should = require('should'),
- moment = require('moment'),
+var request = require('supertest'),
+ should = require('should'),
+ moment = require('moment'),
- testUtils = require('../../utils'),
- config = require('../../../server/config');
+ testUtils = require('../../utils'),
+ config = require('../../../server/config'),
+
+ ONE_HOUR_S = 60 * 60,
+ ONE_YEAR_S = 365 * 24 * ONE_HOUR_S,
+ cacheRules = {
+ 'public': 'public, max-age=0',
+ 'hour': 'public, max-age=' + ONE_HOUR_S,
+ 'year': 'public, max-age=' + ONE_YEAR_S,
+ 'private': 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'
+ };
describe('Frontend Routing', function () {
function doEnd(done) {
@@ -18,6 +27,12 @@ describe('Frontend Routing', function () {
if (err) {
return done(err);
}
+
+ should.not.exist(res.headers['x-cache-invalidate']);
+ should.not.exist(res.headers['X-CSRF-Token']);
+ should.not.exist(res.headers['set-cookie']);
+ should.exist(res.headers.date);
+
done();
};
}
@@ -38,6 +53,7 @@ describe('Frontend Routing', function () {
it('should respond with html', function (done) {
request.get('/')
.expect('Content-Type', /html/)
+ .expect('Cache-Control', cacheRules['public'])
.expect(200)
.end(doEnd(done));
});
@@ -45,6 +61,7 @@ describe('Frontend Routing', function () {
it('should not have as second page', function (done) {
request.get('/page/2/')
.expect('Location', '/')
+ .expect('Cache-Control', cacheRules['public'])
.expect(302)
.end(doEnd(done));
});
@@ -54,6 +71,7 @@ describe('Frontend Routing', function () {
it('should redirect without slash', function (done) {
request.get('/welcome-to-ghost')
.expect('Location', '/welcome-to-ghost/')
+ .expect('Cache-Control', cacheRules.year)
.expect(301)
.end(doEnd(done));
});
@@ -61,6 +79,7 @@ describe('Frontend Routing', function () {
it('should respond with html', function (done) {
request.get('/welcome-to-ghost/')
.expect('Content-Type', /html/)
+ .expect('Cache-Control', cacheRules['public'])
.expect(200)
.end(doEnd(done));
});
@@ -72,17 +91,17 @@ describe('Frontend Routing', function () {
console.log('date', date);
request.get('/' + date + '/welcome-to-ghost/')
+ .expect('Cache-Control', cacheRules.hour)
.expect(404)
- // TODO this error message is inconsistent
.expect(/Page Not Found/)
.end(doEnd(done));
});
it('should 404 for unknown post', function (done) {
request.get('/spectacular/')
+ .expect('Cache-Control', cacheRules.hour)
.expect(404)
- // TODO this error message is inconsistent
- .expect(/Post not found/)
+ .expect(/Page Not Found/)
.end(doEnd(done));
});
});
@@ -91,6 +110,7 @@ describe('Frontend Routing', function () {
it('should redirect without slash', function (done) {
request.get('/rss')
.expect('Location', '/rss/')
+ .expect('Cache-Control', cacheRules.year)
.expect(301)
.end(doEnd(done));
});
@@ -98,14 +118,16 @@ describe('Frontend Routing', function () {
it('should respond with xml', function (done) {
request.get('/rss/')
.expect('Content-Type', /xml/)
+ .expect('Cache-Control', cacheRules['public'])
.expect(200)
.end(doEnd(done));
});
it('should not have as second page', function (done) {
request.get('/rss/2/')
- // TODO this should probably redirect straight to /rss/ ?
+ // TODO this should probably redirect straight to /rss/ with 301?
.expect('Location', '/rss/1/')
+ .expect('Cache-Control', cacheRules['public'])
.expect(302)
.end(doEnd(done));
});
@@ -129,6 +151,7 @@ describe('Frontend Routing', function () {
it('should redirect without slash', function (done) {
request.get('/page/2')
.expect('Location', '/page/2/')
+ .expect('Cache-Control', cacheRules.year)
.expect(301)
.end(doEnd(done));
});
@@ -136,6 +159,7 @@ describe('Frontend Routing', function () {
it('should respond with html', function (done) {
request.get('/page/2/')
.expect('Content-Type', /html/)
+ .expect('Cache-Control', cacheRules['public'])
.expect(200)
.end(doEnd(done));
});
@@ -143,6 +167,7 @@ describe('Frontend Routing', function () {
it('should redirect page 1', function (done) {
request.get('/page/1/')
.expect('Location', '/')
+ .expect('Cache-Control', cacheRules['public'])
// TODO: This should probably be a 301?
.expect(302)
.end(doEnd(done));
@@ -151,6 +176,7 @@ describe('Frontend Routing', function () {
it('should redirect to last page is page too high', function (done) {
request.get('/page/4/')
.expect('Location', '/page/3/')
+ .expect('Cache-Control', cacheRules['public'])
.expect(302)
.end(doEnd(done));
});
@@ -158,6 +184,7 @@ describe('Frontend Routing', function () {
it('should redirect to first page is page too low', function (done) {
request.get('/page/0/')
.expect('Location', '/')
+ .expect('Cache-Control', cacheRules['public'])
.expect(302)
.end(doEnd(done));
});
@@ -167,6 +194,7 @@ describe('Frontend Routing', function () {
it('should redirect without slash', function (done) {
request.get('/rss/2')
.expect('Location', '/rss/2/')
+ .expect('Cache-Control', cacheRules.year)
.expect(301)
.end(doEnd(done));
});
@@ -174,6 +202,7 @@ describe('Frontend Routing', function () {
it('should respond with xml', function (done) {
request.get('/rss/2/')
.expect('Content-Type', /xml/)
+ .expect('Cache-Control', cacheRules['public'])
.expect(200)
.end(doEnd(done));
});
@@ -181,6 +210,7 @@ describe('Frontend Routing', function () {
it('should redirect page 1', function (done) {
request.get('/rss/1/')
.expect('Location', '/rss/')
+ .expect('Cache-Control', cacheRules['public'])
// TODO: This should probably be a 301?
.expect(302)
.end(doEnd(done));
@@ -189,6 +219,7 @@ describe('Frontend Routing', function () {
it('should redirect to last page is page too high', function (done) {
request.get('/rss/3/')
.expect('Location', '/rss/2/')
+ .expect('Cache-Control', cacheRules['public'])
.expect(302)
.end(doEnd(done));
});
@@ -196,6 +227,7 @@ describe('Frontend Routing', function () {
it('should redirect to first page is page too low', function (done) {
request.get('/rss/0/')
.expect('Location', '/rss/')
+ .expect('Cache-Control', cacheRules['public'])
.expect(302)
.end(doEnd(done));
});
@@ -205,6 +237,7 @@ describe('Frontend Routing', function () {
it('should redirect without slash', function (done) {
request.get('/static-page-test')
.expect('Location', '/static-page-test/')
+ .expect('Cache-Control', cacheRules.year)
.expect(301)
.end(doEnd(done));
});
@@ -212,11 +245,39 @@ describe('Frontend Routing', function () {
it('should respond with xml', function (done) {
request.get('/static-page-test/')
.expect('Content-Type', /html/)
+ .expect('Cache-Control', cacheRules['public'])
.expect(200)
.end(doEnd(done));
});
});
+ describe('Static assets', function () {
+ it('should retrieve shared assets', function (done) {
+ request.get('/shared/img/usr-image.png')
+ .expect('Cache-Control', cacheRules.year)
+ .end(doEnd(done));
+ });
+
+ it('should retrieve theme assets', function (done) {
+ request.get('/assets/css/screen.css')
+ .expect('Cache-Control', cacheRules.hour)
+ .end(doEnd(done));
+ });
+
+ it('should retrieve built assets', function (done) {
+ request.get('/ghost/built/vendor.js')
+ .expect('Cache-Control', cacheRules.year)
+ .end(doEnd(done));
+ });
+
+ // at the moment there is no image fixture to test
+ // it('should retrieve image assets', function (done) {
+ // request.get('/assets/css/screen.css')
+ // .expect('Cache-Control', cacheRules.year)
+ // .end(doEnd(done));
+ // });
+ });
+
// ### The rest of the tests switch to date permalinks
// describe('Date permalinks', function () {
diff --git a/core/test/unit/middleware_spec.js b/core/test/unit/middleware_spec.js
index 76405d3418..b519dc239f 100644
--- a/core/test/unit/middleware_spec.js
+++ b/core/test/unit/middleware_spec.js
@@ -1,11 +1,11 @@
-/*globals describe, beforeEach, it*/
+/*globals describe, beforeEach, afterEach, it*/
var assert = require('assert'),
should = require('should'),
sinon = require('sinon'),
when = require('when'),
_ = require('underscore'),
express = require('express'),
- api = require('../../server/api');
+ api = require('../../server/api'),
middleware = require('../../server/middleware').middleware;
describe('Middleware', function () {
@@ -33,9 +33,9 @@ describe('Middleware', function () {
middleware.auth(req, res, null).then(function () {
assert(res.redirect.calledWithMatch('/ghost/signin/'));
- return done();
+ return done();
});
-
+
});
it('should redirect to signin path with redirect paramater stripped of /ghost/', function(done) {
@@ -145,22 +145,24 @@ describe('Middleware', function () {
beforeEach(function (done) {
api.notifications.add({
- id: 0,
+ id: 0,
+ status: 'passive',
+ message: 'passive-one'
+ }).then(function () {
+ return api.notifications.add({
+ id: 1,
status: 'passive',
- message: 'passive-one'
- }).then(function () {
- return api.notifications.add({
- id: 1,
- status: 'passive',
- message: 'passive-two'});
- }).then(function () {
- return api.notifications.add({
- id: 2,
- status: 'aggressive',
- message: 'aggressive'});
- }).then(function () {
- done();
+ message: 'passive-two'
});
+ }).then(function () {
+ return api.notifications.add({
+ id: 2,
+ status: 'aggressive',
+ message: 'aggressive'
+ });
+ }).then(function () {
+ done();
+ });
});
it('should clean all passive messages', function (done) {
@@ -177,7 +179,7 @@ describe('Middleware', function () {
});
});
- describe('disableCachedResult', function () {
+ describe('cacheControl', function () {
var res;
beforeEach(function () {
@@ -186,12 +188,28 @@ describe('Middleware', function () {
};
});
- it('should set correct cache headers', function (done) {
- middleware.disableCachedResult(null, res, function () {
- assert(res.set.calledWith({
- 'Cache-Control': 'no-cache, must-revalidate',
- 'Expires': 'Sat, 26 Jul 1997 05:00:00 GMT'
- }));
+ it('correctly sets the public profile headers', function (done) {
+ middleware.cacheControl('public')(null, res, function (a) {
+ should.not.exist(a);
+ res.set.calledOnce.should.be.true;
+ res.set.calledWith({'Cache-Control': 'public, max-age=0'});
+ return done();
+ });
+ });
+
+ it('correctly sets the private profile headers', function (done) {
+ middleware.cacheControl('private')(null, res, function (a) {
+ should.not.exist(a);
+ res.set.calledOnce.should.be.true;
+ res.set.calledWith({'Cache-Control': 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'});
+ return done();
+ });
+ });
+
+ it('will not set headers without a profile', function (done) {
+ middleware.cacheControl()(null, res, function (a) {
+ should.not.exist(a);
+ res.set.called.should.be.false;
return done();
});
});
@@ -235,8 +253,6 @@ describe('Middleware', function () {
});
describe('staticTheme', function () {
- var realExpressStatic = express.static;
-
beforeEach(function () {
sinon.stub(middleware, 'forwardToExpressStatic').yields();
});
diff --git a/core/test/unit/server_helpers_index_spec.js b/core/test/unit/server_helpers_index_spec.js
index 75e8ad9e68..92166eeb7e 100644
--- a/core/test/unit/server_helpers_index_spec.js
+++ b/core/test/unit/server_helpers_index_spec.js
@@ -1,17 +1,19 @@
/*globals describe, beforeEach, afterEach, it*/
-var testUtils = require('../utils'),
- should = require('should'),
- sinon = require('sinon'),
- when = require('when'),
- _ = require('underscore'),
- path = require('path'),
- api = require('../../server/api'),
- hbs = require('express-hbs'),
+var testUtils = require('../utils'),
+ should = require('should'),
+ sinon = require('sinon'),
+ when = require('when'),
+ _ = require('underscore'),
+ path = require('path'),
+ rewire = require('rewire'),
+ api = require('../../server/api'),
+ hbs = require('express-hbs'),
+
// Stuff we are testing
handlebars = hbs.handlebars,
- helpers = require('../../server/helpers'),
- config = require('../../server/config');
+ helpers = require('../../server/helpers'),
+ config = require('../../server/config');
describe('Core Helpers', function () {
@@ -302,9 +304,12 @@ describe('Core Helpers', function () {
});
it('returns meta tag string', function (done) {
- helpers.ghost_foot.call({version: "0.9"}).then(function (rendered) {
+
+ helpers.assetHash = 'abc';
+
+ helpers.ghost_foot.call().then(function (rendered) {
should.exist(rendered);
- rendered.string.should.match(/' +
+ '' +
+ '' +
+ '' +
+ ''
+ );
+
+ configStub = sinon.stub(config, 'paths', function () {
+ return {'subdir': '/blog'};
+ });
+
+ // with subdirectory
+ rendered = helpers.ghostScriptTags();
+ should.exist(rendered);
+ String(rendered).should.equal(
+ '' +
+ '' +
+ '' +
+ '' +
+ ''
+ );
+ });
+
+ it('outputs correct scripts for production mode', function () {
+
+ helpers.__set__('isProduction', true);
+
+ rendered = helpers.ghostScriptTags();
+ should.exist(rendered);
+ String(rendered).should.equal('');
+
+ configStub = sinon.stub(config, 'paths', function () {
+ return {'subdir': '/blog'};
+ });
+
+ // with subdirectory
+ rendered = helpers.ghostScriptTags();
+ should.exist(rendered);
+ String(rendered).should.equal('');
+ });
+ });
+
});
\ No newline at end of file