diff --git a/core/server/api/index.js b/core/server/api/index.js index 462d51af71..f7951c5d8a 100644 --- a/core/server/api/index.js +++ b/core/server/api/index.js @@ -55,6 +55,33 @@ function cacheInvalidationHeader(req, result) { return when(cacheInvalidate); } +// if api request results in the creation of a new object, construct +// a Location: header that points to the new resource. +// +// arguments: request object, result object from the api call +// returns: a promise that will be fulfilled with the location of the +// resource +function locationHeader(req, result) { + var apiRoot = config.urlFor('api'), + location, + post, + notification, + parsedUrl = req._parsedUrl.pathname.replace(/\/$/, '').split('/'), + endpoint = parsedUrl[4]; + + if (req.method === 'POST') { + if (result.hasOwnProperty('posts')) { + post = result.posts[0]; + location = apiRoot + '/posts/' + post.id + '/?status=' + post.status; + } else if (endpoint === 'notifications') { + notification = result; + location = apiRoot + '/notifications/' + notification.id; + } + } + + return when(location); +} + // ### requestHandler // decorator for api functions which are called via an HTTP request // takes the API method and wraps it so that it gets data from the request and returns a sensible JSON response @@ -72,7 +99,17 @@ requestHandler = function (apiMethod) { "X-Cache-Invalidate": header }); } - res.json(result || {}); + }) + .then(function () { + return locationHeader(req, result).then(function (header) { + if (header) { + res.set({ + 'Location': header + }); + } + + res.json(result || {}); + }); }); }, function (error) { var errorCode = error.code || 500, diff --git a/core/server/api/notifications.js b/core/server/api/notifications.js index d2a5347844..d20d98c89e 100644 --- a/core/server/api/notifications.js +++ b/core/server/api/notifications.js @@ -40,7 +40,8 @@ notifications = { // ``` add: function add(notification) { // **returns:** a promise for all notifications as a json object - return when(notificationsStore.push(notification)); + notificationsStore.push(notification); + return when(notification); } }; diff --git a/core/server/config/url.js b/core/server/config/url.js index 7e1f4453af..971a80a5a3 100644 --- a/core/server/config/url.js +++ b/core/server/config/url.js @@ -103,7 +103,13 @@ function urlFor(context, data, absolute) { var urlPath = '/', secure, knownObjects = ['post', 'tag', 'user'], - knownPaths = {'home': '/', 'rss': '/rss/'}; // this will become really big + + // this will become really big + knownPaths = { + 'home': '/', + 'rss': '/rss/', + 'api': '/ghost/api/v0.1' + }; // Make data properly optional if (_.isBoolean(data)) { diff --git a/core/test/functional/routes/api/notifications_test.js b/core/test/functional/routes/api/notifications_test.js new file mode 100644 index 0000000000..2bd29e94ce --- /dev/null +++ b/core/test/functional/routes/api/notifications_test.js @@ -0,0 +1,170 @@ +var supertest = require('supertest'), + express = require('express'), + should = require('should'), + _ = require('lodash'), + testUtils = require('../../../utils'), + + ghost = require('../../../../../core'), + + httpServer, + request, + agent; + +describe('Notifications API', function () { + var user = testUtils.DataGenerator.forModel.users[0], + csrfToken = ''; + + before(function (done) { + var app = express(); + + ghost({app: app}).then(function (_httpServer) { + httpServer = _httpServer; + + request = supertest.agent(app); + + testUtils.clearData() + .then(function () { + return testUtils.initData(); + }) + .then(function () { + return testUtils.insertDefaultFixtures(); + }) + .then(function () { + request.get('/ghost/signin/') + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + var pattern_meta = //i; + pattern_meta.should.exist; + csrfToken = res.text.match(pattern_meta)[1]; + + setTimeout(function () { + request.post('/ghost/signin/') + .set('X-CSRF-Token', csrfToken) + .send({email: user.email, password: user.password}) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + request.saveCookies(res); + request.get('/ghost/') + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + csrfToken = res.text.match(pattern_meta)[1]; + done(); + }); + }); + }, 2000); + }); + }, done); + }).otherwise(function (e) { + console.log('Ghost Error: ', e); + console.log(e.stack); + }); + }); + + after(function () { + httpServer.close(); + }); + + describe('Add', function () { + var newNotification = { + type: 'info', + message: 'test notification', + status: 'persistent', + id: 'add-test-1' + }; + + it('creates a new notification', function (done) { + request.post(testUtils.API.getApiQuery('notifications/')) + .set('X-CSRF-Token', csrfToken) + .send(newNotification) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + res.headers['location'].should.equal('/ghost/api/v0.1/notifications/' + newNotification.id); + + var jsonResponse = res.body; + + testUtils.API.checkResponse(jsonResponse, 'notification'); + + jsonResponse.type.should.equal(newNotification.type); + jsonResponse.message.should.equal(newNotification.message); + jsonResponse.status.should.equal(newNotification.status); + jsonResponse.id.should.equal(newNotification.id); + + done(); + }); + }); + }); + + describe('Delete', function () { + var newNotification = { + type: 'info', + message: 'test notification', + status: 'persistent', + id: 'delete-test-1' + }; + + it('deletes a notification', function (done) { + // create the notification that is to be deleted + request.post(testUtils.API.getApiQuery('notifications/')) + .set('X-CSRF-Token', csrfToken) + .send(newNotification) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + var location = res.headers['location']; + location.should.equal('/ghost/api/v0.1/notifications/' + newNotification.id); + + var jsonResponse = res.body; + + testUtils.API.checkResponse(jsonResponse, 'notification'); + + jsonResponse.type.should.equal(newNotification.type); + jsonResponse.message.should.equal(newNotification.message); + jsonResponse.status.should.equal(newNotification.status); + jsonResponse.id.should.equal(newNotification.id); + + // begin delete test + request.del(location) + .set('X-CSRF-Token', csrfToken) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + // a delete returns a JSON object containing all notifications + // so we can make sure the notification we just deleted isn't + // included + var notifications = res.body; + + var success; + notifications.forEach(function (n) { + success = n.id !== newNotification.id; + }); + + success.should.be.true; + + done(); + }); + }); + }); + }); +}); diff --git a/core/test/functional/routes/api/posts_test.js b/core/test/functional/routes/api/posts_test.js index a273b83eab..c6d0481717 100644 --- a/core/test/functional/routes/api/posts_test.js +++ b/core/test/functional/routes/api/posts_test.js @@ -334,6 +334,7 @@ describe('Post API', function () { res.should.be.json; var draftPost = res.body; + res.headers['location'].should.equal('/ghost/api/v0.1/posts/' + draftPost.posts[0].id + '/?status=draft'); draftPost.posts.should.exist; draftPost.posts.length.should.be.above(0); draftPost.posts[0].title.should.eql(newTitle);