Implement user and related content deletion

Closes #3100

* Introduces `destroyByAuhor`, given a context and an id, it will check if context has permission to delete the user by the id, and then deletes all the content where `author_id` is id, and then deletes the user
* Does multiple checks to make sure user exists
* Added a fixture `posts:mu` that creates 4 users belonging to 4 roles, 50 posts that have authors evenly distributed, 5 tags and all 50 have one tag attached to it, evenly distributed.

Caveats / questions

* Started testing
This commit is contained in:
Gabor Javorszky 2014-07-23 09:32:27 +01:00
parent 9f21730648
commit 8487dada0b
6 changed files with 535 additions and 400 deletions

View File

@ -255,9 +255,22 @@ users = {
destroy: function destroy(options) {
return canThis(options.context).destroy.user(options.id).then(function () {
return users.read(options).then(function (result) {
return dataProvider.User.destroy(options).then(function () {
return dataProvider.Base.transaction(function (t) {
options.transacting = t;
dataProvider.Post.destroyByAuthor(options).then(function () {
return dataProvider.User.destroy(options);
}).then(function () {
t.commit();
}).catch(function (error) {
t.rollback(error);
});
}).then(function () {
return result;
}, function (error) {
return when.reject(new errors.InternalServerError(error));
});
}, function (error) {
return errors.handleAPIError(error);
});
}).catch(function (error) {
return errors.handleAPIError(error);

View File

@ -521,6 +521,31 @@ Post = ghostBookshelf.Model.extend({
});
},
/**
* ### destroyByAuthor
* @param {[type]} options has context and id. Context is the user doing the destroy, id is the user to destroy
*/
destroyByAuthor: function (options) {
var postCollection = Posts.forge(),
authorId = options.id;
options = this.filterOptions(options, 'destroyByAuthor');
if (authorId) {
return postCollection.query('where', 'author_id', '=', authorId).fetch(options).then(function (results) {
return when.map(results.models, function (post) {
return post.related('tags').detach(null, options).then(function () {
return post.destroy(options);
});
});
}, function (error) {
return when.reject(new errors.InternalServerError(error.message || error));
});
}
return when.reject(new errors.NotFoundError('No user found'));
},
permissible: function (postModelOrId, action, context, loadedPermissions, hasUserPermission, hasAppPermission) {
var self = this,
postModel = postModelOrId,

View File

@ -745,7 +745,7 @@ User = ghostBookshelf.Model.extend({
contextUser = ctxUser;
return User.findOne({id: object.id});
}).then(function (user) {
var currentRoles = user.toJSON().roles;
if (!_.contains(currentRoles, adminRole.id)) {
return when.reject(new errors.ValidationError('Only administrators can be assigned the owner role.'));

View File

@ -12,70 +12,57 @@ var testUtils = require('../../utils'),
describe('Post Model', function () {
// Keep the DB clean
before(testUtils.teardown);
afterEach(testUtils.teardown);
beforeEach(testUtils.setup('owner', 'posts', 'apps'));
should.exist(PostModel);
describe('Single author posts', function () {
function checkFirstPostData(firstPost) {
should.not.exist(firstPost.author_id);
firstPost.author.should.be.an.Object;
firstPost.fields.should.be.an.Array;
firstPost.tags.should.be.an.Array;
firstPost.author.name.should.equal(DataGenerator.Content.users[0].name);
firstPost.fields[0].key.should.equal(DataGenerator.Content.app_fields[0].key);
firstPost.created_at.should.be.an.instanceof(Date);
firstPost.created_by.should.be.an.Object;
firstPost.updated_by.should.be.an.Object;
firstPost.published_by.should.be.an.Object;
firstPost.created_by.name.should.equal(DataGenerator.Content.users[0].name);
firstPost.updated_by.name.should.equal(DataGenerator.Content.users[0].name);
firstPost.published_by.name.should.equal(DataGenerator.Content.users[0].name);
firstPost.tags[0].name.should.equal(DataGenerator.Content.tags[0].name);
}
before(testUtils.teardown);
afterEach(testUtils.teardown);
beforeEach(testUtils.setup('owner', 'posts', 'apps'));
it('can findAll', function (done) {
PostModel.findAll().then(function (results) {
should.exist(results);
results.length.should.be.above(1);
should.exist(PostModel);
done();
}).catch(done);
});
function checkFirstPostData(firstPost) {
should.not.exist(firstPost.author_id);
firstPost.author.should.be.an.Object;
firstPost.fields.should.be.an.Array;
firstPost.tags.should.be.an.Array;
firstPost.author.name.should.equal(DataGenerator.Content.users[0].name);
firstPost.fields[0].key.should.equal(DataGenerator.Content.app_fields[0].key);
firstPost.created_at.should.be.an.instanceof(Date);
firstPost.created_by.should.be.an.Object;
firstPost.updated_by.should.be.an.Object;
firstPost.published_by.should.be.an.Object;
firstPost.created_by.name.should.equal(DataGenerator.Content.users[0].name);
firstPost.updated_by.name.should.equal(DataGenerator.Content.users[0].name);
firstPost.published_by.name.should.equal(DataGenerator.Content.users[0].name);
firstPost.tags[0].name.should.equal(DataGenerator.Content.tags[0].name);
}
it('can findAll, returning all related data', function (done) {
var firstPost;
PostModel.findAll({include: ['author_id', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']})
.then(function (results) {
it('can findAll', function (done) {
PostModel.findAll().then(function (results) {
should.exist(results);
results.length.should.be.above(0);
firstPost = results.models[0].toJSON();
checkFirstPostData(firstPost);
results.length.should.be.above(1);
done();
}).catch(done);
});
});
it('can findPage (default)', function (done) {
PostModel.findPage().then(function (results) {
should.exist(results);
it('can findAll, returning all related data', function (done) {
var firstPost;
results.meta.pagination.page.should.equal(1);
results.meta.pagination.limit.should.equal(15);
results.meta.pagination.pages.should.equal(1);
results.posts.length.should.equal(4);
PostModel.findAll({include: ['author_id', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']})
.then(function (results) {
should.exist(results);
results.length.should.be.above(0);
firstPost = results.models[0].toJSON();
checkFirstPostData(firstPost);
done();
}).catch(done);
});
done();
}).catch(done);
});
it('can findPage, returning all related data', function (done) {
var firstPost;
PostModel.findPage({include: ['author_id', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']})
.then(function (results) {
it('can findPage (default)', function (done) {
PostModel.findPage().then(function (results) {
should.exist(results);
results.meta.pagination.page.should.equal(1);
@ -83,397 +70,444 @@ describe('Post Model', function () {
results.meta.pagination.pages.should.equal(1);
results.posts.length.should.equal(4);
done();
}).catch(done);
});
it('can findPage, returning all related data', function (done) {
var firstPost;
PostModel.findPage({include: ['author_id', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']})
.then(function (results) {
should.exist(results);
results.meta.pagination.page.should.equal(1);
results.meta.pagination.limit.should.equal(15);
results.meta.pagination.pages.should.equal(1);
results.posts.length.should.equal(4);
firstPost = results.posts[0];
checkFirstPostData(firstPost);
done();
}).catch(done);
});
it('can findOne', function (done) {
var firstPost;
PostModel.findPage().then(function (results) {
should.exist(results);
should.exist(results.posts);
results.posts.length.should.be.above(0);
firstPost = results.posts[0];
checkFirstPostData(firstPost);
return PostModel.findOne({slug: firstPost.slug});
}).then(function (found) {
should.exist(found);
found.attributes.title.should.equal(firstPost.title);
done();
}).catch(done);
});
});
it('can findOne, returning all related data', function (done) {
var firstPost;
// TODO: should take author :-/
PostModel.findOne({}, {include: ['author_id', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']})
.then(function (result) {
should.exist(result);
firstPost = result.toJSON();
it('can findOne', function (done) {
var firstPost;
checkFirstPostData(firstPost);
PostModel.findPage().then(function (results) {
should.exist(results);
should.exist(results.posts);
results.posts.length.should.be.above(0);
firstPost = results.posts[0];
done();
}).catch(done);
});
return PostModel.findOne({slug: firstPost.slug});
}).then(function (found) {
should.exist(found);
found.attributes.title.should.equal(firstPost.title);
it('can edit', function (done) {
var firstPost = 1;
done();
}).catch(done);
});
PostModel.findOne({id: firstPost}).then(function (results) {
var post;
should.exist(results);
post = results.toJSON();
post.id.should.equal(firstPost);
post.title.should.not.equal('new title');
it('can findOne, returning all related data', function (done) {
var firstPost;
// TODO: should take author :-/
PostModel.findOne({}, {include: ['author_id', 'fields', 'tags', 'created_by', 'updated_by', 'published_by']})
.then(function (result) {
should.exist(result);
firstPost = result.toJSON();
checkFirstPostData(firstPost);
return PostModel.edit({title: 'new title'}, _.extend(context, {id: firstPost}));
}).then(function (edited) {
should.exist(edited);
edited.attributes.title.should.equal('new title');
done();
}).catch(done);
});
it('can edit', function (done) {
var firstPost = 1;
PostModel.findOne({id: firstPost}).then(function (results) {
var post;
should.exist(results);
post = results.toJSON();
post.id.should.equal(firstPost);
post.title.should.not.equal('new title');
return PostModel.edit({title: 'new title'}, _.extend(context, {id: firstPost}));
}).then(function (edited) {
should.exist(edited);
edited.attributes.title.should.equal('new title');
done();
}).catch(done);
});
});
it('can add, defaults are all correct', function (done) {
var createdPostUpdatedDate,
newPost = testUtils.DataGenerator.forModel.posts[2],
newPostDB = testUtils.DataGenerator.Content.posts[2];
it('can add, defaults are all correct', function (done) {
var createdPostUpdatedDate,
newPost = testUtils.DataGenerator.forModel.posts[2],
newPostDB = testUtils.DataGenerator.Content.posts[2];
PostModel.add(newPost, context).then(function (createdPost) {
return new PostModel({id: createdPost.id}).fetch();
}).then(function (createdPost) {
should.exist(createdPost);
createdPost.has('uuid').should.equal(true);
createdPost.get('status').should.equal('draft');
createdPost.get('title').should.equal(newPost.title, 'title is correct');
createdPost.get('markdown').should.equal(newPost.markdown, 'markdown is correct');
createdPost.has('html').should.equal(true);
createdPost.get('html').should.equal(newPostDB.html);
createdPost.get('slug').should.equal(newPostDB.slug + '-2');
(!!createdPost.get('featured')).should.equal(false);
(!!createdPost.get('page')).should.equal(false);
createdPost.get('language').should.equal('en_US');
// testing for nulls
(createdPost.get('image') === null).should.equal(true);
(createdPost.get('meta_title') === null).should.equal(true);
(createdPost.get('meta_description') === null).should.equal(true);
PostModel.add(newPost, context).then(function (createdPost) {
return new PostModel({id: createdPost.id}).fetch();
}).then(function (createdPost) {
should.exist(createdPost);
createdPost.has('uuid').should.equal(true);
createdPost.get('status').should.equal('draft');
createdPost.get('title').should.equal(newPost.title, 'title is correct');
createdPost.get('markdown').should.equal(newPost.markdown, 'markdown is correct');
createdPost.has('html').should.equal(true);
createdPost.get('html').should.equal(newPostDB.html);
createdPost.get('slug').should.equal(newPostDB.slug + '-2');
(!!createdPost.get('featured')).should.equal(false);
(!!createdPost.get('page')).should.equal(false);
createdPost.get('language').should.equal('en_US');
// testing for nulls
(createdPost.get('image') === null).should.equal(true);
(createdPost.get('meta_title') === null).should.equal(true);
(createdPost.get('meta_description') === null).should.equal(true);
createdPost.get('created_at').should.be.above(new Date(0).getTime());
createdPost.get('created_by').should.equal(1);
createdPost.get('author_id').should.equal(1);
createdPost.has('author').should.equal(false);
createdPost.get('created_by').should.equal(createdPost.get('author_id'));
createdPost.get('updated_at').should.be.above(new Date(0).getTime());
createdPost.get('updated_by').should.equal(1);
should.equal(createdPost.get('published_at'), null);
should.equal(createdPost.get('published_by'), null);
createdPost.get('created_at').should.be.above(new Date(0).getTime());
createdPost.get('created_by').should.equal(1);
createdPost.get('author_id').should.equal(1);
createdPost.has('author').should.equal(false);
createdPost.get('created_by').should.equal(createdPost.get('author_id'));
createdPost.get('updated_at').should.be.above(new Date(0).getTime());
createdPost.get('updated_by').should.equal(1);
should.equal(createdPost.get('published_at'), null);
should.equal(createdPost.get('published_by'), null);
createdPostUpdatedDate = createdPost.get('updated_at');
createdPostUpdatedDate = createdPost.get('updated_at');
// Set the status to published to check that `published_at` is set.
return createdPost.save({status: 'published'}, context);
}).then(function (publishedPost) {
publishedPost.get('published_at').should.be.instanceOf(Date);
publishedPost.get('published_by').should.equal(1);
publishedPost.get('updated_at').should.be.instanceOf(Date);
publishedPost.get('updated_by').should.equal(1);
publishedPost.get('updated_at').should.not.equal(createdPostUpdatedDate);
// Set the status to published to check that `published_at` is set.
return createdPost.save({status: 'published'}, context);
}).then(function (publishedPost) {
publishedPost.get('published_at').should.be.instanceOf(Date);
publishedPost.get('published_by').should.equal(1);
publishedPost.get('updated_at').should.be.instanceOf(Date);
publishedPost.get('updated_by').should.equal(1);
publishedPost.get('updated_at').should.not.equal(createdPostUpdatedDate);
done();
}).catch(done);
done();
}).catch(done);
});
});
it('can add, with previous published_at date', function (done) {
var previousPublishedAtDate = new Date(2013, 8, 21, 12);
it('can add, with previous published_at date', function (done) {
var previousPublishedAtDate = new Date(2013, 8, 21, 12);
PostModel.add({
status: 'published',
published_at: previousPublishedAtDate,
title: 'published_at test',
markdown: 'This is some content'
}, context).then(function (newPost) {
PostModel.add({
status: 'published',
published_at: previousPublishedAtDate,
title: 'published_at test',
markdown: 'This is some content'
}, context).then(function (newPost) {
should.exist(newPost);
new Date(newPost.get('published_at')).getTime().should.equal(previousPublishedAtDate.getTime());
should.exist(newPost);
new Date(newPost.get('published_at')).getTime().should.equal(previousPublishedAtDate.getTime());
done();
done();
}).catch(done);
});
}).catch(done);
});
it('can trim title', function (done) {
var untrimmedCreateTitle = ' test trimmed create title ',
untrimmedUpdateTitle = ' test trimmed update title ',
newPost = {
title: untrimmedCreateTitle,
markdown: 'Test Content'
it('can trim title', function (done) {
var untrimmedCreateTitle = ' test trimmed create title ',
untrimmedUpdateTitle = ' test trimmed update title ',
newPost = {
title: untrimmedCreateTitle,
markdown: 'Test Content'
};
PostModel.add(newPost, context).then(function (createdPost) {
return new PostModel({ id: createdPost.id }).fetch();
}).then(function (createdPost) {
should.exist(createdPost);
createdPost.get('title').should.equal(untrimmedCreateTitle.trim());
return createdPost.save({ title: untrimmedUpdateTitle }, context);
}).then(function (updatedPost) {
updatedPost.get('title').should.equal(untrimmedUpdateTitle.trim());
done();
}).catch(done);
});
it('can generate a non conflicting slug', function (done) {
// Create 12 posts with the same title
sequence(_.times(12, function (i) {
return function () {
return PostModel.add({
title: 'Test Title',
markdown: 'Test Content ' + (i+1)
}, context);
};
})).then(function (createdPosts) {
// Should have created 12 posts
createdPosts.length.should.equal(12);
// Should have unique slugs and contents
_(createdPosts).each(function (post, i) {
var num = i + 1;
// First one has normal title
if (num === 1) {
post.get('slug').should.equal('test-title');
return;
}
post.get('slug').should.equal('test-title-' + num);
post.get('markdown').should.equal('Test Content ' + num);
});
done();
}).catch(done);
});
it('can generate slugs without duplicate hyphens', function (done) {
var newPost = {
title: 'apprehensive titles have too many spaces—and m-dashes — and also n-dashes ',
markdown: 'Test Content 1'
};
PostModel.add(newPost, context).then(function (createdPost) {
return new PostModel({ id: createdPost.id }).fetch();
}).then(function (createdPost) {
should.exist(createdPost);
createdPost.get('title').should.equal(untrimmedCreateTitle.trim());
PostModel.add(newPost, context).then(function (createdPost) {
return createdPost.save({ title: untrimmedUpdateTitle }, context);
}).then(function (updatedPost) {
updatedPost.get('title').should.equal(untrimmedUpdateTitle.trim());
createdPost.get('slug').should.equal('apprehensive-titles-have-too-many-spaces-and-m-dashes-and-also-n-dashes');
done();
}).catch(done);
});
done();
}).catch(done);
});
it('can generate a non conflicting slug', function (done) {
// Create 12 posts with the same title
sequence(_.times(12, function (i) {
return function () {
return PostModel.add({
title: 'Test Title',
markdown: 'Test Content ' + (i+1)
}, context);
it('can generate a safe slug when a reserved keyword is used', function(done) {
var newPost = {
title: 'rss',
markdown: 'Test Content 1'
};
})).then(function (createdPosts) {
// Should have created 12 posts
createdPosts.length.should.equal(12);
// Should have unique slugs and contents
_(createdPosts).each(function (post, i) {
var num = i + 1;
// First one has normal title
if (num === 1) {
post.get('slug').should.equal('test-title');
return;
}
post.get('slug').should.equal('test-title-' + num);
post.get('markdown').should.equal('Test Content ' + num);
PostModel.add(newPost, context).then(function (createdPost) {
createdPost.get('slug').should.not.equal('rss');
done();
});
});
done();
}).catch(done);
});
it('can generate slugs without non-ascii characters', function (done) {
var newPost = {
title: 'भुते धडकी भरवणारा आहेत',
markdown: 'Test Content 1'
};
it('can generate slugs without duplicate hyphens', function (done) {
var newPost = {
title: 'apprehensive titles have too many spaces—and m-dashes — and also n-dashes ',
markdown: 'Test Content 1'
};
PostModel.add(newPost, context).then(function (createdPost) {
createdPost.get('slug').should.equal('bhute-dhddkii-bhrvnnaaraa-aahet');
done();
}).catch(done);
});
PostModel.add(newPost, context).then(function (createdPost) {
it('detects duplicate slugs before saving', function (done) {
var firstPost = {
title: 'First post',
markdown: 'First content 1'
},
secondPost = {
title: 'Second post',
markdown: 'Second content 1'
};
createdPost.get('slug').should.equal('apprehensive-titles-have-too-many-spaces-and-m-dashes-and-also-n-dashes');
// Create the first post
PostModel.add(firstPost, context)
.then(function (createdFirstPost) {
// Store the slug for later
firstPost.slug = createdFirstPost.get('slug');
done();
}).catch(done);
});
// Create the second post
return PostModel.add(secondPost, context);
}).then(function (createdSecondPost) {
// Store the slug for comparison later
secondPost.slug = createdSecondPost.get('slug');
it('can generate a safe slug when a reserved keyword is used', function(done) {
var newPost = {
title: 'rss',
markdown: 'Test Content 1'
};
// Update with a conflicting slug from the first post
return createdSecondPost.save({
slug: firstPost.slug
}, context);
}).then(function (updatedSecondPost) {
PostModel.add(newPost, context).then(function (createdPost) {
createdPost.get('slug').should.not.equal('rss');
done();
// Should have updated from original
updatedSecondPost.get('slug').should.not.equal(secondPost.slug);
// Should not have a conflicted slug from the first
updatedSecondPost.get('slug').should.not.equal(firstPost.slug);
return PostModel.findOne({
id: updatedSecondPost.id,
status: 'all'
});
}).then(function (foundPost) {
// Should have updated from original
foundPost.get('slug').should.not.equal(secondPost.slug);
// Should not have a conflicted slug from the first
foundPost.get('slug').should.not.equal(firstPost.slug);
done();
}).catch(done);
});
it('can destroy', function (done) {
// We're going to try deleting post id 1 which also has tag id 1
var firstItemData = {id: 1};
// Test that we have the post we expect, with exactly one tag
PostModel.findOne(firstItemData).then(function (results) {
var post;
should.exist(results);
post = results.toJSON();
post.id.should.equal(firstItemData.id);
post.tags.should.have.length(2);
post.tags[0].should.equal(firstItemData.id);
// Destroy the post
return PostModel.destroy(firstItemData);
}).then(function (response) {
var deleted = response.toJSON();
deleted.tags.should.be.empty;
should.equal(deleted.author, undefined);
// Double check we can't find the post again
return PostModel.findOne(firstItemData);
}).then(function (newResults) {
should.equal(newResults, null);
done();
}).catch(done);
});
it('can findPage, with various options', function (done) {
testUtils.fixtures.insertMorePosts().then(function () {
return testUtils.fixtures.insertMorePostsTags();
}).then(function () {
return PostModel.findPage({page: 2});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(2);
paginationResult.meta.pagination.limit.should.equal(15);
paginationResult.meta.pagination.pages.should.equal(4);
paginationResult.posts.length.should.equal(15);
return PostModel.findPage({page: 5});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(5);
paginationResult.meta.pagination.limit.should.equal(15);
paginationResult.meta.pagination.pages.should.equal(4);
paginationResult.posts.length.should.equal(0);
return PostModel.findPage({limit: 30});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(1);
paginationResult.meta.pagination.limit.should.equal(30);
paginationResult.meta.pagination.pages.should.equal(2);
paginationResult.posts.length.should.equal(30);
// Test both boolean formats
return PostModel.findPage({limit: 10, staticPages: true});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(1);
paginationResult.meta.pagination.limit.should.equal(10);
paginationResult.meta.pagination.pages.should.equal(1);
paginationResult.posts.length.should.equal(1);
// Test both boolean formats
return PostModel.findPage({limit: 10, staticPages: '1'});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(1);
paginationResult.meta.pagination.limit.should.equal(10);
paginationResult.meta.pagination.pages.should.equal(1);
paginationResult.posts.length.should.equal(1);
return PostModel.findPage({limit: 10, page: 2, status: 'all'});
}).then(function (paginationResult) {
paginationResult.meta.pagination.pages.should.equal(11);
done();
}).catch(done);
});
it('can findPage for tag, with various options', function (done) {
testUtils.fixtures.insertMorePosts().then(function () {
return testUtils.fixtures.insertMorePostsTags();
}).then(function () {
// Test tag filter
return PostModel.findPage({page: 1, tag: 'bacon'});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(1);
paginationResult.meta.pagination.limit.should.equal(15);
paginationResult.meta.pagination.pages.should.equal(1);
paginationResult.meta.filters.tags[0].name.should.equal('bacon');
paginationResult.meta.filters.tags[0].slug.should.equal('bacon');
paginationResult.posts.length.should.equal(2);
return PostModel.findPage({page: 1, tag: 'kitchen-sink'});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(1);
paginationResult.meta.pagination.limit.should.equal(15);
paginationResult.meta.pagination.pages.should.equal(1);
paginationResult.meta.filters.tags[0].name.should.equal('kitchen sink');
paginationResult.meta.filters.tags[0].slug.should.equal('kitchen-sink');
paginationResult.posts.length.should.equal(2);
return PostModel.findPage({page: 1, tag: 'injection'});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(1);
paginationResult.meta.pagination.limit.should.equal(15);
paginationResult.meta.pagination.pages.should.equal(2);
paginationResult.meta.filters.tags[0].name.should.equal('injection');
paginationResult.meta.filters.tags[0].slug.should.equal('injection');
paginationResult.posts.length.should.equal(15);
return PostModel.findPage({page: 2, tag: 'injection'});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(2);
paginationResult.meta.pagination.limit.should.equal(15);
paginationResult.meta.pagination.pages.should.equal(2);
paginationResult.meta.filters.tags[0].name.should.equal('injection');
paginationResult.meta.filters.tags[0].slug.should.equal('injection');
paginationResult.posts.length.should.equal(10);
done();
}).catch(done);
});
});
it('can generate slugs without non-ascii characters', function (done) {
var newPost = {
title: 'भुते धडकी भरवणारा आहेत',
markdown: 'Test Content 1'
};
PostModel.add(newPost, context).then(function (createdPost) {
createdPost.get('slug').should.equal('bhute-dhddkii-bhrvnnaaraa-aahet');
done();
}).catch(done);
});
describe('Multiauthor Posts', function () {
before(testUtils.teardown);
afterEach(testUtils.teardown);
beforeEach(testUtils.setup('posts:mu'));
it('detects duplicate slugs before saving', function (done) {
var firstPost = {
title: 'First post',
markdown: 'First content 1'
},
secondPost = {
title: 'Second post',
markdown: 'Second content 1'
};
should.exist(PostModel);
// Create the first post
PostModel.add(firstPost, context)
.then(function (createdFirstPost) {
// Store the slug for later
firstPost.slug = createdFirstPost.get('slug');
it('can destroy multiple posts by author', function (done) {
// Create the second post
return PostModel.add(secondPost, context);
}).then(function (createdSecondPost) {
// Store the slug for comparison later
secondPost.slug = createdSecondPost.get('slug');
// Update with a conflicting slug from the first post
return createdSecondPost.save({
slug: firstPost.slug
}, context);
}).then(function (updatedSecondPost) {
// Should have updated from original
updatedSecondPost.get('slug').should.not.equal(secondPost.slug);
// Should not have a conflicted slug from the first
updatedSecondPost.get('slug').should.not.equal(firstPost.slug);
return PostModel.findOne({
id: updatedSecondPost.id,
status: 'all'
});
}).then(function (foundPost) {
// Should have updated from original
foundPost.get('slug').should.not.equal(secondPost.slug);
// Should not have a conflicted slug from the first
foundPost.get('slug').should.not.equal(firstPost.slug);
// We're going to delete all posts by user 1
var authorData = {id: 1};
PostModel.findAll().then(function (found) {
// There are 50 posts to begin with
found.length.should.equal(50);
return PostModel.destroyByAuthor(authorData);
}).then(function (results) {
// User 1 has 13 posts in the database
results.length.should.equal(13);
return PostModel.findAll();
}).then(function (found) {
// Only 37 should remain
found.length.should.equal(37);
done();
}).catch(done);
});
it('can destroy', function (done) {
// We're going to try deleting post id 1 which also has tag id 1
var firstItemData = {id: 1};
// Test that we have the post we expect, with exactly one tag
PostModel.findOne(firstItemData).then(function (results) {
var post;
should.exist(results);
post = results.toJSON();
post.id.should.equal(firstItemData.id);
post.tags.should.have.length(2);
post.tags[0].should.equal(firstItemData.id);
// Destroy the post
return PostModel.destroy(firstItemData);
}).then(function (response) {
var deleted = response.toJSON();
deleted.tags.should.be.empty;
should.equal(deleted.author, undefined);
// Double check we can't find the post again
return PostModel.findOne(firstItemData);
}).then(function (newResults) {
should.equal(newResults, null);
done();
}).catch(done);
});
it('can findPage, with various options', function (done) {
testUtils.fixtures.insertMorePosts().then(function () {
return testUtils.fixtures.insertMorePostsTags();
}).then(function () {
return PostModel.findPage({page: 2});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(2);
paginationResult.meta.pagination.limit.should.equal(15);
paginationResult.meta.pagination.pages.should.equal(4);
paginationResult.posts.length.should.equal(15);
return PostModel.findPage({page: 5});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(5);
paginationResult.meta.pagination.limit.should.equal(15);
paginationResult.meta.pagination.pages.should.equal(4);
paginationResult.posts.length.should.equal(0);
return PostModel.findPage({limit: 30});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(1);
paginationResult.meta.pagination.limit.should.equal(30);
paginationResult.meta.pagination.pages.should.equal(2);
paginationResult.posts.length.should.equal(30);
// Test both boolean formats
return PostModel.findPage({limit: 10, staticPages: true});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(1);
paginationResult.meta.pagination.limit.should.equal(10);
paginationResult.meta.pagination.pages.should.equal(1);
paginationResult.posts.length.should.equal(1);
// Test both boolean formats
return PostModel.findPage({limit: 10, staticPages: '1'});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(1);
paginationResult.meta.pagination.limit.should.equal(10);
paginationResult.meta.pagination.pages.should.equal(1);
paginationResult.posts.length.should.equal(1);
return PostModel.findPage({limit: 10, page: 2, status: 'all'});
}).then(function (paginationResult) {
paginationResult.meta.pagination.pages.should.equal(11);
done();
}).catch(done);
});
it('can findPage for tag, with various options', function (done) {
testUtils.fixtures.insertMorePosts().then(function () {
return testUtils.fixtures.insertMorePostsTags();
}).then(function () {
// Test tag filter
return PostModel.findPage({page: 1, tag: 'bacon'});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(1);
paginationResult.meta.pagination.limit.should.equal(15);
paginationResult.meta.pagination.pages.should.equal(1);
paginationResult.meta.filters.tags[0].name.should.equal('bacon');
paginationResult.meta.filters.tags[0].slug.should.equal('bacon');
paginationResult.posts.length.should.equal(2);
return PostModel.findPage({page: 1, tag: 'kitchen-sink'});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(1);
paginationResult.meta.pagination.limit.should.equal(15);
paginationResult.meta.pagination.pages.should.equal(1);
paginationResult.meta.filters.tags[0].name.should.equal('kitchen sink');
paginationResult.meta.filters.tags[0].slug.should.equal('kitchen-sink');
paginationResult.posts.length.should.equal(2);
return PostModel.findPage({page: 1, tag: 'injection'});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(1);
paginationResult.meta.pagination.limit.should.equal(15);
paginationResult.meta.pagination.pages.should.equal(2);
paginationResult.meta.filters.tags[0].name.should.equal('injection');
paginationResult.meta.filters.tags[0].slug.should.equal('injection');
paginationResult.posts.length.should.equal(15);
return PostModel.findPage({page: 2, tag: 'injection'});
}).then(function (paginationResult) {
paginationResult.meta.pagination.page.should.equal(2);
paginationResult.meta.pagination.limit.should.equal(15);
paginationResult.meta.pagination.pages.should.equal(2);
paginationResult.meta.filters.tags[0].name.should.equal('injection');
paginationResult.meta.filters.tags[0].slug.should.equal('injection');
paginationResult.posts.length.should.equal(10);
done();
}).catch(done);
});
});
// disabling sanitization until we can implement a better version

File diff suppressed because one or more lines are too long

View File

@ -31,12 +31,71 @@ var when = require('when'),
fixtures = {
insertPosts: function insertPosts() {
var knex = config.database.knex;
// ToDo: Get rid of pyramid of doom
return when(knex('posts').insert(DataGenerator.forKnex.posts).then(function () {
return knex('tags').insert(DataGenerator.forKnex.tags).then(function () {
return knex('posts_tags').insert(DataGenerator.forKnex.posts_tags);
});
}));
return when(knex('posts').insert(DataGenerator.forKnex.posts)).then(function () {
return knex('tags').insert(DataGenerator.forKnex.tags);
}).then(function () {
return knex('posts_tags').insert(DataGenerator.forKnex.posts_tags);
});
},
insertMultiAuthorPosts: function insertMultiAuthorPosts(max) {
var knex = config.database.knex,
tags,
author,
authors,
i, j, k = postsInserted,
posts = [];
max = max || 50;
// insert users of different roles
return when(fixtures.createUsersWithRoles()).then(function (results) {
// create the tags
return knex('tags').insert(DataGenerator.forKnex.tags);
}).then(function (results) {
return knex('users').select('id');
}).then(function (results) {
authors = _.pluck(results, 'id');
// Let's insert posts with random authors
for (i = 0; i < max; i += 1) {
author = authors[i % authors.length];
posts.push(DataGenerator.forKnex.createGenericPost(k++, null, null, author));
}
// Keep track so we can run this function again safely
postsInserted = k;
return sequence(_.times(posts.length, function (index) {
return function () {
return knex('posts').insert(posts[index]);
};
}));
}).then(function () {
return when.all([
// PostgreSQL can return results in any order
knex('posts').orderBy('id', 'asc').select('id'),
knex('tags').select('id')
]);
}).then(function (results) {
var posts = _.pluck(results[0], 'id'),
tags = _.pluck(results[1], 'id'),
promises = [],
i;
if (max > posts.length) {
throw new Error('Trying to add more posts_tags than the number of posts.');
}
for (i = 0; i < max; i += 1) {
promises.push(DataGenerator.forKnex.createPostsTags(posts[i], tags[i % tags.length]));
}
return sequence(_.times(promises.length, function (index) {
return function () {
return knex('posts_tags').insert(promises[index]);
};
}));
});
},
insertMorePosts: function insertMorePosts(max) {
@ -263,14 +322,16 @@ toDoList = {
'permission': function insertPermission() { return fixtures.insertOne('permissions', 'createPermission'); },
'role': function insertRole() { return fixtures.insertOne('roles', 'createRole'); },
'roles': function insertRoles() { return fixtures.insertRoles(); },
'tag': function insertRole() { return fixtures.insertOne('tags', 'createTag'); },
'tag': function insertTag() { return fixtures.insertOne('tags', 'createTag'); },
'posts': function insertPosts() { return fixtures.insertPosts(); },
'posts:mu': function insertMultiAuthorPosts() { return fixtures.insertMultiAuthorPosts(); },
'apps': function insertApps() { return fixtures.insertApps(); },
'settings': function populate() {
return settings.populateDefaults().then(function () { return SettingsAPI.updateSettingsCache(); });
},
'users:roles': function createUsersWithRoles() { return fixtures.createUsersWithRoles(); },
'users': function createExtraUsers() { return fixtures.createExtraUsers(); },
'users': function createExtraUsers() { return fixtures.createExtraUsers(); },
'owner': function insertOwnerUser() { return fixtures.insertOwnerUser(); },
'owner:pre': function initOwnerUser() { return fixtures.initOwnerUser(); },
'owner:post': function overrideOwnerUser() { return fixtures.overrideOwnerUser(); },