diff --git a/core/server/api/slugs.js b/core/server/api/slugs.js index 092a6e07c3..e746caccdd 100644 --- a/core/server/api/slugs.js +++ b/core/server/api/slugs.js @@ -9,7 +9,9 @@ var canThis = require('../permissions').canThis, // `allowedTypes` is used to define allowed slug types and map them against its model class counterpart allowedTypes = { post: dataProvider.Post, - tag: dataProvider.Tag + tag: dataProvider.Tag, + user: dataProvider.User, + app: dataProvider.App }; /** @@ -21,10 +23,9 @@ slugs = { /** * ## Generate Slug - * Create a unique slug for a given post title - * TODO: make it generic for all objects: post, tag, user + * Create a unique slug for the given type and its name * - * @param {{type (required), title (required), context, transacting}} options + * @param {{type (required), name (required), context, transacting}} options * @returns {Promise(String)} Unique string */ generate: function (options) { @@ -35,14 +36,18 @@ slugs = { return when.reject(new errors.BadRequestError('Unknown slug type \'' + options.type + '\'.')); } - return dataProvider.Base.Model.generateSlug(allowedTypes[options.type], options.title, {status: 'all'}).then(function (slug) { + return dataProvider.Base.Model.generateSlug(allowedTypes[options.type], options.name, {status: 'all'}).then(function (slug) { if (!slug) { return when.reject(new errors.InternalServerError('Could not generate slug.')); } return { slugs: [{ slug: slug }] }; }); - }, function () { + }).catch(function (err) { + if (err) { + return when.reject(err); + } + return when.reject(new errors.NoPermissionError('You do not have permission to generate a slug.')); }); } diff --git a/core/server/models/base.js b/core/server/models/base.js index 7b5fbc3ed8..12995931b5 100644 --- a/core/server/models/base.js +++ b/core/server/models/base.js @@ -288,7 +288,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ var slug, slugTryCount = 1, baseName = Model.prototype.tableName.replace(/s$/, ''), - // Look for a post with a matching slug, append an incrementing number if so + // Look for a matching slug, append an incrementing number if so checkIfSlugExists; checkIfSlugExists = function (slugToFind) { @@ -339,10 +339,10 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ slug = slug.charAt(slug.length - 1) === '-' ? slug.substr(0, slug.length - 1) : slug; // Check the filtered slug doesn't match any of the reserved keywords - slug = /^(ghost|ghost\-admin|admin|wp\-admin|wp\-login|dashboard|logout|login|signin|signup|signout|register|archive|archives|category|categories|tag|tags|page|pages|post|posts|public|user|users|rss|feed)$/g + slug = /^(ghost|ghost\-admin|admin|wp\-admin|wp\-login|dashboard|logout|login|signin|signup|signout|register|archive|archives|category|categories|tag|tags|page|pages|post|posts|public|user|users|rss|feed|app|apps)$/g .test(slug) ? slug + '-' + baseName : slug; - //if slug is empty after trimming use "post" + //if slug is empty after trimming use the model name if (!slug) { slug = baseName; } diff --git a/core/server/routes/api.js b/core/server/routes/api.js index 371d813957..6879fb97a6 100644 --- a/core/server/routes/api.js +++ b/core/server/routes/api.js @@ -36,7 +36,7 @@ apiRoutes = function (server) { server.post('/ghost/api/v0.1/mail', api.http(api.mail.send)); server.post('/ghost/api/v0.1/mail/test', api.http(api.mail.sendTest)); // #### Slugs - server.get('/ghost/api/v0.1/slugs/:type/:title', api.http(api.slugs.generate)); + server.get('/ghost/api/v0.1/slugs/:type/:name', api.http(api.slugs.generate)); }; module.exports = apiRoutes; diff --git a/core/test/functional/routes/api/slugs_test.js b/core/test/functional/routes/api/slugs_test.js new file mode 100644 index 0000000000..36330b9a70 --- /dev/null +++ b/core/test/functional/routes/api/slugs_test.js @@ -0,0 +1,181 @@ +/*global describe, it, before, after */ +var supertest = require('supertest'), + express = require('express'), + should = require('should'), + _ = require('lodash'), + testUtils = require('../../../utils'), + + ghost = require('../../../../../core'), + + httpServer, + request, + agent; + + +describe('Slug 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]; + + process.nextTick(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(); + }); + }); + + }); + }); + }).catch(done); + }).catch(function (e) { + console.log('Ghost Error: ', e); + console.log(e.stack); + }); + }); + + after(function () { + httpServer.close(); + }); + + it('should be able to get a post slug', function (done) { + request.get(testUtils.API.getApiQuery('slugs/post/a post title/')) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + res.should.be.json; + var jsonResponse = res.body; + jsonResponse.should.exist; + jsonResponse.slugs.should.exist; + jsonResponse.slugs.should.have.length(1); + testUtils.API.checkResponse(jsonResponse.slugs[0], 'slug'); + jsonResponse.slugs[0].slug.should.equal('a-post-title'); + + done(); + }); + }); + + it('should be able to get a tag slug', function (done) { + request.get(testUtils.API.getApiQuery('slugs/post/atag/')) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + res.should.be.json; + var jsonResponse = res.body; + jsonResponse.should.exist; + jsonResponse.slugs.should.exist; + jsonResponse.slugs.should.have.length(1); + testUtils.API.checkResponse(jsonResponse.slugs[0], 'slug'); + jsonResponse.slugs[0].slug.should.equal('atag'); + + done(); + }); + }); + + it('should be able to get a user slug', function (done) { + request.get(testUtils.API.getApiQuery('slugs/user/user name/')) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + res.should.be.json; + var jsonResponse = res.body; + jsonResponse.should.exist; + jsonResponse.slugs.should.exist; + jsonResponse.slugs.should.have.length(1); + testUtils.API.checkResponse(jsonResponse.slugs[0], 'slug'); + jsonResponse.slugs[0].slug.should.equal('user-name'); + + done(); + }); + }); + + it('should be able to get an app slug', function (done) { + request.get(testUtils.API.getApiQuery('slugs/app/cool app/')) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + res.should.be.json; + var jsonResponse = res.body; + jsonResponse.should.exist; + jsonResponse.slugs.should.exist; + jsonResponse.slugs.should.have.length(1); + testUtils.API.checkResponse(jsonResponse.slugs[0], 'slug'); + jsonResponse.slugs[0].slug.should.equal('cool-app'); + + done(); + }); + }); + + it('should not be able to get a slug for an unknown type', function (done) { + request.get(testUtils.API.getApiQuery('slugs/unknown/who knows/')) + .expect(400) + .end(function (err, res) { + if (err) { + return done(err); + } + + res.should.be.json; + var jsonResponse = res.body; + jsonResponse.should.not.exist; + + done(); + }); + }); +}); diff --git a/core/test/integration/api/api_slugs_spec.js b/core/test/integration/api/api_slugs_spec.js index 5dee1f43c8..bb1df2c6d7 100644 --- a/core/test/integration/api/api_slugs_spec.js +++ b/core/test/integration/api/api_slugs_spec.js @@ -15,6 +15,8 @@ describe('Slug API', function () { beforeEach(function (done) { testUtils.initData().then(function () { return testUtils.insertDefaultFixtures(); + }).then(function () { + return permissions.init(); }).then(function () { done(); }).catch(done); @@ -27,9 +29,8 @@ describe('Slug API', function () { }); it('can generate post slug', function (done) { - permissions.init().then(function () { - return slugAPI.generate({ context: {user: 1}, type: 'post', title: 'A fancy Title'}); - }).then(function (results) { + slugAPI.generate({ context: { user: 1 }, type: 'post', name: 'A fancy Title' }) + .then(function (results) { should.exist(results); testUtils.API.checkResponse(results, 'slugs'); results.slugs.length.should.be.above(0); @@ -40,9 +41,8 @@ describe('Slug API', function () { }); it('can generate tag slug', function (done) { - permissions.init().then(function () { - return slugAPI.generate({ context: {user: 1}, type: 'tag', title: 'A fancy Title'}); - }).then(function (results) { + slugAPI.generate({ context: { user: 1 }, type: 'tag', name: 'A fancy Title' }) + .then(function (results) { should.exist(results); testUtils.API.checkResponse(results, 'slugs'); results.slugs.length.should.be.above(0); @@ -52,13 +52,36 @@ describe('Slug API', function () { }).catch(done); }); - it('reject unknown type', function (done) { - permissions.init().then(function () { - return slugAPI.generate({ context: {user: 1}, type: 'unknown type', title: 'A fancy Title'}); - }).then(function () { - done(new Error('Generate a slug for a unknown type is not rejected.')); - }, function (error) { - error.type.should.eql('BadRequestError'); + it('can generate user slug', function (done) { + slugAPI.generate({ context: { user: 1 }, type: 'tag', name: 'user name' }) + .then(function (results) { + should.exist(results); + testUtils.API.checkResponse(results, 'slugs'); + results.slugs.length.should.be.above(0); + testUtils.API.checkResponse(results.slugs[0], 'slug'); + results.slugs[0].slug.should.equal('user-name'); + done(); + }).catch(done); + }); + + it('can generate app slug', function (done) { + slugAPI.generate({ context: { user: 1 }, type: 'tag', name: 'app name' }) + .then(function (results) { + should.exist(results); + testUtils.API.checkResponse(results, 'slugs'); + results.slugs.length.should.be.above(0); + testUtils.API.checkResponse(results.slugs[0], 'slug'); + results.slugs[0].slug.should.equal('app-name'); + done(); + }).catch(done); + }); + + it('rejects unknown types', function (done) { + slugAPI.generate({ context: { user: 1 }, type: 'unknown type', name: 'A fancy Title' }) + .then(function () { + done(new Error('Generate a slug for an unknown type is not rejected.')); + }).catch(function (error) { + error.type.should.equal('BadRequestError'); done(); }).catch(done); });