diff --git a/core/server/services/auth/api-key/admin.js b/core/server/services/auth/api-key/admin.js index 9842293d8c..da2f88dc2f 100644 --- a/core/server/services/auth/api-key/admin.js +++ b/core/server/services/auth/api-key/admin.js @@ -35,9 +35,10 @@ const _extractTokenFromHeader = function extractTokenFromHeader(header) { * https://tools.ietf.org/html/rfc7519#section-4.1.3 */ const authenticate = (req, res, next) => { - // we don't have an Authorization header so allow fallthrough to other + // CASE: we don't have an Authorization header so allow fallthrough to other // auth middleware or final "ensure authenticated" check if (!req.headers || !req.headers.authorization) { + req.api_key = null; return next(); } diff --git a/core/server/services/auth/authenticate.js b/core/server/services/auth/authenticate.js index b642a9161a..12aeaa04e4 100644 --- a/core/server/services/auth/authenticate.js +++ b/core/server/services/auth/authenticate.js @@ -102,9 +102,10 @@ const authenticate = { )(req, res, next); }, - // ### v2 API auth middleware - authenticateAdminApi: [session.safeGetSession, session.getUser], - authenticateAdminApiKey: apiKeyAuth.admin.authenticateAdminApiKey, + // @NOTE: authentication for admin api keys is disabled + // authenticateAdminApi: [apiKeyAuth.admin.authenticate, session.authenticate], + authenticateAdminApi: [session.authenticate], + authenticateContentApi: [apiKeyAuth.content.authenticateContentApiKey, members.authenticateMembersToken] }; diff --git a/core/server/services/auth/session/index.js b/core/server/services/auth/session/index.js index 2f6a434b45..9f2b16d2a0 100644 --- a/core/server/services/auth/session/index.js +++ b/core/server/services/auth/session/index.js @@ -1,16 +1,5 @@ module.exports = { - get getSession() { - return require('./middleware').getSession; - }, - - get cookieCsrfProtection() { - return require('./middleware').cookieCsrfProtection; - }, - - get safeGetSession() { - return require('./middleware').safeGetSession; - }, - + // @TODO: expose files/units and not functions of units get createSession() { return require('./middleware').createSession; }, @@ -19,7 +8,7 @@ module.exports = { return require('./middleware').destroySession; }, - get getUser() { - return require('./middleware').getUser; + get authenticate() { + return require('./middleware').authenticate; } }; diff --git a/core/server/services/auth/session/middleware.js b/core/server/services/auth/session/middleware.js index aec36535d3..e702122d37 100644 --- a/core/server/services/auth/session/middleware.js +++ b/core/server/services/auth/session/middleware.js @@ -1,10 +1,10 @@ const url = require('url'); +const session = require('express-session'); const common = require('../../../lib/common'); const constants = require('../../../lib/constants'); const config = require('../../../config'); const settingsCache = require('../../settings/cache'); const models = require('../../../models'); -const session = require('express-session'); const SessionStore = require('./store'); const urlService = require('../../url'); @@ -76,46 +76,65 @@ const destroySession = (req, res, next) => { }); }; -const getUser = (req, res, next) => { - if (!req.session || !req.session.user_id) { - req.user = null; - return next(); - } - models.User.findOne({id: req.session.user_id}) - .then((user) => { - req.user = user; - next(); - }).catch(() => { - req.user = null; - next(); - }); -}; - -const cookieCsrfProtection = (req, res, next) => { +const cookieCsrfProtection = (req) => { // If there is no origin on the session object it means this is a *new* // session, that hasn't been initialised yet. So we don't need CSRF protection if (!req.session.origin) { - return next(); + return; } const origin = getOrigin(req); + if (req.session.origin !== origin) { - return next(new common.errors.BadRequestError({ + throw new common.errors.BadRequestError({ message: common.i18n.t('errors.middleware.auth.mismatchedOrigin', { expected: req.session.origin, actual: origin }) - })); + }); + } +}; + +const authenticate = (req, res, next) => { + // CASE: we don't have a cookie header so allow fallthrough to other + // auth middleware or final "ensure authenticated" check + if (!req.headers || !req.headers.cookie) { + req.user = null; + return next(); } - return next(); + getSession(req, res, function (err) { + if (err) { + return next(err); + } + + try { + cookieCsrfProtection(req); + } catch (err) { + return next(err); + } + + if (!req.session || !req.session.user_id) { + req.user = null; + return next(); + } + + models.User.findOne({id: req.session.user_id}) + .then((user) => { + req.user = user; + next(); + }) + .catch(() => { + req.user = null; + next(); + }); + }); }; +// @TODO: this interface exposes private functions module.exports = exports = { - getSession, - cookieCsrfProtection, - safeGetSession: [getSession, cookieCsrfProtection], createSession, destroySession, - getUser + cookieCsrfProtection, + authenticate }; diff --git a/core/test/functional/api/v2/admin/posts_spec.js b/core/test/functional/api/v2/admin/posts_spec.js index af7220460e..e8064a9226 100644 --- a/core/test/functional/api/v2/admin/posts_spec.js +++ b/core/test/functional/api/v2/admin/posts_spec.js @@ -11,10 +11,8 @@ const ghost = testUtils.startGhost; let request; describe('Posts API V2', function () { - let ghostServer; - - describe('As Owner', function () { - let ownerCookie; + describe('...with admin api_key', function () { + let ghostServer; before(function () { return ghost() @@ -23,551 +21,590 @@ describe('Posts API V2', function () { request = supertest.agent(config.get('url')); }) .then(function () { - return localUtils.doAuth(request, 'users:extra', 'posts'); - }) - .then(function (cookie) { - ownerCookie = cookie; + // @NOTE: We don't authenticate! + return testUtils.initFixtures('api_keys'); }); }); - describe('Browse', function () { - it('retrieves all published posts only by default', function (done) { - request.get(localUtils.API.getApiQuery('posts/')) - .set('Origin', config.get('url')) - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(200) - .end(function (err, res) { - if (err) { - return done(err); - } + it('example: add post', function () { + const post = { + // @TODO: required for now, needs proper validation + author_id: '1', + title: 'Post created with api_key' + }; - should.not.exist(res.headers['x-cache-invalidate']); - const jsonResponse = res.body; - should.exist(jsonResponse.posts); - localUtils.API.checkResponse(jsonResponse, 'posts'); - jsonResponse.posts.should.have.length(11); - localUtils.API.checkResponse(jsonResponse.posts[0], 'post'); - localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); - _.isBoolean(jsonResponse.posts[0].featured).should.eql(true); - _.isBoolean(jsonResponse.posts[0].page).should.eql(true); - - // Ensure default order - jsonResponse.posts[0].slug.should.eql('welcome'); - jsonResponse.posts[10].slug.should.eql('html-ipsum'); - - // Absolute urls by default - jsonResponse.posts[0].url.should.eql(`${config.get('url')}/welcome/`); - jsonResponse.posts[9].feature_image.should.eql(`${config.get('url')}/content/images/2018/hey.jpg`); - - done(); - }); - }); - - it('can retrieve multiple post formats', function (done) { - request.get(localUtils.API.getApiQuery('posts/?formats=plaintext,mobiledoc&limit=3&order=title%20ASC')) - .set('Origin', config.get('url')) - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(200) - .end(function (err, res) { - if (err) { - return done(err); - } - - should.not.exist(res.headers['x-cache-invalidate']); - const jsonResponse = res.body; - should.exist(jsonResponse.posts); - localUtils.API.checkResponse(jsonResponse, 'posts'); - jsonResponse.posts.should.have.length(3); - localUtils.API.checkResponse(jsonResponse.posts[0], 'post', ['mobiledoc', 'plaintext'], ['html']); - localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); - _.isBoolean(jsonResponse.posts[0].featured).should.eql(true); - _.isBoolean(jsonResponse.posts[0].page).should.eql(true); - - // ensure order works - jsonResponse.posts[0].slug.should.eql('apps-integrations'); - - done(); - }); - }); - - it('fields & formats combined', function (done) { - request.get(localUtils.API.getApiQuery('posts/?formats=mobiledoc,html&fields=id,title')) - .set('Origin', config.get('url')) - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(200) - .end(function (err, res) { - if (err) { - return done(err); - } - - should.not.exist(res.headers['x-cache-invalidate']); - const jsonResponse = res.body; - should.exist(jsonResponse.posts); - localUtils.API.checkResponse(jsonResponse, 'posts'); - jsonResponse.posts.should.have.length(11); - - localUtils.API.checkResponse( - jsonResponse.posts[0], - 'post', - null, - null, - ['mobiledoc', 'id', 'title', 'html'] - ); - - localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); - - done(); - }); - }); - - it('with includes', function (done) { - request.get(localUtils.API.getApiQuery('posts/?include=tags,authors')) - .set('Origin', config.get('url')) - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(200) - .end(function (err, res) { - if (err) { - return done(err); - } - - should.not.exist(res.headers['x-cache-invalidate']); - const jsonResponse = res.body; - should.exist(jsonResponse.posts); - localUtils.API.checkResponse(jsonResponse, 'posts'); - jsonResponse.posts.should.have.length(11); - localUtils.API.checkResponse( - jsonResponse.posts[0], - 'post', - ['tags', 'authors'] - ); - - localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); - - jsonResponse.posts[0].tags.length.should.eql(1); - jsonResponse.posts[0].authors.length.should.eql(1); - jsonResponse.posts[0].tags[0].url.should.eql(`${config.get('url')}/tag/getting-started/`); - jsonResponse.posts[0].authors[0].url.should.eql(`${config.get('url')}/author/ghost/`); - - done(); - }); - }); - - it('fields combined with formats and include', function (done) { - request.get(localUtils.API.getApiQuery('posts/?formats=mobiledoc,html&fields=id,title&include=authors')) - .set('Origin', config.get('url')) - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(200) - .end(function (err, res) { - if (err) { - return done(err); - } - - should.not.exist(res.headers['x-cache-invalidate']); - const jsonResponse = res.body; - should.exist(jsonResponse.posts); - localUtils.API.checkResponse(jsonResponse, 'posts'); - jsonResponse.posts.should.have.length(11); - localUtils.API.checkResponse( - jsonResponse.posts[0], - 'post', - null, - null, - ['mobiledoc', 'id', 'title', 'html', 'authors'] - ); - - localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); - - done(); - }); - }); - - it('can use a filter', function (done) { - request.get(localUtils.API.getApiQuery('posts/?filter=page:[false,true]&status=all')) - .set('Origin', config.get('url')) - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(200) - .end(function (err, res) { - if (err) { - return done(err); - } - - should.not.exist(res.headers['x-cache-invalidate']); - const jsonResponse = res.body; - should.exist(jsonResponse.posts); - localUtils.API.checkResponse(jsonResponse, 'posts'); - jsonResponse.posts.should.have.length(15); - localUtils.API.checkResponse(jsonResponse.posts[0], 'post'); - localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); - done(); - }); - }); - - it('supports usage of the page param', function (done) { - request.get(localUtils.API.getApiQuery('posts/?page=2')) - .set('Origin', config.get('url')) - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(200) - .end(function (err, res) { - if (err) { - return done(err); - } - - const jsonResponse = res.body; - should.equal(jsonResponse.meta.pagination.page, 2); - done(); - }); - }); + return request + .post(localUtils.API.getApiQuery('posts')) + .set('Origin', config.get('url')) + .set('Authorization', `Ghost ${localUtils.getValidAdminKey(localUtils.API.getApiQuery('posts'))}`) + .send({ + posts: [post] + }) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(403); }); + }); - describe('read', function () { - it('by id', function (done) { - request.get(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/')) - .set('Origin', config.get('url')) - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(200) - .end(function (err, res) { - if (err) { - return done(err); - } + describe('...with sessions', function () { + let ghostServer; - should.not.exist(res.headers['x-cache-invalidate']); - var jsonResponse = res.body; - should.exist(jsonResponse); - should.exist(jsonResponse.posts); - localUtils.API.checkResponse(jsonResponse.posts[0], 'post'); - jsonResponse.posts[0].id.should.equal(testUtils.DataGenerator.Content.posts[0].id); - jsonResponse.posts[0].page.should.not.be.ok(); - _.isBoolean(jsonResponse.posts[0].featured).should.eql(true); - _.isBoolean(jsonResponse.posts[0].page).should.eql(true); - jsonResponse.posts[0].author.should.be.a.String(); - testUtils.API.isISO8601(jsonResponse.posts[0].created_at).should.be.true(); - // Tags aren't included by default - should.not.exist(jsonResponse.posts[0].tags); - done(); - }); - }); + describe('As Owner', function () { + let ownerCookie; - it('by id, with formats', function (done) { - request - .get(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/?formats=plaintext,mobiledoc')) - .set('Origin', config.get('url')) - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(200) - .end(function (err, res) { - if (err) { - return done(err); - } - - should.not.exist(res.headers['x-cache-invalidate']); - var jsonResponse = res.body; - should.exist(jsonResponse.posts); - jsonResponse.posts.should.have.length(1); - jsonResponse.posts[0].id.should.equal(testUtils.DataGenerator.Content.posts[0].id); - - localUtils.API.checkResponse(jsonResponse.posts[0], 'post', ['mobiledoc', 'plaintext'], ['html']); - - done(); - }); - }); - - it('can retrieve a post by slug', function (done) { - request.get(localUtils.API.getApiQuery('posts/slug/welcome/')) - .set('Origin', config.get('url')) - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(200) - .end(function (err, res) { - if (err) { - return done(err); - } - - should.not.exist(res.headers['x-cache-invalidate']); - var jsonResponse = res.body; - should.exist(jsonResponse); - should.exist(jsonResponse.posts); - localUtils.API.checkResponse(jsonResponse.posts[0], 'post'); - jsonResponse.posts[0].slug.should.equal('welcome'); - jsonResponse.posts[0].page.should.not.be.ok(); - _.isBoolean(jsonResponse.posts[0].featured).should.eql(true); - _.isBoolean(jsonResponse.posts[0].page).should.eql(true); - jsonResponse.posts[0].author.should.be.a.String(); - // Tags aren't included by default - should.not.exist(jsonResponse.posts[0].tags); - done(); - }); - }); - - it('with includes', function (done) { - request - .get(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/?include=authors,tags')) - .set('Origin', config.get('url')) - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(200) - .end(function (err, res) { - if (err) { - return done(err); - } - - should.not.exist(res.headers['x-cache-invalidate']); - var jsonResponse = res.body; - should.exist(jsonResponse); - should.exist(jsonResponse.posts); - - localUtils.API.checkResponse(jsonResponse.posts[0], 'post', ['tags', 'authors']); - - jsonResponse.posts[0].author.should.be.a.String(); - jsonResponse.posts[0].page.should.not.be.ok(); - - jsonResponse.posts[0].authors[0].should.be.an.Object(); - localUtils.API.checkResponse(jsonResponse.posts[0].authors[0], 'user', ['url']); - - jsonResponse.posts[0].tags[0].should.be.an.Object(); - localUtils.API.checkResponse(jsonResponse.posts[0].tags[0], 'tag', ['url']); - done(); - }); - }); - - it('can\'t retrieve non existent post', function (done) { - request.get(localUtils.API.getApiQuery(`posts/${ObjectId.generate()}/`)) - .set('Origin', config.get('url')) - .set('Accept', 'application/json') - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(404) - .end(function (err, res) { - if (err) { - return done(err); - } - - should.not.exist(res.headers['x-cache-invalidate']); - var jsonResponse = res.body; - should.exist(jsonResponse); - should.exist(jsonResponse.errors); - testUtils.API.checkResponseValue(jsonResponse.errors[0], ['message', 'errorType']); - done(); - }); - }); - }); - - describe('add', function () { - it('default', function () { - const post = { - title: 'My post', - status: 'draft', - published_at: '2016-05-30T07:00:00.000Z', - mobiledoc: testUtils.DataGenerator.markdownToMobiledoc('my post'), - created_at: moment().subtract(2, 'days').toDate(), - updated_at: moment().subtract(2, 'days').toDate(), - created_by: ObjectId.generate(), - updated_by: ObjectId.generate() - }; - - return request.post(localUtils.API.getApiQuery('posts')) - .set('Origin', config.get('url')) - .send({posts: [post]}) - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(201) - .then((res) => { - res.body.posts.length.should.eql(1); - localUtils.API.checkResponse(res.body.posts[0], 'post'); - should.not.exist(res.headers['x-cache-invalidate']); - - return models.Post.findOne({ - id: res.body.posts[0].id, - status: 'draft' - }, testUtils.context.internal); + before(function () { + return ghost() + .then(function (_ghostServer) { + ghostServer = _ghostServer; + request = supertest.agent(config.get('url')); }) - .then((model) => { - model.get('title').should.eql(post.title); - model.get('status').should.eql(post.status); - model.get('published_at').toISOString().should.eql('2016-05-30T07:00:00.000Z'); - model.get('created_at').toISOString().should.not.eql(post.created_at.toISOString()); - model.get('updated_at').toISOString().should.not.eql(post.updated_at.toISOString()); - model.get('updated_by').should.not.eql(post.updated_by); - model.get('created_by').should.not.eql(post.created_by); - }); - }); - - it('published post with response timestamps in UTC format respecting original UTC offset', function () { - const post = { - posts: [{ - status: 'published', - published_at: '2016-05-31T07:00:00.000+06:00', - created_at: '2016-05-30T03:00:00.000Z', - updated_at: '2016-05-30T07:00:00.000' - }] - }; - - return request.post(localUtils.API.getApiQuery('posts')) - .set('Origin', config.get('url')) - .send(post) - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(201) - .then((res) => { - res.body.posts.length.should.eql(1); - localUtils.API.checkResponse(res.body.posts[0], 'post'); - res.body.posts[0].status.should.eql('published'); - res.headers['x-cache-invalidate'].should.eql('/*'); - - res.body.posts[0].published_at.should.eql('2016-05-31T01:00:00.000Z'); - res.body.posts[0].created_at.should.eql('2016-05-30T03:00:00.000Z'); - res.body.posts[0].updated_at.should.eql('2016-05-30T07:00:00.000Z'); - }); - }); - }); - - describe('edit', function () { - it('default', function () { - const post = { - title: 'My new Title', - author: testUtils.DataGenerator.Content.extraUsers[0].id, - custom_template: 'custom-about' - }; - - return request - .put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/')) - .set('Origin', config.get('url')) - .send({posts: [post]}) - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(200) - .then((res) => { - res.headers['x-cache-invalidate'].should.eql('/*'); - localUtils.API.checkResponse(res.body.posts[0], 'post'); - - res.body.posts[0].title.should.eql(post.title); - res.body.posts[0].author.should.eql(post.author); - res.body.posts[0].status.should.eql('published'); - res.body.posts[0].custom_template.should.eql('custom-about'); - }); - }); - - it('update dates', function () { - const post = { - created_by: ObjectId.generate(), - updated_by: ObjectId.generate(), - created_at: moment().add(2, 'days').format(), - updated_at: moment().add(2, 'days').format() - }; - - return request - .put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/')) - .set('Origin', config.get('url')) - .send({posts: [post]}) - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(200) - .then((res) => { - res.headers['x-cache-invalidate'].should.eql('/*'); - localUtils.API.checkResponse(res.body.posts[0], 'post'); - - return models.Post.findOne({ - id: res.body.posts[0].id - }, testUtils.context.internal); + .then(function () { + return localUtils.doAuth(request, 'users:extra', 'posts'); }) - .then((model) => { - // We expect that the changed properties aren't changed, they are still the same than before. - model.get('created_at').toISOString().should.not.eql(post.created_at); - model.get('updated_by').should.not.eql(post.updated_by); - model.get('created_by').should.not.eql(post.created_by); - - // `updated_at` is automatically set, but it's not the date we send to override. - model.get('updated_at').toISOString().should.not.eql(post.updated_at); + .then(function (cookie) { + ownerCookie = cookie; }); }); - it('update draft', function () { - const post = { - title: 'update draft' - }; + describe('Browse', function () { + it('retrieves all published posts only by default', function (done) { + request.get(localUtils.API.getApiQuery('posts/')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } - return request.put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[3].id)) - .set('Origin', config.get('url')) - .send({posts: [post]}) - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(200) - .then((res) => { - res.headers['x-cache-invalidate'].should.eql('/p/' + res.body.posts[0].uuid + '/'); - }); + should.not.exist(res.headers['x-cache-invalidate']); + const jsonResponse = res.body; + should.exist(jsonResponse.posts); + localUtils.API.checkResponse(jsonResponse, 'posts'); + jsonResponse.posts.should.have.length(11); + localUtils.API.checkResponse(jsonResponse.posts[0], 'post'); + localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); + _.isBoolean(jsonResponse.posts[0].featured).should.eql(true); + _.isBoolean(jsonResponse.posts[0].page).should.eql(true); + + // Ensure default order + jsonResponse.posts[0].slug.should.eql('welcome'); + jsonResponse.posts[10].slug.should.eql('html-ipsum'); + + // Absolute urls by default + jsonResponse.posts[0].url.should.eql(`${config.get('url')}/welcome/`); + jsonResponse.posts[9].feature_image.should.eql(`${config.get('url')}/content/images/2018/hey.jpg`); + + done(); + }); + }); + + it('can retrieve multiple post formats', function (done) { + request.get(localUtils.API.getApiQuery('posts/?formats=plaintext,mobiledoc&limit=3&order=title%20ASC')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + const jsonResponse = res.body; + should.exist(jsonResponse.posts); + localUtils.API.checkResponse(jsonResponse, 'posts'); + jsonResponse.posts.should.have.length(3); + localUtils.API.checkResponse(jsonResponse.posts[0], 'post', ['mobiledoc', 'plaintext'], ['html']); + localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); + _.isBoolean(jsonResponse.posts[0].featured).should.eql(true); + _.isBoolean(jsonResponse.posts[0].page).should.eql(true); + + // ensure order works + jsonResponse.posts[0].slug.should.eql('apps-integrations'); + + done(); + }); + }); + + it('fields & formats combined', function (done) { + request.get(localUtils.API.getApiQuery('posts/?formats=mobiledoc,html&fields=id,title')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + const jsonResponse = res.body; + should.exist(jsonResponse.posts); + localUtils.API.checkResponse(jsonResponse, 'posts'); + jsonResponse.posts.should.have.length(11); + + localUtils.API.checkResponse( + jsonResponse.posts[0], + 'post', + null, + null, + ['mobiledoc', 'id', 'title', 'html'] + ); + + localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); + + done(); + }); + }); + + it('with includes', function (done) { + request.get(localUtils.API.getApiQuery('posts/?include=tags,authors')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + const jsonResponse = res.body; + should.exist(jsonResponse.posts); + localUtils.API.checkResponse(jsonResponse, 'posts'); + jsonResponse.posts.should.have.length(11); + localUtils.API.checkResponse( + jsonResponse.posts[0], + 'post', + ['tags', 'authors'] + ); + + localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); + + jsonResponse.posts[0].tags.length.should.eql(1); + jsonResponse.posts[0].authors.length.should.eql(1); + jsonResponse.posts[0].tags[0].url.should.eql(`${config.get('url')}/tag/getting-started/`); + jsonResponse.posts[0].authors[0].url.should.eql(`${config.get('url')}/author/ghost/`); + + done(); + }); + }); + + it('fields combined with formats and include', function (done) { + request.get(localUtils.API.getApiQuery('posts/?formats=mobiledoc,html&fields=id,title&include=authors')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + const jsonResponse = res.body; + should.exist(jsonResponse.posts); + localUtils.API.checkResponse(jsonResponse, 'posts'); + jsonResponse.posts.should.have.length(11); + localUtils.API.checkResponse( + jsonResponse.posts[0], + 'post', + null, + null, + ['mobiledoc', 'id', 'title', 'html', 'authors'] + ); + + localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); + + done(); + }); + }); + + it('can use a filter', function (done) { + request.get(localUtils.API.getApiQuery('posts/?filter=page:[false,true]&status=all')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + const jsonResponse = res.body; + should.exist(jsonResponse.posts); + localUtils.API.checkResponse(jsonResponse, 'posts'); + jsonResponse.posts.should.have.length(15); + localUtils.API.checkResponse(jsonResponse.posts[0], 'post'); + localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); + done(); + }); + }); + + it('supports usage of the page param', function (done) { + request.get(localUtils.API.getApiQuery('posts/?page=2')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + const jsonResponse = res.body; + should.equal(jsonResponse.meta.pagination.page, 2); + done(); + }); + }); }); - it('unpublish', function () { - const post = { - status: 'draft' - }; + describe('read', function () { + it('by id', function (done) { + request.get(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } - return request - .put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[1].id + '/')) - .set('Origin', config.get('url')) - .send({posts: [post]}) - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(200) - .then((res) => { - res.headers['x-cache-invalidate'].should.eql('/*'); - res.body.posts[0].status.should.eql('draft'); - }); + should.not.exist(res.headers['x-cache-invalidate']); + var jsonResponse = res.body; + should.exist(jsonResponse); + should.exist(jsonResponse.posts); + localUtils.API.checkResponse(jsonResponse.posts[0], 'post'); + jsonResponse.posts[0].id.should.equal(testUtils.DataGenerator.Content.posts[0].id); + jsonResponse.posts[0].page.should.not.be.ok(); + _.isBoolean(jsonResponse.posts[0].featured).should.eql(true); + _.isBoolean(jsonResponse.posts[0].page).should.eql(true); + jsonResponse.posts[0].author.should.be.a.String(); + testUtils.API.isISO8601(jsonResponse.posts[0].created_at).should.be.true(); + // Tags aren't included by default + should.not.exist(jsonResponse.posts[0].tags); + done(); + }); + }); + + it('by id, with formats', function (done) { + request + .get(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/?formats=plaintext,mobiledoc')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + var jsonResponse = res.body; + should.exist(jsonResponse.posts); + jsonResponse.posts.should.have.length(1); + jsonResponse.posts[0].id.should.equal(testUtils.DataGenerator.Content.posts[0].id); + + localUtils.API.checkResponse(jsonResponse.posts[0], 'post', ['mobiledoc', 'plaintext'], ['html']); + + done(); + }); + }); + + it('can retrieve a post by slug', function (done) { + request.get(localUtils.API.getApiQuery('posts/slug/welcome/')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + var jsonResponse = res.body; + should.exist(jsonResponse); + should.exist(jsonResponse.posts); + localUtils.API.checkResponse(jsonResponse.posts[0], 'post'); + jsonResponse.posts[0].slug.should.equal('welcome'); + jsonResponse.posts[0].page.should.not.be.ok(); + _.isBoolean(jsonResponse.posts[0].featured).should.eql(true); + _.isBoolean(jsonResponse.posts[0].page).should.eql(true); + jsonResponse.posts[0].author.should.be.a.String(); + // Tags aren't included by default + should.not.exist(jsonResponse.posts[0].tags); + done(); + }); + }); + + it('with includes', function (done) { + request + .get(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/?include=authors,tags')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + var jsonResponse = res.body; + should.exist(jsonResponse); + should.exist(jsonResponse.posts); + + localUtils.API.checkResponse(jsonResponse.posts[0], 'post', ['tags', 'authors']); + + jsonResponse.posts[0].author.should.be.a.String(); + jsonResponse.posts[0].page.should.not.be.ok(); + + jsonResponse.posts[0].authors[0].should.be.an.Object(); + localUtils.API.checkResponse(jsonResponse.posts[0].authors[0], 'user', ['url']); + + jsonResponse.posts[0].tags[0].should.be.an.Object(); + localUtils.API.checkResponse(jsonResponse.posts[0].tags[0], 'tag', ['url']); + done(); + }); + }); + + it('can\'t retrieve non existent post', function (done) { + request.get(localUtils.API.getApiQuery(`posts/${ObjectId.generate()}/`)) + .set('Origin', config.get('url')) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(404) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + var jsonResponse = res.body; + should.exist(jsonResponse); + should.exist(jsonResponse.errors); + testUtils.API.checkResponseValue(jsonResponse.errors[0], ['message', 'errorType']); + done(); + }); + }); }); - it('published_at = null', function () { - return request - .put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/')) - .set('Origin', config.get('url')) - .send({ - posts: [{published_at: null}] - }) - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(200) - .then((res) => { - res.headers['x-cache-invalidate'].should.eql('/*'); - should.exist(res.body.posts); - should.exist(res.body.posts[0].published_at); - localUtils.API.checkResponse(res.body.posts[0], 'post'); - }); - }); - }); + describe('add', function () { + it('default', function () { + const post = { + title: 'My post', + status: 'draft', + published_at: '2016-05-30T07:00:00.000Z', + mobiledoc: testUtils.DataGenerator.markdownToMobiledoc('my post'), + created_at: moment().subtract(2, 'days').toDate(), + updated_at: moment().subtract(2, 'days').toDate(), + created_by: ObjectId.generate(), + updated_by: ObjectId.generate() + }; - describe('destroy', function () { - it('default', function () { - return request - .del(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/')) - .set('Origin', config.get('url')) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(204) - .then((res) => { - res.body.should.be.empty(); - res.headers['x-cache-invalidate'].should.eql('/*'); - }); + return request.post(localUtils.API.getApiQuery('posts')) + .set('Origin', config.get('url')) + .send({posts: [post]}) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(201) + .then((res) => { + res.body.posts.length.should.eql(1); + localUtils.API.checkResponse(res.body.posts[0], 'post'); + should.not.exist(res.headers['x-cache-invalidate']); + + return models.Post.findOne({ + id: res.body.posts[0].id, + status: 'draft' + }, testUtils.context.internal); + }) + .then((model) => { + model.get('title').should.eql(post.title); + model.get('status').should.eql(post.status); + model.get('published_at').toISOString().should.eql('2016-05-30T07:00:00.000Z'); + model.get('created_at').toISOString().should.not.eql(post.created_at.toISOString()); + model.get('updated_at').toISOString().should.not.eql(post.updated_at.toISOString()); + model.get('updated_by').should.not.eql(post.updated_by); + model.get('created_by').should.not.eql(post.created_by); + }); + }); + + it('published post with response timestamps in UTC format respecting original UTC offset', function () { + const post = { + posts: [{ + status: 'published', + published_at: '2016-05-31T07:00:00.000+06:00', + created_at: '2016-05-30T03:00:00.000Z', + updated_at: '2016-05-30T07:00:00.000' + }] + }; + + return request.post(localUtils.API.getApiQuery('posts')) + .set('Origin', config.get('url')) + .send(post) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(201) + .then((res) => { + res.body.posts.length.should.eql(1); + localUtils.API.checkResponse(res.body.posts[0], 'post'); + res.body.posts[0].status.should.eql('published'); + res.headers['x-cache-invalidate'].should.eql('/*'); + + res.body.posts[0].published_at.should.eql('2016-05-31T01:00:00.000Z'); + res.body.posts[0].created_at.should.eql('2016-05-30T03:00:00.000Z'); + res.body.posts[0].updated_at.should.eql('2016-05-30T07:00:00.000Z'); + }); + }); }); - it('non existent post', function () { - return request - .del(localUtils.API.getApiQuery('posts/' + ObjectId.generate() + '/')) - .set('Origin', config.get('url')) - .set('Accept', 'application/json') - .expect('Content-Type', /json/) - .expect('Cache-Control', testUtils.cacheRules.private) - .expect(404) - .then((res) => { - should.not.exist(res.headers['x-cache-invalidate']); - should.exist(res.body); - should.exist(res.body.errors); - testUtils.API.checkResponseValue(res.body.errors[0], ['message', 'errorType']); - }); + describe('edit', function () { + it('default', function () { + const post = { + title: 'My new Title', + author: testUtils.DataGenerator.Content.extraUsers[0].id, + custom_template: 'custom-about' + }; + + return request + .put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/')) + .set('Origin', config.get('url')) + .send({posts: [post]}) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .then((res) => { + res.headers['x-cache-invalidate'].should.eql('/*'); + localUtils.API.checkResponse(res.body.posts[0], 'post'); + + res.body.posts[0].title.should.eql(post.title); + res.body.posts[0].author.should.eql(post.author); + res.body.posts[0].status.should.eql('published'); + res.body.posts[0].custom_template.should.eql('custom-about'); + }); + }); + + it('update dates', function () { + const post = { + created_by: ObjectId.generate(), + updated_by: ObjectId.generate(), + created_at: moment().add(2, 'days').format(), + updated_at: moment().add(2, 'days').format() + }; + + return request + .put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/')) + .set('Origin', config.get('url')) + .send({posts: [post]}) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .then((res) => { + res.headers['x-cache-invalidate'].should.eql('/*'); + localUtils.API.checkResponse(res.body.posts[0], 'post'); + + return models.Post.findOne({ + id: res.body.posts[0].id + }, testUtils.context.internal); + }) + .then((model) => { + // We expect that the changed properties aren't changed, they are still the same than before. + model.get('created_at').toISOString().should.not.eql(post.created_at); + model.get('updated_by').should.not.eql(post.updated_by); + model.get('created_by').should.not.eql(post.created_by); + + // `updated_at` is automatically set, but it's not the date we send to override. + model.get('updated_at').toISOString().should.not.eql(post.updated_at); + }); + }); + + it('update draft', function () { + const post = { + title: 'update draft' + }; + + return request.put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[3].id)) + .set('Origin', config.get('url')) + .send({posts: [post]}) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .then((res) => { + res.headers['x-cache-invalidate'].should.eql('/p/' + res.body.posts[0].uuid + '/'); + }); + }); + + it('unpublish', function () { + const post = { + status: 'draft' + }; + + return request + .put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[1].id + '/')) + .set('Origin', config.get('url')) + .send({posts: [post]}) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .then((res) => { + res.headers['x-cache-invalidate'].should.eql('/*'); + res.body.posts[0].status.should.eql('draft'); + }); + }); + + it('published_at = null', function () { + return request + .put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/')) + .set('Origin', config.get('url')) + .send({ + posts: [{published_at: null}] + }) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .then((res) => { + res.headers['x-cache-invalidate'].should.eql('/*'); + should.exist(res.body.posts); + should.exist(res.body.posts[0].published_at); + localUtils.API.checkResponse(res.body.posts[0], 'post'); + }); + }); + }); + + describe('destroy', function () { + it('default', function () { + return request + .del(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/')) + .set('Origin', config.get('url')) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(204) + .then((res) => { + res.body.should.be.empty(); + res.headers['x-cache-invalidate'].should.eql('/*'); + }); + }); + + it('non existent post', function () { + return request + .del(localUtils.API.getApiQuery('posts/' + ObjectId.generate() + '/')) + .set('Origin', config.get('url')) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(404) + .then((res) => { + should.not.exist(res.headers['x-cache-invalidate']); + should.exist(res.body); + should.exist(res.body.errors); + testUtils.API.checkResponseValue(res.body.errors[0], ['message', 'errorType']); + }); + }); }); }); }); diff --git a/core/test/unit/services/auth/session/index_spec.js b/core/test/unit/services/auth/session/middleware_spec.js similarity index 59% rename from core/test/unit/services/auth/session/index_spec.js rename to core/test/unit/services/auth/session/middleware_spec.js index 1f9f1caf47..365bd71ac1 100644 --- a/core/test/unit/services/auth/session/index_spec.js +++ b/core/test/unit/services/auth/session/middleware_spec.js @@ -1,6 +1,4 @@ -const sessionService = require('../../../../../server/services/auth/session'); -const SessionStore = require('../../../../../server/services/auth/session/store'); -const config = require('../../../../../server/config'); +const sessionMiddleware = require('../../../../../server/services/auth/session/middleware'); const models = require('../../../../../server/models'); const sinon = require('sinon'); const should = require('should'); @@ -44,7 +42,7 @@ describe('Session Service', function () { .withArgs('origin').returns('') .withArgs('referrer').returns(''); - sessionService.createSession(req, fakeRes(), function next(err) { + sessionMiddleware.createSession(req, fakeRes(), function next(err) { should.equal(err instanceof BadRequestError, true); done(); }); @@ -68,7 +66,7 @@ describe('Session Service', function () { done(); }); - sessionService.createSession(req, res); + sessionMiddleware.createSession(req, res); }); it('sets req.session.user_id,origin,user_agent,ip and calls sendStatus with 201 if the check succeeds', function (done) { @@ -92,7 +90,7 @@ describe('Session Service', function () { done(); }); - sessionService.createSession(req, res); + sessionMiddleware.createSession(req, res); }); }); @@ -102,7 +100,7 @@ describe('Session Service', function () { const res = fakeRes(); const destroyStub = sandbox.stub(req.session, 'destroy'); - sessionService.destroySession(req, res); + sessionMiddleware.destroySession(req, res); should.equal(destroyStub.callCount, 1); }); @@ -115,7 +113,7 @@ describe('Session Service', function () { fn(new Error('oops')); }); - sessionService.destroySession(req, res, function next(err) { + sessionMiddleware.destroySession(req, res, function next(err) { should.equal(err instanceof InternalServerError, true); done(); }); @@ -134,71 +132,7 @@ describe('Session Service', function () { done(); }); - sessionService.destroySession(req, res); - }); - }); - - describe('getUser', function () { - it('sets req.user to null and calls next if there is no session', function (done) { - const req = fakeReq(); - const res = fakeRes(); - - delete req.session; - - sessionService.getUser(req, res, function next() { - should.equal(req.user, null); - done(); - }); - }); - - it('sets req.user to null and calls next if there is no session', function (done) { - const req = fakeReq(); - const res = fakeRes(); - - sessionService.getUser(req, res, function next() { - should.equal(req.user, null); - done(); - }); - }); - - it('calls User.findOne with id set to req.session.user_id', function (done) { - const req = fakeReq(); - const res = fakeRes(); - sandbox.stub(models.User, 'findOne') - .callsFake(function (opts) { - should.equal(opts.id, 23); - done(); - }); - - req.session.user_id = 23; - sessionService.getUser(req, res); - }); - - it('sets req.user to null and calls next if the user is not found', function (done) { - const req = fakeReq(); - const res = fakeRes(); - sandbox.stub(models.User, 'findOne') - .rejects(); - - req.session.user_id = 23; - sessionService.getUser(req, res, function next() { - should.equal(req.user, null); - done(); - }); - }); - - it('calls next after settign req.user to the found user', function (done) { - const req = fakeReq(); - const res = fakeRes(); - const user = models.User.forge({id: 23}); - sandbox.stub(models.User, 'findOne') - .resolves(user); - - req.session.user_id = 23; - sessionService.getUser(req, res, function next() { - should.equal(req.user, user); - done(); - }); + sessionMiddleware.destroySession(req, res); }); }); @@ -207,10 +141,8 @@ describe('Session Service', function () { const req = fakeReq(); const res = fakeRes(); - sessionService.cookieCsrfProtection(req, res, function next(err) { - should.not.exist(err); - done(); - }); + sessionMiddleware.cookieCsrfProtection(req); + done(); }); it('calls next if req origin matches the session origin', function (done) { @@ -220,10 +152,8 @@ describe('Session Service', function () { .withArgs('origin').returns('http://host.tld'); req.session.origin = 'http://host.tld'; - sessionService.cookieCsrfProtection(req, res, function next(err) { - should.not.exist(err); - done(); - }); + sessionMiddleware.cookieCsrfProtection(req); + done(); }); it('calls next with BadRequestError if the origin of req does not match the session', function (done) { @@ -233,19 +163,12 @@ describe('Session Service', function () { .withArgs('origin').returns('http://host.tld'); req.session.origin = 'http://different-host.tld'; - sessionService.cookieCsrfProtection(req, res, function next(err) { + try { + sessionMiddleware.cookieCsrfProtection(req); + } catch (err) { should.equal(err instanceof BadRequestError, true); done(); - }); - }); - }); - - describe('safeGetSession', function () { - it('is an array of getSession and cookieCsrfProtection', function () { - should.deepEqual(sessionService.safeGetSession, [ - sessionService.getSession, - sessionService.cookieCsrfProtection - ]); + } }); }); });